From 5a0c911abe6fefcbfd81dec6128a630229358b4e Mon Sep 17 00:00:00 2001 From: stuartpa Date: Thu, 10 Nov 2022 07:11:08 -0800 Subject: [PATCH 1/5] Normalize eol --- .gitattributes | 2 + .github/workflows/golangci-lint.yml | 40 +- .pipelines/TestSql2017.yml | 106 +- .pipelines/include-install-go-tools.yml | 70 +- .pipelines/include-runtests-linux.yml | 92 +- SUPPORT.md | 50 +- build/azure-pipelines/build-common.yml | 140 +-- build/azure-pipelines/build-product.yml | 394 +++--- cmd/sqlcmd/testdata/select100.sql | 2 +- pkg/sqlcmd/azure_auth.go | 110 +- pkg/sqlcmd/batch.go | 526 ++++---- pkg/sqlcmd/batch_test.go | 446 +++---- pkg/sqlcmd/commands.go | 1014 +++++++-------- pkg/sqlcmd/commands_test.go | 604 ++++----- pkg/sqlcmd/errors.go | 188 +-- pkg/sqlcmd/format.go | 1326 ++++++++++---------- pkg/sqlcmd/format_darwin.go | 14 +- pkg/sqlcmd/format_linux.go | 14 +- pkg/sqlcmd/format_test.go | 280 ++--- pkg/sqlcmd/format_windows.go | 14 +- pkg/sqlcmd/parse.go | 202 +-- pkg/sqlcmd/parse_test.go | 8 +- pkg/sqlcmd/sqlcmd_test.go | 1184 ++++++++--------- pkg/sqlcmd/testdata/singlebatchnogo.sql | 4 +- pkg/sqlcmd/testdata/twobatchnoendinggo.sql | 6 +- pkg/sqlcmd/testdata/twobatchwithgo.sql | 8 +- pkg/sqlcmd/util.go | 146 +-- pkg/sqlcmd/variables.go | 670 +++++----- pkg/sqlcmd/variables_test.go | 232 ++-- testdata/sql.txt | 6 +- 30 files changed, 3950 insertions(+), 3948 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..032c1bd1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto + diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e18f537f..a28ef0ed 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,20 +1,20 @@ -name: golangci-lint -on: - push: - branches: - - main - pull_request: -jobs: - golangci-pr: - name: lint-pr-changes - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - uses: actions/checkout@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest - only-new-issues: true +name: golangci-lint +on: + push: + branches: + - main + pull_request: +jobs: + golangci-pr: + name: lint-pr-changes + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml index 776b310b..589234cb 100644 --- a/.pipelines/TestSql2017.yml +++ b/.pipelines/TestSql2017.yml @@ -1,53 +1,53 @@ -variables: - # AZURE_CLIENT_SECRET and SQLPASSWORD must be defined as secret variables in the pipeline. - # AZURE_TENANT_ID and AZURE_CLIENT_ID are not expected to be secret variables, just regular variables - AZURECLIENTSECRET: $(AZURE_CLIENT_SECRET) - PASSWORD: $(SQLPASSWORD) -pool: - vmImage: 'ubuntu-latest' - -steps: - - template: include-install-go-tools.yml - - - task: Docker@2 - displayName: 'Run SQL 2017 docker image' - inputs: - command: run - arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(PASSWORD) mcr.microsoft.com/mssql/server:2017-latest' - - - template: include-runtests-linux.yml - parameters: - RunName: 'SQL2017' - SQLCMDUSER: sa - SQLPASSWORD: $(PASSWORD) - - - template: include-runtests-linux.yml - parameters: - RunName: 'SQLDB' - # AZURESERVER must be defined as a variable in the pipeline - SQLCMDSERVER: $(AZURESERVER) - AZURECLIENTSECRET: $(AZURECLIENTSECRET) - - - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - displayName: Merge coverage data - inputs: - reports: '**/*.coverage.xml"' # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported. - targetdir: 'coverage' # REQUIRED # The directory where the generated report should be saved. - reporttypes: 'HtmlInline_AzurePipelines;Cobertura' # The output formats and scope (separated by semicolon) Values: Badges, Clover, Cobertura, CsvSummary, Html, HtmlChart, HtmlInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HtmlSummary, JsonSummary, Latex, LatexSummary, lcov, MarkdownSummary, MHtml, PngChart, SonarQube, TeamCitySummary, TextSummary, Xml, XmlSummary - sourcedirs: '$(Build.SourcesDirectory)' # Optional directories which contain the corresponding source code (separated by semicolon). The source directories are used if coverage report contains classes without path information. - verbosity: 'Info' # The verbosity level of the log messages. Values: Verbose, Info, Warning, Error, Off - tag: '$(build.buildnumber)_#$(build.buildid)_$(Build.SourceBranchName)' # Optional tag or build version. - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - pathToSources: '$(Build.SourcesDirectory)' - summaryFileLocation: $(Build.SourcesDirectory)/coverage/*.xml - reportDirectory: $(Build.SourcesDirectory)/coverage - failIfCoverageEmpty: true - condition: always() - continueOnError: true - env: - disable.coverage.autogenerate: 'true' - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: ‘Component Detection’ +variables: + # AZURE_CLIENT_SECRET and SQLPASSWORD must be defined as secret variables in the pipeline. + # AZURE_TENANT_ID and AZURE_CLIENT_ID are not expected to be secret variables, just regular variables + AZURECLIENTSECRET: $(AZURE_CLIENT_SECRET) + PASSWORD: $(SQLPASSWORD) +pool: + vmImage: 'ubuntu-latest' + +steps: + - template: include-install-go-tools.yml + + - task: Docker@2 + displayName: 'Run SQL 2017 docker image' + inputs: + command: run + arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(PASSWORD) mcr.microsoft.com/mssql/server:2017-latest' + + - template: include-runtests-linux.yml + parameters: + RunName: 'SQL2017' + SQLCMDUSER: sa + SQLPASSWORD: $(PASSWORD) + + - template: include-runtests-linux.yml + parameters: + RunName: 'SQLDB' + # AZURESERVER must be defined as a variable in the pipeline + SQLCMDSERVER: $(AZURESERVER) + AZURECLIENTSECRET: $(AZURECLIENTSECRET) + + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + displayName: Merge coverage data + inputs: + reports: '**/*.coverage.xml"' # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported. + targetdir: 'coverage' # REQUIRED # The directory where the generated report should be saved. + reporttypes: 'HtmlInline_AzurePipelines;Cobertura' # The output formats and scope (separated by semicolon) Values: Badges, Clover, Cobertura, CsvSummary, Html, HtmlChart, HtmlInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HtmlSummary, JsonSummary, Latex, LatexSummary, lcov, MarkdownSummary, MHtml, PngChart, SonarQube, TeamCitySummary, TextSummary, Xml, XmlSummary + sourcedirs: '$(Build.SourcesDirectory)' # Optional directories which contain the corresponding source code (separated by semicolon). The source directories are used if coverage report contains classes without path information. + verbosity: 'Info' # The verbosity level of the log messages. Values: Verbose, Info, Warning, Error, Off + tag: '$(build.buildnumber)_#$(build.buildid)_$(Build.SourceBranchName)' # Optional tag or build version. + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + pathToSources: '$(Build.SourcesDirectory)' + summaryFileLocation: $(Build.SourcesDirectory)/coverage/*.xml + reportDirectory: $(Build.SourcesDirectory)/coverage + failIfCoverageEmpty: true + condition: always() + continueOnError: true + env: + disable.coverage.autogenerate: 'true' + + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: ‘Component Detection’ diff --git a/.pipelines/include-install-go-tools.yml b/.pipelines/include-install-go-tools.yml index 53aeb1f2..3e33cf47 100644 --- a/.pipelines/include-install-go-tools.yml +++ b/.pipelines/include-install-go-tools.yml @@ -1,36 +1,36 @@ -steps: - - task: GoTool@0 - inputs: - version: '1.18' - - task: Go@0 - displayName: 'Go: get dependencies' - inputs: - command: 'get' - arguments: '-d' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - - - - task: Go@0 - displayName: 'Go: install gotest.tools/gotestsum' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'gotest.tools/gotestsum@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' - - - task: Go@0 - displayName: 'Go: install github.com/axw/gocov/gocov' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/axw/gocov/gocov@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' - - - task: Go@0 - displayName: 'Go: install github.com/axw/gocov/gocov' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/AlekSi/gocov-xml@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' +steps: + - task: GoTool@0 + inputs: + version: '1.18' + - task: Go@0 + displayName: 'Go: get dependencies' + inputs: + command: 'get' + arguments: '-d' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + + + - task: Go@0 + displayName: 'Go: install gotest.tools/gotestsum' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'gotest.tools/gotestsum@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - task: Go@0 + displayName: 'Go: install github.com/axw/gocov/gocov' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/axw/gocov/gocov@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - task: Go@0 + displayName: 'Go: install github.com/axw/gocov/gocov' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/AlekSi/gocov-xml@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' \ No newline at end of file diff --git a/.pipelines/include-runtests-linux.yml b/.pipelines/include-runtests-linux.yml index 2735a10e..e1aa0d42 100644 --- a/.pipelines/include-runtests-linux.yml +++ b/.pipelines/include-runtests-linux.yml @@ -1,46 +1,46 @@ -parameters: -- name: RunName - type: string -- name: SQLCMDUSER - type: string - default: '' -- name: SQLPASSWORD - type: string - default: '' -- name: AZURECLIENTSECRET - type: string - default: '' -- name: SQLCMDSERVER - type: string - default: . -- name: SQLCMDDBNAME - type: string - default: '' -steps: - - script: | - ~/go/bin/gotestsum --junitfile "${{ parameters.RunName }}.testresults.xml" -- ./... -coverprofile="${{ parameters.RunName }}.coverage.txt" -covermode count - ~/go/bin/gocov convert "${{ parameters.RunName }}.coverage.txt" > "${{ parameters.RunName }}.coverage.json" - ~/go/bin/gocov-xml < "${{ parameters.RunName }}.coverage.json" > ${{ parameters.RunName }}.coverage.xml - mkdir -p coverage - workingDirectory: '$(Build.SourcesDirectory)' - displayName: 'run tests' - env: - SQLPASSWORD: ${{ parameters.SQLPASSWORD }} - SQLCMDUSER: ${{ parameters.SQLCMDUSER }} - SQLCMDPASSWORD: ${{ parameters.SQLPASSWORD }} - AZURE_TENANT_ID: $(AZURE_TENANT_ID) - AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) - AZURE_CLIENT_SECRET: ${{ parameters.AZURECLIENTSECRET }} - SQLCMDSERVER: ${{ parameters.SQLCMDSERVER }} - SQLCMDDBNAME: ${{ parameters.SQLCMDDBNAME }} - continueOnError: true - - - task: PublishTestResults@2 - displayName: "Publish junit-style results" - inputs: - testResultsFiles: '${{ parameters.RunName }}.testresults.xml' - testResultsFormat: JUnit - searchFolder: '$(Build.SourcesDirectory)' - testRunTitle: '${{ parameters.RunName }} - $(Build.SourceBranchName)' - failTaskOnFailedTests: true - condition: always() +parameters: +- name: RunName + type: string +- name: SQLCMDUSER + type: string + default: '' +- name: SQLPASSWORD + type: string + default: '' +- name: AZURECLIENTSECRET + type: string + default: '' +- name: SQLCMDSERVER + type: string + default: . +- name: SQLCMDDBNAME + type: string + default: '' +steps: + - script: | + ~/go/bin/gotestsum --junitfile "${{ parameters.RunName }}.testresults.xml" -- ./... -coverprofile="${{ parameters.RunName }}.coverage.txt" -covermode count + ~/go/bin/gocov convert "${{ parameters.RunName }}.coverage.txt" > "${{ parameters.RunName }}.coverage.json" + ~/go/bin/gocov-xml < "${{ parameters.RunName }}.coverage.json" > ${{ parameters.RunName }}.coverage.xml + mkdir -p coverage + workingDirectory: '$(Build.SourcesDirectory)' + displayName: 'run tests' + env: + SQLPASSWORD: ${{ parameters.SQLPASSWORD }} + SQLCMDUSER: ${{ parameters.SQLCMDUSER }} + SQLCMDPASSWORD: ${{ parameters.SQLPASSWORD }} + AZURE_TENANT_ID: $(AZURE_TENANT_ID) + AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) + AZURE_CLIENT_SECRET: ${{ parameters.AZURECLIENTSECRET }} + SQLCMDSERVER: ${{ parameters.SQLCMDSERVER }} + SQLCMDDBNAME: ${{ parameters.SQLCMDDBNAME }} + continueOnError: true + + - task: PublishTestResults@2 + displayName: "Publish junit-style results" + inputs: + testResultsFiles: '${{ parameters.RunName }}.testresults.xml' + testResultsFormat: JUnit + searchFolder: '$(Build.SourcesDirectory)' + testRunTitle: '${{ parameters.RunName }} - $(Build.SourceBranchName)' + failTaskOnFailedTests: true + condition: always() diff --git a/SUPPORT.md b/SUPPORT.md index dc72f0e5..8b05616f 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,25 +1,25 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). -- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. +# TODO: The maintainer of this repo has not yet edited this file + +**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? + +- **No CSS support:** Fill out this template with information about how to file issues and get help. +- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). +- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. + +*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* + +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE +FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER +CHANNEL. WHERE WILL YOU HELP PEOPLE?**. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/build/azure-pipelines/build-common.yml b/build/azure-pipelines/build-common.yml index 62e91668..3ad9659b 100644 --- a/build/azure-pipelines/build-common.yml +++ b/build/azure-pipelines/build-common.yml @@ -1,70 +1,70 @@ -parameters: -- name: OS - type: string - default: -- name: Arch - type: string - default: -- name: ArtifactName - type: string -- name: VersionTag - type: string - default: $(Build.BuildNumber) - -steps: -- task: GoTool@0 - inputs: - version: '1.18' - goBin: $(Build.SourcesDirectory) - -- task: Go@0 - displayName: 'Go install go-winres' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/tc-hib/go-winres@latest' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOBIN: $(Build.SourcesDirectory) - -- task: CmdLine@2 - displayName: 'generate version resource' - inputs: - script: $(Build.SourcesDirectory)/go-winres make --file-version git-tag --product-version git-tag - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - -- task: Go@0 - displayName: 'Go: get dependencies' - inputs: - command: 'get' - arguments: '-d' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOOS: ${{ parameters.OS }} - GOARCH: ${{ parameters.Arch }} - GOBIN: $(Build.SourcesDirectory) - -- task: Go@0 - displayName: 'Go: build sqlcmd' - inputs: - command: 'build' - arguments: '-o $(Build.BinariesDirectory) -ldflags="-X main.version=${{ parameters.VersionTag }}"' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOOS: ${{ parameters.OS }} - GOARCH: ${{ parameters.Arch }} - GOBIN: $(Build.SourcesDirectory) - CGO_ENABLED: 0 # Enables Docker image based off 'scratch' - -- task: CopyFiles@2 - inputs: - TargetFolder: '$(Build.ArtifactStagingDirectory)' - SourceFolder: '$(Build.BinariesDirectory)' - Contents: '**' - -- task: PublishPipelineArtifact@1 - displayName: 'Publish binary' - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: 'Sqlcmd${{ parameters.ArtifactName }}' - +parameters: +- name: OS + type: string + default: +- name: Arch + type: string + default: +- name: ArtifactName + type: string +- name: VersionTag + type: string + default: $(Build.BuildNumber) + +steps: +- task: GoTool@0 + inputs: + version: '1.18' + goBin: $(Build.SourcesDirectory) + +- task: Go@0 + displayName: 'Go install go-winres' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/tc-hib/go-winres@latest' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOBIN: $(Build.SourcesDirectory) + +- task: CmdLine@2 + displayName: 'generate version resource' + inputs: + script: $(Build.SourcesDirectory)/go-winres make --file-version git-tag --product-version git-tag + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + +- task: Go@0 + displayName: 'Go: get dependencies' + inputs: + command: 'get' + arguments: '-d' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOOS: ${{ parameters.OS }} + GOARCH: ${{ parameters.Arch }} + GOBIN: $(Build.SourcesDirectory) + +- task: Go@0 + displayName: 'Go: build sqlcmd' + inputs: + command: 'build' + arguments: '-o $(Build.BinariesDirectory) -ldflags="-X main.version=${{ parameters.VersionTag }}"' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOOS: ${{ parameters.OS }} + GOARCH: ${{ parameters.Arch }} + GOBIN: $(Build.SourcesDirectory) + CGO_ENABLED: 0 # Enables Docker image based off 'scratch' + +- task: CopyFiles@2 + inputs: + TargetFolder: '$(Build.ArtifactStagingDirectory)' + SourceFolder: '$(Build.BinariesDirectory)' + Contents: '**' + +- task: PublishPipelineArtifact@1 + displayName: 'Publish binary' + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: 'Sqlcmd${{ parameters.ArtifactName }}' + diff --git a/build/azure-pipelines/build-product.yml b/build/azure-pipelines/build-product.yml index c028c4ae..f9a6a75a 100644 --- a/build/azure-pipelines/build-product.yml +++ b/build/azure-pipelines/build-product.yml @@ -1,197 +1,197 @@ -trigger: - tags: - include: - - v* - -pr: none - -parameters: - - name: PushToGithub - default: true - type: boolean - displayName: Push packages to github - -stages: - - stage: Compile - displayName: Compile sqlcmd on all supported platforms - jobs: - - job: Sqlcmd - strategy: - matrix: - linux: - imageName: 'ubuntu-latest' - artifact: LinuxAmd64 - os: - arch: - mac: - imageName: 'macOS-latest' - artifact: DarwinAmd64 - os: - arch: - windows: - imageName: 'windows-latest' - artifact: WindowsAmd64 - os: - arch: - linuxArm: - imageName: 'ubuntu-latest' - artifact: LinuxArm64 - os: - arch: arm64 - windowsArm: - imageName: 'windows-latest' - artifact: WindowsArm - os: - arch: arm - linuxs390x: - imageName: 'ubuntu-latest' - artifact: LinuxS390x - os: - arch: s390x - pool: - vmImage: $(imageName) - steps: - - template: build-tag.yml - - script: | - echo $(getVersion.VERSION_TAG) - - template: build-common.yml - parameters: - OS: $(os) - Arch: $(arch) - ArtifactName: $(artifact) - VersionTag: $(getVersion.VERSION_TAG) - - - stage: CreatePackages - displayName: Create packages to publish - jobs: - - job: Sign_and_pack - pool: - vmImage: 'windows-latest' - steps: - - template: build-tag.yml - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - targetPath: '$(Pipeline.Workspace)' - - task: EsrpCodeSigning@1 - displayName: Sign Windows binary - inputs: - ConnectedServiceName: 'Code Signing' - FolderPath: '$(Pipeline.Workspace)' - Pattern: 'sqlcmd.exe' - signConfigType: 'inlineSignParams' - SessionTimeout: '600' - MaxConcurrency: '5' - MaxRetryAttempts: '5' - inlineOperation: | - [ - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "go-sqlcmd" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://github.com/microsoft/go-sqlcmd" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd sha256" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - } - ] - - task: ArchiveFiles@2 - displayName: Zip Windows amd64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsAmd64\Sqlcmd.exe' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-x64.zip' - - - task: ArchiveFiles@2 - displayName: Zip Windows arm binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsArm\Sqlcmd.exe' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-arm.zip' - - - task: ArchiveFiles@2 - displayName: Tar Linux amd64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxAmd64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-x64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Darwin binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdDarwinAmd64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-darwin-x64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Linux arm64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxArm64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-arm64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Linux s390x binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxS390x' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-s390x.tar.bz2' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish release archives' - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: SqlcmdRelease - - - task: GitHubRelease@1 - condition: eq('${{ parameters.PushToGithub}}', 'true') - inputs: - gitHubConnection: 'gosqlcmd_github' - repositoryName: '$(Build.Repository.Name)' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(getVersion.VERSION_TAG)' - changeLogCompareToRelease: 'lastFullRelease' - changeLogType: 'commitBased' +trigger: + tags: + include: + - v* + +pr: none + +parameters: + - name: PushToGithub + default: true + type: boolean + displayName: Push packages to github + +stages: + - stage: Compile + displayName: Compile sqlcmd on all supported platforms + jobs: + - job: Sqlcmd + strategy: + matrix: + linux: + imageName: 'ubuntu-latest' + artifact: LinuxAmd64 + os: + arch: + mac: + imageName: 'macOS-latest' + artifact: DarwinAmd64 + os: + arch: + windows: + imageName: 'windows-latest' + artifact: WindowsAmd64 + os: + arch: + linuxArm: + imageName: 'ubuntu-latest' + artifact: LinuxArm64 + os: + arch: arm64 + windowsArm: + imageName: 'windows-latest' + artifact: WindowsArm + os: + arch: arm + linuxs390x: + imageName: 'ubuntu-latest' + artifact: LinuxS390x + os: + arch: s390x + pool: + vmImage: $(imageName) + steps: + - template: build-tag.yml + - script: | + echo $(getVersion.VERSION_TAG) + - template: build-common.yml + parameters: + OS: $(os) + Arch: $(arch) + ArtifactName: $(artifact) + VersionTag: $(getVersion.VERSION_TAG) + + - stage: CreatePackages + displayName: Create packages to publish + jobs: + - job: Sign_and_pack + pool: + vmImage: 'windows-latest' + steps: + - template: build-tag.yml + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + targetPath: '$(Pipeline.Workspace)' + - task: EsrpCodeSigning@1 + displayName: Sign Windows binary + inputs: + ConnectedServiceName: 'Code Signing' + FolderPath: '$(Pipeline.Workspace)' + Pattern: 'sqlcmd.exe' + signConfigType: 'inlineSignParams' + SessionTimeout: '600' + MaxConcurrency: '5' + MaxRetryAttempts: '5' + inlineOperation: | + [ + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "go-sqlcmd" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://github.com/microsoft/go-sqlcmd" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd sha256" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + } + ] + - task: ArchiveFiles@2 + displayName: Zip Windows amd64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsAmd64\Sqlcmd.exe' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-x64.zip' + + - task: ArchiveFiles@2 + displayName: Zip Windows arm binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsArm\Sqlcmd.exe' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-arm.zip' + + - task: ArchiveFiles@2 + displayName: Tar Linux amd64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxAmd64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-x64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Darwin binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdDarwinAmd64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-darwin-x64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Linux arm64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxArm64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-arm64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Linux s390x binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxS390x' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-s390x.tar.bz2' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish release archives' + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: SqlcmdRelease + + - task: GitHubRelease@1 + condition: eq('${{ parameters.PushToGithub}}', 'true') + inputs: + gitHubConnection: 'gosqlcmd_github' + repositoryName: '$(Build.Repository.Name)' + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: 'userSpecifiedTag' + tag: '$(getVersion.VERSION_TAG)' + changeLogCompareToRelease: 'lastFullRelease' + changeLogType: 'commitBased' diff --git a/cmd/sqlcmd/testdata/select100.sql b/cmd/sqlcmd/testdata/select100.sql index 1b87fa39..718c071f 100644 --- a/cmd/sqlcmd/testdata/select100.sql +++ b/cmd/sqlcmd/testdata/select100.sql @@ -1 +1 @@ -select 100 +select 100 diff --git a/pkg/sqlcmd/azure_auth.go b/pkg/sqlcmd/azure_auth.go index 5d924390..f554a181 100644 --- a/pkg/sqlcmd/azure_auth.go +++ b/pkg/sqlcmd/azure_auth.go @@ -1,55 +1,55 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "database/sql/driver" - "fmt" - "net/url" - "os" - - "github.com/microsoft/go-mssqldb/azuread" -) - -const ( - NotSpecified = "NotSpecified" - SqlPassword = "SqlPassword" - sqlClientId = "a94f9c62-97fe-4d19-b06d-472bed8d2bcf" -) - -func getSqlClientId() string { - if clientId := os.Getenv("SQLCMDCLIENTID"); clientId != "" { - return clientId - } - return sqlClientId -} - -func GetTokenBasedConnection(connstr string, authenticationMethod string) (driver.Connector, error) { - - connectionUrl, err := url.Parse(connstr) - if err != nil { - return nil, err - } - - query := connectionUrl.Query() - query.Set("fedauth", authenticationMethod) - query.Set("applicationclientid", getSqlClientId()) - switch authenticationMethod { - case azuread.ActiveDirectoryServicePrincipal, azuread.ActiveDirectoryApplication: - query.Set("clientcertpath", os.Getenv("AZURE_CLIENT_CERTIFICATE_PATH")) - case azuread.ActiveDirectoryInteractive: - loginTimeout := query.Get("connection timeout") - loginTimeoutSeconds := 0 - if loginTimeout != "" { - _, _ = fmt.Sscanf(loginTimeout, "%d", &loginTimeoutSeconds) - } - // AAD interactive needs minutes at minimum - if loginTimeoutSeconds > 0 && loginTimeoutSeconds < 120 { - query.Set("connection timeout", "120") - } - } - - connectionUrl.RawQuery = query.Encode() - return azuread.NewConnector(connectionUrl.String()) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "database/sql/driver" + "fmt" + "net/url" + "os" + + "github.com/microsoft/go-mssqldb/azuread" +) + +const ( + NotSpecified = "NotSpecified" + SqlPassword = "SqlPassword" + sqlClientId = "a94f9c62-97fe-4d19-b06d-472bed8d2bcf" +) + +func getSqlClientId() string { + if clientId := os.Getenv("SQLCMDCLIENTID"); clientId != "" { + return clientId + } + return sqlClientId +} + +func GetTokenBasedConnection(connstr string, authenticationMethod string) (driver.Connector, error) { + + connectionUrl, err := url.Parse(connstr) + if err != nil { + return nil, err + } + + query := connectionUrl.Query() + query.Set("fedauth", authenticationMethod) + query.Set("applicationclientid", getSqlClientId()) + switch authenticationMethod { + case azuread.ActiveDirectoryServicePrincipal, azuread.ActiveDirectoryApplication: + query.Set("clientcertpath", os.Getenv("AZURE_CLIENT_CERTIFICATE_PATH")) + case azuread.ActiveDirectoryInteractive: + loginTimeout := query.Get("connection timeout") + loginTimeoutSeconds := 0 + if loginTimeout != "" { + _, _ = fmt.Sscanf(loginTimeout, "%d", &loginTimeoutSeconds) + } + // AAD interactive needs minutes at minimum + if loginTimeoutSeconds > 0 && loginTimeoutSeconds < 120 { + query.Set("connection timeout", "120") + } + } + + connectionUrl.RawQuery = query.Encode() + return azuread.NewConnector(connectionUrl.String()) +} diff --git a/pkg/sqlcmd/batch.go b/pkg/sqlcmd/batch.go index 9afaf812..7b8082e5 100644 --- a/pkg/sqlcmd/batch.go +++ b/pkg/sqlcmd/batch.go @@ -1,263 +1,263 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -const minCapIncrease = 512 - -// lineend is the slice to use when appending a line. -var lineend = []rune(SqlcmdEol) - -// Batch provides the query text to run -type Batch struct { - // read provides the next chunk of runes - read batchScan - // Buffer is the current batch text - Buffer []rune - // Length is the length of the statement - Length int - // raw is the unprocessed runes - raw []rune - // rawlen is the number of unprocessed runes - rawlen int - // quote indicates currently processing a quoted string - quote rune - // comment is the state of multi-line comment processing - comment bool - // batchline is the 1-based index of the next line. - // Used for the prompt in interactive mode - batchline int - // linecount is the total number of batch lines processed in the session - linecount uint - // varmap tracks the location of expandable variables for the entire batch - varmap map[int]string - // linevarmap tracks the location of expandable variables on the current line - linevarmap map[int]string - // cmd is the set of Commands available - cmd Commands -} - -type batchScan func() (string, error) - -// NewBatch creates a Batch which converts runes provided by reader into SQL batches -func NewBatch(reader batchScan, cmd Commands) *Batch { - b := &Batch{ - read: reader, - cmd: cmd, - } - b.Reset(nil) - return b -} - -// String returns the current SQL batch text -func (b *Batch) String() string { - return string(b.Buffer) -} - -// Reset clears the current batch text and replaces it with new runes -func (b *Batch) Reset(r []rune) { - b.Buffer, b.Length = nil, 0 - b.quote = 0 - b.comment = false - b.batchline = 1 - if r != nil { - b.raw, b.rawlen = r, len(r) - } else { - b.rawlen = 0 - } - b.varmap = make(map[int]string) -} - -// Next processes the next chunk of input and sets the Batch state accordingly. -// If the input contains a command to run, Next returns the Command and its -// parameters. -// Upon exit from Next, the caller can use the State method to determine if -// it represents a runnable SQL batch text. -func (b *Batch) Next() (*Command, []string, error) { - b.linevarmap = nil - var err error - var i int - if b.rawlen == 0 { - s, err := b.read() - if err != nil { - return nil, nil, err - } - b.raw = []rune(s) - b.rawlen = len(b.raw) - } - - var command *Command - var args []string - var ok bool - var scannedCommand bool - b.linecount++ -parse: - for ; i < b.rawlen; i++ { - c, next := b.raw[i], grab(b.raw, i+1, b.rawlen) - switch { - // we're in a quoted string - case b.quote != 0: - i, ok, err = b.readString(b.raw, i, b.rawlen, b.quote, b.linecount) - if err != nil { - break parse - } - if ok { - b.quote = 0 - } - // inside a multiline comment - case b.comment: - i, ok = readMultilineComment(b.raw, i, b.rawlen) - b.comment = !ok - // start of a string - case c == '\'' || c == '"': - b.quote = c - // inline sql comment, skip to end of line - case c == '-' && next == '-': - i = b.rawlen - // start a multi-line comment - case c == '/' && next == '*': - b.comment = true - i++ - // continue processing quoted string or multiline comment - case b.quote != 0 || b.comment: - - // Handle variable references - case c == '$' && next == '(': - vi, ok := readVariableReference(b.raw, i+2, b.rawlen) - if ok { - b.addVariableLocation(i, string(b.raw[i+2:vi])) - i = vi - - } else { - err = syntaxError(b.linecount) - break parse - } - // Commands have to be alone on the line - case !scannedCommand && b.cmd != nil: - var cend int - scannedCommand = true - command, args, cend = readCommand(b.cmd, b.raw, i, b.rawlen) - if command != nil { - // remove the command from raw - b.raw = append(b.raw[:i], b.raw[cend:]...) - break parse - } - } - } - if err == nil { - i = min(i, b.rawlen) - empty := isEmptyLine(b.raw, 0, i) - appendLine := true - if !b.comment && command != nil && empty { - appendLine = false - } - if appendLine { - // any variables on the line need to be added to the global map - inc := 0 - if b.Length > 0 { - inc = len(lineend) - } - if b.linevarmap != nil { - for v := range b.linevarmap { - b.varmap[v+b.Length+inc] = b.linevarmap[v] - } - } - // log.Printf(">> appending: `%s`", string(r[st:i])) - b.append(b.raw[:i], lineend) - b.batchline++ - } - b.raw = b.raw[i:] - b.rawlen = len(b.raw) - } else { - b.Reset(nil) - } - return command, args, err -} - -// append appends r to b.Buffer separated by sep when b.Buffer is not already empty. -// -// Dynamically grows b.Buf as necessary to accommodate r and the separator. -// Specifically, when b.Buf is not empty, b.Buf will grow by increments of -// MinCapIncrease. -// -// After a call to append, b.Len will be len(b.Buf)+len(sep)+len(r). Call Reset -// to reset the Buf. -func (b *Batch) append(r, sep []rune) { - rlen := len(r) - // initial - if b.Buffer == nil { - b.Buffer, b.Length = r, rlen - return - } - blen, seplen := b.Length, len(sep) - tlen := blen + rlen + seplen - // grow - if bcap := cap(b.Buffer); tlen > bcap { - n := tlen + 2*rlen - n += minCapIncrease - (n % minCapIncrease) - z := make([]rune, blen, n) - copy(z, b.Buffer) - b.Buffer = z - } - b.Buffer = b.Buffer[:tlen] - copy(b.Buffer[blen:], sep) - copy(b.Buffer[blen+seplen:], r) - b.Length = tlen -} - -// State returns a string representing the state of statement parsing. -// * Is in the middle of a multi-line comment -// - Has a non-empty batch ready to run -// = Is empty -// ' " Is in the middle of a multi-line quoted string -func (b *Batch) State() string { - switch { - case b.quote != 0: - return string(b.quote) - case b.comment: - return "*" - case b.Length != 0: - return "-" - } - return "=" -} - -// readString seeks to the end of a string returning the position and whether -// or not the string's end was found. -// -// If the string's terminator was not found, then the result will be the passed -// end. -// An error is returned if the string contains a malformed variable reference -func (b *Batch) readString(r []rune, i, end int, quote rune, line uint) (int, bool, error) { - var prev, c, next rune - for ; i < end; i++ { - c, next = r[i], grab(r, i+1, end) - switch { - case c == '$' && next == '(': - vl, ok := readVariableReference(r, i+2, end) - if ok { - b.addVariableLocation(i, string(r[i+2:vl])) - i = vl - - } else { - return i, false, syntaxError(line) - } - case quote == '\'' && c == '\'' && next == '\'': - i++ - continue - case quote == '\'' && c == '\'' && prev != '\'', - quote == '"' && c == '"': - return i, true, nil - } - prev = c - } - return end, false, nil -} - -// addVariableLocation is called for each variable on the current line -func (b *Batch) addVariableLocation(i int, v string) { - if b.linevarmap == nil { - b.linevarmap = make(map[int]string) - } - b.linevarmap[i] = v -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +const minCapIncrease = 512 + +// lineend is the slice to use when appending a line. +var lineend = []rune(SqlcmdEol) + +// Batch provides the query text to run +type Batch struct { + // read provides the next chunk of runes + read batchScan + // Buffer is the current batch text + Buffer []rune + // Length is the length of the statement + Length int + // raw is the unprocessed runes + raw []rune + // rawlen is the number of unprocessed runes + rawlen int + // quote indicates currently processing a quoted string + quote rune + // comment is the state of multi-line comment processing + comment bool + // batchline is the 1-based index of the next line. + // Used for the prompt in interactive mode + batchline int + // linecount is the total number of batch lines processed in the session + linecount uint + // varmap tracks the location of expandable variables for the entire batch + varmap map[int]string + // linevarmap tracks the location of expandable variables on the current line + linevarmap map[int]string + // cmd is the set of Commands available + cmd Commands +} + +type batchScan func() (string, error) + +// NewBatch creates a Batch which converts runes provided by reader into SQL batches +func NewBatch(reader batchScan, cmd Commands) *Batch { + b := &Batch{ + read: reader, + cmd: cmd, + } + b.Reset(nil) + return b +} + +// String returns the current SQL batch text +func (b *Batch) String() string { + return string(b.Buffer) +} + +// Reset clears the current batch text and replaces it with new runes +func (b *Batch) Reset(r []rune) { + b.Buffer, b.Length = nil, 0 + b.quote = 0 + b.comment = false + b.batchline = 1 + if r != nil { + b.raw, b.rawlen = r, len(r) + } else { + b.rawlen = 0 + } + b.varmap = make(map[int]string) +} + +// Next processes the next chunk of input and sets the Batch state accordingly. +// If the input contains a command to run, Next returns the Command and its +// parameters. +// Upon exit from Next, the caller can use the State method to determine if +// it represents a runnable SQL batch text. +func (b *Batch) Next() (*Command, []string, error) { + b.linevarmap = nil + var err error + var i int + if b.rawlen == 0 { + s, err := b.read() + if err != nil { + return nil, nil, err + } + b.raw = []rune(s) + b.rawlen = len(b.raw) + } + + var command *Command + var args []string + var ok bool + var scannedCommand bool + b.linecount++ +parse: + for ; i < b.rawlen; i++ { + c, next := b.raw[i], grab(b.raw, i+1, b.rawlen) + switch { + // we're in a quoted string + case b.quote != 0: + i, ok, err = b.readString(b.raw, i, b.rawlen, b.quote, b.linecount) + if err != nil { + break parse + } + if ok { + b.quote = 0 + } + // inside a multiline comment + case b.comment: + i, ok = readMultilineComment(b.raw, i, b.rawlen) + b.comment = !ok + // start of a string + case c == '\'' || c == '"': + b.quote = c + // inline sql comment, skip to end of line + case c == '-' && next == '-': + i = b.rawlen + // start a multi-line comment + case c == '/' && next == '*': + b.comment = true + i++ + // continue processing quoted string or multiline comment + case b.quote != 0 || b.comment: + + // Handle variable references + case c == '$' && next == '(': + vi, ok := readVariableReference(b.raw, i+2, b.rawlen) + if ok { + b.addVariableLocation(i, string(b.raw[i+2:vi])) + i = vi + + } else { + err = syntaxError(b.linecount) + break parse + } + // Commands have to be alone on the line + case !scannedCommand && b.cmd != nil: + var cend int + scannedCommand = true + command, args, cend = readCommand(b.cmd, b.raw, i, b.rawlen) + if command != nil { + // remove the command from raw + b.raw = append(b.raw[:i], b.raw[cend:]...) + break parse + } + } + } + if err == nil { + i = min(i, b.rawlen) + empty := isEmptyLine(b.raw, 0, i) + appendLine := true + if !b.comment && command != nil && empty { + appendLine = false + } + if appendLine { + // any variables on the line need to be added to the global map + inc := 0 + if b.Length > 0 { + inc = len(lineend) + } + if b.linevarmap != nil { + for v := range b.linevarmap { + b.varmap[v+b.Length+inc] = b.linevarmap[v] + } + } + // log.Printf(">> appending: `%s`", string(r[st:i])) + b.append(b.raw[:i], lineend) + b.batchline++ + } + b.raw = b.raw[i:] + b.rawlen = len(b.raw) + } else { + b.Reset(nil) + } + return command, args, err +} + +// append appends r to b.Buffer separated by sep when b.Buffer is not already empty. +// +// Dynamically grows b.Buf as necessary to accommodate r and the separator. +// Specifically, when b.Buf is not empty, b.Buf will grow by increments of +// MinCapIncrease. +// +// After a call to append, b.Len will be len(b.Buf)+len(sep)+len(r). Call Reset +// to reset the Buf. +func (b *Batch) append(r, sep []rune) { + rlen := len(r) + // initial + if b.Buffer == nil { + b.Buffer, b.Length = r, rlen + return + } + blen, seplen := b.Length, len(sep) + tlen := blen + rlen + seplen + // grow + if bcap := cap(b.Buffer); tlen > bcap { + n := tlen + 2*rlen + n += minCapIncrease - (n % minCapIncrease) + z := make([]rune, blen, n) + copy(z, b.Buffer) + b.Buffer = z + } + b.Buffer = b.Buffer[:tlen] + copy(b.Buffer[blen:], sep) + copy(b.Buffer[blen+seplen:], r) + b.Length = tlen +} + +// State returns a string representing the state of statement parsing. +// * Is in the middle of a multi-line comment +// - Has a non-empty batch ready to run +// = Is empty +// ' " Is in the middle of a multi-line quoted string +func (b *Batch) State() string { + switch { + case b.quote != 0: + return string(b.quote) + case b.comment: + return "*" + case b.Length != 0: + return "-" + } + return "=" +} + +// readString seeks to the end of a string returning the position and whether +// or not the string's end was found. +// +// If the string's terminator was not found, then the result will be the passed +// end. +// An error is returned if the string contains a malformed variable reference +func (b *Batch) readString(r []rune, i, end int, quote rune, line uint) (int, bool, error) { + var prev, c, next rune + for ; i < end; i++ { + c, next = r[i], grab(r, i+1, end) + switch { + case c == '$' && next == '(': + vl, ok := readVariableReference(r, i+2, end) + if ok { + b.addVariableLocation(i, string(r[i+2:vl])) + i = vl + + } else { + return i, false, syntaxError(line) + } + case quote == '\'' && c == '\'' && next == '\'': + i++ + continue + case quote == '\'' && c == '\'' && prev != '\'', + quote == '"' && c == '"': + return i, true, nil + } + prev = c + } + return end, false, nil +} + +// addVariableLocation is called for each variable on the current line +func (b *Batch) addVariableLocation(i int, v string) { + if b.linevarmap == nil { + b.linevarmap = make(map[int]string) + } + b.linevarmap[i] = v +} diff --git a/pkg/sqlcmd/batch_test.go b/pkg/sqlcmd/batch_test.go index 4c4ddd8f..a00c85f7 100644 --- a/pkg/sqlcmd/batch_test.go +++ b/pkg/sqlcmd/batch_test.go @@ -1,223 +1,223 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "io" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBatchNext(t *testing.T) { - tests := []struct { - s string - stmts []string - cmds []string - state string - }{ - {"", nil, nil, "="}, - {"select 1", []string{"select 1"}, nil, "-"}, - {"select $(x)\nquit", []string{"select $(x)"}, []string{"QUIT"}, "-"}, - {"select '$ (X' \nquite", []string{"select '$ (X' " + SqlcmdEol + "quite"}, nil, "-"}, - {":list\n:reset\n", nil, []string{"LIST", "RESET"}, "="}, - {"select 1\n:list\nselect 2", []string{"select 1" + SqlcmdEol + "select 2"}, []string{"LIST"}, "-"}, - {"select '1\n", []string{"select '1" + SqlcmdEol + ""}, nil, "'"}, - {"select 1 /* comment\nGO", []string{"select 1 /* comment" + SqlcmdEol + "GO"}, nil, "*"}, - {"select '1\n00' \n/* comm\nent*/\nGO 4", []string{"select '1" + SqlcmdEol + "00' " + SqlcmdEol + "/* comm" + SqlcmdEol + "ent*/"}, []string{"GO"}, "-"}, - {"$(x) $(y) 100\nquit", []string{"$(x) $(y) 100"}, []string{"QUIT"}, "-"}, - {"select 1\n:list", []string{"select 1"}, []string{"LIST"}, "-"}, - {"select 1\n:reset", []string{"select 1"}, []string{"RESET"}, "-"}, - {"select 1\n:exit()", []string{"select 1"}, []string{"EXIT"}, "-"}, - {"select 1\n:exit (select 10)", []string{"select 1"}, []string{"EXIT"}, "-"}, - {"select 1\n:exit", []string{"select 1"}, []string{"EXIT"}, "-"}, - } - for _, test := range tests { - b := NewBatch(sp(test.s, "\n"), newCommands()) - var stmts, cmds []string - loop: - for { - cmd, _, err := b.Next() - switch { - case err == io.EOF: - // if we get EOF before a command we will try to run - // whatever is in the buffer - if s := b.String(); s != "" { - stmts = append(stmts, s) - } - break loop - case err != nil: - t.Fatalf("test %s did not expect error, got: %v", test.s, err) - } - if cmd != nil { - cmds = append(cmds, cmd.name) - } - } - assert.Equal(t, test.stmts, stmts, "Statements for %s", test.s) - assert.Equal(t, test.state, b.State(), "State for %s", test.s) - assert.Equal(t, test.cmds, cmds, "Commands for %s", test.s) - b.Reset(nil) - assert.Zero(t, b.Length, "Length after Reset") - assert.Zero(t, len(b.Buffer), "len(Buffer) after Reset") - assert.Zero(t, b.quote, "quote after Reset") - assert.False(t, b.comment, "comment after Reset") - assert.Equal(t, "=", b.State(), "State() after Reset") - } -} - -func sp(a, sep string) func() (string, error) { - s := strings.Split(a, sep) - return func() (string, error) { - if len(s) > 0 { - z := s[0] - s = s[1:] - return z, nil - } - return "", io.EOF - } -} - -func TestBatchNextErrOnInvalidVariable(t *testing.T) { - tests := []string{ - "select $(x", - "$((x", - "alter $( x)", - } - for _, test := range tests { - b := NewBatch(sp(test, "\n"), newCommands()) - cmd, _, err := b.Next() - assert.Nil(t, cmd, "cmd for "+test) - assert.Equal(t, uint(1), b.linecount, "linecount should increment on a variable syntax error") - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1.", "expected err for %s", test) - } -} - -func TestReadString(t *testing.T) { - tests := []struct { - // input string - s string - // index to start inside s - i int - // expected return string - exp string - // expected return bool - ok bool - }{ - {`'`, 0, ``, false}, - {` '`, 1, ``, false}, - {`'str' `, 0, `'str'`, true}, - {` 'str' `, 1, `'str'`, true}, - {`"str"`, 0, `"str"`, true}, - {`'str''str'`, 0, `'str''str'`, true}, - {` 'str''str' `, 1, `'str''str'`, true}, - {` "str''str" `, 1, `"str''str"`, true}, - // escaped \" aren't allowed in strings, so the second " would be next - // double quoted string - {`"str\""`, 0, `"str\"`, true}, - {` "str\"" `, 1, `"str\"`, true}, - {`'str\'`, 0, `'str\'`, true}, - {`''''`, 0, `''''`, true}, - {` '''' `, 1, `''''`, true}, - {`''''''`, 0, `''''''`, true}, - {` '''''' `, 1, `''''''`, true}, - {`'''`, 0, ``, false}, - {` ''' `, 1, ``, false}, - {`'''''`, 0, ``, false}, - {` ''''' `, 1, ``, false}, - {`"st'r"`, 0, `"st'r"`, true}, - {` "st'r" `, 1, `"st'r"`, true}, - {`"st''r"`, 0, `"st''r"`, true}, - {` "st''r" `, 1, `"st''r"`, true}, - {`'$(v)'`, 0, `'$(v)'`, true}, - {`'var $(var1) var2 $(var2)'`, 0, `'var $(var1) var2 $(var2)'`, true}, - {`'var $(var1) $`, 0, `'var $(var1) $`, false}, - } - b := NewBatch(nil, newCommands()) - - for _, test := range tests { - r := []rune(test.s) - c, end := rune(strings.TrimSpace(test.s)[0]), len(r) - if c != '\'' && c != '"' { - t.Fatalf("test %+v incorrect!", test) - } - pos, ok, err := b.readString(r, test.i+1, end, c, uint(0)) - assert.NoErrorf(t, err, "should be no error for %s", test) - assert.Equal(t, test.ok, ok, "test %+v ok", test) - if !ok { - continue - } - assert.Equal(t, c, r[pos], "test %+v last character") - v := string(r[test.i : pos+1]) - assert.Equal(t, test.exp, v, "test %+v returned string", test) - } -} - -func TestReadStringMalformedVariable(t *testing.T) { - tests := []string{ - "'select $(x'", - "' $((x'", - "'alter $( x)", - } - b := NewBatch(nil, newCommands()) - for _, test := range tests { - r := []rune(test) - _, ok, err := b.readString(r, 1, len(test), '\'', 10) - assert.Falsef(t, ok, "ok for %s", test) - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10.", "expected err for %s", test) - } -} - -func TestReadStringVarmap(t *testing.T) { - type mapTest struct { - s string - m map[int]string - } - tests := []mapTest{ - {`'var $(var1) var2 $(var2)'`, map[int]string{5: "var1", 18: "var2"}}, - {`'var $(va_1) var2 $(va-2)'`, map[int]string{5: "va_1", 18: "va-2"}}, - } - for _, test := range tests { - b := NewBatch(nil, newCommands()) - b.linevarmap = make(map[int]string) - i, ok, err := b.readString([]rune(test.s), 1, len(test.s), '\'', 0) - assert.Truef(t, ok, "ok returned by readString for %s", test.s) - assert.NoErrorf(t, err, "readString for %s", test.s) - assert.Equal(t, len(test.s)-1, i, "index returned by readString for %s", test.s) - assert.Equalf(t, test.m, b.linevarmap, "linevarmap after readString %s", test.s) - } -} - -func TestBatchNextVarMap(t *testing.T) { - type mapTest struct { - s string - m map[int]string - } - tests := []mapTest{ - {"'var $(var1)\nvar2 $(var2)\n'", map[int]string{5: "var1", 17 + len(SqlcmdEol): "var2"}}, - {"$(var1) select $(var2)\nselect 100\nselect '$(var3)'", map[int]string{ - 0: "var1", - 15: "var2", - 40 + 2*len(SqlcmdEol): "var3"}, - }, - } -loop: - for _, test := range tests { - var err error - b := NewBatch(sp(test.s, "\n"), newCommands()) - for { - _, _, err = b.Next() - if err == io.EOF { - assert.Equalf(t, test.m, b.varmap, "varmap after Next %s. Batch:%s", test.s, escapeeol(b.String())) - break loop - } else { - assert.NoErrorf(t, err, "Should have no error from Next") - } - } - } -} - -func escapeeol(s string) string { - return strings.Replace(strings.Replace(s, "\n", `\n`, -1), "\r", `\r`, -1) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBatchNext(t *testing.T) { + tests := []struct { + s string + stmts []string + cmds []string + state string + }{ + {"", nil, nil, "="}, + {"select 1", []string{"select 1"}, nil, "-"}, + {"select $(x)\nquit", []string{"select $(x)"}, []string{"QUIT"}, "-"}, + {"select '$ (X' \nquite", []string{"select '$ (X' " + SqlcmdEol + "quite"}, nil, "-"}, + {":list\n:reset\n", nil, []string{"LIST", "RESET"}, "="}, + {"select 1\n:list\nselect 2", []string{"select 1" + SqlcmdEol + "select 2"}, []string{"LIST"}, "-"}, + {"select '1\n", []string{"select '1" + SqlcmdEol + ""}, nil, "'"}, + {"select 1 /* comment\nGO", []string{"select 1 /* comment" + SqlcmdEol + "GO"}, nil, "*"}, + {"select '1\n00' \n/* comm\nent*/\nGO 4", []string{"select '1" + SqlcmdEol + "00' " + SqlcmdEol + "/* comm" + SqlcmdEol + "ent*/"}, []string{"GO"}, "-"}, + {"$(x) $(y) 100\nquit", []string{"$(x) $(y) 100"}, []string{"QUIT"}, "-"}, + {"select 1\n:list", []string{"select 1"}, []string{"LIST"}, "-"}, + {"select 1\n:reset", []string{"select 1"}, []string{"RESET"}, "-"}, + {"select 1\n:exit()", []string{"select 1"}, []string{"EXIT"}, "-"}, + {"select 1\n:exit (select 10)", []string{"select 1"}, []string{"EXIT"}, "-"}, + {"select 1\n:exit", []string{"select 1"}, []string{"EXIT"}, "-"}, + } + for _, test := range tests { + b := NewBatch(sp(test.s, "\n"), newCommands()) + var stmts, cmds []string + loop: + for { + cmd, _, err := b.Next() + switch { + case err == io.EOF: + // if we get EOF before a command we will try to run + // whatever is in the buffer + if s := b.String(); s != "" { + stmts = append(stmts, s) + } + break loop + case err != nil: + t.Fatalf("test %s did not expect error, got: %v", test.s, err) + } + if cmd != nil { + cmds = append(cmds, cmd.name) + } + } + assert.Equal(t, test.stmts, stmts, "Statements for %s", test.s) + assert.Equal(t, test.state, b.State(), "State for %s", test.s) + assert.Equal(t, test.cmds, cmds, "Commands for %s", test.s) + b.Reset(nil) + assert.Zero(t, b.Length, "Length after Reset") + assert.Zero(t, len(b.Buffer), "len(Buffer) after Reset") + assert.Zero(t, b.quote, "quote after Reset") + assert.False(t, b.comment, "comment after Reset") + assert.Equal(t, "=", b.State(), "State() after Reset") + } +} + +func sp(a, sep string) func() (string, error) { + s := strings.Split(a, sep) + return func() (string, error) { + if len(s) > 0 { + z := s[0] + s = s[1:] + return z, nil + } + return "", io.EOF + } +} + +func TestBatchNextErrOnInvalidVariable(t *testing.T) { + tests := []string{ + "select $(x", + "$((x", + "alter $( x)", + } + for _, test := range tests { + b := NewBatch(sp(test, "\n"), newCommands()) + cmd, _, err := b.Next() + assert.Nil(t, cmd, "cmd for "+test) + assert.Equal(t, uint(1), b.linecount, "linecount should increment on a variable syntax error") + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1.", "expected err for %s", test) + } +} + +func TestReadString(t *testing.T) { + tests := []struct { + // input string + s string + // index to start inside s + i int + // expected return string + exp string + // expected return bool + ok bool + }{ + {`'`, 0, ``, false}, + {` '`, 1, ``, false}, + {`'str' `, 0, `'str'`, true}, + {` 'str' `, 1, `'str'`, true}, + {`"str"`, 0, `"str"`, true}, + {`'str''str'`, 0, `'str''str'`, true}, + {` 'str''str' `, 1, `'str''str'`, true}, + {` "str''str" `, 1, `"str''str"`, true}, + // escaped \" aren't allowed in strings, so the second " would be next + // double quoted string + {`"str\""`, 0, `"str\"`, true}, + {` "str\"" `, 1, `"str\"`, true}, + {`'str\'`, 0, `'str\'`, true}, + {`''''`, 0, `''''`, true}, + {` '''' `, 1, `''''`, true}, + {`''''''`, 0, `''''''`, true}, + {` '''''' `, 1, `''''''`, true}, + {`'''`, 0, ``, false}, + {` ''' `, 1, ``, false}, + {`'''''`, 0, ``, false}, + {` ''''' `, 1, ``, false}, + {`"st'r"`, 0, `"st'r"`, true}, + {` "st'r" `, 1, `"st'r"`, true}, + {`"st''r"`, 0, `"st''r"`, true}, + {` "st''r" `, 1, `"st''r"`, true}, + {`'$(v)'`, 0, `'$(v)'`, true}, + {`'var $(var1) var2 $(var2)'`, 0, `'var $(var1) var2 $(var2)'`, true}, + {`'var $(var1) $`, 0, `'var $(var1) $`, false}, + } + b := NewBatch(nil, newCommands()) + + for _, test := range tests { + r := []rune(test.s) + c, end := rune(strings.TrimSpace(test.s)[0]), len(r) + if c != '\'' && c != '"' { + t.Fatalf("test %+v incorrect!", test) + } + pos, ok, err := b.readString(r, test.i+1, end, c, uint(0)) + assert.NoErrorf(t, err, "should be no error for %s", test) + assert.Equal(t, test.ok, ok, "test %+v ok", test) + if !ok { + continue + } + assert.Equal(t, c, r[pos], "test %+v last character") + v := string(r[test.i : pos+1]) + assert.Equal(t, test.exp, v, "test %+v returned string", test) + } +} + +func TestReadStringMalformedVariable(t *testing.T) { + tests := []string{ + "'select $(x'", + "' $((x'", + "'alter $( x)", + } + b := NewBatch(nil, newCommands()) + for _, test := range tests { + r := []rune(test) + _, ok, err := b.readString(r, 1, len(test), '\'', 10) + assert.Falsef(t, ok, "ok for %s", test) + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10.", "expected err for %s", test) + } +} + +func TestReadStringVarmap(t *testing.T) { + type mapTest struct { + s string + m map[int]string + } + tests := []mapTest{ + {`'var $(var1) var2 $(var2)'`, map[int]string{5: "var1", 18: "var2"}}, + {`'var $(va_1) var2 $(va-2)'`, map[int]string{5: "va_1", 18: "va-2"}}, + } + for _, test := range tests { + b := NewBatch(nil, newCommands()) + b.linevarmap = make(map[int]string) + i, ok, err := b.readString([]rune(test.s), 1, len(test.s), '\'', 0) + assert.Truef(t, ok, "ok returned by readString for %s", test.s) + assert.NoErrorf(t, err, "readString for %s", test.s) + assert.Equal(t, len(test.s)-1, i, "index returned by readString for %s", test.s) + assert.Equalf(t, test.m, b.linevarmap, "linevarmap after readString %s", test.s) + } +} + +func TestBatchNextVarMap(t *testing.T) { + type mapTest struct { + s string + m map[int]string + } + tests := []mapTest{ + {"'var $(var1)\nvar2 $(var2)\n'", map[int]string{5: "var1", 17 + len(SqlcmdEol): "var2"}}, + {"$(var1) select $(var2)\nselect 100\nselect '$(var3)'", map[int]string{ + 0: "var1", + 15: "var2", + 40 + 2*len(SqlcmdEol): "var3"}, + }, + } +loop: + for _, test := range tests { + var err error + b := NewBatch(sp(test.s, "\n"), newCommands()) + for { + _, _, err = b.Next() + if err == io.EOF { + assert.Equalf(t, test.m, b.varmap, "varmap after Next %s. Batch:%s", test.s, escapeeol(b.String())) + break loop + } else { + assert.NoErrorf(t, err, "Should have no error from Next") + } + } + } +} + +func escapeeol(s string) string { + return strings.Replace(strings.Replace(s, "\n", `\n`, -1), "\r", `\r`, -1) +} diff --git a/pkg/sqlcmd/commands.go b/pkg/sqlcmd/commands.go index 2a4c1b5b..9d2b3926 100644 --- a/pkg/sqlcmd/commands.go +++ b/pkg/sqlcmd/commands.go @@ -1,507 +1,507 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "fmt" - "os" - "regexp" - "sort" - "strconv" - "strings" - - "github.com/alecthomas/kong" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" -) - -// Command defines a sqlcmd action which can be intermixed with the SQL batch -// Commands for sqlcmd are defined at https://docs.microsoft.com/sql/tools/sqlcmd-utility#sqlcmd-commands -type Command struct { - // regex must include at least one group if it has parameters - // Will be matched using FindStringSubmatch - regex *regexp.Regexp - // The function that implements the command. Third parameter is the line number - action func(*Sqlcmd, []string, uint) error - // Name of the command - name string - // whether the command is a system command - isSystem bool -} - -// Commands is the set of sqlcmd command implementations -type Commands map[string]*Command - -func newCommands() Commands { - // Commands is the set of Command implementations - return map[string]*Command{ - "EXIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?EXIT(?:[ \t]*(\(?.*\)?$)|$)`), - action: exitCommand, - name: "EXIT", - }, - "QUIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?QUIT(?:[ \t]+(.*$)|$)`), - action: quitCommand, - name: "QUIT", - }, - "GO": { - regex: regexp.MustCompile(batchTerminatorRegex("GO")), - action: goCommand, - name: "GO", - }, - "OUT": { - regex: regexp.MustCompile(`(?im)^[ \t]*:OUT(?:[ \t]+(.*$)|$)`), - action: outCommand, - name: "OUT", - }, - "ERROR": { - regex: regexp.MustCompile(`(?im)^[ \t]*:ERROR(?:[ \t]+(.*$)|$)`), - action: errorCommand, - name: "ERROR", - }, "READFILE": { - regex: regexp.MustCompile(`(?im)^[ \t]*:R(?:[ \t]+(.*$)|$)`), - action: readFileCommand, - name: "READFILE", - }, - "SETVAR": { - regex: regexp.MustCompile(`(?im)^[ \t]*:SETVAR(?:[ \t]+(.*$)|$)`), - action: setVarCommand, - name: "SETVAR", - }, - "LISTVAR": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:LISTVAR(?:[ \t]+(.*$)|$)`), - action: listVarCommand, - name: "LISTVAR", - }, - "RESET": { - regex: regexp.MustCompile(`(?im)^[ \t]*:RESET(?:[ \t]+(.*$)|$)`), - action: resetCommand, - name: "RESET", - }, - "LIST": { - regex: regexp.MustCompile(`(?im)^[ \t]*:LIST(?:[ \t]+(.*$)|$)`), - action: listCommand, - name: "LIST", - }, - "CONNECT": { - regex: regexp.MustCompile(`(?im)^[ \t]*:CONNECT(?:[ \t]+(.*$)|$)`), - action: connectCommand, - name: "CONNECT", - }, - "EXEC": { - regex: regexp.MustCompile(`(?im)^[ \t]*?:?!!(.*$)`), - action: execCommand, - name: "EXEC", - isSystem: true, - }, - "EDIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?ED(?:[ \t]+(.*$)|$)`), - action: editCommand, - name: "EDIT", - isSystem: true, - }, - } -} - -// DisableSysCommands disables the ED and :!! commands. -// When exitOnCall is true, running those commands will exit the process. -func (c Commands) DisableSysCommands(exitOnCall bool) { - f := warnDisabled - if exitOnCall { - f = errorDisabled - } - for _, cmd := range c { - if cmd.isSystem { - cmd.action = f - } - } -} - -func (c Commands) matchCommand(line string) (*Command, []string) { - for _, cmd := range c { - matchedCommand := cmd.regex.FindStringSubmatch(line) - if matchedCommand != nil { - return cmd, matchedCommand[1:] - } - } - return nil, nil -} - -func warnDisabled(s *Sqlcmd, args []string, line uint) error { - s.WriteError(s.GetError(), ErrCommandsDisabled) - return nil -} - -func errorDisabled(s *Sqlcmd, args []string, line uint) error { - s.WriteError(s.GetError(), ErrCommandsDisabled) - s.Exitcode = 1 - return ErrExitRequested -} - -func batchTerminatorRegex(terminator string) string { - return fmt.Sprintf(`(?im)^[\t ]*?%s(?:[ ]+(.*$)|$)`, regexp.QuoteMeta(terminator)) -} - -// SetBatchTerminator attempts to set the batch terminator to the given value -// Returns an error if the new value is not usable in the regex -func (c Commands) SetBatchTerminator(terminator string) error { - cmd := c["GO"] - regex, err := regexp.Compile(batchTerminatorRegex(terminator)) - if err != nil { - return err - } - cmd.regex = regex - return nil -} - -// exitCommand has 3 modes. -// With no (), it just exits without running any query -// With () it runs whatever batch is in the buffer then exits -// With any text between () it runs the text as a query then exits -func exitCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 { - return ErrExitRequested - } - params := strings.TrimSpace(args[0]) - if params == "" { - return ErrExitRequested - } - if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") { - return InvalidCommandError("EXIT", line) - } - // First we run the current batch - query := s.batch.String() - if query != "" { - query = s.getRunnableQuery(query) - if exitCode, err := s.runQuery(query); err != nil { - s.Exitcode = exitCode - return ErrExitRequested - } - } - query = strings.TrimSpace(params[1 : len(params)-1]) - s.batch.Reset([]rune(query)) - _, _, err := s.batch.Next() - if err != nil { - return err - } - query = s.batch.String() - if s.batch.String() != "" { - query = s.getRunnableQuery(query) - s.Exitcode, _ = s.runQuery(query) - } - return ErrExitRequested -} - -// quitCommand immediately exits the program without running any more batches -func quitCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("QUIT", line) - } - return ErrExitRequested -} - -// goCommand runs the current batch the number of times specified -func goCommand(s *Sqlcmd, args []string, line uint) error { - // default to 1 execution - n := 1 - var err error - if len(args) > 0 { - cnt := strings.TrimSpace(args[0]) - if cnt != "" { - if cnt, err = resolveArgumentVariables(s, []rune(cnt), true); err != nil { - return err - } - _, err = fmt.Sscanf(cnt, "%d", &n) - } - } - if err != nil || n < 1 { - return InvalidCommandError("GO", line) - } - query := s.batch.String() - if query == "" { - return nil - } - query = s.getRunnableQuery(query) - for i := 0; i < n; i++ { - if retcode, err := s.runQuery(query); err != nil { - s.Exitcode = retcode - return err - } - } - s.batch.Reset(nil) - return nil -} - -// outCommand changes the output writer to use a file -func outCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 || args[0] == "" { - return InvalidCommandError("OUT", line) - } - switch { - case strings.EqualFold(args[0], "stdout"): - s.SetOutput(os.Stdout) - case strings.EqualFold(args[0], "stderr"): - s.SetOutput(os.Stderr) - default: - o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return InvalidFileError(err, args[0]) - } - if s.UnicodeOutputFile { - // ODBC sqlcmd doesn't write a BOM but we will. - // Maybe the endian-ness should be configurable. - win16le := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) - encoder := transform.NewWriter(o, win16le.NewEncoder()) - s.SetOutput(encoder) - } else { - s.SetOutput(o) - } - } - return nil -} - -// errorCommand changes the error writer to use a file -func errorCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 || args[0] == "" { - return InvalidCommandError("OUT", line) - } - switch { - case strings.EqualFold(args[0], "stderr"): - s.SetError(os.Stderr) - case strings.EqualFold(args[0], "stdout"): - s.SetError(os.Stdout) - default: - o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return InvalidFileError(err, args[0]) - } - s.SetError(o) - } - return nil -} - -func readFileCommand(s *Sqlcmd, args []string, line uint) error { - if args == nil || len(args) != 1 { - return InvalidCommandError(":R", line) - } - fileName, _ := resolveArgumentVariables(s, []rune(args[0]), false) - return s.IncludeFile(fileName, false) -} - -// setVarCommand parses a variable setting and applies it to the current Sqlcmd variables -func setVarCommand(s *Sqlcmd, args []string, line uint) error { - if args == nil || len(args) != 1 || args[0] == "" { - return InvalidCommandError(":SETVAR", line) - } - - varname := args[0] - val := "" - // The prior incarnation of sqlcmd doesn't require a space between the variable name and its value - // in some very unexpected cases. This version will require the space. - sp := strings.IndexRune(args[0], ' ') - if sp > -1 { - val = strings.TrimSpace(varname[sp:]) - varname = varname[:sp] - } - if err := s.vars.Setvar(varname, val); err != nil { - switch e := err.(type) { - case *VariableError: - return e - default: - return InvalidCommandError(":SETVAR", line) - } - } - return nil -} - -// listVarCommand prints the set of Sqlcmd scripting variables. -// Builtin values are printed first, followed by user-set values in sorted order. -func listVarCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("LISTVAR", line) - } - - vars := s.vars.All() - keys := make([]string, 0, len(vars)) - for k := range vars { - if !contains(builtinVariables, k) { - keys = append(keys, k) - } - } - sort.Strings(keys) - keys = append(builtinVariables, keys...) - for _, k := range keys { - fmt.Fprintf(s.GetOutput(), `%s = "%s"%s`, k, vars[k], SqlcmdEol) - } - return nil -} - -// resetCommand resets the statement cache -func resetCommand(s *Sqlcmd, args []string, line uint) error { - if s.batch != nil { - s.batch.Reset(nil) - } - - return nil -} - -// listCommand displays statements currently in the statement cache -func listCommand(s *Sqlcmd, args []string, line uint) error { - if s.batch != nil && s.batch.String() != "" { - fmt.Fprintf(s.GetOutput(), `%s%s`, []byte(s.batch.String()), SqlcmdEol) - } - - return nil -} - -type connectData struct { - Server string `arg:""` - Database string `short:"D"` - Username string `short:"U"` - Password string `short:"P"` - LoginTimeout string `short:"l"` - AuthenticationMethod string `short:"G"` -} - -func connectCommand(s *Sqlcmd, args []string, line uint) error { - - if len(args) == 0 { - return InvalidCommandError("CONNECT", line) - } - cmdLine := strings.TrimSpace(args[0]) - if cmdLine == "" { - return InvalidCommandError("CONNECT", line) - } - arguments := &connectData{} - parser, err := kong.New(arguments) - if err != nil { - return InvalidCommandError("CONNECT", line) - } - - // Fields removes extra whitespace. - // Note :connect doesn't support passwords with spaces - if _, err = parser.Parse(strings.Fields(cmdLine)); err != nil { - return InvalidCommandError("CONNECT", line) - } - - connect := *s.Connect - connect.UserName, _ = resolveArgumentVariables(s, []rune(arguments.Username), false) - connect.Password, _ = resolveArgumentVariables(s, []rune(arguments.Password), false) - connect.ServerName, _ = resolveArgumentVariables(s, []rune(arguments.Server), false) - timeout, _ := resolveArgumentVariables(s, []rune(arguments.LoginTimeout), false) - if timeout != "" { - if timeoutSeconds, err := strconv.ParseInt(timeout, 10, 32); err == nil { - if timeoutSeconds < 0 { - return InvalidCommandError("CONNECT", line) - } - connect.LoginTimeoutSeconds = int(timeoutSeconds) - } - } - connect.AuthenticationMethod = arguments.AuthenticationMethod - // If no user name is provided we switch to integrated auth - _ = s.ConnectDb(&connect, s.lineIo == nil) - // ConnectDb prints connection errors already, and failure to connect is not fatal even with -b option - return nil -} - -func execCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 { - return InvalidCommandError("EXEC", line) - } - cmdLine := strings.TrimSpace(args[0]) - if cmdLine == "" { - return InvalidCommandError("EXEC", line) - } - if cmdLine, err := resolveArgumentVariables(s, []rune(cmdLine), true); err != nil { - return err - } else { - cmd := sysCommand(cmdLine) - cmd.Stderr = s.GetError() - cmd.Stdout = s.GetOutput() - _ = cmd.Run() - } - return nil -} - -func editCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("ED", line) - } - file, err := os.CreateTemp("", "sq*.sql") - if err != nil { - return err - } - fileName := file.Name() - defer os.Remove(fileName) - text := s.batch.String() - if s.batch.State() == "-" { - text = fmt.Sprintf("%s%s", text, SqlcmdEol) - } - _, err = file.WriteString(text) - if err != nil { - return err - } - file.Close() - cmd := sysCommand(s.vars.TextEditor() + " " + `"` + fileName + `"`) - cmd.Stderr = s.GetError() - cmd.Stdout = s.GetOutput() - err = cmd.Run() - if err != nil { - return err - } - wasEcho := s.echoFileLines - s.echoFileLines = true - s.batch.Reset(nil) - _ = s.IncludeFile(fileName, false) - s.echoFileLines = wasEcho - return nil -} - -func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) { - var b *strings.Builder - end := len(arg) - for i := 0; i < end; { - c, next := arg[i], grab(arg, i+1, end) - switch { - case c == '$' && next == '(': - vl, ok := readVariableReference(arg, i+2, end) - if ok { - varName := string(arg[i+2 : vl]) - val, ok := s.resolveVariable(varName) - if ok { - if b == nil { - b = new(strings.Builder) - b.Grow(len(arg)) - b.WriteString(string(arg[0:i])) - } - b.WriteString(val) - } else { - if failOnUnresolved { - return "", UndefinedVariable(varName) - } - s.WriteError(s.GetError(), UndefinedVariable(varName)) - if b != nil { - b.WriteString(string(arg[i : vl+1])) - } - } - i += ((vl - i) + 1) - } else { - if b != nil { - b.WriteString("$(") - } - i += 2 - } - default: - if b != nil { - b.WriteRune(c) - } - i++ - } - } - if b == nil { - return string(arg), nil - } - return b.String(), nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/alecthomas/kong" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +// Command defines a sqlcmd action which can be intermixed with the SQL batch +// Commands for sqlcmd are defined at https://docs.microsoft.com/sql/tools/sqlcmd-utility#sqlcmd-commands +type Command struct { + // regex must include at least one group if it has parameters + // Will be matched using FindStringSubmatch + regex *regexp.Regexp + // The function that implements the command. Third parameter is the line number + action func(*Sqlcmd, []string, uint) error + // Name of the command + name string + // whether the command is a system command + isSystem bool +} + +// Commands is the set of sqlcmd command implementations +type Commands map[string]*Command + +func newCommands() Commands { + // Commands is the set of Command implementations + return map[string]*Command{ + "EXIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?EXIT(?:[ \t]*(\(?.*\)?$)|$)`), + action: exitCommand, + name: "EXIT", + }, + "QUIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?QUIT(?:[ \t]+(.*$)|$)`), + action: quitCommand, + name: "QUIT", + }, + "GO": { + regex: regexp.MustCompile(batchTerminatorRegex("GO")), + action: goCommand, + name: "GO", + }, + "OUT": { + regex: regexp.MustCompile(`(?im)^[ \t]*:OUT(?:[ \t]+(.*$)|$)`), + action: outCommand, + name: "OUT", + }, + "ERROR": { + regex: regexp.MustCompile(`(?im)^[ \t]*:ERROR(?:[ \t]+(.*$)|$)`), + action: errorCommand, + name: "ERROR", + }, "READFILE": { + regex: regexp.MustCompile(`(?im)^[ \t]*:R(?:[ \t]+(.*$)|$)`), + action: readFileCommand, + name: "READFILE", + }, + "SETVAR": { + regex: regexp.MustCompile(`(?im)^[ \t]*:SETVAR(?:[ \t]+(.*$)|$)`), + action: setVarCommand, + name: "SETVAR", + }, + "LISTVAR": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:LISTVAR(?:[ \t]+(.*$)|$)`), + action: listVarCommand, + name: "LISTVAR", + }, + "RESET": { + regex: regexp.MustCompile(`(?im)^[ \t]*:RESET(?:[ \t]+(.*$)|$)`), + action: resetCommand, + name: "RESET", + }, + "LIST": { + regex: regexp.MustCompile(`(?im)^[ \t]*:LIST(?:[ \t]+(.*$)|$)`), + action: listCommand, + name: "LIST", + }, + "CONNECT": { + regex: regexp.MustCompile(`(?im)^[ \t]*:CONNECT(?:[ \t]+(.*$)|$)`), + action: connectCommand, + name: "CONNECT", + }, + "EXEC": { + regex: regexp.MustCompile(`(?im)^[ \t]*?:?!!(.*$)`), + action: execCommand, + name: "EXEC", + isSystem: true, + }, + "EDIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?ED(?:[ \t]+(.*$)|$)`), + action: editCommand, + name: "EDIT", + isSystem: true, + }, + } +} + +// DisableSysCommands disables the ED and :!! commands. +// When exitOnCall is true, running those commands will exit the process. +func (c Commands) DisableSysCommands(exitOnCall bool) { + f := warnDisabled + if exitOnCall { + f = errorDisabled + } + for _, cmd := range c { + if cmd.isSystem { + cmd.action = f + } + } +} + +func (c Commands) matchCommand(line string) (*Command, []string) { + for _, cmd := range c { + matchedCommand := cmd.regex.FindStringSubmatch(line) + if matchedCommand != nil { + return cmd, matchedCommand[1:] + } + } + return nil, nil +} + +func warnDisabled(s *Sqlcmd, args []string, line uint) error { + s.WriteError(s.GetError(), ErrCommandsDisabled) + return nil +} + +func errorDisabled(s *Sqlcmd, args []string, line uint) error { + s.WriteError(s.GetError(), ErrCommandsDisabled) + s.Exitcode = 1 + return ErrExitRequested +} + +func batchTerminatorRegex(terminator string) string { + return fmt.Sprintf(`(?im)^[\t ]*?%s(?:[ ]+(.*$)|$)`, regexp.QuoteMeta(terminator)) +} + +// SetBatchTerminator attempts to set the batch terminator to the given value +// Returns an error if the new value is not usable in the regex +func (c Commands) SetBatchTerminator(terminator string) error { + cmd := c["GO"] + regex, err := regexp.Compile(batchTerminatorRegex(terminator)) + if err != nil { + return err + } + cmd.regex = regex + return nil +} + +// exitCommand has 3 modes. +// With no (), it just exits without running any query +// With () it runs whatever batch is in the buffer then exits +// With any text between () it runs the text as a query then exits +func exitCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 { + return ErrExitRequested + } + params := strings.TrimSpace(args[0]) + if params == "" { + return ErrExitRequested + } + if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") { + return InvalidCommandError("EXIT", line) + } + // First we run the current batch + query := s.batch.String() + if query != "" { + query = s.getRunnableQuery(query) + if exitCode, err := s.runQuery(query); err != nil { + s.Exitcode = exitCode + return ErrExitRequested + } + } + query = strings.TrimSpace(params[1 : len(params)-1]) + s.batch.Reset([]rune(query)) + _, _, err := s.batch.Next() + if err != nil { + return err + } + query = s.batch.String() + if s.batch.String() != "" { + query = s.getRunnableQuery(query) + s.Exitcode, _ = s.runQuery(query) + } + return ErrExitRequested +} + +// quitCommand immediately exits the program without running any more batches +func quitCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("QUIT", line) + } + return ErrExitRequested +} + +// goCommand runs the current batch the number of times specified +func goCommand(s *Sqlcmd, args []string, line uint) error { + // default to 1 execution + n := 1 + var err error + if len(args) > 0 { + cnt := strings.TrimSpace(args[0]) + if cnt != "" { + if cnt, err = resolveArgumentVariables(s, []rune(cnt), true); err != nil { + return err + } + _, err = fmt.Sscanf(cnt, "%d", &n) + } + } + if err != nil || n < 1 { + return InvalidCommandError("GO", line) + } + query := s.batch.String() + if query == "" { + return nil + } + query = s.getRunnableQuery(query) + for i := 0; i < n; i++ { + if retcode, err := s.runQuery(query); err != nil { + s.Exitcode = retcode + return err + } + } + s.batch.Reset(nil) + return nil +} + +// outCommand changes the output writer to use a file +func outCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 || args[0] == "" { + return InvalidCommandError("OUT", line) + } + switch { + case strings.EqualFold(args[0], "stdout"): + s.SetOutput(os.Stdout) + case strings.EqualFold(args[0], "stderr"): + s.SetOutput(os.Stderr) + default: + o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return InvalidFileError(err, args[0]) + } + if s.UnicodeOutputFile { + // ODBC sqlcmd doesn't write a BOM but we will. + // Maybe the endian-ness should be configurable. + win16le := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) + encoder := transform.NewWriter(o, win16le.NewEncoder()) + s.SetOutput(encoder) + } else { + s.SetOutput(o) + } + } + return nil +} + +// errorCommand changes the error writer to use a file +func errorCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 || args[0] == "" { + return InvalidCommandError("OUT", line) + } + switch { + case strings.EqualFold(args[0], "stderr"): + s.SetError(os.Stderr) + case strings.EqualFold(args[0], "stdout"): + s.SetError(os.Stdout) + default: + o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return InvalidFileError(err, args[0]) + } + s.SetError(o) + } + return nil +} + +func readFileCommand(s *Sqlcmd, args []string, line uint) error { + if args == nil || len(args) != 1 { + return InvalidCommandError(":R", line) + } + fileName, _ := resolveArgumentVariables(s, []rune(args[0]), false) + return s.IncludeFile(fileName, false) +} + +// setVarCommand parses a variable setting and applies it to the current Sqlcmd variables +func setVarCommand(s *Sqlcmd, args []string, line uint) error { + if args == nil || len(args) != 1 || args[0] == "" { + return InvalidCommandError(":SETVAR", line) + } + + varname := args[0] + val := "" + // The prior incarnation of sqlcmd doesn't require a space between the variable name and its value + // in some very unexpected cases. This version will require the space. + sp := strings.IndexRune(args[0], ' ') + if sp > -1 { + val = strings.TrimSpace(varname[sp:]) + varname = varname[:sp] + } + if err := s.vars.Setvar(varname, val); err != nil { + switch e := err.(type) { + case *VariableError: + return e + default: + return InvalidCommandError(":SETVAR", line) + } + } + return nil +} + +// listVarCommand prints the set of Sqlcmd scripting variables. +// Builtin values are printed first, followed by user-set values in sorted order. +func listVarCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("LISTVAR", line) + } + + vars := s.vars.All() + keys := make([]string, 0, len(vars)) + for k := range vars { + if !contains(builtinVariables, k) { + keys = append(keys, k) + } + } + sort.Strings(keys) + keys = append(builtinVariables, keys...) + for _, k := range keys { + fmt.Fprintf(s.GetOutput(), `%s = "%s"%s`, k, vars[k], SqlcmdEol) + } + return nil +} + +// resetCommand resets the statement cache +func resetCommand(s *Sqlcmd, args []string, line uint) error { + if s.batch != nil { + s.batch.Reset(nil) + } + + return nil +} + +// listCommand displays statements currently in the statement cache +func listCommand(s *Sqlcmd, args []string, line uint) error { + if s.batch != nil && s.batch.String() != "" { + fmt.Fprintf(s.GetOutput(), `%s%s`, []byte(s.batch.String()), SqlcmdEol) + } + + return nil +} + +type connectData struct { + Server string `arg:""` + Database string `short:"D"` + Username string `short:"U"` + Password string `short:"P"` + LoginTimeout string `short:"l"` + AuthenticationMethod string `short:"G"` +} + +func connectCommand(s *Sqlcmd, args []string, line uint) error { + + if len(args) == 0 { + return InvalidCommandError("CONNECT", line) + } + cmdLine := strings.TrimSpace(args[0]) + if cmdLine == "" { + return InvalidCommandError("CONNECT", line) + } + arguments := &connectData{} + parser, err := kong.New(arguments) + if err != nil { + return InvalidCommandError("CONNECT", line) + } + + // Fields removes extra whitespace. + // Note :connect doesn't support passwords with spaces + if _, err = parser.Parse(strings.Fields(cmdLine)); err != nil { + return InvalidCommandError("CONNECT", line) + } + + connect := *s.Connect + connect.UserName, _ = resolveArgumentVariables(s, []rune(arguments.Username), false) + connect.Password, _ = resolveArgumentVariables(s, []rune(arguments.Password), false) + connect.ServerName, _ = resolveArgumentVariables(s, []rune(arguments.Server), false) + timeout, _ := resolveArgumentVariables(s, []rune(arguments.LoginTimeout), false) + if timeout != "" { + if timeoutSeconds, err := strconv.ParseInt(timeout, 10, 32); err == nil { + if timeoutSeconds < 0 { + return InvalidCommandError("CONNECT", line) + } + connect.LoginTimeoutSeconds = int(timeoutSeconds) + } + } + connect.AuthenticationMethod = arguments.AuthenticationMethod + // If no user name is provided we switch to integrated auth + _ = s.ConnectDb(&connect, s.lineIo == nil) + // ConnectDb prints connection errors already, and failure to connect is not fatal even with -b option + return nil +} + +func execCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 { + return InvalidCommandError("EXEC", line) + } + cmdLine := strings.TrimSpace(args[0]) + if cmdLine == "" { + return InvalidCommandError("EXEC", line) + } + if cmdLine, err := resolveArgumentVariables(s, []rune(cmdLine), true); err != nil { + return err + } else { + cmd := sysCommand(cmdLine) + cmd.Stderr = s.GetError() + cmd.Stdout = s.GetOutput() + _ = cmd.Run() + } + return nil +} + +func editCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("ED", line) + } + file, err := os.CreateTemp("", "sq*.sql") + if err != nil { + return err + } + fileName := file.Name() + defer os.Remove(fileName) + text := s.batch.String() + if s.batch.State() == "-" { + text = fmt.Sprintf("%s%s", text, SqlcmdEol) + } + _, err = file.WriteString(text) + if err != nil { + return err + } + file.Close() + cmd := sysCommand(s.vars.TextEditor() + " " + `"` + fileName + `"`) + cmd.Stderr = s.GetError() + cmd.Stdout = s.GetOutput() + err = cmd.Run() + if err != nil { + return err + } + wasEcho := s.echoFileLines + s.echoFileLines = true + s.batch.Reset(nil) + _ = s.IncludeFile(fileName, false) + s.echoFileLines = wasEcho + return nil +} + +func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) { + var b *strings.Builder + end := len(arg) + for i := 0; i < end; { + c, next := arg[i], grab(arg, i+1, end) + switch { + case c == '$' && next == '(': + vl, ok := readVariableReference(arg, i+2, end) + if ok { + varName := string(arg[i+2 : vl]) + val, ok := s.resolveVariable(varName) + if ok { + if b == nil { + b = new(strings.Builder) + b.Grow(len(arg)) + b.WriteString(string(arg[0:i])) + } + b.WriteString(val) + } else { + if failOnUnresolved { + return "", UndefinedVariable(varName) + } + s.WriteError(s.GetError(), UndefinedVariable(varName)) + if b != nil { + b.WriteString(string(arg[i : vl+1])) + } + } + i += ((vl - i) + 1) + } else { + if b != nil { + b.WriteString("$(") + } + i += 2 + } + default: + if b != nil { + b.WriteRune(c) + } + i++ + } + } + if b == nil { + return string(arg), nil + } + return b.String(), nil +} diff --git a/pkg/sqlcmd/commands_test.go b/pkg/sqlcmd/commands_test.go index 584867ba..c5aa4feb 100644 --- a/pkg/sqlcmd/commands_test.go +++ b/pkg/sqlcmd/commands_test.go @@ -1,302 +1,302 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "bytes" - "fmt" - "os" - "strings" - "testing" - - "github.com/microsoft/go-mssqldb/azuread" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestQuitCommand(t *testing.T) { - s := &Sqlcmd{} - err := quitCommand(s, nil, 1) - require.ErrorIs(t, err, ErrExitRequested) - err = quitCommand(s, []string{"extra parameters"}, 2) - require.Error(t, err, "Quit should error out with extra parameters") - assert.NotErrorIs(t, err, ErrExitRequested, "Error with extra arguments") -} - -func TestCommandParsing(t *testing.T) { - type commandTest struct { - line string - cmd string - args []string - } - c := newCommands() - commands := []commandTest{ - {"quite", "", nil}, - {"quit", "QUIT", []string{""}}, - {":QUIT\n", "QUIT", []string{""}}, - {" QUIT \n", "QUIT", []string{""}}, - {"quit extra\n", "QUIT", []string{"extra"}}, - {`:Out c:\folder\file`, "OUT", []string{`c:\folder\file`}}, - {` :Error c:\folder\file`, "ERROR", []string{`c:\folder\file`}}, - {`:Setvar A1 "some value" `, "SETVAR", []string{`A1 "some value" `}}, - {` :Listvar`, "LISTVAR", []string{""}}, - {`:EXIT (select 100 as count)`, "EXIT", []string{"(select 100 as count)"}}, - {`:EXIT ( )`, "EXIT", []string{"( )"}}, - {`EXIT `, "EXIT", []string{""}}, - {`:Connect someserver -U someuser`, "CONNECT", []string{"someserver -U someuser"}}, - {`:r c:\$(var)\file.sql`, "READFILE", []string{`c:\$(var)\file.sql`}}, - {`:!! notepad`, "EXEC", []string{" notepad"}}, - {`:!!notepad`, "EXEC", []string{"notepad"}}, - {` !! dir c:\`, "EXEC", []string{` dir c:\`}}, - {`!!dir c:\`, "EXEC", []string{`dir c:\`}}, - } - - for _, test := range commands { - cmd, args := c.matchCommand(test.line) - if test.cmd != "" { - if assert.NotNil(t, cmd, "No command found for `%s`", test.line) { - assert.Equal(t, test.cmd, cmd.name, "Incorrect command for `%s`", test.line) - assert.Equal(t, test.args, args, "Incorrect arguments for `%s`", test.line) - } - } else { - assert.Nil(t, cmd, "Unexpected match for %s", test.line) - } - } -} - -func TestCustomBatchSeparator(t *testing.T) { - c := newCommands() - err := c.SetBatchTerminator("me!") - if assert.NoError(t, err, "SetBatchTerminator should succeed") { - cmd, args := c.matchCommand(" me! 5 \n") - if assert.NotNil(t, cmd, "matchCommand didn't find GO for custom batch separator") { - assert.Equal(t, "GO", cmd.name, "command name") - assert.Equal(t, "5", strings.TrimSpace(args[0]), "go argument") - } - } -} - -func TestVarCommands(t *testing.T) { - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - err := setVarCommand(s, []string{"ABC 100"}, 1) - assert.NoError(t, err, "setVarCommand ABC 100") - err = setVarCommand(s, []string{"XYZ 200"}, 2) - assert.NoError(t, err, "setVarCommand XYZ 200") - err = listVarCommand(s, []string{""}, 3) - assert.NoError(t, err, "listVarCommand") - s.SetOutput(nil) - varmap := s.vars.All() - o := buf.buf.String() - t.Logf("Listvar output:\n'%s'", o) - output := strings.Split(o, SqlcmdEol) - for i, v := range builtinVariables { - line := strings.Split(output[i], " = ") - assert.Equalf(t, v, line[0], "unexpected variable printed at index %d", i) - val := strings.Trim(line[1], `"`) - assert.Equalf(t, varmap[v], val, "Unexpected value for variable %s", v) - } - assert.Equalf(t, `ABC = "100"`, output[len(output)-3], "Penultimate non-empty line should be ABC") - assert.Equalf(t, `XYZ = "200"`, output[len(output)-2], "Last non-empty line should be XYZ") - assert.Equalf(t, "", output[len(output)-1], "Last line should be empty") - -} - -// memoryBuffer has both Write and Close methods for use as io.WriteCloser -type memoryBuffer struct { - buf *bytes.Buffer -} - -func (b *memoryBuffer) Write(p []byte) (n int, err error) { - return b.buf.Write(p) -} - -func (b *memoryBuffer) Close() error { - return nil -} - -func TestResetCommand(t *testing.T) { - var err error - - // setup a test sqlcmd - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - - // insert a test batch - s.batch.Reset([]rune("select 1")) - _, _, err = s.batch.Next() - assert.NoError(t, err, "Inserting test batch") - assert.Equal(t, s.batch.batchline, int(2), "Batch line updated after test batch insert") - - // execute reset command and validate results - err = resetCommand(s, nil, 1) - assert.Equal(t, s.batch.batchline, int(1), "Batch line not reset properly") - assert.NoError(t, err, "Executing :reset command") -} - -func TestListCommand(t *testing.T) { - var err error - - // setup a test sqlcmd - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - - // insert test batch - s.batch.Reset([]rune("select 1")) - _, _, err = s.batch.Next() - assert.NoError(t, err, "Inserting test batch") - - // execute list command and verify results - err = listCommand(s, nil, 1) - assert.NoError(t, err, "Executing :list command") - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, o, "select 1"+SqlcmdEol, ":list output not equal to batch") -} - -func TestConnectCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - prompted := false - s.lineIo = &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - prompted = true - return []byte{}, nil - }, - } - err := connectCommand(s, []string{"someserver -U someuser"}, 1) - assert.NoError(t, err, "connectCommand with valid arguments doesn't return an error on connect failure") - assert.True(t, prompted, "connectCommand with user name and no password should prompt for password") - assert.NotEqual(t, "someserver", s.Connect.ServerName, "On connection failure, sqlCmd.Connect does not copy inputs") - - err = connectCommand(s, []string{}, 2) - assert.EqualError(t, err, InvalidCommandError("CONNECT", 2).Error(), ":Connect with no arguments should return an error") - c := newConnect(t) - - authenticationMethod := "" - password := "" - username := "" - if canTestAzureAuth() { - authenticationMethod = "-G " + azuread.ActiveDirectoryDefault - } - if c.Password != "" { - password = "-P " + c.Password - } - if c.UserName != "" { - username = "-U " + c.UserName - } - s.vars.Set("servername", c.ServerName) - s.vars.Set("to", "111") - buf.buf.Reset() - err = connectCommand(s, []string{fmt.Sprintf("$(servername) %s %s %s -l $(to)", username, password, authenticationMethod)}, 3) - if assert.NoError(t, err, "connectCommand with valid parameters should not return an error") { - // not using assert to avoid printing passwords in the log - assert.NotContains(t, buf.buf.String(), "$(servername)", "ConnectDB should have succeeded") - if s.Connect.UserName != c.UserName || c.Password != s.Connect.Password || s.Connect.LoginTimeoutSeconds != 111 { - t.Fatalf("After connect, sqlCmd.Connect is not updated %+v", s.Connect) - } - } -} - -func TestErrorCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - file, err := os.CreateTemp("", "sqlcmderr") - assert.NoError(t, err, "os.CreateTemp") - defer os.Remove(file.Name()) - fileName := file.Name() - _ = file.Close() - err = errorCommand(s, []string{""}, 1) - assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name") - err = errorCommand(s, []string{fileName}, 1) - assert.NoError(t, err, "errorCommand") - // Only some error kinds go to the error output - err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"}) - assert.NoError(t, err, "runSqlCmd") - s.SetError(nil) - errText, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "ReadFile") { - assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents") - } -} - -func TestResolveArgumentVariables(t *testing.T) { - type argTest struct { - arg string - val string - err string - } - - args := []argTest{ - {"$(var1)", "var1val", ""}, - {"$(var1", "$(var1", ""}, - {`C:\folder\$(var1)\$(var2)\$(var1)\file.sql`, `C:\folder\var1val\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, - {`C:\folder\$(var1\$(var2)\$(var1)\file.sql`, `C:\folder\$(var1\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, - } - vars := InitializeVariables(false) - s := New(nil, "", vars) - s.vars.Set("var1", "var1val") - buf := &memoryBuffer{buf: new(bytes.Buffer)} - defer buf.Close() - s.SetError(buf) - for _, test := range args { - actual, _ := resolveArgumentVariables(s, []rune(test.arg), false) - assert.Equal(t, test.val, actual, "Incorrect argument parsing of "+test.arg) - assert.Contains(t, buf.buf.String(), test.err, "Error output mismatch for "+test.arg) - buf.buf.Reset() - } - actual, err := resolveArgumentVariables(s, []rune("$(var1)$(var2)"), true) - if assert.ErrorContains(t, err, UndefinedVariable("var2").Error(), "fail on unresolved variable") { - assert.Empty(t, actual, "fail on unresolved variable") - } -} - -func TestExecCommand(t *testing.T) { - vars := InitializeVariables(false) - s := New(nil, "", vars) - s.vars.Set("var1", "hello") - buf := &memoryBuffer{buf: new(bytes.Buffer)} - defer buf.Close() - s.SetOutput(buf) - err := execCommand(s, []string{`echo $(var1)`}, 1) - if assert.NoError(t, err, "execCommand with valid arguments") { - assert.Equal(t, buf.buf.String(), "hello"+SqlcmdEol, "echo output should be in sqlcmd output") - } -} - -func TestDisableSysCommandBlocksExec(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Cmd.DisableSysCommands(false) - c := []string{"set nocount on", ":!! echo hello", "select 100", "go"} - err := runSqlCmd(t, s, c) - if assert.NoError(t, err, ":!! with warning should not raise error") { - assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol+"100"+SqlcmdEol) - assert.Equal(t, 0, s.Exitcode, "ExitCode after warning") - } - buf.buf.Reset() - s.Cmd.DisableSysCommands(true) - err = runSqlCmd(t, s, c) - if assert.NoError(t, err, ":!! with error should not return error") { - assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol) - assert.NotContains(t, buf.buf.String(), "100", "query should not run when syscommand disabled") - assert.Equal(t, 1, s.Exitcode, "ExitCode after error") - } -} - -func TestEditCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDEDITOR, "echo select 5000> ") - c := []string{"set nocount on", "go", "select 100", ":ed", "go"} - err := runSqlCmd(t, s, c) - if assert.NoError(t, err, ":ed should not raise error") { - assert.Equal(t, "1> select 5000"+SqlcmdEol+"5000"+SqlcmdEol+SqlcmdEol, buf.buf.String(), "Incorrect output from query after :ed command") - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" + + "github.com/microsoft/go-mssqldb/azuread" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQuitCommand(t *testing.T) { + s := &Sqlcmd{} + err := quitCommand(s, nil, 1) + require.ErrorIs(t, err, ErrExitRequested) + err = quitCommand(s, []string{"extra parameters"}, 2) + require.Error(t, err, "Quit should error out with extra parameters") + assert.NotErrorIs(t, err, ErrExitRequested, "Error with extra arguments") +} + +func TestCommandParsing(t *testing.T) { + type commandTest struct { + line string + cmd string + args []string + } + c := newCommands() + commands := []commandTest{ + {"quite", "", nil}, + {"quit", "QUIT", []string{""}}, + {":QUIT\n", "QUIT", []string{""}}, + {" QUIT \n", "QUIT", []string{""}}, + {"quit extra\n", "QUIT", []string{"extra"}}, + {`:Out c:\folder\file`, "OUT", []string{`c:\folder\file`}}, + {` :Error c:\folder\file`, "ERROR", []string{`c:\folder\file`}}, + {`:Setvar A1 "some value" `, "SETVAR", []string{`A1 "some value" `}}, + {` :Listvar`, "LISTVAR", []string{""}}, + {`:EXIT (select 100 as count)`, "EXIT", []string{"(select 100 as count)"}}, + {`:EXIT ( )`, "EXIT", []string{"( )"}}, + {`EXIT `, "EXIT", []string{""}}, + {`:Connect someserver -U someuser`, "CONNECT", []string{"someserver -U someuser"}}, + {`:r c:\$(var)\file.sql`, "READFILE", []string{`c:\$(var)\file.sql`}}, + {`:!! notepad`, "EXEC", []string{" notepad"}}, + {`:!!notepad`, "EXEC", []string{"notepad"}}, + {` !! dir c:\`, "EXEC", []string{` dir c:\`}}, + {`!!dir c:\`, "EXEC", []string{`dir c:\`}}, + } + + for _, test := range commands { + cmd, args := c.matchCommand(test.line) + if test.cmd != "" { + if assert.NotNil(t, cmd, "No command found for `%s`", test.line) { + assert.Equal(t, test.cmd, cmd.name, "Incorrect command for `%s`", test.line) + assert.Equal(t, test.args, args, "Incorrect arguments for `%s`", test.line) + } + } else { + assert.Nil(t, cmd, "Unexpected match for %s", test.line) + } + } +} + +func TestCustomBatchSeparator(t *testing.T) { + c := newCommands() + err := c.SetBatchTerminator("me!") + if assert.NoError(t, err, "SetBatchTerminator should succeed") { + cmd, args := c.matchCommand(" me! 5 \n") + if assert.NotNil(t, cmd, "matchCommand didn't find GO for custom batch separator") { + assert.Equal(t, "GO", cmd.name, "command name") + assert.Equal(t, "5", strings.TrimSpace(args[0]), "go argument") + } + } +} + +func TestVarCommands(t *testing.T) { + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + err := setVarCommand(s, []string{"ABC 100"}, 1) + assert.NoError(t, err, "setVarCommand ABC 100") + err = setVarCommand(s, []string{"XYZ 200"}, 2) + assert.NoError(t, err, "setVarCommand XYZ 200") + err = listVarCommand(s, []string{""}, 3) + assert.NoError(t, err, "listVarCommand") + s.SetOutput(nil) + varmap := s.vars.All() + o := buf.buf.String() + t.Logf("Listvar output:\n'%s'", o) + output := strings.Split(o, SqlcmdEol) + for i, v := range builtinVariables { + line := strings.Split(output[i], " = ") + assert.Equalf(t, v, line[0], "unexpected variable printed at index %d", i) + val := strings.Trim(line[1], `"`) + assert.Equalf(t, varmap[v], val, "Unexpected value for variable %s", v) + } + assert.Equalf(t, `ABC = "100"`, output[len(output)-3], "Penultimate non-empty line should be ABC") + assert.Equalf(t, `XYZ = "200"`, output[len(output)-2], "Last non-empty line should be XYZ") + assert.Equalf(t, "", output[len(output)-1], "Last line should be empty") + +} + +// memoryBuffer has both Write and Close methods for use as io.WriteCloser +type memoryBuffer struct { + buf *bytes.Buffer +} + +func (b *memoryBuffer) Write(p []byte) (n int, err error) { + return b.buf.Write(p) +} + +func (b *memoryBuffer) Close() error { + return nil +} + +func TestResetCommand(t *testing.T) { + var err error + + // setup a test sqlcmd + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + + // insert a test batch + s.batch.Reset([]rune("select 1")) + _, _, err = s.batch.Next() + assert.NoError(t, err, "Inserting test batch") + assert.Equal(t, s.batch.batchline, int(2), "Batch line updated after test batch insert") + + // execute reset command and validate results + err = resetCommand(s, nil, 1) + assert.Equal(t, s.batch.batchline, int(1), "Batch line not reset properly") + assert.NoError(t, err, "Executing :reset command") +} + +func TestListCommand(t *testing.T) { + var err error + + // setup a test sqlcmd + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + + // insert test batch + s.batch.Reset([]rune("select 1")) + _, _, err = s.batch.Next() + assert.NoError(t, err, "Inserting test batch") + + // execute list command and verify results + err = listCommand(s, nil, 1) + assert.NoError(t, err, "Executing :list command") + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, o, "select 1"+SqlcmdEol, ":list output not equal to batch") +} + +func TestConnectCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + prompted := false + s.lineIo = &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + prompted = true + return []byte{}, nil + }, + } + err := connectCommand(s, []string{"someserver -U someuser"}, 1) + assert.NoError(t, err, "connectCommand with valid arguments doesn't return an error on connect failure") + assert.True(t, prompted, "connectCommand with user name and no password should prompt for password") + assert.NotEqual(t, "someserver", s.Connect.ServerName, "On connection failure, sqlCmd.Connect does not copy inputs") + + err = connectCommand(s, []string{}, 2) + assert.EqualError(t, err, InvalidCommandError("CONNECT", 2).Error(), ":Connect with no arguments should return an error") + c := newConnect(t) + + authenticationMethod := "" + password := "" + username := "" + if canTestAzureAuth() { + authenticationMethod = "-G " + azuread.ActiveDirectoryDefault + } + if c.Password != "" { + password = "-P " + c.Password + } + if c.UserName != "" { + username = "-U " + c.UserName + } + s.vars.Set("servername", c.ServerName) + s.vars.Set("to", "111") + buf.buf.Reset() + err = connectCommand(s, []string{fmt.Sprintf("$(servername) %s %s %s -l $(to)", username, password, authenticationMethod)}, 3) + if assert.NoError(t, err, "connectCommand with valid parameters should not return an error") { + // not using assert to avoid printing passwords in the log + assert.NotContains(t, buf.buf.String(), "$(servername)", "ConnectDB should have succeeded") + if s.Connect.UserName != c.UserName || c.Password != s.Connect.Password || s.Connect.LoginTimeoutSeconds != 111 { + t.Fatalf("After connect, sqlCmd.Connect is not updated %+v", s.Connect) + } + } +} + +func TestErrorCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + file, err := os.CreateTemp("", "sqlcmderr") + assert.NoError(t, err, "os.CreateTemp") + defer os.Remove(file.Name()) + fileName := file.Name() + _ = file.Close() + err = errorCommand(s, []string{""}, 1) + assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name") + err = errorCommand(s, []string{fileName}, 1) + assert.NoError(t, err, "errorCommand") + // Only some error kinds go to the error output + err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"}) + assert.NoError(t, err, "runSqlCmd") + s.SetError(nil) + errText, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "ReadFile") { + assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents") + } +} + +func TestResolveArgumentVariables(t *testing.T) { + type argTest struct { + arg string + val string + err string + } + + args := []argTest{ + {"$(var1)", "var1val", ""}, + {"$(var1", "$(var1", ""}, + {`C:\folder\$(var1)\$(var2)\$(var1)\file.sql`, `C:\folder\var1val\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, + {`C:\folder\$(var1\$(var2)\$(var1)\file.sql`, `C:\folder\$(var1\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, + } + vars := InitializeVariables(false) + s := New(nil, "", vars) + s.vars.Set("var1", "var1val") + buf := &memoryBuffer{buf: new(bytes.Buffer)} + defer buf.Close() + s.SetError(buf) + for _, test := range args { + actual, _ := resolveArgumentVariables(s, []rune(test.arg), false) + assert.Equal(t, test.val, actual, "Incorrect argument parsing of "+test.arg) + assert.Contains(t, buf.buf.String(), test.err, "Error output mismatch for "+test.arg) + buf.buf.Reset() + } + actual, err := resolveArgumentVariables(s, []rune("$(var1)$(var2)"), true) + if assert.ErrorContains(t, err, UndefinedVariable("var2").Error(), "fail on unresolved variable") { + assert.Empty(t, actual, "fail on unresolved variable") + } +} + +func TestExecCommand(t *testing.T) { + vars := InitializeVariables(false) + s := New(nil, "", vars) + s.vars.Set("var1", "hello") + buf := &memoryBuffer{buf: new(bytes.Buffer)} + defer buf.Close() + s.SetOutput(buf) + err := execCommand(s, []string{`echo $(var1)`}, 1) + if assert.NoError(t, err, "execCommand with valid arguments") { + assert.Equal(t, buf.buf.String(), "hello"+SqlcmdEol, "echo output should be in sqlcmd output") + } +} + +func TestDisableSysCommandBlocksExec(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Cmd.DisableSysCommands(false) + c := []string{"set nocount on", ":!! echo hello", "select 100", "go"} + err := runSqlCmd(t, s, c) + if assert.NoError(t, err, ":!! with warning should not raise error") { + assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol+"100"+SqlcmdEol) + assert.Equal(t, 0, s.Exitcode, "ExitCode after warning") + } + buf.buf.Reset() + s.Cmd.DisableSysCommands(true) + err = runSqlCmd(t, s, c) + if assert.NoError(t, err, ":!! with error should not return error") { + assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol) + assert.NotContains(t, buf.buf.String(), "100", "query should not run when syscommand disabled") + assert.Equal(t, 1, s.Exitcode, "ExitCode after error") + } +} + +func TestEditCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDEDITOR, "echo select 5000> ") + c := []string{"set nocount on", "go", "select 100", ":ed", "go"} + err := runSqlCmd(t, s, c) + if assert.NoError(t, err, ":ed should not raise error") { + assert.Equal(t, "1> select 5000"+SqlcmdEol+"5000"+SqlcmdEol+SqlcmdEol, buf.buf.String(), "Incorrect output from query after :ed command") + } +} diff --git a/pkg/sqlcmd/errors.go b/pkg/sqlcmd/errors.go index 0a977543..391bf416 100644 --- a/pkg/sqlcmd/errors.go +++ b/pkg/sqlcmd/errors.go @@ -1,94 +1,94 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "errors" - "fmt" - "strings" -) - -// ErrorPrefix is the prefix for all sqlcmd-generated errors -const ErrorPrefix = "Sqlcmd: Error: " - -// WarningPrefix is the prefix for all sqlcmd-generated warnings -const WarningPrefix = "Sqlcmd: Warning: " - -// ArgumentError is related to command line switch validation not handled by kong -type ArgumentError struct { - Parameter string - Rule string -} - -func (e *ArgumentError) Error() string { - return ErrorPrefix + e.Rule -} - -// InvalidServerName indicates the SQLCMDSERVER variable has an incorrect format -var InvalidServerName = ArgumentError{ - Parameter: "server", - Rule: "server must be of the form [tcp]:server[[/instance]|[,port]]", -} - -// VariableError is an error about scripting variables -type VariableError struct { - Variable string - MessageFormat string -} - -func (e *VariableError) Error() string { - return ErrorPrefix + fmt.Sprintf(e.MessageFormat, e.Variable) -} - -// ReadOnlyVariable indicates the user tried to set a value to a read-only variable -func ReadOnlyVariable(variable string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "The scripting variable: '%s' is read-only", - } -} - -// UndefinedVariable indicates the user tried to reference an undefined variable -func UndefinedVariable(variable string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "'%s' scripting variable not defined.", - } -} - -// InvalidVariableValue indicates the variable was set to an invalid value -func InvalidVariableValue(variable string, value string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "The environment variable: '%s' has invalid value: '" + strings.ReplaceAll(value, `%`, `%%`) + "'.", - } -} - -// CommandError indicates syntax errors for specific sqlcmd commands -type CommandError struct { - Command string - LineNumber uint -} - -func (e *CommandError) Error() string { - return ErrorPrefix + fmt.Sprintf("Syntax error at line %d near command '%s'.", e.LineNumber, e.Command) -} - -// InvalidCommandError creates a SQLCmdCommandError -func InvalidCommandError(command string, lineNumber uint) *CommandError { - return &CommandError{ - Command: command, - LineNumber: lineNumber, - } -} - -// InvalidFileError indicates a file could not be opened -func InvalidFileError(err error, path string) error { - return errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + path + " (Reason: " + err.Error() + ").") -} - -// SyntaxError indicates a malformed sqlcmd statement -func syntaxError(lineNumber uint) error { - return fmt.Errorf("%sSyntax error at line %d.", ErrorPrefix, lineNumber) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "errors" + "fmt" + "strings" +) + +// ErrorPrefix is the prefix for all sqlcmd-generated errors +const ErrorPrefix = "Sqlcmd: Error: " + +// WarningPrefix is the prefix for all sqlcmd-generated warnings +const WarningPrefix = "Sqlcmd: Warning: " + +// ArgumentError is related to command line switch validation not handled by kong +type ArgumentError struct { + Parameter string + Rule string +} + +func (e *ArgumentError) Error() string { + return ErrorPrefix + e.Rule +} + +// InvalidServerName indicates the SQLCMDSERVER variable has an incorrect format +var InvalidServerName = ArgumentError{ + Parameter: "server", + Rule: "server must be of the form [tcp]:server[[/instance]|[,port]]", +} + +// VariableError is an error about scripting variables +type VariableError struct { + Variable string + MessageFormat string +} + +func (e *VariableError) Error() string { + return ErrorPrefix + fmt.Sprintf(e.MessageFormat, e.Variable) +} + +// ReadOnlyVariable indicates the user tried to set a value to a read-only variable +func ReadOnlyVariable(variable string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "The scripting variable: '%s' is read-only", + } +} + +// UndefinedVariable indicates the user tried to reference an undefined variable +func UndefinedVariable(variable string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "'%s' scripting variable not defined.", + } +} + +// InvalidVariableValue indicates the variable was set to an invalid value +func InvalidVariableValue(variable string, value string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "The environment variable: '%s' has invalid value: '" + strings.ReplaceAll(value, `%`, `%%`) + "'.", + } +} + +// CommandError indicates syntax errors for specific sqlcmd commands +type CommandError struct { + Command string + LineNumber uint +} + +func (e *CommandError) Error() string { + return ErrorPrefix + fmt.Sprintf("Syntax error at line %d near command '%s'.", e.LineNumber, e.Command) +} + +// InvalidCommandError creates a SQLCmdCommandError +func InvalidCommandError(command string, lineNumber uint) *CommandError { + return &CommandError{ + Command: command, + LineNumber: lineNumber, + } +} + +// InvalidFileError indicates a file could not be opened +func InvalidFileError(err error, path string) error { + return errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + path + " (Reason: " + err.Error() + ").") +} + +// SyntaxError indicates a malformed sqlcmd statement +func syntaxError(lineNumber uint) error { + return fmt.Errorf("%sSyntax error at line %d.", ErrorPrefix, lineNumber) +} diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 9de7430a..b44e07e9 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -1,663 +1,663 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "database/sql" - "fmt" - "io" - "strings" - "time" - - "github.com/google/uuid" - mssql "github.com/microsoft/go-mssqldb" -) - -const ( - defaultMaxDisplayWidth = 1024 * 1024 - maxPadWidth = 8000 -) - -// Formatter defines methods to process query output -type Formatter interface { - // BeginBatch is called before the query runs - BeginBatch(query string, vars *Variables, out io.Writer, err io.Writer) - // EndBatch is the last function called during batch execution and signals the end of the batch - EndBatch() - // BeginResultSet is called when a new result set is encountered - BeginResultSet([]*sql.ColumnType) - // EndResultSet is called after all rows in a result set have been processed - EndResultSet() - // AddRow is called for each row in a result set. It returns the value of the first column - AddRow(*sql.Rows) string - // AddMessage is called for every information message returned by the server during the batch - AddMessage(string) - // AddError is called for each error encountered during batch execution - AddError(err error) -} - -// ControlCharacterBehavior specifies the text handling required for control characters in the output -type ControlCharacterBehavior int - -const ( - // ControlIgnore preserves control characters in the output - ControlIgnore ControlCharacterBehavior = iota - // ControlReplace replaces control characters with spaces, 1 space per character - ControlReplace - // ControlRemove removes control characters from the output - ControlRemove - // ControlReplaceConsecutive replaces multiple consecutive control characters with a single space - ControlReplaceConsecutive -) - -type columnDetail struct { - displayWidth int64 - leftJustify bool - zeroesAfterDecimal bool - col sql.ColumnType - precision int - scale int -} - -// The default formatter based on the native sqlcmd style -// It supports both horizontal (default) and vertical layout for results. -// Both vertical and horizontal layouts respect column widths set by SQLCMD variables. -type sqlCmdFormatterType struct { - out io.Writer - err io.Writer - vars *Variables - colsep string - removeTrailingSpaces bool - ccb ControlCharacterBehavior - columnDetails []columnDetail - rowcount int - writepos int64 - format string - maxColNameLen int -} - -// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter -func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool) Formatter { - return &sqlCmdFormatterType{ - removeTrailingSpaces: removeTrailingSpaces, - format: "horizontal", - } -} - -// Adds the given string to the current line, wrapping it based on the screen width setting -func (f *sqlCmdFormatterType) writeOut(s string) { - w := f.vars.ScreenWidth() - if w == 0 { - f.mustWriteOut(s) - return - } - - r := []rune(s) - for i := 0; true; { - if i == len(r) { - f.mustWriteOut(string(r)) - return - } else if f.writepos == w { - f.mustWriteOut(string(r[:i])) - f.mustWriteOut(SqlcmdEol) - r = []rune(string(r[i:])) - f.writepos = 0 - i = 0 - } else { - c := r[i] - if c != '\r' && c != '\n' { - f.writepos++ - } else { - f.writepos = 0 - } - i++ - } - } -} - -// Stores the settings to use for processing the current batch -// TODO: add a third io.Writer for messages when we add -r support -func (f *sqlCmdFormatterType) BeginBatch(_ string, vars *Variables, out io.Writer, err io.Writer) { - f.out = out - f.err = err - f.vars = vars - f.colsep = vars.ColumnSeparator() - f.format = vars.Format() -} - -func (f *sqlCmdFormatterType) EndBatch() { -} - -// Calculate the widths for each column and print the column names -// Since sql.ColumnType only provides sizes for variable length types we will -// base our numbers for most types on https://docs.microsoft.com/sql/odbc/reference/appendixes/column-size -func (f *sqlCmdFormatterType) BeginResultSet(cols []*sql.ColumnType) { - f.rowcount = 0 - f.columnDetails, f.maxColNameLen = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth()) - if f.vars.RowsBetweenHeaders() > -1 && f.format == "horizontal" { - f.printColumnHeadings() - } -} - -// Writes a blank line to the designated output writer -func (f *sqlCmdFormatterType) EndResultSet() { - f.writeOut(SqlcmdEol) -} - -// Writes the current row to the designated output writer -func (f *sqlCmdFormatterType) AddRow(row *sql.Rows) string { - retval := "" - values, err := f.scanRow(row) - if err != nil { - f.mustWriteErr(err.Error()) - return retval - } - retval = values[0] - if f.format == "horizontal" { - // values are the full values, look at the displaywidth of each column and truncate accordingly - for i, v := range values { - if i > 0 { - f.writeOut(f.vars.ColumnSeparator()) - } - f.printColumnValue(v, i) - } - f.rowcount++ - gap := f.vars.RowsBetweenHeaders() - if gap > 0 && (int64(f.rowcount)%gap == 0) { - f.writeOut(SqlcmdEol) - f.printColumnHeadings() - } - } else { - f.addVerticalRow(values) - } - f.writeOut(SqlcmdEol) - return retval - -} - -func (f *sqlCmdFormatterType) addVerticalRow(values []string) { - for i, v := range values { - if f.vars.RowsBetweenHeaders() > -1 { - builder := new(strings.Builder) - name := f.columnDetails[i].col.Name() - builder.WriteString(name) - builder = padRight(builder, int64(f.maxColNameLen-len(name)+1), " ") - f.writeOut(builder.String()) - } - f.printColumnValue(v, i) - f.writeOut(SqlcmdEol) - } -} - -// Writes a non-error message to the designated message writer -func (f *sqlCmdFormatterType) AddMessage(msg string) { - f.mustWriteOut(msg + SqlcmdEol) -} - -// Writes an error to the designated err Writer -func (f *sqlCmdFormatterType) AddError(err error) { - print := true - b := new(strings.Builder) - msg := err.Error() - switch e := (err).(type) { - case mssql.Error: - if print = f.vars.ErrorLevel() <= 0 || e.Class >= uint8(f.vars.ErrorLevel()); print { - b.WriteString(fmt.Sprintf("Msg %d, Level %d, State %d, Server %s, Line %d%s", e.Number, e.Class, e.State, e.ServerName, e.LineNo, SqlcmdEol)) - msg = strings.TrimPrefix(msg, "mssql: ") - } - } - if print { - b.WriteString(msg) - b.WriteString(SqlcmdEol) - f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String()) - } -} - -// Prints column headings based on columnDetail, variables, and command line arguments -func (f *sqlCmdFormatterType) printColumnHeadings() { - names := new(strings.Builder) - sep := new(strings.Builder) - - var leftPad, rightPad int64 - for i, c := range f.columnDetails { - rightPad = 0 - nameLen := int64(len([]rune(c.col.Name()))) - if f.removeTrailingSpaces { - if nameLen == 0 { - // special case for unnamed columns when using -W - // print a single - - rightPad = 1 - sep = padRight(sep, 1, "-") - } else { - sep = padRight(sep, nameLen, "-") - } - } else { - length := min64(c.displayWidth, maxPadWidth) - if nameLen < length { - rightPad = length - nameLen - } - sep = padRight(sep, length, "-") - } - names = padRight(names, leftPad, " ") - names.WriteString(c.col.Name()[:min64(nameLen, c.displayWidth)]) - names = padRight(names, rightPad, " ") - if i != len(f.columnDetails)-1 { - names.WriteString(f.colsep) - sep.WriteString(f.colsep) - } - } - names.WriteString(SqlcmdEol) - sep.WriteString(SqlcmdEol) - names = fitToScreen(names, f.vars.ScreenWidth()) - sep = fitToScreen(sep, f.vars.ScreenWidth()) - f.mustWriteOut(names.String()) - f.mustWriteOut(sep.String()) -} - -// Wraps the input string every width characters when width > 0 -// When width == 0 returns the input Builder -// When width > 0 returns a new Builder containing the wrapped string -func fitToScreen(s *strings.Builder, width int64) *strings.Builder { - str := s.String() - runes := []rune(str) - if width == 0 || int64(len(runes)) < width { - return s - } - - line := new(strings.Builder) - line.Grow(len(str)) - var c int64 - for i, r := range runes { - if c == width { - // We have printed a line's worth - // if the next character is not part of a carriage return write our Eol - if (SqlcmdEol == "\r\n" && (i == len(runes)-1 || (i < len(runes)-1 && string(runes[i:i+2]) != SqlcmdEol))) || (SqlcmdEol == "\n" && r != '\n') { - line.WriteString(SqlcmdEol) - c = 0 - } - } - line.WriteRune(r) - if r == '\n' { - c = 0 - // we are assuming \r is a non-printed character - // The likelihood of a \r not being followed by \n is low - } else if r == '\r' && SqlcmdEol == "\r\n" { - c = 0 - } else { - c++ - } - } - return line -} - -// Given the array of driver-provided columnType values and the sqlcmd size limits, -// Return an array of columnDetail objects describing the output format for each column. -// Return the length of the longest column name. -func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) ([]columnDetail, int) { - columnDetails := make([]columnDetail, len(cols)) - maxNameLen := 0 - for i, c := range cols { - length, _ := c.Length() - nameLen := int64(len([]rune(c.Name()))) - if nameLen > int64(maxNameLen) { - maxNameLen = int(nameLen) - } - columnDetails[i].col = *c - columnDetails[i].leftJustify = true - columnDetails[i].zeroesAfterDecimal = false - p, s, ok := c.DecimalSize() - if ok { - columnDetails[i].precision = int(p) - columnDetails[i].scale = int(s) - } - if length == 0 { - columnDetails[i].displayWidth = defaultMaxDisplayWidth - } else { - columnDetails[i].displayWidth = length - } - typeName := c.DatabaseTypeName() - - switch typeName { - // Types with 0 size from sql.ColumnType - case "BIT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(1, nameLen) - case "TINYINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(3, nameLen) - case "SMALLINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(6, nameLen) - case "INT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(11, nameLen) - case "BIGINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(21, nameLen) - case "REAL", "SMALLMONEY": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(14, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "FLOAT", "MONEY": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(24, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DECIMAL": - columnDetails[i].leftJustify = false - d, _, ok := c.DecimalSize() - // maybe panic on !ok? - if !ok { - d = 24 - } - columnDetails[i].displayWidth = max64(d+2, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DATE": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(16, nameLen) - case "DATETIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(23, nameLen) - case "SMALLDATETIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(19, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DATETIME2": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(38, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "TIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(16, nameLen) - case "DATETIMEOFFSET": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(45, nameLen) - case "UNIQUEIDENTIFIER": - columnDetails[i].displayWidth = max64(36, nameLen) - // Types that can be fixed or variable - case "VARCHAR": - if length > 8000 { - columnDetails[i].displayWidth = variable - } else { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } - case "NVARCHAR": - if length > 4000 { - columnDetails[i].displayWidth = variable - } else { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } - case "VARBINARY": - if length <= 8000 { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } else { - columnDetails[i].displayWidth = variable - } - case "SQL_VARIANT": - if fixed > 0 { - columnDetails[i].displayWidth = min64(fixed, 8000) - } else { - columnDetails[i].displayWidth = 8000 - } - // Fixed length types - case "CHAR", "NCHAR": - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - // Variable length types - // TODO: Fix BINARY once we have a driver with fix for https://github.com/denisenkom/go-mssqldb/issues/685 - case "XML", "TEXT", "NTEXT", "IMAGE", "BINARY": - columnDetails[i].displayWidth = variable - default: - columnDetails[i].displayWidth = length - } - // When max var length is 0 we don't print column headers and print every value with unlimited width - if variable == 0 { - columnDetails[i].displayWidth = 0 - } - } - return columnDetails, maxNameLen -} - -// scanRow fetches the next row and converts each value to the appropriate string representation -func (f *sqlCmdFormatterType) scanRow(rows *sql.Rows) ([]string, error) { - r := make([]interface{}, len(f.columnDetails)) - for i := range r { - r[i] = new(interface{}) - } - if err := rows.Scan(r...); err != nil { - return nil, err - } - row := make([]string, len(f.columnDetails)) - for n, z := range r { - j := z.(*interface{}) - if *j == nil { - row[n] = "NULL" - } else { - switch x := (*j).(type) { - case []byte: - if isBinaryDataType(&f.columnDetails[n].col) { - row[n] = decodeBinary(x) - } else if f.columnDetails[n].col.DatabaseTypeName() == "UNIQUEIDENTIFIER" { - // Unscramble the guid - // see https://github.com/denisenkom/go-mssqldb/issues/56 - x[0], x[1], x[2], x[3] = x[3], x[2], x[1], x[0] - x[4], x[5] = x[5], x[4] - x[6], x[7] = x[7], x[6] - if guid, err := uuid.FromBytes(x); err == nil { - row[n] = guid.String() - } else { - // this should never happen - row[n] = uuid.New().String() - } - } else { - row[n] = string(x) - } - case string: - row[n] = x - case time.Time: - // Go lacks any way to get the user's preferred time format or even the system default - switch f.columnDetails[n].col.DatabaseTypeName() { - case "DATE": - row[n] = x.Format("2006-01-02") - case "DATETIME": - row[n] = x.Format(dateTimeFormatString(3, false)) - case "DATETIME2": - row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, false)) - case "SMALLDATETIME": - row[n] = x.Format(dateTimeFormatString(0, false)) - case "DATETIMEOFFSET": - row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, true)) - case "TIME": - format := "15:04:05" - if f.columnDetails[n].scale > 0 { - format = fmt.Sprintf("%s.%0*d", format, f.columnDetails[n].scale, 0) - } - row[n] = x.Format(format) - default: - row[n] = x.Format(time.RFC3339) - } - case fmt.Stringer: - row[n] = x.String() - // not sure why go-mssql reports bit as bool - case bool: - if x { - row[n] = "1" - } else { - row[n] = "0" - } - default: - var err error - if row[n], err = fmt.Sprintf("%v", x), nil; err != nil { - return nil, err - } - } - } - } - return row, nil -} - -func dateTimeFormatString(scale int, addOffset bool) string { - format := `2006-01-02 15:04:05` - if scale > 0 { - format = fmt.Sprintf("%s.%0*d", format, scale, 0) - } - if addOffset { - format += " -07:00" - } - return format -} - -// Prints the final version of a cell based on formatting variables and command line parameters -func (f *sqlCmdFormatterType) printColumnValue(val string, col int) { - c := f.columnDetails[col] - s := new(strings.Builder) - if isNeedingControlCharacterTreatment(&c.col) { - val = applyControlCharacterBehavior(val, f.ccb) - } - - if isNeedingHexPrefix(&c.col) { - val = "0x" + val - } - - s.WriteString(val) - r := []rune(val) - if f.format == "horizontal" { - if !f.removeTrailingSpaces { - if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) { - padding := c.displayWidth - min64(c.displayWidth, int64(len(r))) - if padding > 0 { - if c.leftJustify { - s = padRight(s, padding, " ") - } else { - s = padLeft(s, padding, " ") - } - } - } - } - - r = []rune(s.String()) - } - if c.displayWidth > 0 && int64(len(r)) > c.displayWidth { - s.Reset() - s.WriteString(string(r[:c.displayWidth])) - } - f.writeOut(s.String()) -} - -func (f *sqlCmdFormatterType) mustWriteOut(s string) { - _, err := f.out.Write([]byte(s)) - if err != nil { - panic(err) - } -} - -func (f *sqlCmdFormatterType) mustWriteErr(s string) { - _, err := f.err.Write([]byte(s)) - if err != nil { - panic(err) - } -} - -func isLargeVariableType(col *sql.ColumnType) bool { - l, _ := col.Length() - switch col.DatabaseTypeName() { - - case "VARCHAR", "VARBINARY": - return l > 8000 - case "NVARCHAR": - return l > 4000 - case "XML", "TEXT", "NTEXT", "IMAGE": - return true - } - return false -} - -func isNeedingControlCharacterTreatment(col *sql.ColumnType) bool { - switch col.DatabaseTypeName() { - case "CHAR", "VARCHAR", "TEXT", "NTEXT", "NCHAR", "NVARCHAR", "XML": - return true - } - return false -} -func isBinaryDataType(col *sql.ColumnType) bool { - switch col.DatabaseTypeName() { - case "BINARY", "VARBINARY": - return true - } - return false -} - -func isNeedingHexPrefix(col *sql.ColumnType) bool { - return isBinaryDataType(col) // || col.DatabaseTypeName() == "UDT" -} - -func isControlChar(r rune) bool { - c := int(r) - return c == 0x7f || (c >= 0 && c <= 0x1f) -} - -func applyControlCharacterBehavior(val string, ccb ControlCharacterBehavior) string { - if ccb == ControlIgnore { - return val - } - b := new(strings.Builder) - r := []rune(val) - if ccb == ControlReplace { - for _, l := range r { - if isControlChar(l) { - b.WriteRune(' ') - } else { - b.WriteRune(l) - } - } - } else { - for i := 0; i < len(r); { - if !isControlChar(r[i]) { - b.WriteRune(r[i]) - i++ - } else { - for ; i < len(r) && isControlChar(r[i]); i++ { - } - if ccb == ControlReplaceConsecutive { - b.WriteRune(' ') - } - } - } - } - return b.String() -} - -// Per https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-binary -var hexDigits = []rune{'A', 'B', 'C', 'D', 'E', 'F'} - -func decodeBinary(b []byte) string { - - s := new(strings.Builder) - s.Grow(len(b) * 2) - for _, ch := range b { - b1 := ch >> 4 - b2 := ch & 0x0f - if b1 >= 10 { - s.WriteRune(hexDigits[b1-10]) - } else { - s.WriteRune(rune('0' + b1)) - } - if b2 >= 10 { - s.WriteRune(hexDigits[b2-10]) - } else { - s.WriteRune(rune('0' + b2)) - } - } - return s.String() -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "database/sql" + "fmt" + "io" + "strings" + "time" + + "github.com/google/uuid" + mssql "github.com/microsoft/go-mssqldb" +) + +const ( + defaultMaxDisplayWidth = 1024 * 1024 + maxPadWidth = 8000 +) + +// Formatter defines methods to process query output +type Formatter interface { + // BeginBatch is called before the query runs + BeginBatch(query string, vars *Variables, out io.Writer, err io.Writer) + // EndBatch is the last function called during batch execution and signals the end of the batch + EndBatch() + // BeginResultSet is called when a new result set is encountered + BeginResultSet([]*sql.ColumnType) + // EndResultSet is called after all rows in a result set have been processed + EndResultSet() + // AddRow is called for each row in a result set. It returns the value of the first column + AddRow(*sql.Rows) string + // AddMessage is called for every information message returned by the server during the batch + AddMessage(string) + // AddError is called for each error encountered during batch execution + AddError(err error) +} + +// ControlCharacterBehavior specifies the text handling required for control characters in the output +type ControlCharacterBehavior int + +const ( + // ControlIgnore preserves control characters in the output + ControlIgnore ControlCharacterBehavior = iota + // ControlReplace replaces control characters with spaces, 1 space per character + ControlReplace + // ControlRemove removes control characters from the output + ControlRemove + // ControlReplaceConsecutive replaces multiple consecutive control characters with a single space + ControlReplaceConsecutive +) + +type columnDetail struct { + displayWidth int64 + leftJustify bool + zeroesAfterDecimal bool + col sql.ColumnType + precision int + scale int +} + +// The default formatter based on the native sqlcmd style +// It supports both horizontal (default) and vertical layout for results. +// Both vertical and horizontal layouts respect column widths set by SQLCMD variables. +type sqlCmdFormatterType struct { + out io.Writer + err io.Writer + vars *Variables + colsep string + removeTrailingSpaces bool + ccb ControlCharacterBehavior + columnDetails []columnDetail + rowcount int + writepos int64 + format string + maxColNameLen int +} + +// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter +func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool) Formatter { + return &sqlCmdFormatterType{ + removeTrailingSpaces: removeTrailingSpaces, + format: "horizontal", + } +} + +// Adds the given string to the current line, wrapping it based on the screen width setting +func (f *sqlCmdFormatterType) writeOut(s string) { + w := f.vars.ScreenWidth() + if w == 0 { + f.mustWriteOut(s) + return + } + + r := []rune(s) + for i := 0; true; { + if i == len(r) { + f.mustWriteOut(string(r)) + return + } else if f.writepos == w { + f.mustWriteOut(string(r[:i])) + f.mustWriteOut(SqlcmdEol) + r = []rune(string(r[i:])) + f.writepos = 0 + i = 0 + } else { + c := r[i] + if c != '\r' && c != '\n' { + f.writepos++ + } else { + f.writepos = 0 + } + i++ + } + } +} + +// Stores the settings to use for processing the current batch +// TODO: add a third io.Writer for messages when we add -r support +func (f *sqlCmdFormatterType) BeginBatch(_ string, vars *Variables, out io.Writer, err io.Writer) { + f.out = out + f.err = err + f.vars = vars + f.colsep = vars.ColumnSeparator() + f.format = vars.Format() +} + +func (f *sqlCmdFormatterType) EndBatch() { +} + +// Calculate the widths for each column and print the column names +// Since sql.ColumnType only provides sizes for variable length types we will +// base our numbers for most types on https://docs.microsoft.com/sql/odbc/reference/appendixes/column-size +func (f *sqlCmdFormatterType) BeginResultSet(cols []*sql.ColumnType) { + f.rowcount = 0 + f.columnDetails, f.maxColNameLen = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth()) + if f.vars.RowsBetweenHeaders() > -1 && f.format == "horizontal" { + f.printColumnHeadings() + } +} + +// Writes a blank line to the designated output writer +func (f *sqlCmdFormatterType) EndResultSet() { + f.writeOut(SqlcmdEol) +} + +// Writes the current row to the designated output writer +func (f *sqlCmdFormatterType) AddRow(row *sql.Rows) string { + retval := "" + values, err := f.scanRow(row) + if err != nil { + f.mustWriteErr(err.Error()) + return retval + } + retval = values[0] + if f.format == "horizontal" { + // values are the full values, look at the displaywidth of each column and truncate accordingly + for i, v := range values { + if i > 0 { + f.writeOut(f.vars.ColumnSeparator()) + } + f.printColumnValue(v, i) + } + f.rowcount++ + gap := f.vars.RowsBetweenHeaders() + if gap > 0 && (int64(f.rowcount)%gap == 0) { + f.writeOut(SqlcmdEol) + f.printColumnHeadings() + } + } else { + f.addVerticalRow(values) + } + f.writeOut(SqlcmdEol) + return retval + +} + +func (f *sqlCmdFormatterType) addVerticalRow(values []string) { + for i, v := range values { + if f.vars.RowsBetweenHeaders() > -1 { + builder := new(strings.Builder) + name := f.columnDetails[i].col.Name() + builder.WriteString(name) + builder = padRight(builder, int64(f.maxColNameLen-len(name)+1), " ") + f.writeOut(builder.String()) + } + f.printColumnValue(v, i) + f.writeOut(SqlcmdEol) + } +} + +// Writes a non-error message to the designated message writer +func (f *sqlCmdFormatterType) AddMessage(msg string) { + f.mustWriteOut(msg + SqlcmdEol) +} + +// Writes an error to the designated err Writer +func (f *sqlCmdFormatterType) AddError(err error) { + print := true + b := new(strings.Builder) + msg := err.Error() + switch e := (err).(type) { + case mssql.Error: + if print = f.vars.ErrorLevel() <= 0 || e.Class >= uint8(f.vars.ErrorLevel()); print { + b.WriteString(fmt.Sprintf("Msg %d, Level %d, State %d, Server %s, Line %d%s", e.Number, e.Class, e.State, e.ServerName, e.LineNo, SqlcmdEol)) + msg = strings.TrimPrefix(msg, "mssql: ") + } + } + if print { + b.WriteString(msg) + b.WriteString(SqlcmdEol) + f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String()) + } +} + +// Prints column headings based on columnDetail, variables, and command line arguments +func (f *sqlCmdFormatterType) printColumnHeadings() { + names := new(strings.Builder) + sep := new(strings.Builder) + + var leftPad, rightPad int64 + for i, c := range f.columnDetails { + rightPad = 0 + nameLen := int64(len([]rune(c.col.Name()))) + if f.removeTrailingSpaces { + if nameLen == 0 { + // special case for unnamed columns when using -W + // print a single - + rightPad = 1 + sep = padRight(sep, 1, "-") + } else { + sep = padRight(sep, nameLen, "-") + } + } else { + length := min64(c.displayWidth, maxPadWidth) + if nameLen < length { + rightPad = length - nameLen + } + sep = padRight(sep, length, "-") + } + names = padRight(names, leftPad, " ") + names.WriteString(c.col.Name()[:min64(nameLen, c.displayWidth)]) + names = padRight(names, rightPad, " ") + if i != len(f.columnDetails)-1 { + names.WriteString(f.colsep) + sep.WriteString(f.colsep) + } + } + names.WriteString(SqlcmdEol) + sep.WriteString(SqlcmdEol) + names = fitToScreen(names, f.vars.ScreenWidth()) + sep = fitToScreen(sep, f.vars.ScreenWidth()) + f.mustWriteOut(names.String()) + f.mustWriteOut(sep.String()) +} + +// Wraps the input string every width characters when width > 0 +// When width == 0 returns the input Builder +// When width > 0 returns a new Builder containing the wrapped string +func fitToScreen(s *strings.Builder, width int64) *strings.Builder { + str := s.String() + runes := []rune(str) + if width == 0 || int64(len(runes)) < width { + return s + } + + line := new(strings.Builder) + line.Grow(len(str)) + var c int64 + for i, r := range runes { + if c == width { + // We have printed a line's worth + // if the next character is not part of a carriage return write our Eol + if (SqlcmdEol == "\r\n" && (i == len(runes)-1 || (i < len(runes)-1 && string(runes[i:i+2]) != SqlcmdEol))) || (SqlcmdEol == "\n" && r != '\n') { + line.WriteString(SqlcmdEol) + c = 0 + } + } + line.WriteRune(r) + if r == '\n' { + c = 0 + // we are assuming \r is a non-printed character + // The likelihood of a \r not being followed by \n is low + } else if r == '\r' && SqlcmdEol == "\r\n" { + c = 0 + } else { + c++ + } + } + return line +} + +// Given the array of driver-provided columnType values and the sqlcmd size limits, +// Return an array of columnDetail objects describing the output format for each column. +// Return the length of the longest column name. +func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) ([]columnDetail, int) { + columnDetails := make([]columnDetail, len(cols)) + maxNameLen := 0 + for i, c := range cols { + length, _ := c.Length() + nameLen := int64(len([]rune(c.Name()))) + if nameLen > int64(maxNameLen) { + maxNameLen = int(nameLen) + } + columnDetails[i].col = *c + columnDetails[i].leftJustify = true + columnDetails[i].zeroesAfterDecimal = false + p, s, ok := c.DecimalSize() + if ok { + columnDetails[i].precision = int(p) + columnDetails[i].scale = int(s) + } + if length == 0 { + columnDetails[i].displayWidth = defaultMaxDisplayWidth + } else { + columnDetails[i].displayWidth = length + } + typeName := c.DatabaseTypeName() + + switch typeName { + // Types with 0 size from sql.ColumnType + case "BIT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(1, nameLen) + case "TINYINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(3, nameLen) + case "SMALLINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(6, nameLen) + case "INT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(11, nameLen) + case "BIGINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(21, nameLen) + case "REAL", "SMALLMONEY": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(14, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "FLOAT", "MONEY": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(24, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DECIMAL": + columnDetails[i].leftJustify = false + d, _, ok := c.DecimalSize() + // maybe panic on !ok? + if !ok { + d = 24 + } + columnDetails[i].displayWidth = max64(d+2, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DATE": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(16, nameLen) + case "DATETIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(23, nameLen) + case "SMALLDATETIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(19, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DATETIME2": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(38, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "TIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(16, nameLen) + case "DATETIMEOFFSET": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(45, nameLen) + case "UNIQUEIDENTIFIER": + columnDetails[i].displayWidth = max64(36, nameLen) + // Types that can be fixed or variable + case "VARCHAR": + if length > 8000 { + columnDetails[i].displayWidth = variable + } else { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } + case "NVARCHAR": + if length > 4000 { + columnDetails[i].displayWidth = variable + } else { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } + case "VARBINARY": + if length <= 8000 { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } else { + columnDetails[i].displayWidth = variable + } + case "SQL_VARIANT": + if fixed > 0 { + columnDetails[i].displayWidth = min64(fixed, 8000) + } else { + columnDetails[i].displayWidth = 8000 + } + // Fixed length types + case "CHAR", "NCHAR": + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + // Variable length types + // TODO: Fix BINARY once we have a driver with fix for https://github.com/denisenkom/go-mssqldb/issues/685 + case "XML", "TEXT", "NTEXT", "IMAGE", "BINARY": + columnDetails[i].displayWidth = variable + default: + columnDetails[i].displayWidth = length + } + // When max var length is 0 we don't print column headers and print every value with unlimited width + if variable == 0 { + columnDetails[i].displayWidth = 0 + } + } + return columnDetails, maxNameLen +} + +// scanRow fetches the next row and converts each value to the appropriate string representation +func (f *sqlCmdFormatterType) scanRow(rows *sql.Rows) ([]string, error) { + r := make([]interface{}, len(f.columnDetails)) + for i := range r { + r[i] = new(interface{}) + } + if err := rows.Scan(r...); err != nil { + return nil, err + } + row := make([]string, len(f.columnDetails)) + for n, z := range r { + j := z.(*interface{}) + if *j == nil { + row[n] = "NULL" + } else { + switch x := (*j).(type) { + case []byte: + if isBinaryDataType(&f.columnDetails[n].col) { + row[n] = decodeBinary(x) + } else if f.columnDetails[n].col.DatabaseTypeName() == "UNIQUEIDENTIFIER" { + // Unscramble the guid + // see https://github.com/denisenkom/go-mssqldb/issues/56 + x[0], x[1], x[2], x[3] = x[3], x[2], x[1], x[0] + x[4], x[5] = x[5], x[4] + x[6], x[7] = x[7], x[6] + if guid, err := uuid.FromBytes(x); err == nil { + row[n] = guid.String() + } else { + // this should never happen + row[n] = uuid.New().String() + } + } else { + row[n] = string(x) + } + case string: + row[n] = x + case time.Time: + // Go lacks any way to get the user's preferred time format or even the system default + switch f.columnDetails[n].col.DatabaseTypeName() { + case "DATE": + row[n] = x.Format("2006-01-02") + case "DATETIME": + row[n] = x.Format(dateTimeFormatString(3, false)) + case "DATETIME2": + row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, false)) + case "SMALLDATETIME": + row[n] = x.Format(dateTimeFormatString(0, false)) + case "DATETIMEOFFSET": + row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, true)) + case "TIME": + format := "15:04:05" + if f.columnDetails[n].scale > 0 { + format = fmt.Sprintf("%s.%0*d", format, f.columnDetails[n].scale, 0) + } + row[n] = x.Format(format) + default: + row[n] = x.Format(time.RFC3339) + } + case fmt.Stringer: + row[n] = x.String() + // not sure why go-mssql reports bit as bool + case bool: + if x { + row[n] = "1" + } else { + row[n] = "0" + } + default: + var err error + if row[n], err = fmt.Sprintf("%v", x), nil; err != nil { + return nil, err + } + } + } + } + return row, nil +} + +func dateTimeFormatString(scale int, addOffset bool) string { + format := `2006-01-02 15:04:05` + if scale > 0 { + format = fmt.Sprintf("%s.%0*d", format, scale, 0) + } + if addOffset { + format += " -07:00" + } + return format +} + +// Prints the final version of a cell based on formatting variables and command line parameters +func (f *sqlCmdFormatterType) printColumnValue(val string, col int) { + c := f.columnDetails[col] + s := new(strings.Builder) + if isNeedingControlCharacterTreatment(&c.col) { + val = applyControlCharacterBehavior(val, f.ccb) + } + + if isNeedingHexPrefix(&c.col) { + val = "0x" + val + } + + s.WriteString(val) + r := []rune(val) + if f.format == "horizontal" { + if !f.removeTrailingSpaces { + if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) { + padding := c.displayWidth - min64(c.displayWidth, int64(len(r))) + if padding > 0 { + if c.leftJustify { + s = padRight(s, padding, " ") + } else { + s = padLeft(s, padding, " ") + } + } + } + } + + r = []rune(s.String()) + } + if c.displayWidth > 0 && int64(len(r)) > c.displayWidth { + s.Reset() + s.WriteString(string(r[:c.displayWidth])) + } + f.writeOut(s.String()) +} + +func (f *sqlCmdFormatterType) mustWriteOut(s string) { + _, err := f.out.Write([]byte(s)) + if err != nil { + panic(err) + } +} + +func (f *sqlCmdFormatterType) mustWriteErr(s string) { + _, err := f.err.Write([]byte(s)) + if err != nil { + panic(err) + } +} + +func isLargeVariableType(col *sql.ColumnType) bool { + l, _ := col.Length() + switch col.DatabaseTypeName() { + + case "VARCHAR", "VARBINARY": + return l > 8000 + case "NVARCHAR": + return l > 4000 + case "XML", "TEXT", "NTEXT", "IMAGE": + return true + } + return false +} + +func isNeedingControlCharacterTreatment(col *sql.ColumnType) bool { + switch col.DatabaseTypeName() { + case "CHAR", "VARCHAR", "TEXT", "NTEXT", "NCHAR", "NVARCHAR", "XML": + return true + } + return false +} +func isBinaryDataType(col *sql.ColumnType) bool { + switch col.DatabaseTypeName() { + case "BINARY", "VARBINARY": + return true + } + return false +} + +func isNeedingHexPrefix(col *sql.ColumnType) bool { + return isBinaryDataType(col) // || col.DatabaseTypeName() == "UDT" +} + +func isControlChar(r rune) bool { + c := int(r) + return c == 0x7f || (c >= 0 && c <= 0x1f) +} + +func applyControlCharacterBehavior(val string, ccb ControlCharacterBehavior) string { + if ccb == ControlIgnore { + return val + } + b := new(strings.Builder) + r := []rune(val) + if ccb == ControlReplace { + for _, l := range r { + if isControlChar(l) { + b.WriteRune(' ') + } else { + b.WriteRune(l) + } + } + } else { + for i := 0; i < len(r); { + if !isControlChar(r[i]) { + b.WriteRune(r[i]) + i++ + } else { + for ; i < len(r) && isControlChar(r[i]); i++ { + } + if ccb == ControlReplaceConsecutive { + b.WriteRune(' ') + } + } + } + } + return b.String() +} + +// Per https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-binary +var hexDigits = []rune{'A', 'B', 'C', 'D', 'E', 'F'} + +func decodeBinary(b []byte) string { + + s := new(strings.Builder) + s.Grow(len(b) * 2) + for _, ch := range b { + b1 := ch >> 4 + b2 := ch & 0x0f + if b1 >= 10 { + s.WriteRune(hexDigits[b1-10]) + } else { + s.WriteRune(rune('0' + b1)) + } + if b2 >= 10 { + s.WriteRune(hexDigits[b2-10]) + } else { + s.WriteRune(rune('0' + b2)) + } + } + return s.String() +} diff --git a/pkg/sqlcmd/format_darwin.go b/pkg/sqlcmd/format_darwin.go index fc16bf8c..f9bc89cf 100644 --- a/pkg/sqlcmd/format_darwin.go +++ b/pkg/sqlcmd/format_darwin.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\n" diff --git a/pkg/sqlcmd/format_linux.go b/pkg/sqlcmd/format_linux.go index fc16bf8c..f9bc89cf 100644 --- a/pkg/sqlcmd/format_linux.go +++ b/pkg/sqlcmd/format_linux.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\n" diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 39224500..48387ebc 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -1,140 +1,140 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "context" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFitToScreen(t *testing.T) { - type fitTest struct { - width int64 - raw string - fit string - } - - tests := []fitTest{ - {0, "this is a string", "this is a string"}, - {9, "12345678", "12345678"}, - {9, "123456789", "123456789"}, - {9, "123456789A", "123456789" + SqlcmdEol + "A"}, - {9, "123456789" + SqlcmdEol, "123456789" + SqlcmdEol}, - {9, "12345678" + SqlcmdEol + "9A", "12345678" + SqlcmdEol + "9A"}, - {9, "123456789\rA", "123456789" + SqlcmdEol + "\rA"}, - } - - for _, test := range tests { - - line := new(strings.Builder) - line.WriteString(test.raw) - t.Log(test.raw) - f := fitToScreen(line, test.width).String() - assert.Equal(t, test.fit, f, "Mismatched fit for raw string: '%s'", test.raw) - } -} - -func TestCalcColumnDetails(t *testing.T) { - type colTest struct { - fixed int64 - variable int64 - query string - details []columnDetail - max int - } - - tests := []colTest{ - {8, 8, - "select 100 as '123456789ABC', getdate() as '987654321', 'string' as col1", - []columnDetail{ - {leftJustify: false, displayWidth: 12}, - {leftJustify: false, displayWidth: 23}, - {leftJustify: true, displayWidth: 6}, - }, - 12, - }, - } - - db, err := ConnectDb(t) - if assert.NoError(t, err, "ConnectDB failed") { - defer db.Close() - for x, test := range tests { - rows, err := db.QueryContext(context.Background(), test.query) - if assert.NoError(t, err, "Query failed: %s", test.query) { - defer rows.Close() - cols, err := rows.ColumnTypes() - if assert.NoError(t, err, "ColumnTypes failed:%s", test.query) { - actual, max := calcColumnDetails(cols, test.fixed, test.variable) - for i, a := range actual { - if test.details[i].displayWidth != a.displayWidth || - test.details[i].leftJustify != a.leftJustify || - test.details[i].zeroesAfterDecimal != a.zeroesAfterDecimal { - assert.Failf(t, "", "[%d] Incorrect test details for column [%s] in query '%s':%+v", x, cols[i].Name(), test.query, a) - } - assert.Equal(t, test.max, max, "[%d] Max column name length incorrect", x) - } - } - } - } - } -} - -func TestControlCharacterBehavior(t *testing.T) { - type ccbTest struct { - raw string - replaced string - removed string - consecutivereplaced string - } - - tests := []ccbTest{ - {"no control", "no control", "no control", "no control"}, - {string(rune(1)) + "tabs\t\treturns\r\n\r\n", " tabs returns ", "tabsreturns", " tabs returns "}, - } - - for _, test := range tests { - s := applyControlCharacterBehavior(test.raw, ControlReplace) - assert.Equalf(t, test.replaced, s, "Incorrect Replaced for '%s'", test.raw) - s = applyControlCharacterBehavior(test.raw, ControlRemove) - assert.Equalf(t, test.removed, s, "Incorrect Remove for '%s'", test.raw) - s = applyControlCharacterBehavior(test.raw, ControlReplaceConsecutive) - assert.Equalf(t, test.consecutivereplaced, s, "Incorrect ReplaceConsecutive for '%s'", test.raw) - } -} - -func TestDecodeBinary(t *testing.T) { - type decodeTest struct { - b []byte - s string - } - - tests := []decodeTest{ - {[]byte("123456ABCDEF"), "313233343536414243444546"}, - {[]byte{0x12, 0x34, 0x56}, "123456"}, - } - for _, test := range tests { - a := decodeBinary(test.b) - assert.Equalf(t, test.s, a, "Incorrect decoded binary string for %v", test.b) - } -} - -func BenchmarkDecodeBinary(b *testing.B) { - b.ReportAllocs() - bytes := make([]byte, 10000) - for i := 0; i < 10000; i++ { - bytes[i] = byte(i % 0xff) - } - b.ResetTimer() - - for i := 0; i < b.N; i++ { - s := decodeBinary(bytes) - if len(s) != 20000 { - b.Fatalf("Incorrect length of returned string. Should be 20k, was %d", len(s)) - } - } - -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFitToScreen(t *testing.T) { + type fitTest struct { + width int64 + raw string + fit string + } + + tests := []fitTest{ + {0, "this is a string", "this is a string"}, + {9, "12345678", "12345678"}, + {9, "123456789", "123456789"}, + {9, "123456789A", "123456789" + SqlcmdEol + "A"}, + {9, "123456789" + SqlcmdEol, "123456789" + SqlcmdEol}, + {9, "12345678" + SqlcmdEol + "9A", "12345678" + SqlcmdEol + "9A"}, + {9, "123456789\rA", "123456789" + SqlcmdEol + "\rA"}, + } + + for _, test := range tests { + + line := new(strings.Builder) + line.WriteString(test.raw) + t.Log(test.raw) + f := fitToScreen(line, test.width).String() + assert.Equal(t, test.fit, f, "Mismatched fit for raw string: '%s'", test.raw) + } +} + +func TestCalcColumnDetails(t *testing.T) { + type colTest struct { + fixed int64 + variable int64 + query string + details []columnDetail + max int + } + + tests := []colTest{ + {8, 8, + "select 100 as '123456789ABC', getdate() as '987654321', 'string' as col1", + []columnDetail{ + {leftJustify: false, displayWidth: 12}, + {leftJustify: false, displayWidth: 23}, + {leftJustify: true, displayWidth: 6}, + }, + 12, + }, + } + + db, err := ConnectDb(t) + if assert.NoError(t, err, "ConnectDB failed") { + defer db.Close() + for x, test := range tests { + rows, err := db.QueryContext(context.Background(), test.query) + if assert.NoError(t, err, "Query failed: %s", test.query) { + defer rows.Close() + cols, err := rows.ColumnTypes() + if assert.NoError(t, err, "ColumnTypes failed:%s", test.query) { + actual, max := calcColumnDetails(cols, test.fixed, test.variable) + for i, a := range actual { + if test.details[i].displayWidth != a.displayWidth || + test.details[i].leftJustify != a.leftJustify || + test.details[i].zeroesAfterDecimal != a.zeroesAfterDecimal { + assert.Failf(t, "", "[%d] Incorrect test details for column [%s] in query '%s':%+v", x, cols[i].Name(), test.query, a) + } + assert.Equal(t, test.max, max, "[%d] Max column name length incorrect", x) + } + } + } + } + } +} + +func TestControlCharacterBehavior(t *testing.T) { + type ccbTest struct { + raw string + replaced string + removed string + consecutivereplaced string + } + + tests := []ccbTest{ + {"no control", "no control", "no control", "no control"}, + {string(rune(1)) + "tabs\t\treturns\r\n\r\n", " tabs returns ", "tabsreturns", " tabs returns "}, + } + + for _, test := range tests { + s := applyControlCharacterBehavior(test.raw, ControlReplace) + assert.Equalf(t, test.replaced, s, "Incorrect Replaced for '%s'", test.raw) + s = applyControlCharacterBehavior(test.raw, ControlRemove) + assert.Equalf(t, test.removed, s, "Incorrect Remove for '%s'", test.raw) + s = applyControlCharacterBehavior(test.raw, ControlReplaceConsecutive) + assert.Equalf(t, test.consecutivereplaced, s, "Incorrect ReplaceConsecutive for '%s'", test.raw) + } +} + +func TestDecodeBinary(t *testing.T) { + type decodeTest struct { + b []byte + s string + } + + tests := []decodeTest{ + {[]byte("123456ABCDEF"), "313233343536414243444546"}, + {[]byte{0x12, 0x34, 0x56}, "123456"}, + } + for _, test := range tests { + a := decodeBinary(test.b) + assert.Equalf(t, test.s, a, "Incorrect decoded binary string for %v", test.b) + } +} + +func BenchmarkDecodeBinary(b *testing.B) { + b.ReportAllocs() + bytes := make([]byte, 10000) + for i := 0; i < 10000; i++ { + bytes[i] = byte(i % 0xff) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + s := decodeBinary(bytes) + if len(s) != 20000 { + b.Fatalf("Incorrect length of returned string. Should be 20k, was %d", len(s)) + } + } + +} diff --git a/pkg/sqlcmd/format_windows.go b/pkg/sqlcmd/format_windows.go index 9d7b6f32..7d22ae36 100644 --- a/pkg/sqlcmd/format_windows.go +++ b/pkg/sqlcmd/format_windows.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\r\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\r\n" diff --git a/pkg/sqlcmd/parse.go b/pkg/sqlcmd/parse.go index e9192c78..f2549c7a 100644 --- a/pkg/sqlcmd/parse.go +++ b/pkg/sqlcmd/parse.go @@ -1,101 +1,101 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "strings" - "unicode" -) - -// grab grabs i from r, or returns 0 if i >= end. -func grab(r []rune, i, end int) rune { - if i < end { - return r[i] - } - return 0 -} - -// findNonSpace finds first non space rune in r, returning end if not found. -func findNonSpace(r []rune, i, end int) (int, bool) { - for ; i < end; i++ { - if !isSpaceOrControl(r[i]) { - return i, true - } - } - return i, false -} - -// isEmptyLine returns true when r is empty or composed of only whitespace. -func isEmptyLine(r []rune, i, end int) bool { - _, ok := findNonSpace(r, i, end) - return !ok -} - -// readMultilineComment finds the end of a multiline comment (ie, '*/'). -func readMultilineComment(r []rune, i, end int) (int, bool) { - i++ - for ; i < end; i++ { - if r[i-1] == '*' && r[i] == '/' { - return i, true - } - } - return end, false -} - -// readCommand reads to the next control character to find -// a command in the string. Command regexes constrain matches -// to the beginning of the string, and all commands consume -// an entire line. -func readCommand(c Commands, r []rune, i, end int) (*Command, []string, int) { - for ; i < end; i++ { - next := grab(r, i, end) - if next == 0 || unicode.IsControl(next) { - break - } - } - cmd, args := c.matchCommand(string(r[:i])) - return cmd, args, i -} - -// readVariableReference returns the index of the end of the variable reference or false if it's not a valid identifier -func readVariableReference(r []rune, i int, end int) (int, bool) { - for ; i < end; i++ { - if r[i] == ')' { - return i, true - } - if (r[i] >= 'a' && r[i] <= 'z') || (r[i] >= 'A' && r[i] <= 'Z') || (r[i] >= '0' && r[i] <= '9') || strings.ContainsRune(validVariableRunes, r[i]) { - continue - } - break - } - return 0, false -} - -func max64(a, b int64) int64 { - if a > b { - return a - } - return b -} - -// min returns the minimum of a, b. -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func min64(a, b int64) int64 { - if a < b { - return a - } - return b -} - -// isSpaceOrControl is a special test for either a space or a control (ie, \b) -// characters. -func isSpaceOrControl(r rune) bool { - return unicode.IsSpace(r) || unicode.IsControl(r) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "strings" + "unicode" +) + +// grab grabs i from r, or returns 0 if i >= end. +func grab(r []rune, i, end int) rune { + if i < end { + return r[i] + } + return 0 +} + +// findNonSpace finds first non space rune in r, returning end if not found. +func findNonSpace(r []rune, i, end int) (int, bool) { + for ; i < end; i++ { + if !isSpaceOrControl(r[i]) { + return i, true + } + } + return i, false +} + +// isEmptyLine returns true when r is empty or composed of only whitespace. +func isEmptyLine(r []rune, i, end int) bool { + _, ok := findNonSpace(r, i, end) + return !ok +} + +// readMultilineComment finds the end of a multiline comment (ie, '*/'). +func readMultilineComment(r []rune, i, end int) (int, bool) { + i++ + for ; i < end; i++ { + if r[i-1] == '*' && r[i] == '/' { + return i, true + } + } + return end, false +} + +// readCommand reads to the next control character to find +// a command in the string. Command regexes constrain matches +// to the beginning of the string, and all commands consume +// an entire line. +func readCommand(c Commands, r []rune, i, end int) (*Command, []string, int) { + for ; i < end; i++ { + next := grab(r, i, end) + if next == 0 || unicode.IsControl(next) { + break + } + } + cmd, args := c.matchCommand(string(r[:i])) + return cmd, args, i +} + +// readVariableReference returns the index of the end of the variable reference or false if it's not a valid identifier +func readVariableReference(r []rune, i int, end int) (int, bool) { + for ; i < end; i++ { + if r[i] == ')' { + return i, true + } + if (r[i] >= 'a' && r[i] <= 'z') || (r[i] >= 'A' && r[i] <= 'Z') || (r[i] >= '0' && r[i] <= '9') || strings.ContainsRune(validVariableRunes, r[i]) { + continue + } + break + } + return 0, false +} + +func max64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +// min returns the minimum of a, b. +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func min64(a, b int64) int64 { + if a < b { + return a + } + return b +} + +// isSpaceOrControl is a special test for either a space or a control (ie, \b) +// characters. +func isSpaceOrControl(r rune) bool { + return unicode.IsSpace(r) || unicode.IsControl(r) +} diff --git a/pkg/sqlcmd/parse_test.go b/pkg/sqlcmd/parse_test.go index 143d5f85..9b809c52 100644 --- a/pkg/sqlcmd/parse_test.go +++ b/pkg/sqlcmd/parse_test.go @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index 0ae6605e..46cfa746 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -1,592 +1,592 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "bytes" - "database/sql" - "fmt" - "io" - "os" - "os/user" - "strings" - "testing" - - "github.com/microsoft/go-mssqldb/azuread" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" -) - -const oneRowAffected = "(1 row affected)" - -func TestConnectionStringFromSqlCmd(t *testing.T) { - type connectionStringTest struct { - settings *ConnectSettings - connectionString string - } - - pwd := uuid.New().String() - - commands := []connectionStringTest{ - - {&ConnectSettings{}, "sqlserver://."}, - { - &ConnectSettings{TrustServerCertificate: true, WorkstationName: "mystation", Database: "somedatabase"}, - "sqlserver://.?database=somedatabase&trustservercertificate=true&workstation+id=mystation", - }, - { - &ConnectSettings{WorkstationName: "mystation", Encrypt: "false", Database: "somedatabase"}, - "sqlserver://.?database=somedatabase&encrypt=false&workstation+id=mystation", - }, - { - &ConnectSettings{TrustServerCertificate: true, Password: pwd, ServerName: `someserver\instance`, Database: "somedatabase", UserName: "someuser"}, - fmt.Sprintf("sqlserver://someuser:%s@someserver/instance?database=somedatabase&trustservercertificate=true", pwd), - }, - { - &ConnectSettings{TrustServerCertificate: true, UseTrustedConnection: true, Password: pwd, ServerName: `tcp:someserver,1045`, UserName: "someuser"}, - "sqlserver://someserver:1045?trustservercertificate=true", - }, - { - &ConnectSettings{ServerName: `tcp:someserver,1045`}, - "sqlserver://someserver:1045", - }, - { - &ConnectSettings{ServerName: "someserver", AuthenticationMethod: azuread.ActiveDirectoryServicePrincipal, UserName: "myapp@mytenant", Password: pwd}, - fmt.Sprintf("sqlserver://myapp%%40mytenant:%s@someserver", pwd), - }, - } - - for i, test := range commands { - - connectionString, err := test.settings.ConnectionString() - if assert.NoError(t, err, "Unexpected error from [%d] %+v", i, test.settings) { - assert.Equal(t, test.connectionString, connectionString, "Wrong connection string from [%d]: %+v", i, test.settings) - } - } -} - -/* The following tests require a working SQL instance and rely on SqlCmd environment variables -to manage the initial connection string. The default connection when no environment variables are -set will be to localhost using Windows auth. - -*/ -func TestSqlCmdConnectDb(t *testing.T) { - v := InitializeVariables(true) - s := &Sqlcmd{vars: v} - s.Connect = newConnect(t) - err := s.ConnectDb(nil, false) - if assert.NoError(t, err, "ConnectDb should succeed") { - sqlcmduser := os.Getenv(SQLCMDUSER) - if sqlcmduser == "" { - u, _ := user.Current() - sqlcmduser = u.Username - } - assert.Equal(t, sqlcmduser, s.vars.SQLCmdUser(), "SQLCMDUSER variable should match connected user") - } -} - -func ConnectDb(t testing.TB) (*sql.Conn, error) { - v := InitializeVariables(true) - s := &Sqlcmd{vars: v} - s.Connect = newConnect(t) - err := s.ConnectDb(nil, false) - return s.db, err -} - -func TestSqlCmdQueryAndExit(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - s.Query = "select $(X" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - s.SetOutput(nil) - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } -} - -// Simulate :r command -func TestIncludeFileNoExecutions(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"singlebatchnogo.sql", false) - s.SetOutput(nil) - if assert.NoError(t, err, "IncludeFile singlebatchnogo.sql false") { - assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile singlebatchnogo.sql false") - assert.Equal(t, "select 100 as num"+SqlcmdEol+"select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile singlebatchnogo.sql false") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "", string(bytes), "Incorrect output from Run") - } - file, err = os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - defer os.Remove(file.Name()) - s.SetOutput(file) - // The second file has a go so it will execute all statements before it - err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", false) - if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql false") { - assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql false") - assert.Equal(t, "select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql false") - s.SetOutput(nil) - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } - } -} - -// Simulate -i command line usage -func TestIncludeFileProcessAll(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"twobatchwithgo.sql", true) - s.SetOutput(nil) - if assert.NoError(t, err, "IncludeFile twobatchwithgo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchwithgo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchwithgo.sql true") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - file, err = os.CreateTemp("", "sqlcmdout") - defer os.Remove(file.Name()) - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(file) - err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", true) - if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql true") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } - } -} - -func TestIncludeFileWithVariables(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"variablesnogo.sql", true) - if assert.NoError(t, err, "IncludeFile variablesnogo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile variablesnogo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile variablesnogo.sql true") - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol, o) - } -} - -func TestGetRunnableQuery(t *testing.T) { - v := InitializeVariables(false) - v.Set("var1", "v1") - v.Set("var2", "variable2") - - type test struct { - raw string - q string - } - tests := []test{ - {"$(var1)", "v1"}, - {"$ (var2)", "$ (var2)"}, - {"select '$(VAR1) $(VAR2)' as c", "select 'v1 variable2' as c"}, - {" $(VAR1) ' $(VAR2) ' as $(VAR1)", " v1 ' variable2 ' as v1"}, - } - s := New(nil, "", v) - for _, test := range tests { - s.batch.Reset([]rune(test.raw)) - _, _, _ = s.batch.Next() - s.Connect.DisableVariableSubstitution = false - t.Log(test.raw) - r := s.getRunnableQuery(test.raw) - assert.Equalf(t, test.q, r, `runnableQuery for "%s"`, test.raw) - s.Connect.DisableVariableSubstitution = true - r = s.getRunnableQuery(test.raw) - assert.Equalf(t, test.raw, r, `runnableQuery without variable subs for "%s"`, test.raw) - } -} - -func TestExitInitialQuery(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _ = s.vars.Setvar("var1", "1200") - s.Query = "EXIT(SELECT '$(var1)', 2100)" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, "1200 2100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Output") - assert.Equal(t, 1200, s.Exitcode, "ExitCode") - } - -} - -func TestExitCodeSetOnError(t *testing.T) { - s, _ := setupSqlCmdWithMemoryOutput(t) - s.Connect.ErrorSeverityLevel = 12 - retcode, err := s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.NoError(t, err, "!ExitOnError 11") - assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") - assert.NoError(t, err, "!ExitOnError 14") - assert.Equal(t, 14, retcode, "Raiserror above ErrorSeverityLevel") - s.Connect.ExitOnError = true - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.NoError(t, err, "ExitOnError and Raiserror below ErrorSeverityLevel") - assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") - assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and Raiserror above ErrorSeverityLevel") - assert.Equal(t, 14, retcode, "ExitOnError and Raiserror above ErrorSeverityLevel") - s.Connect.ErrorSeverityLevel = 0 - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") - assert.Equal(t, 1, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 5, 1)") - assert.NoError(t, err, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") - assert.Equal(t, -101, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") - retcode, err = s.runQuery("RAISERROR (15001, 10, 127)") - assert.ErrorIs(t, err, ErrExitRequested, "RAISERROR with state 127") - assert.Equal(t, 15001, retcode, "RAISERROR (15001, 10, 127)") -} - -func TestSqlCmdExitOnError(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Connect.ExitOnError = true - err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"}) - o := buf.buf.String() - assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error") - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'."+SqlcmdEol, o, "Only first select should run") - assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error") - - s, buf = setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Connect.ExitOnError = true - s.Connect.ErrorSeverityLevel = 15 - s.vars.Set(SQLCMDERRORLEVEL, "14") - err = runSqlCmd(t, s, []string{"raiserror(N'13', 13, 1)", "GO", "raiserror(N'14', 14, 1)", "GO", "raiserror(N'15', 15, 1)", "GO", "SELECT 'nope'", "GO"}) - o = buf.buf.String() - assert.NotContains(t, o, "Level 13", "Level 13 should be filtered from the output") - assert.NotContains(t, o, "nope", "Last select should not be run") - assert.Contains(t, o, "Level 14", "Level 14 should be in the output") - assert.Contains(t, o, "Level 15", "Level 15 should be in the output") - assert.Equal(t, 15, s.Exitcode, "s.ExitCode for a syntax error") - assert.NoError(t, err, "Run should not return an error for a SQL error") -} - -func TestSqlCmdSetErrorLevel(t *testing.T) { - s, _ := setupSqlCmdWithMemoryOutput(t) - s.Connect.ErrorSeverityLevel = 15 - err := runSqlCmd(t, s, []string{"select bad as bad", "GO", "select 1", "GO"}) - assert.NoError(t, err, "runSqlCmd should have no error") - assert.Equal(t, 16, s.Exitcode, "Select error should be the exit code") -} - -type testConsole struct { - PromptText string - OnPasswordPrompt func(prompt string) ([]byte, error) - OnReadLine func() (string, error) -} - -func (tc *testConsole) Readline() (string, error) { - return tc.OnReadLine() -} - -func (tc *testConsole) ReadPassword(prompt string) ([]byte, error) { - return tc.OnPasswordPrompt(prompt) -} - -func (tc *testConsole) SetPrompt(s string) { - tc.PromptText = s -} - -func (tc *testConsole) Close() { - -} - -func TestPromptForPasswordNegative(t *testing.T) { - prompted := false - console := &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - assert.Equal(t, "Password:", prompt, "Incorrect password prompt") - prompted = true - return []byte{}, nil - }, - OnReadLine: func() (string, error) { - assert.Fail(t, "ReadLine should not be called") - return "", nil - }, - } - v := InitializeVariables(true) - s := New(console, "", v) - s.Connect.UserName = "someuser" - err := s.ConnectDb(nil, false) - assert.True(t, prompted, "Password prompt not shown for SQL auth") - assert.Error(t, err, "ConnectDb") - prompted = false - s.Connect.AuthenticationMethod = azuread.ActiveDirectoryPassword - err = s.ConnectDb(nil, false) - assert.True(t, prompted, "Password prompt not shown for AD Password auth") - assert.Error(t, err, "ConnectDb") - prompted = false -} - -func TestPromptForPasswordPositive(t *testing.T) { - prompted := false - c := newConnect(t) - if c.Password == "" { - // See if azure variables are set for activedirectoryserviceprincipal - c.UserName = os.Getenv("AZURE_CLIENT_ID") + "@" + os.Getenv("AZURE_TENANT_ID") - c.Password = os.Getenv("AZURE_CLIENT_SECRET") - c.AuthenticationMethod = azuread.ActiveDirectoryServicePrincipal - if c.Password == "" { - t.Skip("No password available") - } - } - password := c.Password - c.Password = "" - console := &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - assert.Equal(t, "Password:", prompt, "Incorrect password prompt") - prompted = true - return []byte(password), nil - }, - OnReadLine: func() (string, error) { - assert.Fail(t, "ReadLine should not be called") - return "", nil - }, - } - v := InitializeVariables(true) - s := New(console, "", v) - // attempt without password prompt - err := s.ConnectDb(c, true) - assert.False(t, prompted, "ConnectDb with nopw=true should not prompt for password") - assert.Error(t, err, "ConnectDb with nopw==true and no password provided") - err = s.ConnectDb(c, false) - assert.True(t, prompted, "ConnectDb with !nopw should prompt for password") - assert.NoError(t, err, "ConnectDb with !nopw and valid password returned from prompt") - if s.Connect.Password != password { - t.Fatal(t, err, "Password not stored in the connection") - } -} - -func TestVerticalLayoutNoColumns(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDFORMAT, "vert") - _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, - "100"+SqlcmdEol+"2000"+SqlcmdEol+"300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, - buf.buf.String(), "Query without column headers") -} - -func TestSelectGuidColumn(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _, err := s.runQuery("select convert(uniqueidentifier, N'3ddba21e-ff0f-4d24-90b4-f355864d7865')") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, "3ddba21e-ff0f-4d24-90b4-f355864d7865"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a uniqueidentifier should work") -} - -func TestSelectNullGuidColumn(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _, err := s.runQuery("select convert(uniqueidentifier,null)") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, "NULL"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a null uniqueidentifier should work") -} - -func TestVerticalLayoutWithColumns(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDFORMAT, "vert") - s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "256") - _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, - "column1 100"+SqlcmdEol+"col2 2000"+SqlcmdEol+" 300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, - buf.buf.String(), "Query without column headers") - -} - -func TestSqlCmdDefersToPrintError(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.PrintError = func(msg string, severity uint8) bool { - return severity > 10 - } - err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"}) - if assert.NoError(t, err, "runSqlCmd failed") { - assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError") - } -} - -func TestSqlCmdMaintainsConnectionBetweenBatches(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - err := runSqlCmd(t, s, []string{"CREATE TABLE #tmp1 (col1 int)", "insert into #tmp1 values (1)", "GO", "select * from #tmp1", "drop table #tmp1", "GO"}) - if assert.NoError(t, err, "runSqlCmd failed") { - assert.Equal(t, oneRowAffected+SqlcmdEol+"1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "Sqlcmd uses the same connection for all queries") - } -} - -func TestDateTimeFormats(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - err := s.IncludeFile(`testdata/selectdates.sql`, true) - if assert.NoError(t, err, "selectdates.sql") { - assert.Equal(t, - `2022-03-05 14:01:02.000 2021-01-02 11:06:02.2000 2021-05-05 00:00:00.000000 +00:00 2019-01-11 13:00:00 14:01:02.0000000 2011-02-03`+SqlcmdEol+SqlcmdEol, - buf.buf.String(), - "Unexpected date format output") - - } -} - -func TestQueryServerPropertyReturnsColumnName(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "100") - defer buf.Close() - err := runSqlCmd(t, s, []string{"select SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition", "GO"}) - if assert.NoError(t, err, "select should succeed") { - assert.Contains(t, buf.buf.String(), "DatabaseEngineEdition", "Column name missing from output") - } -} - -func TestSqlCmdOutputAndError(t *testing.T) { - s, outfile, errfile := setupSqlcmdWithFileErrorOutput(t) - defer os.Remove(outfile.Name()) - defer os.Remove(errfile.Name()) - s.Query = "select $(X" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - bytes, err := os.ReadFile(errfile.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") - } - } - s.Query = "select '1'" - err = s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - bytes, err := os.ReadFile(outfile.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for query execution") - } - } - - s, outfile, errfile = setupSqlcmdWithFileErrorOutput(t) - defer os.Remove(outfile.Name()) - defer os.Remove(errfile.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err = s.IncludeFile(dataPath+"testerrorredirection.sql", false) - if assert.NoError(t, err, "IncludeFile testerrorredirection.sql false") { - bytes, err := os.ReadFile(outfile.Name()) - if assert.NoError(t, err, "os.ReadFile outfile") { - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for sql file execution in outfile") - } - bytes, err = os.ReadFile(errfile.Name()) - if assert.NoError(t, err, "os.ReadFile errfile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") - } - } -} - -// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input -func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error { - t.Helper() - i := 0 - s.batch.read = func() (string, error) { - if i < len(lines) { - index := i - i++ - return lines[index], nil - } - return "", io.EOF - } - return s.Run(false, false) -} - -func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - err := s.ConnectDb(nil, true) - assert.NoError(t, err, "s.ConnectDB") - return s, buf -} - -func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - file, err := os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(file) - err = s.ConnectDb(nil, true) - if err != nil { - os.Remove(file.Name()) - } - assert.NoError(t, err, "s.ConnectDB") - return s, file -} - -func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - outfile, err := os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - errfile, err := os.CreateTemp("", "sqlcmderr") - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(outfile) - s.SetError(errfile) - err = s.ConnectDb(nil, true) - if err != nil { - os.Remove(outfile.Name()) - os.Remove(errfile.Name()) - } - assert.NoError(t, err, "s.ConnectDB") - return s, outfile, errfile -} - -// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set -func canTestAzureAuth() bool { - server := os.Getenv(SQLCMDSERVER) - userName := os.Getenv(SQLCMDUSER) - return strings.Contains(server, ".database.windows.net") && userName == "" -} - -func newConnect(t testing.TB) *ConnectSettings { - t.Helper() - connect := ConnectSettings{ - UserName: os.Getenv(SQLCMDUSER), - Database: os.Getenv(SQLCMDDBNAME), - ServerName: os.Getenv(SQLCMDSERVER), - Password: os.Getenv(SQLCMDPASSWORD), - } - if canTestAzureAuth() { - t.Log("Using ActiveDirectoryDefault") - connect.AuthenticationMethod = azuread.ActiveDirectoryDefault - } - return &connect -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "bytes" + "database/sql" + "fmt" + "io" + "os" + "os/user" + "strings" + "testing" + + "github.com/microsoft/go-mssqldb/azuread" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +const oneRowAffected = "(1 row affected)" + +func TestConnectionStringFromSqlCmd(t *testing.T) { + type connectionStringTest struct { + settings *ConnectSettings + connectionString string + } + + pwd := uuid.New().String() + + commands := []connectionStringTest{ + + {&ConnectSettings{}, "sqlserver://."}, + { + &ConnectSettings{TrustServerCertificate: true, WorkstationName: "mystation", Database: "somedatabase"}, + "sqlserver://.?database=somedatabase&trustservercertificate=true&workstation+id=mystation", + }, + { + &ConnectSettings{WorkstationName: "mystation", Encrypt: "false", Database: "somedatabase"}, + "sqlserver://.?database=somedatabase&encrypt=false&workstation+id=mystation", + }, + { + &ConnectSettings{TrustServerCertificate: true, Password: pwd, ServerName: `someserver\instance`, Database: "somedatabase", UserName: "someuser"}, + fmt.Sprintf("sqlserver://someuser:%s@someserver/instance?database=somedatabase&trustservercertificate=true", pwd), + }, + { + &ConnectSettings{TrustServerCertificate: true, UseTrustedConnection: true, Password: pwd, ServerName: `tcp:someserver,1045`, UserName: "someuser"}, + "sqlserver://someserver:1045?trustservercertificate=true", + }, + { + &ConnectSettings{ServerName: `tcp:someserver,1045`}, + "sqlserver://someserver:1045", + }, + { + &ConnectSettings{ServerName: "someserver", AuthenticationMethod: azuread.ActiveDirectoryServicePrincipal, UserName: "myapp@mytenant", Password: pwd}, + fmt.Sprintf("sqlserver://myapp%%40mytenant:%s@someserver", pwd), + }, + } + + for i, test := range commands { + + connectionString, err := test.settings.ConnectionString() + if assert.NoError(t, err, "Unexpected error from [%d] %+v", i, test.settings) { + assert.Equal(t, test.connectionString, connectionString, "Wrong connection string from [%d]: %+v", i, test.settings) + } + } +} + +/* The following tests require a working SQL instance and rely on SqlCmd environment variables +to manage the initial connection string. The default connection when no environment variables are +set will be to localhost using Windows auth. + +*/ +func TestSqlCmdConnectDb(t *testing.T) { + v := InitializeVariables(true) + s := &Sqlcmd{vars: v} + s.Connect = newConnect(t) + err := s.ConnectDb(nil, false) + if assert.NoError(t, err, "ConnectDb should succeed") { + sqlcmduser := os.Getenv(SQLCMDUSER) + if sqlcmduser == "" { + u, _ := user.Current() + sqlcmduser = u.Username + } + assert.Equal(t, sqlcmduser, s.vars.SQLCmdUser(), "SQLCMDUSER variable should match connected user") + } +} + +func ConnectDb(t testing.TB) (*sql.Conn, error) { + v := InitializeVariables(true) + s := &Sqlcmd{vars: v} + s.Connect = newConnect(t) + err := s.ConnectDb(nil, false) + return s.db, err +} + +func TestSqlCmdQueryAndExit(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + s.Query = "select $(X" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + s.SetOutput(nil) + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } +} + +// Simulate :r command +func TestIncludeFileNoExecutions(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"singlebatchnogo.sql", false) + s.SetOutput(nil) + if assert.NoError(t, err, "IncludeFile singlebatchnogo.sql false") { + assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile singlebatchnogo.sql false") + assert.Equal(t, "select 100 as num"+SqlcmdEol+"select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile singlebatchnogo.sql false") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "", string(bytes), "Incorrect output from Run") + } + file, err = os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + defer os.Remove(file.Name()) + s.SetOutput(file) + // The second file has a go so it will execute all statements before it + err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", false) + if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql false") { + assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql false") + assert.Equal(t, "select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql false") + s.SetOutput(nil) + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } + } +} + +// Simulate -i command line usage +func TestIncludeFileProcessAll(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"twobatchwithgo.sql", true) + s.SetOutput(nil) + if assert.NoError(t, err, "IncludeFile twobatchwithgo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchwithgo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchwithgo.sql true") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + file, err = os.CreateTemp("", "sqlcmdout") + defer os.Remove(file.Name()) + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(file) + err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", true) + if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql true") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } + } +} + +func TestIncludeFileWithVariables(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"variablesnogo.sql", true) + if assert.NoError(t, err, "IncludeFile variablesnogo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile variablesnogo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile variablesnogo.sql true") + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol, o) + } +} + +func TestGetRunnableQuery(t *testing.T) { + v := InitializeVariables(false) + v.Set("var1", "v1") + v.Set("var2", "variable2") + + type test struct { + raw string + q string + } + tests := []test{ + {"$(var1)", "v1"}, + {"$ (var2)", "$ (var2)"}, + {"select '$(VAR1) $(VAR2)' as c", "select 'v1 variable2' as c"}, + {" $(VAR1) ' $(VAR2) ' as $(VAR1)", " v1 ' variable2 ' as v1"}, + } + s := New(nil, "", v) + for _, test := range tests { + s.batch.Reset([]rune(test.raw)) + _, _, _ = s.batch.Next() + s.Connect.DisableVariableSubstitution = false + t.Log(test.raw) + r := s.getRunnableQuery(test.raw) + assert.Equalf(t, test.q, r, `runnableQuery for "%s"`, test.raw) + s.Connect.DisableVariableSubstitution = true + r = s.getRunnableQuery(test.raw) + assert.Equalf(t, test.raw, r, `runnableQuery without variable subs for "%s"`, test.raw) + } +} + +func TestExitInitialQuery(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _ = s.vars.Setvar("var1", "1200") + s.Query = "EXIT(SELECT '$(var1)', 2100)" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, "1200 2100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Output") + assert.Equal(t, 1200, s.Exitcode, "ExitCode") + } + +} + +func TestExitCodeSetOnError(t *testing.T) { + s, _ := setupSqlCmdWithMemoryOutput(t) + s.Connect.ErrorSeverityLevel = 12 + retcode, err := s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.NoError(t, err, "!ExitOnError 11") + assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") + assert.NoError(t, err, "!ExitOnError 14") + assert.Equal(t, 14, retcode, "Raiserror above ErrorSeverityLevel") + s.Connect.ExitOnError = true + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.NoError(t, err, "ExitOnError and Raiserror below ErrorSeverityLevel") + assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") + assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and Raiserror above ErrorSeverityLevel") + assert.Equal(t, 14, retcode, "ExitOnError and Raiserror above ErrorSeverityLevel") + s.Connect.ErrorSeverityLevel = 0 + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") + assert.Equal(t, 1, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 5, 1)") + assert.NoError(t, err, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") + assert.Equal(t, -101, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") + retcode, err = s.runQuery("RAISERROR (15001, 10, 127)") + assert.ErrorIs(t, err, ErrExitRequested, "RAISERROR with state 127") + assert.Equal(t, 15001, retcode, "RAISERROR (15001, 10, 127)") +} + +func TestSqlCmdExitOnError(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Connect.ExitOnError = true + err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"}) + o := buf.buf.String() + assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error") + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'."+SqlcmdEol, o, "Only first select should run") + assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error") + + s, buf = setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Connect.ExitOnError = true + s.Connect.ErrorSeverityLevel = 15 + s.vars.Set(SQLCMDERRORLEVEL, "14") + err = runSqlCmd(t, s, []string{"raiserror(N'13', 13, 1)", "GO", "raiserror(N'14', 14, 1)", "GO", "raiserror(N'15', 15, 1)", "GO", "SELECT 'nope'", "GO"}) + o = buf.buf.String() + assert.NotContains(t, o, "Level 13", "Level 13 should be filtered from the output") + assert.NotContains(t, o, "nope", "Last select should not be run") + assert.Contains(t, o, "Level 14", "Level 14 should be in the output") + assert.Contains(t, o, "Level 15", "Level 15 should be in the output") + assert.Equal(t, 15, s.Exitcode, "s.ExitCode for a syntax error") + assert.NoError(t, err, "Run should not return an error for a SQL error") +} + +func TestSqlCmdSetErrorLevel(t *testing.T) { + s, _ := setupSqlCmdWithMemoryOutput(t) + s.Connect.ErrorSeverityLevel = 15 + err := runSqlCmd(t, s, []string{"select bad as bad", "GO", "select 1", "GO"}) + assert.NoError(t, err, "runSqlCmd should have no error") + assert.Equal(t, 16, s.Exitcode, "Select error should be the exit code") +} + +type testConsole struct { + PromptText string + OnPasswordPrompt func(prompt string) ([]byte, error) + OnReadLine func() (string, error) +} + +func (tc *testConsole) Readline() (string, error) { + return tc.OnReadLine() +} + +func (tc *testConsole) ReadPassword(prompt string) ([]byte, error) { + return tc.OnPasswordPrompt(prompt) +} + +func (tc *testConsole) SetPrompt(s string) { + tc.PromptText = s +} + +func (tc *testConsole) Close() { + +} + +func TestPromptForPasswordNegative(t *testing.T) { + prompted := false + console := &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + assert.Equal(t, "Password:", prompt, "Incorrect password prompt") + prompted = true + return []byte{}, nil + }, + OnReadLine: func() (string, error) { + assert.Fail(t, "ReadLine should not be called") + return "", nil + }, + } + v := InitializeVariables(true) + s := New(console, "", v) + s.Connect.UserName = "someuser" + err := s.ConnectDb(nil, false) + assert.True(t, prompted, "Password prompt not shown for SQL auth") + assert.Error(t, err, "ConnectDb") + prompted = false + s.Connect.AuthenticationMethod = azuread.ActiveDirectoryPassword + err = s.ConnectDb(nil, false) + assert.True(t, prompted, "Password prompt not shown for AD Password auth") + assert.Error(t, err, "ConnectDb") + prompted = false +} + +func TestPromptForPasswordPositive(t *testing.T) { + prompted := false + c := newConnect(t) + if c.Password == "" { + // See if azure variables are set for activedirectoryserviceprincipal + c.UserName = os.Getenv("AZURE_CLIENT_ID") + "@" + os.Getenv("AZURE_TENANT_ID") + c.Password = os.Getenv("AZURE_CLIENT_SECRET") + c.AuthenticationMethod = azuread.ActiveDirectoryServicePrincipal + if c.Password == "" { + t.Skip("No password available") + } + } + password := c.Password + c.Password = "" + console := &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + assert.Equal(t, "Password:", prompt, "Incorrect password prompt") + prompted = true + return []byte(password), nil + }, + OnReadLine: func() (string, error) { + assert.Fail(t, "ReadLine should not be called") + return "", nil + }, + } + v := InitializeVariables(true) + s := New(console, "", v) + // attempt without password prompt + err := s.ConnectDb(c, true) + assert.False(t, prompted, "ConnectDb with nopw=true should not prompt for password") + assert.Error(t, err, "ConnectDb with nopw==true and no password provided") + err = s.ConnectDb(c, false) + assert.True(t, prompted, "ConnectDb with !nopw should prompt for password") + assert.NoError(t, err, "ConnectDb with !nopw and valid password returned from prompt") + if s.Connect.Password != password { + t.Fatal(t, err, "Password not stored in the connection") + } +} + +func TestVerticalLayoutNoColumns(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDFORMAT, "vert") + _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, + "100"+SqlcmdEol+"2000"+SqlcmdEol+"300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, + buf.buf.String(), "Query without column headers") +} + +func TestSelectGuidColumn(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _, err := s.runQuery("select convert(uniqueidentifier, N'3ddba21e-ff0f-4d24-90b4-f355864d7865')") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, "3ddba21e-ff0f-4d24-90b4-f355864d7865"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a uniqueidentifier should work") +} + +func TestSelectNullGuidColumn(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _, err := s.runQuery("select convert(uniqueidentifier,null)") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, "NULL"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a null uniqueidentifier should work") +} + +func TestVerticalLayoutWithColumns(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDFORMAT, "vert") + s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "256") + _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, + "column1 100"+SqlcmdEol+"col2 2000"+SqlcmdEol+" 300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, + buf.buf.String(), "Query without column headers") + +} + +func TestSqlCmdDefersToPrintError(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.PrintError = func(msg string, severity uint8) bool { + return severity > 10 + } + err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"}) + if assert.NoError(t, err, "runSqlCmd failed") { + assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError") + } +} + +func TestSqlCmdMaintainsConnectionBetweenBatches(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + err := runSqlCmd(t, s, []string{"CREATE TABLE #tmp1 (col1 int)", "insert into #tmp1 values (1)", "GO", "select * from #tmp1", "drop table #tmp1", "GO"}) + if assert.NoError(t, err, "runSqlCmd failed") { + assert.Equal(t, oneRowAffected+SqlcmdEol+"1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "Sqlcmd uses the same connection for all queries") + } +} + +func TestDateTimeFormats(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + err := s.IncludeFile(`testdata/selectdates.sql`, true) + if assert.NoError(t, err, "selectdates.sql") { + assert.Equal(t, + `2022-03-05 14:01:02.000 2021-01-02 11:06:02.2000 2021-05-05 00:00:00.000000 +00:00 2019-01-11 13:00:00 14:01:02.0000000 2011-02-03`+SqlcmdEol+SqlcmdEol, + buf.buf.String(), + "Unexpected date format output") + + } +} + +func TestQueryServerPropertyReturnsColumnName(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "100") + defer buf.Close() + err := runSqlCmd(t, s, []string{"select SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition", "GO"}) + if assert.NoError(t, err, "select should succeed") { + assert.Contains(t, buf.buf.String(), "DatabaseEngineEdition", "Column name missing from output") + } +} + +func TestSqlCmdOutputAndError(t *testing.T) { + s, outfile, errfile := setupSqlcmdWithFileErrorOutput(t) + defer os.Remove(outfile.Name()) + defer os.Remove(errfile.Name()) + s.Query = "select $(X" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + bytes, err := os.ReadFile(errfile.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") + } + } + s.Query = "select '1'" + err = s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + bytes, err := os.ReadFile(outfile.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for query execution") + } + } + + s, outfile, errfile = setupSqlcmdWithFileErrorOutput(t) + defer os.Remove(outfile.Name()) + defer os.Remove(errfile.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err = s.IncludeFile(dataPath+"testerrorredirection.sql", false) + if assert.NoError(t, err, "IncludeFile testerrorredirection.sql false") { + bytes, err := os.ReadFile(outfile.Name()) + if assert.NoError(t, err, "os.ReadFile outfile") { + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for sql file execution in outfile") + } + bytes, err = os.ReadFile(errfile.Name()) + if assert.NoError(t, err, "os.ReadFile errfile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") + } + } +} + +// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input +func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error { + t.Helper() + i := 0 + s.batch.read = func() (string, error) { + if i < len(lines) { + index := i + i++ + return lines[index], nil + } + return "", io.EOF + } + return s.Run(false, false) +} + +func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + err := s.ConnectDb(nil, true) + assert.NoError(t, err, "s.ConnectDB") + return s, buf +} + +func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + file, err := os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(file) + err = s.ConnectDb(nil, true) + if err != nil { + os.Remove(file.Name()) + } + assert.NoError(t, err, "s.ConnectDB") + return s, file +} + +func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + outfile, err := os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + errfile, err := os.CreateTemp("", "sqlcmderr") + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(outfile) + s.SetError(errfile) + err = s.ConnectDb(nil, true) + if err != nil { + os.Remove(outfile.Name()) + os.Remove(errfile.Name()) + } + assert.NoError(t, err, "s.ConnectDB") + return s, outfile, errfile +} + +// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set +func canTestAzureAuth() bool { + server := os.Getenv(SQLCMDSERVER) + userName := os.Getenv(SQLCMDUSER) + return strings.Contains(server, ".database.windows.net") && userName == "" +} + +func newConnect(t testing.TB) *ConnectSettings { + t.Helper() + connect := ConnectSettings{ + UserName: os.Getenv(SQLCMDUSER), + Database: os.Getenv(SQLCMDDBNAME), + ServerName: os.Getenv(SQLCMDSERVER), + Password: os.Getenv(SQLCMDPASSWORD), + } + if canTestAzureAuth() { + t.Log("Using ActiveDirectoryDefault") + connect.AuthenticationMethod = azuread.ActiveDirectoryDefault + } + return &connect +} diff --git a/pkg/sqlcmd/testdata/singlebatchnogo.sql b/pkg/sqlcmd/testdata/singlebatchnogo.sql index 8d4c9bf2..29b68f16 100644 --- a/pkg/sqlcmd/testdata/singlebatchnogo.sql +++ b/pkg/sqlcmd/testdata/singlebatchnogo.sql @@ -1,2 +1,2 @@ -select 100 as num -select 'string' as title +select 100 as num +select 'string' as title diff --git a/pkg/sqlcmd/testdata/twobatchnoendinggo.sql b/pkg/sqlcmd/testdata/twobatchnoendinggo.sql index 0be51209..90c4d289 100644 --- a/pkg/sqlcmd/testdata/twobatchnoendinggo.sql +++ b/pkg/sqlcmd/testdata/twobatchnoendinggo.sql @@ -1,3 +1,3 @@ -select 100 as num -go -select 'string' as title +select 100 as num +go +select 'string' as title diff --git a/pkg/sqlcmd/testdata/twobatchwithgo.sql b/pkg/sqlcmd/testdata/twobatchwithgo.sql index 26554bc9..58439168 100644 --- a/pkg/sqlcmd/testdata/twobatchwithgo.sql +++ b/pkg/sqlcmd/testdata/twobatchwithgo.sql @@ -1,4 +1,4 @@ -select 100 as num -GO -select 'string' as title -GO +select 100 as num +GO +select 'string' as title +GO diff --git a/pkg/sqlcmd/util.go b/pkg/sqlcmd/util.go index fc0fb08a..79f75b67 100644 --- a/pkg/sqlcmd/util.go +++ b/pkg/sqlcmd/util.go @@ -1,73 +1,73 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "strconv" - "strings" -) - -// splitServer extracts connection parameters from a server name input -func splitServer(serverName string) (string, string, uint64, error) { - instance := "" - port := uint64(0) - if strings.HasPrefix(serverName, "tcp:") { - if len(serverName) == 4 { - return "", "", 0, &InvalidServerName - } - serverName = serverName[4:] - } - serverNameParts := strings.Split(serverName, ",") - if len(serverNameParts) > 2 { - return "", "", 0, &InvalidServerName - } - if len(serverNameParts) == 2 { - var err error - port, err = strconv.ParseUint(serverNameParts[1], 10, 16) - if err != nil { - return "", "", 0, &InvalidServerName - } - serverName = serverNameParts[0] - } else { - serverNameParts = strings.Split(serverName, "\\") - if len(serverNameParts) > 2 { - return "", "", 0, &InvalidServerName - } - if len(serverNameParts) == 2 { - instance = serverNameParts[1] - serverName = serverNameParts[0] - } - } - return serverName, instance, port, nil -} - -// padRight appends c instances of s to builder -func padRight(builder *strings.Builder, c int64, s string) *strings.Builder { - var i int64 - for ; i < c; i++ { - builder.WriteString(s) - } - return builder -} - -// padLeft prepends c instances of s to builder -func padLeft(builder *strings.Builder, c int64, s string) *strings.Builder { - newBuilder := new(strings.Builder) - newBuilder.Grow(builder.Len()) - var i int64 - for ; i < c; i++ { - newBuilder.WriteString(s) - } - newBuilder.WriteString(builder.String()) - return newBuilder -} - -func contains(arr []string, s string) bool { - for _, a := range arr { - if a == s { - return true - } - } - return false -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "strconv" + "strings" +) + +// splitServer extracts connection parameters from a server name input +func splitServer(serverName string) (string, string, uint64, error) { + instance := "" + port := uint64(0) + if strings.HasPrefix(serverName, "tcp:") { + if len(serverName) == 4 { + return "", "", 0, &InvalidServerName + } + serverName = serverName[4:] + } + serverNameParts := strings.Split(serverName, ",") + if len(serverNameParts) > 2 { + return "", "", 0, &InvalidServerName + } + if len(serverNameParts) == 2 { + var err error + port, err = strconv.ParseUint(serverNameParts[1], 10, 16) + if err != nil { + return "", "", 0, &InvalidServerName + } + serverName = serverNameParts[0] + } else { + serverNameParts = strings.Split(serverName, "\\") + if len(serverNameParts) > 2 { + return "", "", 0, &InvalidServerName + } + if len(serverNameParts) == 2 { + instance = serverNameParts[1] + serverName = serverNameParts[0] + } + } + return serverName, instance, port, nil +} + +// padRight appends c instances of s to builder +func padRight(builder *strings.Builder, c int64, s string) *strings.Builder { + var i int64 + for ; i < c; i++ { + builder.WriteString(s) + } + return builder +} + +// padLeft prepends c instances of s to builder +func padLeft(builder *strings.Builder, c int64, s string) *strings.Builder { + newBuilder := new(strings.Builder) + newBuilder.Grow(builder.Len()) + var i int64 + for ; i < c; i++ { + newBuilder.WriteString(s) + } + newBuilder.WriteString(builder.String()) + return newBuilder +} + +func contains(arr []string, s string) bool { + for _, a := range arr { + if a == s { + return true + } + } + return false +} diff --git a/pkg/sqlcmd/variables.go b/pkg/sqlcmd/variables.go index ebf6b1b5..e86a5b5e 100644 --- a/pkg/sqlcmd/variables.go +++ b/pkg/sqlcmd/variables.go @@ -1,335 +1,335 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "fmt" - "os" - "strings" - "unicode" -) - -// Variables provides set and get of sqlcmd scripting variables -type Variables map[string]string - -// Built-in scripting variables -const ( - SQLCMDDBNAME = "SQLCMDDBNAME" - SQLCMDINI = "SQLCMDINI" - SQLCMDPACKETSIZE = "SQLCMDPACKETSIZE" - SQLCMDPASSWORD = "SQLCMDPASSWORD" - SQLCMDSERVER = "SQLCMDSERVER" - SQLCMDUSER = "SQLCMDUSER" - SQLCMDWORKSTATION = "SQLCMDWORKSTATION" - SQLCMDLOGINTIMEOUT = "SQLCMDLOGINTIMEOUT" - SQLCMDSTATTIMEOUT = "SQLCMDSTATTIMEOUT" - SQLCMDHEADERS = "SQLCMDHEADERS" - SQLCMDCOLSEP = "SQLCMDCOLSEP" - SQLCMDCOLWIDTH = "SQLCMDCOLWIDTH" - SQLCMDERRORLEVEL = "SQLCMDERRORLEVEL" - SQLCMDFORMAT = "SQLCMDFORMAT" - SQLCMDMAXVARTYPEWIDTH = "SQLCMDMAXVARTYPEWIDTH" - SQLCMDMAXFIXEDTYPEWIDTH = "SQLCMDMAXFIXEDTYPEWIDTH" - SQLCMDEDITOR = "SQLCMDEDITOR" - SQLCMDUSEAAD = "SQLCMDUSEAAD" -) - -// builtinVariables are the predefined SQLCMD variables. Their values are printed first by :listvar -var builtinVariables = []string{ - SQLCMDCOLSEP, - SQLCMDCOLWIDTH, - SQLCMDDBNAME, - SQLCMDEDITOR, - SQLCMDERRORLEVEL, - SQLCMDFORMAT, - SQLCMDHEADERS, - SQLCMDINI, - SQLCMDLOGINTIMEOUT, - SQLCMDMAXFIXEDTYPEWIDTH, - SQLCMDMAXVARTYPEWIDTH, - SQLCMDPACKETSIZE, - SQLCMDSERVER, - SQLCMDSTATTIMEOUT, - SQLCMDUSEAAD, - SQLCMDUSER, - SQLCMDWORKSTATION, -} - -// readonlyVariables are variables that can't be changed via :setvar -var readOnlyVariables = []string{ - SQLCMDDBNAME, - SQLCMDINI, - SQLCMDPACKETSIZE, - SQLCMDSERVER, - SQLCMDUSER, - SQLCMDWORKSTATION, -} - -func (v Variables) checkReadOnly(key string) error { - currentValue, hasValue := v[key] - if hasValue { - for _, variable := range readOnlyVariables { - if variable == key && currentValue != "" { - return ReadOnlyVariable(key) - } - } - } - return nil -} - -// Set sets or adds the value in the map. -func (v Variables) Set(name, value string) { - key := strings.ToUpper(name) - v[key] = value -} - -// Get returns the value of the named variable -// To distinguish an empty value from an unset value use the bool return value -func (v Variables) Get(name string) (string, bool) { - key := strings.ToUpper(name) - s, ok := v[key] - return s, ok -} - -// Unset removes the value from the map -func (v Variables) Unset(name string) { - key := strings.ToUpper(name) - delete(v, key) -} - -// All returns a copy of the current variables -func (v Variables) All() map[string]string { - return map[string]string(v) -} - -// SQLCmdUser returns the SQLCMDUSER variable value -func (v Variables) SQLCmdUser() string { - return v[SQLCMDUSER] -} - -// SQLCmdServer returns the server connection parameters derived from the SQLCMDSERVER variable value -func (v Variables) SQLCmdServer() (serverName string, instance string, port uint64, err error) { - serverName = v[SQLCMDSERVER] - return splitServer(serverName) -} - -// SQLCmdDatabase returns the SQLCMDDBNAME variable value -func (v Variables) SQLCmdDatabase() string { - return v[SQLCMDDBNAME] -} - -// UseAad returns whether the SQLCMDUSEAAD variable value is set to "true" -func (v Variables) UseAad() bool { - return strings.EqualFold(v[SQLCMDUSEAAD], "true") -} - -// ColumnSeparator is the value of SQLCMDCOLSEP variable. It can have 0 or 1 characters -func (v Variables) ColumnSeparator() string { - sep := v[SQLCMDCOLSEP] - if len(sep) > 1 { - return sep[:1] - } - return sep -} - -// MaxFixedColumnWidth is the value of SQLCMDMAXFIXEDTYPEWIDTH variable. -// When non-zero, it limits the width of columns for types CHAR, NCHAR, NVARCHAR, VARCHAR, VARBINARY, VARIANT -func (v Variables) MaxFixedColumnWidth() int64 { - w := v[SQLCMDMAXFIXEDTYPEWIDTH] - return mustValue(w) -} - -// MaxVarColumnWidth is the value of SQLCMDMAXVARTYPEWIDTH variable. -// When non-zero, it limits the width of columns for (max) versions of CHAR, NCHAR, VARBINARY. -// It also limits the width of xml, UDT, text, ntext, and image -func (v Variables) MaxVarColumnWidth() int64 { - w := v[SQLCMDMAXVARTYPEWIDTH] - return mustValue(w) -} - -// ScreenWidth is the value of SQLCMDCOLWIDTH variable. -// It tells the formatter how many characters wide to limit all screen output. -func (v Variables) ScreenWidth() int64 { - w := v[SQLCMDCOLWIDTH] - return mustValue(w) -} - -// RowsBetweenHeaders is the value of SQLCMDHEADERS variable. -// When MaxVarColumnWidth() is 0, it returns -1 -func (v Variables) RowsBetweenHeaders() int64 { - if v.MaxVarColumnWidth() == 0 { - return -1 - } - h := mustValue(v[SQLCMDHEADERS]) - return h -} - -// ErrorLevel controls the minimum level of errors that are printed -func (v Variables) ErrorLevel() int64 { - return mustValue(v[SQLCMDERRORLEVEL]) -} - -// Format is the name of the results format -func (v Variables) Format() string { - switch v[SQLCMDFORMAT] { - case "vert", "vertical": - return "vertical" - } - return "horizontal" -} - -// StartupScriptFile is the path to the file that contains the startup script -func (v Variables) StartupScriptFile() string { - return v[SQLCMDINI] -} - -// TextEditor is the query editor application launched by the :ED command -func (v Variables) TextEditor() string { - return v[SQLCMDEDITOR] -} - -func mustValue(val string) int64 { - var n int64 - _, err := fmt.Sscanf(val, "%d", &n) - if err == nil { - return n - } - panic(err) -} - -// defaultVariables defines variables that cannot be removed from the map, only reset -// to their default values. -var defaultVariables = Variables{ - SQLCMDCOLSEP: " ", - SQLCMDCOLWIDTH: "0", - SQLCMDEDITOR: defaultEditor, - SQLCMDERRORLEVEL: "0", - SQLCMDHEADERS: "0", - SQLCMDLOGINTIMEOUT: "30", - SQLCMDMAXFIXEDTYPEWIDTH: "0", - SQLCMDMAXVARTYPEWIDTH: "256", - SQLCMDSTATTIMEOUT: "0", -} - -// InitializeVariables initializes variables with default values. -// When fromEnvironment is true, then loads from the runtime environment -func InitializeVariables(fromEnvironment bool) *Variables { - variables := Variables{ - SQLCMDCOLSEP: defaultVariables[SQLCMDCOLSEP], - SQLCMDCOLWIDTH: defaultVariables[SQLCMDCOLWIDTH], - SQLCMDDBNAME: "", - SQLCMDEDITOR: defaultVariables[SQLCMDEDITOR], - SQLCMDERRORLEVEL: defaultVariables[SQLCMDERRORLEVEL], - SQLCMDHEADERS: defaultVariables[SQLCMDHEADERS], - SQLCMDINI: "", - SQLCMDLOGINTIMEOUT: defaultVariables[SQLCMDLOGINTIMEOUT], - SQLCMDMAXFIXEDTYPEWIDTH: defaultVariables[SQLCMDMAXFIXEDTYPEWIDTH], - SQLCMDMAXVARTYPEWIDTH: defaultVariables[SQLCMDMAXVARTYPEWIDTH], - SQLCMDPACKETSIZE: "4096", - SQLCMDSERVER: "", - SQLCMDSTATTIMEOUT: defaultVariables[SQLCMDSTATTIMEOUT], - SQLCMDUSER: "", - SQLCMDUSEAAD: "", - } - hostname, _ := os.Hostname() - variables.Set(SQLCMDWORKSTATION, hostname) - - if fromEnvironment { - for v := range variables.All() { - envVar, ok := os.LookupEnv(v) - if ok { - variables.Set(v, envVar) - } - } - } - return &variables -} - -// Setvar implements the :Setvar command -// TODO: Add validation functions for the variables. -func (variables *Variables) Setvar(name, value string) error { - err := ValidIdentifier(name) - if err == nil { - if err = variables.checkReadOnly(name); err != nil { - err = ReadOnlyVariable(name) - } - } - if err != nil { - return err - } - if value == "" { - if _, ok := variables.Get(name); !ok { - return UndefinedVariable(name) - } - if def, ok := defaultVariables.Get(name); ok { - value = def - } else { - variables.Unset(name) - return nil - } - } else { - value, err = ParseValue(value) - } - if err != nil { - return err - } - variables.Set(name, value) - return nil -} - -const validVariableRunes = "_-" - -// ValidIdentifier determines if a given string can be used as a variable name -func ValidIdentifier(name string) error { - - first := true - for _, c := range name { - if !unicode.IsLetter(c) && (first || (!unicode.IsDigit(c) && !strings.ContainsRune(validVariableRunes, c))) { - return fmt.Errorf("Invalid variable identifier %s", name) - } - first = false - } - return nil -} - -// ParseValue returns the string to use as the variable value -// If the string contains a space or a quote, it must be delimited by quotes and literal quotes -// within the value must be escaped by another quote -// "this has a quote "" in it" is valid -// "this has a quote" in it" is not valid -func ParseValue(val string) (string, error) { - quoted := val[0] == '"' - err := fmt.Errorf("Invalid variable value %s", val) - if !quoted { - if strings.ContainsAny(val, "\t\n\r ") { - return "", err - } - return val, nil - } - if len(val) == 1 || val[len(val)-1] != '"' { - return "", err - } - - b := new(strings.Builder) - quoted = false - r := []rune(val) -loop: - for i := 1; i < len(r)-1; i++ { - switch { - case quoted && r[i] == '"': - b.WriteRune('"') - quoted = false - case quoted && r[i] != '"': - break loop - case !quoted && r[i] == '"': - quoted = true - default: - b.WriteRune(r[i]) - } - } - if quoted { - return "", err - } - return b.String(), nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "fmt" + "os" + "strings" + "unicode" +) + +// Variables provides set and get of sqlcmd scripting variables +type Variables map[string]string + +// Built-in scripting variables +const ( + SQLCMDDBNAME = "SQLCMDDBNAME" + SQLCMDINI = "SQLCMDINI" + SQLCMDPACKETSIZE = "SQLCMDPACKETSIZE" + SQLCMDPASSWORD = "SQLCMDPASSWORD" + SQLCMDSERVER = "SQLCMDSERVER" + SQLCMDUSER = "SQLCMDUSER" + SQLCMDWORKSTATION = "SQLCMDWORKSTATION" + SQLCMDLOGINTIMEOUT = "SQLCMDLOGINTIMEOUT" + SQLCMDSTATTIMEOUT = "SQLCMDSTATTIMEOUT" + SQLCMDHEADERS = "SQLCMDHEADERS" + SQLCMDCOLSEP = "SQLCMDCOLSEP" + SQLCMDCOLWIDTH = "SQLCMDCOLWIDTH" + SQLCMDERRORLEVEL = "SQLCMDERRORLEVEL" + SQLCMDFORMAT = "SQLCMDFORMAT" + SQLCMDMAXVARTYPEWIDTH = "SQLCMDMAXVARTYPEWIDTH" + SQLCMDMAXFIXEDTYPEWIDTH = "SQLCMDMAXFIXEDTYPEWIDTH" + SQLCMDEDITOR = "SQLCMDEDITOR" + SQLCMDUSEAAD = "SQLCMDUSEAAD" +) + +// builtinVariables are the predefined SQLCMD variables. Their values are printed first by :listvar +var builtinVariables = []string{ + SQLCMDCOLSEP, + SQLCMDCOLWIDTH, + SQLCMDDBNAME, + SQLCMDEDITOR, + SQLCMDERRORLEVEL, + SQLCMDFORMAT, + SQLCMDHEADERS, + SQLCMDINI, + SQLCMDLOGINTIMEOUT, + SQLCMDMAXFIXEDTYPEWIDTH, + SQLCMDMAXVARTYPEWIDTH, + SQLCMDPACKETSIZE, + SQLCMDSERVER, + SQLCMDSTATTIMEOUT, + SQLCMDUSEAAD, + SQLCMDUSER, + SQLCMDWORKSTATION, +} + +// readonlyVariables are variables that can't be changed via :setvar +var readOnlyVariables = []string{ + SQLCMDDBNAME, + SQLCMDINI, + SQLCMDPACKETSIZE, + SQLCMDSERVER, + SQLCMDUSER, + SQLCMDWORKSTATION, +} + +func (v Variables) checkReadOnly(key string) error { + currentValue, hasValue := v[key] + if hasValue { + for _, variable := range readOnlyVariables { + if variable == key && currentValue != "" { + return ReadOnlyVariable(key) + } + } + } + return nil +} + +// Set sets or adds the value in the map. +func (v Variables) Set(name, value string) { + key := strings.ToUpper(name) + v[key] = value +} + +// Get returns the value of the named variable +// To distinguish an empty value from an unset value use the bool return value +func (v Variables) Get(name string) (string, bool) { + key := strings.ToUpper(name) + s, ok := v[key] + return s, ok +} + +// Unset removes the value from the map +func (v Variables) Unset(name string) { + key := strings.ToUpper(name) + delete(v, key) +} + +// All returns a copy of the current variables +func (v Variables) All() map[string]string { + return map[string]string(v) +} + +// SQLCmdUser returns the SQLCMDUSER variable value +func (v Variables) SQLCmdUser() string { + return v[SQLCMDUSER] +} + +// SQLCmdServer returns the server connection parameters derived from the SQLCMDSERVER variable value +func (v Variables) SQLCmdServer() (serverName string, instance string, port uint64, err error) { + serverName = v[SQLCMDSERVER] + return splitServer(serverName) +} + +// SQLCmdDatabase returns the SQLCMDDBNAME variable value +func (v Variables) SQLCmdDatabase() string { + return v[SQLCMDDBNAME] +} + +// UseAad returns whether the SQLCMDUSEAAD variable value is set to "true" +func (v Variables) UseAad() bool { + return strings.EqualFold(v[SQLCMDUSEAAD], "true") +} + +// ColumnSeparator is the value of SQLCMDCOLSEP variable. It can have 0 or 1 characters +func (v Variables) ColumnSeparator() string { + sep := v[SQLCMDCOLSEP] + if len(sep) > 1 { + return sep[:1] + } + return sep +} + +// MaxFixedColumnWidth is the value of SQLCMDMAXFIXEDTYPEWIDTH variable. +// When non-zero, it limits the width of columns for types CHAR, NCHAR, NVARCHAR, VARCHAR, VARBINARY, VARIANT +func (v Variables) MaxFixedColumnWidth() int64 { + w := v[SQLCMDMAXFIXEDTYPEWIDTH] + return mustValue(w) +} + +// MaxVarColumnWidth is the value of SQLCMDMAXVARTYPEWIDTH variable. +// When non-zero, it limits the width of columns for (max) versions of CHAR, NCHAR, VARBINARY. +// It also limits the width of xml, UDT, text, ntext, and image +func (v Variables) MaxVarColumnWidth() int64 { + w := v[SQLCMDMAXVARTYPEWIDTH] + return mustValue(w) +} + +// ScreenWidth is the value of SQLCMDCOLWIDTH variable. +// It tells the formatter how many characters wide to limit all screen output. +func (v Variables) ScreenWidth() int64 { + w := v[SQLCMDCOLWIDTH] + return mustValue(w) +} + +// RowsBetweenHeaders is the value of SQLCMDHEADERS variable. +// When MaxVarColumnWidth() is 0, it returns -1 +func (v Variables) RowsBetweenHeaders() int64 { + if v.MaxVarColumnWidth() == 0 { + return -1 + } + h := mustValue(v[SQLCMDHEADERS]) + return h +} + +// ErrorLevel controls the minimum level of errors that are printed +func (v Variables) ErrorLevel() int64 { + return mustValue(v[SQLCMDERRORLEVEL]) +} + +// Format is the name of the results format +func (v Variables) Format() string { + switch v[SQLCMDFORMAT] { + case "vert", "vertical": + return "vertical" + } + return "horizontal" +} + +// StartupScriptFile is the path to the file that contains the startup script +func (v Variables) StartupScriptFile() string { + return v[SQLCMDINI] +} + +// TextEditor is the query editor application launched by the :ED command +func (v Variables) TextEditor() string { + return v[SQLCMDEDITOR] +} + +func mustValue(val string) int64 { + var n int64 + _, err := fmt.Sscanf(val, "%d", &n) + if err == nil { + return n + } + panic(err) +} + +// defaultVariables defines variables that cannot be removed from the map, only reset +// to their default values. +var defaultVariables = Variables{ + SQLCMDCOLSEP: " ", + SQLCMDCOLWIDTH: "0", + SQLCMDEDITOR: defaultEditor, + SQLCMDERRORLEVEL: "0", + SQLCMDHEADERS: "0", + SQLCMDLOGINTIMEOUT: "30", + SQLCMDMAXFIXEDTYPEWIDTH: "0", + SQLCMDMAXVARTYPEWIDTH: "256", + SQLCMDSTATTIMEOUT: "0", +} + +// InitializeVariables initializes variables with default values. +// When fromEnvironment is true, then loads from the runtime environment +func InitializeVariables(fromEnvironment bool) *Variables { + variables := Variables{ + SQLCMDCOLSEP: defaultVariables[SQLCMDCOLSEP], + SQLCMDCOLWIDTH: defaultVariables[SQLCMDCOLWIDTH], + SQLCMDDBNAME: "", + SQLCMDEDITOR: defaultVariables[SQLCMDEDITOR], + SQLCMDERRORLEVEL: defaultVariables[SQLCMDERRORLEVEL], + SQLCMDHEADERS: defaultVariables[SQLCMDHEADERS], + SQLCMDINI: "", + SQLCMDLOGINTIMEOUT: defaultVariables[SQLCMDLOGINTIMEOUT], + SQLCMDMAXFIXEDTYPEWIDTH: defaultVariables[SQLCMDMAXFIXEDTYPEWIDTH], + SQLCMDMAXVARTYPEWIDTH: defaultVariables[SQLCMDMAXVARTYPEWIDTH], + SQLCMDPACKETSIZE: "4096", + SQLCMDSERVER: "", + SQLCMDSTATTIMEOUT: defaultVariables[SQLCMDSTATTIMEOUT], + SQLCMDUSER: "", + SQLCMDUSEAAD: "", + } + hostname, _ := os.Hostname() + variables.Set(SQLCMDWORKSTATION, hostname) + + if fromEnvironment { + for v := range variables.All() { + envVar, ok := os.LookupEnv(v) + if ok { + variables.Set(v, envVar) + } + } + } + return &variables +} + +// Setvar implements the :Setvar command +// TODO: Add validation functions for the variables. +func (variables *Variables) Setvar(name, value string) error { + err := ValidIdentifier(name) + if err == nil { + if err = variables.checkReadOnly(name); err != nil { + err = ReadOnlyVariable(name) + } + } + if err != nil { + return err + } + if value == "" { + if _, ok := variables.Get(name); !ok { + return UndefinedVariable(name) + } + if def, ok := defaultVariables.Get(name); ok { + value = def + } else { + variables.Unset(name) + return nil + } + } else { + value, err = ParseValue(value) + } + if err != nil { + return err + } + variables.Set(name, value) + return nil +} + +const validVariableRunes = "_-" + +// ValidIdentifier determines if a given string can be used as a variable name +func ValidIdentifier(name string) error { + + first := true + for _, c := range name { + if !unicode.IsLetter(c) && (first || (!unicode.IsDigit(c) && !strings.ContainsRune(validVariableRunes, c))) { + return fmt.Errorf("Invalid variable identifier %s", name) + } + first = false + } + return nil +} + +// ParseValue returns the string to use as the variable value +// If the string contains a space or a quote, it must be delimited by quotes and literal quotes +// within the value must be escaped by another quote +// "this has a quote "" in it" is valid +// "this has a quote" in it" is not valid +func ParseValue(val string) (string, error) { + quoted := val[0] == '"' + err := fmt.Errorf("Invalid variable value %s", val) + if !quoted { + if strings.ContainsAny(val, "\t\n\r ") { + return "", err + } + return val, nil + } + if len(val) == 1 || val[len(val)-1] != '"' { + return "", err + } + + b := new(strings.Builder) + quoted = false + r := []rune(val) +loop: + for i := 1; i < len(r)-1; i++ { + switch { + case quoted && r[i] == '"': + b.WriteRune('"') + quoted = false + case quoted && r[i] != '"': + break loop + case !quoted && r[i] == '"': + quoted = true + default: + b.WriteRune(r[i]) + } + } + if quoted { + return "", err + } + return b.String(), nil +} diff --git a/pkg/sqlcmd/variables_test.go b/pkg/sqlcmd/variables_test.go index 45b8ae5b..cf4de871 100644 --- a/pkg/sqlcmd/variables_test.go +++ b/pkg/sqlcmd/variables_test.go @@ -1,116 +1,116 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBasicVariableOperations(t *testing.T) { - variables := Variables{ - "var1": "val1", - } - variables.Set("var2", "val2") - assert.Contains(t, variables, "VAR2", "Set should add a capitalized key") - all := variables.All() - keys := make([]string, 0, len(all)) - for k := range all { - keys = append(keys, k) - } - assert.ElementsMatch(t, []string{"var1", "VAR2"}, keys, "All returns every key") - assert.Equal(t, "val2", all["VAR2"], "VAR2 set value") - -} - -func TestSetvarFailsForReadOnlyVariables(t *testing.T) { - variables := Variables{} - variables.Set("SQLCMDDBNAME", "somedatabase") - err := variables.Setvar("SQLCMDDBNAME", "newdatabase") - assert.Error(t, err, "setting a readonly variable fails") - assert.Equal(t, "somedatabase", variables.SQLCmdDatabase(), "readonly variable shouldn't be changed by Setvar") -} - -func TestEnvironmentVariablesAsInput(t *testing.T) { - os.Setenv("SQLCMDSERVER", "someserver") - defer os.Unsetenv("SQLCMDSERVER") - os.Setenv("x", "somevalue") - defer os.Unsetenv("x") - vars := InitializeVariables(true).All() - assert.Equal(t, "someserver", vars["SQLCMDSERVER"], "InitializeVariables should read a valid environment variable from the known list") - _, ok := vars["x"] - assert.False(t, ok, "InitializeVariables should skip variables not in the known list") -} - -func TestSqlServerSplitsName(t *testing.T) { - vars := Variables{ - SQLCMDSERVER: `tcp:someserver\someinstance`, - } - serverName, instance, port, err := vars.SQLCmdServer() - if assert.NoError(t, err, "tcp:server\\someinstance") { - assert.Equal(t, "someserver", serverName, "server name for instance") - assert.Equal(t, uint64(0), port, "port for instance") - assert.Equal(t, "someinstance", instance, "instance for instance") - } - vars = Variables{ - SQLCMDSERVER: `tcp:someserver,1111`, - } - serverName, instance, port, err = vars.SQLCmdServer() - if assert.NoError(t, err, "tcp:server,1111") { - assert.Equal(t, "someserver", serverName, "server name for port number") - assert.Equal(t, uint64(1111), port, "port for port number") - assert.Equal(t, "", instance, "instance for port number") - } -} - -func TestParseValue(t *testing.T) { - type test struct { - raw string - val string - valid bool - } - tests := []test{ - {`""`, "", true}, - {`"`, "", false}, - {`"""`, "", false}, - {`no quotes`, "", false}, - {`"is quoted"`, "is quoted", true}, - {`" " single quote "`, "", false}, - {`" "" escaped quotes "" "`, ` " escaped quotes " `, true}, - } - - for _, tst := range tests { - v, err := ParseValue(tst.raw) - if tst.valid { - if assert.NoErrorf(t, err, "Unexpected error for value %s", tst.raw) { - assert.Equalf(t, tst.val, v, "Incorrect parsed value for %s", tst.raw) - } - } else { - assert.Errorf(t, err, "Expected error for %s", tst.raw) - } - } -} - -func TestValidIdentifier(t *testing.T) { - type test struct { - raw string - valid bool - } - tests := []test{ - {"1A", false}, - {"A1", true}, - {"A+", false}, - {"A-_b", true}, - } - for _, tst := range tests { - err := ValidIdentifier(tst.raw) - if tst.valid { - assert.NoErrorf(t, err, "%s is valid", tst.raw) - } else { - assert.Errorf(t, err, "%s is invalid", tst.raw) - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicVariableOperations(t *testing.T) { + variables := Variables{ + "var1": "val1", + } + variables.Set("var2", "val2") + assert.Contains(t, variables, "VAR2", "Set should add a capitalized key") + all := variables.All() + keys := make([]string, 0, len(all)) + for k := range all { + keys = append(keys, k) + } + assert.ElementsMatch(t, []string{"var1", "VAR2"}, keys, "All returns every key") + assert.Equal(t, "val2", all["VAR2"], "VAR2 set value") + +} + +func TestSetvarFailsForReadOnlyVariables(t *testing.T) { + variables := Variables{} + variables.Set("SQLCMDDBNAME", "somedatabase") + err := variables.Setvar("SQLCMDDBNAME", "newdatabase") + assert.Error(t, err, "setting a readonly variable fails") + assert.Equal(t, "somedatabase", variables.SQLCmdDatabase(), "readonly variable shouldn't be changed by Setvar") +} + +func TestEnvironmentVariablesAsInput(t *testing.T) { + os.Setenv("SQLCMDSERVER", "someserver") + defer os.Unsetenv("SQLCMDSERVER") + os.Setenv("x", "somevalue") + defer os.Unsetenv("x") + vars := InitializeVariables(true).All() + assert.Equal(t, "someserver", vars["SQLCMDSERVER"], "InitializeVariables should read a valid environment variable from the known list") + _, ok := vars["x"] + assert.False(t, ok, "InitializeVariables should skip variables not in the known list") +} + +func TestSqlServerSplitsName(t *testing.T) { + vars := Variables{ + SQLCMDSERVER: `tcp:someserver\someinstance`, + } + serverName, instance, port, err := vars.SQLCmdServer() + if assert.NoError(t, err, "tcp:server\\someinstance") { + assert.Equal(t, "someserver", serverName, "server name for instance") + assert.Equal(t, uint64(0), port, "port for instance") + assert.Equal(t, "someinstance", instance, "instance for instance") + } + vars = Variables{ + SQLCMDSERVER: `tcp:someserver,1111`, + } + serverName, instance, port, err = vars.SQLCmdServer() + if assert.NoError(t, err, "tcp:server,1111") { + assert.Equal(t, "someserver", serverName, "server name for port number") + assert.Equal(t, uint64(1111), port, "port for port number") + assert.Equal(t, "", instance, "instance for port number") + } +} + +func TestParseValue(t *testing.T) { + type test struct { + raw string + val string + valid bool + } + tests := []test{ + {`""`, "", true}, + {`"`, "", false}, + {`"""`, "", false}, + {`no quotes`, "", false}, + {`"is quoted"`, "is quoted", true}, + {`" " single quote "`, "", false}, + {`" "" escaped quotes "" "`, ` " escaped quotes " `, true}, + } + + for _, tst := range tests { + v, err := ParseValue(tst.raw) + if tst.valid { + if assert.NoErrorf(t, err, "Unexpected error for value %s", tst.raw) { + assert.Equalf(t, tst.val, v, "Incorrect parsed value for %s", tst.raw) + } + } else { + assert.Errorf(t, err, "Expected error for %s", tst.raw) + } + } +} + +func TestValidIdentifier(t *testing.T) { + type test struct { + raw string + valid bool + } + tests := []test{ + {"1A", false}, + {"A1", true}, + {"A+", false}, + {"A-_b", true}, + } + for _, tst := range tests { + err := ValidIdentifier(tst.raw) + if tst.valid { + assert.NoErrorf(t, err, "%s is valid", tst.raw) + } else { + assert.Errorf(t, err, "%s is invalid", tst.raw) + } + } +} diff --git a/testdata/sql.txt b/testdata/sql.txt index b98b2681..7519b04f 100644 --- a/testdata/sql.txt +++ b/testdata/sql.txt @@ -1,3 +1,3 @@ -select 1 as col1 -go - +select 1 as col1 +go + From f251bca6783caccbd25e81e23577e38c96c9d978 Mon Sep 17 00:00:00 2001 From: stuartpa Date: Thu, 10 Nov 2022 07:22:36 -0800 Subject: [PATCH 2/5] Saving changes --- .github/workflows/golangci-lint.yml | 40 +- .pipelines/TestSql2017.yml | 106 +- .pipelines/include-install-go-tools.yml | 70 +- .pipelines/include-runtests-linux.yml | 92 +- SUPPORT.md | 50 +- build/azure-pipelines/build-common.yml | 140 +-- build/azure-pipelines/build-product.yml | 394 +++--- cmd/sqlcmd/testdata/select100.sql | 2 +- pkg/sqlcmd/azure_auth.go | 110 +- pkg/sqlcmd/batch.go | 526 ++++---- pkg/sqlcmd/batch_test.go | 446 +++---- pkg/sqlcmd/commands.go | 1014 +++++++-------- pkg/sqlcmd/commands_test.go | 604 ++++----- pkg/sqlcmd/errors.go | 188 +-- pkg/sqlcmd/format.go | 1326 ++++++++++---------- pkg/sqlcmd/format_darwin.go | 14 +- pkg/sqlcmd/format_linux.go | 14 +- pkg/sqlcmd/format_test.go | 280 ++--- pkg/sqlcmd/format_windows.go | 14 +- pkg/sqlcmd/parse.go | 202 +-- pkg/sqlcmd/parse_test.go | 8 +- pkg/sqlcmd/sqlcmd_test.go | 1184 ++++++++--------- pkg/sqlcmd/testdata/singlebatchnogo.sql | 4 +- pkg/sqlcmd/testdata/twobatchnoendinggo.sql | 6 +- pkg/sqlcmd/testdata/twobatchwithgo.sql | 8 +- pkg/sqlcmd/util.go | 146 +-- pkg/sqlcmd/variables.go | 670 +++++----- pkg/sqlcmd/variables_test.go | 232 ++-- testdata/sql.txt | 6 +- 29 files changed, 3948 insertions(+), 3948 deletions(-) diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index e18f537f..a28ef0ed 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,20 +1,20 @@ -name: golangci-lint -on: - push: - branches: - - main - pull_request: -jobs: - golangci-pr: - name: lint-pr-changes - runs-on: ubuntu-latest - steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.18 - - uses: actions/checkout@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest - only-new-issues: true +name: golangci-lint +on: + push: + branches: + - main + pull_request: +jobs: + golangci-pr: + name: lint-pr-changes + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true diff --git a/.pipelines/TestSql2017.yml b/.pipelines/TestSql2017.yml index 776b310b..589234cb 100644 --- a/.pipelines/TestSql2017.yml +++ b/.pipelines/TestSql2017.yml @@ -1,53 +1,53 @@ -variables: - # AZURE_CLIENT_SECRET and SQLPASSWORD must be defined as secret variables in the pipeline. - # AZURE_TENANT_ID and AZURE_CLIENT_ID are not expected to be secret variables, just regular variables - AZURECLIENTSECRET: $(AZURE_CLIENT_SECRET) - PASSWORD: $(SQLPASSWORD) -pool: - vmImage: 'ubuntu-latest' - -steps: - - template: include-install-go-tools.yml - - - task: Docker@2 - displayName: 'Run SQL 2017 docker image' - inputs: - command: run - arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(PASSWORD) mcr.microsoft.com/mssql/server:2017-latest' - - - template: include-runtests-linux.yml - parameters: - RunName: 'SQL2017' - SQLCMDUSER: sa - SQLPASSWORD: $(PASSWORD) - - - template: include-runtests-linux.yml - parameters: - RunName: 'SQLDB' - # AZURESERVER must be defined as a variable in the pipeline - SQLCMDSERVER: $(AZURESERVER) - AZURECLIENTSECRET: $(AZURECLIENTSECRET) - - - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 - displayName: Merge coverage data - inputs: - reports: '**/*.coverage.xml"' # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported. - targetdir: 'coverage' # REQUIRED # The directory where the generated report should be saved. - reporttypes: 'HtmlInline_AzurePipelines;Cobertura' # The output formats and scope (separated by semicolon) Values: Badges, Clover, Cobertura, CsvSummary, Html, HtmlChart, HtmlInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HtmlSummary, JsonSummary, Latex, LatexSummary, lcov, MarkdownSummary, MHtml, PngChart, SonarQube, TeamCitySummary, TextSummary, Xml, XmlSummary - sourcedirs: '$(Build.SourcesDirectory)' # Optional directories which contain the corresponding source code (separated by semicolon). The source directories are used if coverage report contains classes without path information. - verbosity: 'Info' # The verbosity level of the log messages. Values: Verbose, Info, Warning, Error, Off - tag: '$(build.buildnumber)_#$(build.buildid)_$(Build.SourceBranchName)' # Optional tag or build version. - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - pathToSources: '$(Build.SourcesDirectory)' - summaryFileLocation: $(Build.SourcesDirectory)/coverage/*.xml - reportDirectory: $(Build.SourcesDirectory)/coverage - failIfCoverageEmpty: true - condition: always() - continueOnError: true - env: - disable.coverage.autogenerate: 'true' - - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: ‘Component Detection’ +variables: + # AZURE_CLIENT_SECRET and SQLPASSWORD must be defined as secret variables in the pipeline. + # AZURE_TENANT_ID and AZURE_CLIENT_ID are not expected to be secret variables, just regular variables + AZURECLIENTSECRET: $(AZURE_CLIENT_SECRET) + PASSWORD: $(SQLPASSWORD) +pool: + vmImage: 'ubuntu-latest' + +steps: + - template: include-install-go-tools.yml + + - task: Docker@2 + displayName: 'Run SQL 2017 docker image' + inputs: + command: run + arguments: '-m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$(PASSWORD) mcr.microsoft.com/mssql/server:2017-latest' + + - template: include-runtests-linux.yml + parameters: + RunName: 'SQL2017' + SQLCMDUSER: sa + SQLPASSWORD: $(PASSWORD) + + - template: include-runtests-linux.yml + parameters: + RunName: 'SQLDB' + # AZURESERVER must be defined as a variable in the pipeline + SQLCMDSERVER: $(AZURESERVER) + AZURECLIENTSECRET: $(AZURECLIENTSECRET) + + - task: Palmmedia.reportgenerator.reportgenerator-build-release-task.reportgenerator@4 + displayName: Merge coverage data + inputs: + reports: '**/*.coverage.xml"' # REQUIRED # The coverage reports that should be parsed (separated by semicolon). Globbing is supported. + targetdir: 'coverage' # REQUIRED # The directory where the generated report should be saved. + reporttypes: 'HtmlInline_AzurePipelines;Cobertura' # The output formats and scope (separated by semicolon) Values: Badges, Clover, Cobertura, CsvSummary, Html, HtmlChart, HtmlInline, HtmlInline_AzurePipelines, HtmlInline_AzurePipelines_Dark, HtmlSummary, JsonSummary, Latex, LatexSummary, lcov, MarkdownSummary, MHtml, PngChart, SonarQube, TeamCitySummary, TextSummary, Xml, XmlSummary + sourcedirs: '$(Build.SourcesDirectory)' # Optional directories which contain the corresponding source code (separated by semicolon). The source directories are used if coverage report contains classes without path information. + verbosity: 'Info' # The verbosity level of the log messages. Values: Verbose, Info, Warning, Error, Off + tag: '$(build.buildnumber)_#$(build.buildid)_$(Build.SourceBranchName)' # Optional tag or build version. + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + pathToSources: '$(Build.SourcesDirectory)' + summaryFileLocation: $(Build.SourcesDirectory)/coverage/*.xml + reportDirectory: $(Build.SourcesDirectory)/coverage + failIfCoverageEmpty: true + condition: always() + continueOnError: true + env: + disable.coverage.autogenerate: 'true' + + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: ‘Component Detection’ diff --git a/.pipelines/include-install-go-tools.yml b/.pipelines/include-install-go-tools.yml index 53aeb1f2..3e33cf47 100644 --- a/.pipelines/include-install-go-tools.yml +++ b/.pipelines/include-install-go-tools.yml @@ -1,36 +1,36 @@ -steps: - - task: GoTool@0 - inputs: - version: '1.18' - - task: Go@0 - displayName: 'Go: get dependencies' - inputs: - command: 'get' - arguments: '-d' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - - - - task: Go@0 - displayName: 'Go: install gotest.tools/gotestsum' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'gotest.tools/gotestsum@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' - - - task: Go@0 - displayName: 'Go: install github.com/axw/gocov/gocov' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/axw/gocov/gocov@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' - - - task: Go@0 - displayName: 'Go: install github.com/axw/gocov/gocov' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/AlekSi/gocov-xml@latest' - workingDirectory: '$(System.DefaultWorkingDirectory)' +steps: + - task: GoTool@0 + inputs: + version: '1.18' + - task: Go@0 + displayName: 'Go: get dependencies' + inputs: + command: 'get' + arguments: '-d' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + + + - task: Go@0 + displayName: 'Go: install gotest.tools/gotestsum' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'gotest.tools/gotestsum@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - task: Go@0 + displayName: 'Go: install github.com/axw/gocov/gocov' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/axw/gocov/gocov@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' + + - task: Go@0 + displayName: 'Go: install github.com/axw/gocov/gocov' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/AlekSi/gocov-xml@latest' + workingDirectory: '$(System.DefaultWorkingDirectory)' \ No newline at end of file diff --git a/.pipelines/include-runtests-linux.yml b/.pipelines/include-runtests-linux.yml index 2735a10e..e1aa0d42 100644 --- a/.pipelines/include-runtests-linux.yml +++ b/.pipelines/include-runtests-linux.yml @@ -1,46 +1,46 @@ -parameters: -- name: RunName - type: string -- name: SQLCMDUSER - type: string - default: '' -- name: SQLPASSWORD - type: string - default: '' -- name: AZURECLIENTSECRET - type: string - default: '' -- name: SQLCMDSERVER - type: string - default: . -- name: SQLCMDDBNAME - type: string - default: '' -steps: - - script: | - ~/go/bin/gotestsum --junitfile "${{ parameters.RunName }}.testresults.xml" -- ./... -coverprofile="${{ parameters.RunName }}.coverage.txt" -covermode count - ~/go/bin/gocov convert "${{ parameters.RunName }}.coverage.txt" > "${{ parameters.RunName }}.coverage.json" - ~/go/bin/gocov-xml < "${{ parameters.RunName }}.coverage.json" > ${{ parameters.RunName }}.coverage.xml - mkdir -p coverage - workingDirectory: '$(Build.SourcesDirectory)' - displayName: 'run tests' - env: - SQLPASSWORD: ${{ parameters.SQLPASSWORD }} - SQLCMDUSER: ${{ parameters.SQLCMDUSER }} - SQLCMDPASSWORD: ${{ parameters.SQLPASSWORD }} - AZURE_TENANT_ID: $(AZURE_TENANT_ID) - AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) - AZURE_CLIENT_SECRET: ${{ parameters.AZURECLIENTSECRET }} - SQLCMDSERVER: ${{ parameters.SQLCMDSERVER }} - SQLCMDDBNAME: ${{ parameters.SQLCMDDBNAME }} - continueOnError: true - - - task: PublishTestResults@2 - displayName: "Publish junit-style results" - inputs: - testResultsFiles: '${{ parameters.RunName }}.testresults.xml' - testResultsFormat: JUnit - searchFolder: '$(Build.SourcesDirectory)' - testRunTitle: '${{ parameters.RunName }} - $(Build.SourceBranchName)' - failTaskOnFailedTests: true - condition: always() +parameters: +- name: RunName + type: string +- name: SQLCMDUSER + type: string + default: '' +- name: SQLPASSWORD + type: string + default: '' +- name: AZURECLIENTSECRET + type: string + default: '' +- name: SQLCMDSERVER + type: string + default: . +- name: SQLCMDDBNAME + type: string + default: '' +steps: + - script: | + ~/go/bin/gotestsum --junitfile "${{ parameters.RunName }}.testresults.xml" -- ./... -coverprofile="${{ parameters.RunName }}.coverage.txt" -covermode count + ~/go/bin/gocov convert "${{ parameters.RunName }}.coverage.txt" > "${{ parameters.RunName }}.coverage.json" + ~/go/bin/gocov-xml < "${{ parameters.RunName }}.coverage.json" > ${{ parameters.RunName }}.coverage.xml + mkdir -p coverage + workingDirectory: '$(Build.SourcesDirectory)' + displayName: 'run tests' + env: + SQLPASSWORD: ${{ parameters.SQLPASSWORD }} + SQLCMDUSER: ${{ parameters.SQLCMDUSER }} + SQLCMDPASSWORD: ${{ parameters.SQLPASSWORD }} + AZURE_TENANT_ID: $(AZURE_TENANT_ID) + AZURE_CLIENT_ID: $(AZURE_CLIENT_ID) + AZURE_CLIENT_SECRET: ${{ parameters.AZURECLIENTSECRET }} + SQLCMDSERVER: ${{ parameters.SQLCMDSERVER }} + SQLCMDDBNAME: ${{ parameters.SQLCMDDBNAME }} + continueOnError: true + + - task: PublishTestResults@2 + displayName: "Publish junit-style results" + inputs: + testResultsFiles: '${{ parameters.RunName }}.testresults.xml' + testResultsFormat: JUnit + searchFolder: '$(Build.SourcesDirectory)' + testRunTitle: '${{ parameters.RunName }} - $(Build.SourceBranchName)' + failTaskOnFailedTests: true + condition: always() diff --git a/SUPPORT.md b/SUPPORT.md index dc72f0e5..8b05616f 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -1,25 +1,25 @@ -# TODO: The maintainer of this repo has not yet edited this file - -**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? - -- **No CSS support:** Fill out this template with information about how to file issues and get help. -- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). -- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. - -*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* - -# Support - -## How to file issues and get help - -This project uses GitHub Issues to track bugs and feature requests. Please search the existing -issues before filing new issues to avoid duplicates. For new issues, file your bug or -feature request as a new Issue. - -For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE -FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER -CHANNEL. WHERE WILL YOU HELP PEOPLE?**. - -## Microsoft Support Policy - -Support for this **PROJECT or PRODUCT** is limited to the resources listed above. +# TODO: The maintainer of this repo has not yet edited this file + +**REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? + +- **No CSS support:** Fill out this template with information about how to file issues and get help. +- **Yes CSS support:** Fill out an intake form at [aka.ms/spot](https://aka.ms/spot). CSS will work with/help you to determine next steps. More details also available at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). +- **Not sure?** Fill out a SPOT intake as though the answer were "Yes". CSS will help you decide. + +*Then remove this first heading from this SUPPORT.MD file before publishing your repo.* + +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. Please search the existing +issues before filing new issues to avoid duplicates. For new issues, file your bug or +feature request as a new Issue. + +For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE +FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER +CHANNEL. WHERE WILL YOU HELP PEOPLE?**. + +## Microsoft Support Policy + +Support for this **PROJECT or PRODUCT** is limited to the resources listed above. diff --git a/build/azure-pipelines/build-common.yml b/build/azure-pipelines/build-common.yml index 62e91668..3ad9659b 100644 --- a/build/azure-pipelines/build-common.yml +++ b/build/azure-pipelines/build-common.yml @@ -1,70 +1,70 @@ -parameters: -- name: OS - type: string - default: -- name: Arch - type: string - default: -- name: ArtifactName - type: string -- name: VersionTag - type: string - default: $(Build.BuildNumber) - -steps: -- task: GoTool@0 - inputs: - version: '1.18' - goBin: $(Build.SourcesDirectory) - -- task: Go@0 - displayName: 'Go install go-winres' - inputs: - command: 'custom' - customCommand: 'install' - arguments: 'github.com/tc-hib/go-winres@latest' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOBIN: $(Build.SourcesDirectory) - -- task: CmdLine@2 - displayName: 'generate version resource' - inputs: - script: $(Build.SourcesDirectory)/go-winres make --file-version git-tag --product-version git-tag - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - -- task: Go@0 - displayName: 'Go: get dependencies' - inputs: - command: 'get' - arguments: '-d' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOOS: ${{ parameters.OS }} - GOARCH: ${{ parameters.Arch }} - GOBIN: $(Build.SourcesDirectory) - -- task: Go@0 - displayName: 'Go: build sqlcmd' - inputs: - command: 'build' - arguments: '-o $(Build.BinariesDirectory) -ldflags="-X main.version=${{ parameters.VersionTag }}"' - workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' - env: - GOOS: ${{ parameters.OS }} - GOARCH: ${{ parameters.Arch }} - GOBIN: $(Build.SourcesDirectory) - CGO_ENABLED: 0 # Enables Docker image based off 'scratch' - -- task: CopyFiles@2 - inputs: - TargetFolder: '$(Build.ArtifactStagingDirectory)' - SourceFolder: '$(Build.BinariesDirectory)' - Contents: '**' - -- task: PublishPipelineArtifact@1 - displayName: 'Publish binary' - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: 'Sqlcmd${{ parameters.ArtifactName }}' - +parameters: +- name: OS + type: string + default: +- name: Arch + type: string + default: +- name: ArtifactName + type: string +- name: VersionTag + type: string + default: $(Build.BuildNumber) + +steps: +- task: GoTool@0 + inputs: + version: '1.18' + goBin: $(Build.SourcesDirectory) + +- task: Go@0 + displayName: 'Go install go-winres' + inputs: + command: 'custom' + customCommand: 'install' + arguments: 'github.com/tc-hib/go-winres@latest' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOBIN: $(Build.SourcesDirectory) + +- task: CmdLine@2 + displayName: 'generate version resource' + inputs: + script: $(Build.SourcesDirectory)/go-winres make --file-version git-tag --product-version git-tag + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + +- task: Go@0 + displayName: 'Go: get dependencies' + inputs: + command: 'get' + arguments: '-d' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOOS: ${{ parameters.OS }} + GOARCH: ${{ parameters.Arch }} + GOBIN: $(Build.SourcesDirectory) + +- task: Go@0 + displayName: 'Go: build sqlcmd' + inputs: + command: 'build' + arguments: '-o $(Build.BinariesDirectory) -ldflags="-X main.version=${{ parameters.VersionTag }}"' + workingDirectory: '$(Build.SourcesDirectory)/cmd/sqlcmd' + env: + GOOS: ${{ parameters.OS }} + GOARCH: ${{ parameters.Arch }} + GOBIN: $(Build.SourcesDirectory) + CGO_ENABLED: 0 # Enables Docker image based off 'scratch' + +- task: CopyFiles@2 + inputs: + TargetFolder: '$(Build.ArtifactStagingDirectory)' + SourceFolder: '$(Build.BinariesDirectory)' + Contents: '**' + +- task: PublishPipelineArtifact@1 + displayName: 'Publish binary' + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: 'Sqlcmd${{ parameters.ArtifactName }}' + diff --git a/build/azure-pipelines/build-product.yml b/build/azure-pipelines/build-product.yml index c028c4ae..f9a6a75a 100644 --- a/build/azure-pipelines/build-product.yml +++ b/build/azure-pipelines/build-product.yml @@ -1,197 +1,197 @@ -trigger: - tags: - include: - - v* - -pr: none - -parameters: - - name: PushToGithub - default: true - type: boolean - displayName: Push packages to github - -stages: - - stage: Compile - displayName: Compile sqlcmd on all supported platforms - jobs: - - job: Sqlcmd - strategy: - matrix: - linux: - imageName: 'ubuntu-latest' - artifact: LinuxAmd64 - os: - arch: - mac: - imageName: 'macOS-latest' - artifact: DarwinAmd64 - os: - arch: - windows: - imageName: 'windows-latest' - artifact: WindowsAmd64 - os: - arch: - linuxArm: - imageName: 'ubuntu-latest' - artifact: LinuxArm64 - os: - arch: arm64 - windowsArm: - imageName: 'windows-latest' - artifact: WindowsArm - os: - arch: arm - linuxs390x: - imageName: 'ubuntu-latest' - artifact: LinuxS390x - os: - arch: s390x - pool: - vmImage: $(imageName) - steps: - - template: build-tag.yml - - script: | - echo $(getVersion.VERSION_TAG) - - template: build-common.yml - parameters: - OS: $(os) - Arch: $(arch) - ArtifactName: $(artifact) - VersionTag: $(getVersion.VERSION_TAG) - - - stage: CreatePackages - displayName: Create packages to publish - jobs: - - job: Sign_and_pack - pool: - vmImage: 'windows-latest' - steps: - - template: build-tag.yml - - task: DownloadPipelineArtifact@2 - inputs: - buildType: 'current' - targetPath: '$(Pipeline.Workspace)' - - task: EsrpCodeSigning@1 - displayName: Sign Windows binary - inputs: - ConnectedServiceName: 'Code Signing' - FolderPath: '$(Pipeline.Workspace)' - Pattern: 'sqlcmd.exe' - signConfigType: 'inlineSignParams' - SessionTimeout: '600' - MaxConcurrency: '5' - MaxRetryAttempts: '5' - inlineOperation: | - [ - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "go-sqlcmd" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://github.com/microsoft/go-sqlcmd" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd sha256" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - } - ] - - task: ArchiveFiles@2 - displayName: Zip Windows amd64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsAmd64\Sqlcmd.exe' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-x64.zip' - - - task: ArchiveFiles@2 - displayName: Zip Windows arm binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsArm\Sqlcmd.exe' - includeRootFolder: false - archiveType: 'zip' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-arm.zip' - - - task: ArchiveFiles@2 - displayName: Tar Linux amd64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxAmd64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-x64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Darwin binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdDarwinAmd64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-darwin-x64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Linux arm64 binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxArm64' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-arm64.tar.bz2' - - - task: ArchiveFiles@2 - displayName: Tar Linux s390x binary - inputs: - rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxS390x' - includeRootFolder: false - archiveType: 'tar' - tarCompression: 'bz2' - archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-s390x.tar.bz2' - - - task: PublishPipelineArtifact@1 - displayName: 'Publish release archives' - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: SqlcmdRelease - - - task: GitHubRelease@1 - condition: eq('${{ parameters.PushToGithub}}', 'true') - inputs: - gitHubConnection: 'gosqlcmd_github' - repositoryName: '$(Build.Repository.Name)' - action: 'create' - target: '$(Build.SourceVersion)' - tagSource: 'userSpecifiedTag' - tag: '$(getVersion.VERSION_TAG)' - changeLogCompareToRelease: 'lastFullRelease' - changeLogType: 'commitBased' +trigger: + tags: + include: + - v* + +pr: none + +parameters: + - name: PushToGithub + default: true + type: boolean + displayName: Push packages to github + +stages: + - stage: Compile + displayName: Compile sqlcmd on all supported platforms + jobs: + - job: Sqlcmd + strategy: + matrix: + linux: + imageName: 'ubuntu-latest' + artifact: LinuxAmd64 + os: + arch: + mac: + imageName: 'macOS-latest' + artifact: DarwinAmd64 + os: + arch: + windows: + imageName: 'windows-latest' + artifact: WindowsAmd64 + os: + arch: + linuxArm: + imageName: 'ubuntu-latest' + artifact: LinuxArm64 + os: + arch: arm64 + windowsArm: + imageName: 'windows-latest' + artifact: WindowsArm + os: + arch: arm + linuxs390x: + imageName: 'ubuntu-latest' + artifact: LinuxS390x + os: + arch: s390x + pool: + vmImage: $(imageName) + steps: + - template: build-tag.yml + - script: | + echo $(getVersion.VERSION_TAG) + - template: build-common.yml + parameters: + OS: $(os) + Arch: $(arch) + ArtifactName: $(artifact) + VersionTag: $(getVersion.VERSION_TAG) + + - stage: CreatePackages + displayName: Create packages to publish + jobs: + - job: Sign_and_pack + pool: + vmImage: 'windows-latest' + steps: + - template: build-tag.yml + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + targetPath: '$(Pipeline.Workspace)' + - task: EsrpCodeSigning@1 + displayName: Sign Windows binary + inputs: + ConnectedServiceName: 'Code Signing' + FolderPath: '$(Pipeline.Workspace)' + Pattern: 'sqlcmd.exe' + signConfigType: 'inlineSignParams' + SessionTimeout: '600' + MaxConcurrency: '5' + MaxRetryAttempts: '5' + inlineOperation: | + [ + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "go-sqlcmd" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://github.com/microsoft/go-sqlcmd" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd sha256" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + } + ] + - task: ArchiveFiles@2 + displayName: Zip Windows amd64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsAmd64\Sqlcmd.exe' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-x64.zip' + + - task: ArchiveFiles@2 + displayName: Zip Windows arm binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdWindowsArm\Sqlcmd.exe' + includeRootFolder: false + archiveType: 'zip' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-windows-arm.zip' + + - task: ArchiveFiles@2 + displayName: Tar Linux amd64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxAmd64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-x64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Darwin binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdDarwinAmd64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-darwin-x64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Linux arm64 binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxArm64' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-arm64.tar.bz2' + + - task: ArchiveFiles@2 + displayName: Tar Linux s390x binary + inputs: + rootFolderOrFile: '$(Pipeline.Workspace)\SqlcmdLinuxS390x' + includeRootFolder: false + archiveType: 'tar' + tarCompression: 'bz2' + archiveFile: '$(Build.ArtifactStagingDirectory)/sqlcmd-$(getVersion.VERSION_TAG)-linux-s390x.tar.bz2' + + - task: PublishPipelineArtifact@1 + displayName: 'Publish release archives' + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: SqlcmdRelease + + - task: GitHubRelease@1 + condition: eq('${{ parameters.PushToGithub}}', 'true') + inputs: + gitHubConnection: 'gosqlcmd_github' + repositoryName: '$(Build.Repository.Name)' + action: 'create' + target: '$(Build.SourceVersion)' + tagSource: 'userSpecifiedTag' + tag: '$(getVersion.VERSION_TAG)' + changeLogCompareToRelease: 'lastFullRelease' + changeLogType: 'commitBased' diff --git a/cmd/sqlcmd/testdata/select100.sql b/cmd/sqlcmd/testdata/select100.sql index 1b87fa39..718c071f 100644 --- a/cmd/sqlcmd/testdata/select100.sql +++ b/cmd/sqlcmd/testdata/select100.sql @@ -1 +1 @@ -select 100 +select 100 diff --git a/pkg/sqlcmd/azure_auth.go b/pkg/sqlcmd/azure_auth.go index 5d924390..f554a181 100644 --- a/pkg/sqlcmd/azure_auth.go +++ b/pkg/sqlcmd/azure_auth.go @@ -1,55 +1,55 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "database/sql/driver" - "fmt" - "net/url" - "os" - - "github.com/microsoft/go-mssqldb/azuread" -) - -const ( - NotSpecified = "NotSpecified" - SqlPassword = "SqlPassword" - sqlClientId = "a94f9c62-97fe-4d19-b06d-472bed8d2bcf" -) - -func getSqlClientId() string { - if clientId := os.Getenv("SQLCMDCLIENTID"); clientId != "" { - return clientId - } - return sqlClientId -} - -func GetTokenBasedConnection(connstr string, authenticationMethod string) (driver.Connector, error) { - - connectionUrl, err := url.Parse(connstr) - if err != nil { - return nil, err - } - - query := connectionUrl.Query() - query.Set("fedauth", authenticationMethod) - query.Set("applicationclientid", getSqlClientId()) - switch authenticationMethod { - case azuread.ActiveDirectoryServicePrincipal, azuread.ActiveDirectoryApplication: - query.Set("clientcertpath", os.Getenv("AZURE_CLIENT_CERTIFICATE_PATH")) - case azuread.ActiveDirectoryInteractive: - loginTimeout := query.Get("connection timeout") - loginTimeoutSeconds := 0 - if loginTimeout != "" { - _, _ = fmt.Sscanf(loginTimeout, "%d", &loginTimeoutSeconds) - } - // AAD interactive needs minutes at minimum - if loginTimeoutSeconds > 0 && loginTimeoutSeconds < 120 { - query.Set("connection timeout", "120") - } - } - - connectionUrl.RawQuery = query.Encode() - return azuread.NewConnector(connectionUrl.String()) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "database/sql/driver" + "fmt" + "net/url" + "os" + + "github.com/microsoft/go-mssqldb/azuread" +) + +const ( + NotSpecified = "NotSpecified" + SqlPassword = "SqlPassword" + sqlClientId = "a94f9c62-97fe-4d19-b06d-472bed8d2bcf" +) + +func getSqlClientId() string { + if clientId := os.Getenv("SQLCMDCLIENTID"); clientId != "" { + return clientId + } + return sqlClientId +} + +func GetTokenBasedConnection(connstr string, authenticationMethod string) (driver.Connector, error) { + + connectionUrl, err := url.Parse(connstr) + if err != nil { + return nil, err + } + + query := connectionUrl.Query() + query.Set("fedauth", authenticationMethod) + query.Set("applicationclientid", getSqlClientId()) + switch authenticationMethod { + case azuread.ActiveDirectoryServicePrincipal, azuread.ActiveDirectoryApplication: + query.Set("clientcertpath", os.Getenv("AZURE_CLIENT_CERTIFICATE_PATH")) + case azuread.ActiveDirectoryInteractive: + loginTimeout := query.Get("connection timeout") + loginTimeoutSeconds := 0 + if loginTimeout != "" { + _, _ = fmt.Sscanf(loginTimeout, "%d", &loginTimeoutSeconds) + } + // AAD interactive needs minutes at minimum + if loginTimeoutSeconds > 0 && loginTimeoutSeconds < 120 { + query.Set("connection timeout", "120") + } + } + + connectionUrl.RawQuery = query.Encode() + return azuread.NewConnector(connectionUrl.String()) +} diff --git a/pkg/sqlcmd/batch.go b/pkg/sqlcmd/batch.go index 9afaf812..7b8082e5 100644 --- a/pkg/sqlcmd/batch.go +++ b/pkg/sqlcmd/batch.go @@ -1,263 +1,263 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -const minCapIncrease = 512 - -// lineend is the slice to use when appending a line. -var lineend = []rune(SqlcmdEol) - -// Batch provides the query text to run -type Batch struct { - // read provides the next chunk of runes - read batchScan - // Buffer is the current batch text - Buffer []rune - // Length is the length of the statement - Length int - // raw is the unprocessed runes - raw []rune - // rawlen is the number of unprocessed runes - rawlen int - // quote indicates currently processing a quoted string - quote rune - // comment is the state of multi-line comment processing - comment bool - // batchline is the 1-based index of the next line. - // Used for the prompt in interactive mode - batchline int - // linecount is the total number of batch lines processed in the session - linecount uint - // varmap tracks the location of expandable variables for the entire batch - varmap map[int]string - // linevarmap tracks the location of expandable variables on the current line - linevarmap map[int]string - // cmd is the set of Commands available - cmd Commands -} - -type batchScan func() (string, error) - -// NewBatch creates a Batch which converts runes provided by reader into SQL batches -func NewBatch(reader batchScan, cmd Commands) *Batch { - b := &Batch{ - read: reader, - cmd: cmd, - } - b.Reset(nil) - return b -} - -// String returns the current SQL batch text -func (b *Batch) String() string { - return string(b.Buffer) -} - -// Reset clears the current batch text and replaces it with new runes -func (b *Batch) Reset(r []rune) { - b.Buffer, b.Length = nil, 0 - b.quote = 0 - b.comment = false - b.batchline = 1 - if r != nil { - b.raw, b.rawlen = r, len(r) - } else { - b.rawlen = 0 - } - b.varmap = make(map[int]string) -} - -// Next processes the next chunk of input and sets the Batch state accordingly. -// If the input contains a command to run, Next returns the Command and its -// parameters. -// Upon exit from Next, the caller can use the State method to determine if -// it represents a runnable SQL batch text. -func (b *Batch) Next() (*Command, []string, error) { - b.linevarmap = nil - var err error - var i int - if b.rawlen == 0 { - s, err := b.read() - if err != nil { - return nil, nil, err - } - b.raw = []rune(s) - b.rawlen = len(b.raw) - } - - var command *Command - var args []string - var ok bool - var scannedCommand bool - b.linecount++ -parse: - for ; i < b.rawlen; i++ { - c, next := b.raw[i], grab(b.raw, i+1, b.rawlen) - switch { - // we're in a quoted string - case b.quote != 0: - i, ok, err = b.readString(b.raw, i, b.rawlen, b.quote, b.linecount) - if err != nil { - break parse - } - if ok { - b.quote = 0 - } - // inside a multiline comment - case b.comment: - i, ok = readMultilineComment(b.raw, i, b.rawlen) - b.comment = !ok - // start of a string - case c == '\'' || c == '"': - b.quote = c - // inline sql comment, skip to end of line - case c == '-' && next == '-': - i = b.rawlen - // start a multi-line comment - case c == '/' && next == '*': - b.comment = true - i++ - // continue processing quoted string or multiline comment - case b.quote != 0 || b.comment: - - // Handle variable references - case c == '$' && next == '(': - vi, ok := readVariableReference(b.raw, i+2, b.rawlen) - if ok { - b.addVariableLocation(i, string(b.raw[i+2:vi])) - i = vi - - } else { - err = syntaxError(b.linecount) - break parse - } - // Commands have to be alone on the line - case !scannedCommand && b.cmd != nil: - var cend int - scannedCommand = true - command, args, cend = readCommand(b.cmd, b.raw, i, b.rawlen) - if command != nil { - // remove the command from raw - b.raw = append(b.raw[:i], b.raw[cend:]...) - break parse - } - } - } - if err == nil { - i = min(i, b.rawlen) - empty := isEmptyLine(b.raw, 0, i) - appendLine := true - if !b.comment && command != nil && empty { - appendLine = false - } - if appendLine { - // any variables on the line need to be added to the global map - inc := 0 - if b.Length > 0 { - inc = len(lineend) - } - if b.linevarmap != nil { - for v := range b.linevarmap { - b.varmap[v+b.Length+inc] = b.linevarmap[v] - } - } - // log.Printf(">> appending: `%s`", string(r[st:i])) - b.append(b.raw[:i], lineend) - b.batchline++ - } - b.raw = b.raw[i:] - b.rawlen = len(b.raw) - } else { - b.Reset(nil) - } - return command, args, err -} - -// append appends r to b.Buffer separated by sep when b.Buffer is not already empty. -// -// Dynamically grows b.Buf as necessary to accommodate r and the separator. -// Specifically, when b.Buf is not empty, b.Buf will grow by increments of -// MinCapIncrease. -// -// After a call to append, b.Len will be len(b.Buf)+len(sep)+len(r). Call Reset -// to reset the Buf. -func (b *Batch) append(r, sep []rune) { - rlen := len(r) - // initial - if b.Buffer == nil { - b.Buffer, b.Length = r, rlen - return - } - blen, seplen := b.Length, len(sep) - tlen := blen + rlen + seplen - // grow - if bcap := cap(b.Buffer); tlen > bcap { - n := tlen + 2*rlen - n += minCapIncrease - (n % minCapIncrease) - z := make([]rune, blen, n) - copy(z, b.Buffer) - b.Buffer = z - } - b.Buffer = b.Buffer[:tlen] - copy(b.Buffer[blen:], sep) - copy(b.Buffer[blen+seplen:], r) - b.Length = tlen -} - -// State returns a string representing the state of statement parsing. -// * Is in the middle of a multi-line comment -// - Has a non-empty batch ready to run -// = Is empty -// ' " Is in the middle of a multi-line quoted string -func (b *Batch) State() string { - switch { - case b.quote != 0: - return string(b.quote) - case b.comment: - return "*" - case b.Length != 0: - return "-" - } - return "=" -} - -// readString seeks to the end of a string returning the position and whether -// or not the string's end was found. -// -// If the string's terminator was not found, then the result will be the passed -// end. -// An error is returned if the string contains a malformed variable reference -func (b *Batch) readString(r []rune, i, end int, quote rune, line uint) (int, bool, error) { - var prev, c, next rune - for ; i < end; i++ { - c, next = r[i], grab(r, i+1, end) - switch { - case c == '$' && next == '(': - vl, ok := readVariableReference(r, i+2, end) - if ok { - b.addVariableLocation(i, string(r[i+2:vl])) - i = vl - - } else { - return i, false, syntaxError(line) - } - case quote == '\'' && c == '\'' && next == '\'': - i++ - continue - case quote == '\'' && c == '\'' && prev != '\'', - quote == '"' && c == '"': - return i, true, nil - } - prev = c - } - return end, false, nil -} - -// addVariableLocation is called for each variable on the current line -func (b *Batch) addVariableLocation(i int, v string) { - if b.linevarmap == nil { - b.linevarmap = make(map[int]string) - } - b.linevarmap[i] = v -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +const minCapIncrease = 512 + +// lineend is the slice to use when appending a line. +var lineend = []rune(SqlcmdEol) + +// Batch provides the query text to run +type Batch struct { + // read provides the next chunk of runes + read batchScan + // Buffer is the current batch text + Buffer []rune + // Length is the length of the statement + Length int + // raw is the unprocessed runes + raw []rune + // rawlen is the number of unprocessed runes + rawlen int + // quote indicates currently processing a quoted string + quote rune + // comment is the state of multi-line comment processing + comment bool + // batchline is the 1-based index of the next line. + // Used for the prompt in interactive mode + batchline int + // linecount is the total number of batch lines processed in the session + linecount uint + // varmap tracks the location of expandable variables for the entire batch + varmap map[int]string + // linevarmap tracks the location of expandable variables on the current line + linevarmap map[int]string + // cmd is the set of Commands available + cmd Commands +} + +type batchScan func() (string, error) + +// NewBatch creates a Batch which converts runes provided by reader into SQL batches +func NewBatch(reader batchScan, cmd Commands) *Batch { + b := &Batch{ + read: reader, + cmd: cmd, + } + b.Reset(nil) + return b +} + +// String returns the current SQL batch text +func (b *Batch) String() string { + return string(b.Buffer) +} + +// Reset clears the current batch text and replaces it with new runes +func (b *Batch) Reset(r []rune) { + b.Buffer, b.Length = nil, 0 + b.quote = 0 + b.comment = false + b.batchline = 1 + if r != nil { + b.raw, b.rawlen = r, len(r) + } else { + b.rawlen = 0 + } + b.varmap = make(map[int]string) +} + +// Next processes the next chunk of input and sets the Batch state accordingly. +// If the input contains a command to run, Next returns the Command and its +// parameters. +// Upon exit from Next, the caller can use the State method to determine if +// it represents a runnable SQL batch text. +func (b *Batch) Next() (*Command, []string, error) { + b.linevarmap = nil + var err error + var i int + if b.rawlen == 0 { + s, err := b.read() + if err != nil { + return nil, nil, err + } + b.raw = []rune(s) + b.rawlen = len(b.raw) + } + + var command *Command + var args []string + var ok bool + var scannedCommand bool + b.linecount++ +parse: + for ; i < b.rawlen; i++ { + c, next := b.raw[i], grab(b.raw, i+1, b.rawlen) + switch { + // we're in a quoted string + case b.quote != 0: + i, ok, err = b.readString(b.raw, i, b.rawlen, b.quote, b.linecount) + if err != nil { + break parse + } + if ok { + b.quote = 0 + } + // inside a multiline comment + case b.comment: + i, ok = readMultilineComment(b.raw, i, b.rawlen) + b.comment = !ok + // start of a string + case c == '\'' || c == '"': + b.quote = c + // inline sql comment, skip to end of line + case c == '-' && next == '-': + i = b.rawlen + // start a multi-line comment + case c == '/' && next == '*': + b.comment = true + i++ + // continue processing quoted string or multiline comment + case b.quote != 0 || b.comment: + + // Handle variable references + case c == '$' && next == '(': + vi, ok := readVariableReference(b.raw, i+2, b.rawlen) + if ok { + b.addVariableLocation(i, string(b.raw[i+2:vi])) + i = vi + + } else { + err = syntaxError(b.linecount) + break parse + } + // Commands have to be alone on the line + case !scannedCommand && b.cmd != nil: + var cend int + scannedCommand = true + command, args, cend = readCommand(b.cmd, b.raw, i, b.rawlen) + if command != nil { + // remove the command from raw + b.raw = append(b.raw[:i], b.raw[cend:]...) + break parse + } + } + } + if err == nil { + i = min(i, b.rawlen) + empty := isEmptyLine(b.raw, 0, i) + appendLine := true + if !b.comment && command != nil && empty { + appendLine = false + } + if appendLine { + // any variables on the line need to be added to the global map + inc := 0 + if b.Length > 0 { + inc = len(lineend) + } + if b.linevarmap != nil { + for v := range b.linevarmap { + b.varmap[v+b.Length+inc] = b.linevarmap[v] + } + } + // log.Printf(">> appending: `%s`", string(r[st:i])) + b.append(b.raw[:i], lineend) + b.batchline++ + } + b.raw = b.raw[i:] + b.rawlen = len(b.raw) + } else { + b.Reset(nil) + } + return command, args, err +} + +// append appends r to b.Buffer separated by sep when b.Buffer is not already empty. +// +// Dynamically grows b.Buf as necessary to accommodate r and the separator. +// Specifically, when b.Buf is not empty, b.Buf will grow by increments of +// MinCapIncrease. +// +// After a call to append, b.Len will be len(b.Buf)+len(sep)+len(r). Call Reset +// to reset the Buf. +func (b *Batch) append(r, sep []rune) { + rlen := len(r) + // initial + if b.Buffer == nil { + b.Buffer, b.Length = r, rlen + return + } + blen, seplen := b.Length, len(sep) + tlen := blen + rlen + seplen + // grow + if bcap := cap(b.Buffer); tlen > bcap { + n := tlen + 2*rlen + n += minCapIncrease - (n % minCapIncrease) + z := make([]rune, blen, n) + copy(z, b.Buffer) + b.Buffer = z + } + b.Buffer = b.Buffer[:tlen] + copy(b.Buffer[blen:], sep) + copy(b.Buffer[blen+seplen:], r) + b.Length = tlen +} + +// State returns a string representing the state of statement parsing. +// * Is in the middle of a multi-line comment +// - Has a non-empty batch ready to run +// = Is empty +// ' " Is in the middle of a multi-line quoted string +func (b *Batch) State() string { + switch { + case b.quote != 0: + return string(b.quote) + case b.comment: + return "*" + case b.Length != 0: + return "-" + } + return "=" +} + +// readString seeks to the end of a string returning the position and whether +// or not the string's end was found. +// +// If the string's terminator was not found, then the result will be the passed +// end. +// An error is returned if the string contains a malformed variable reference +func (b *Batch) readString(r []rune, i, end int, quote rune, line uint) (int, bool, error) { + var prev, c, next rune + for ; i < end; i++ { + c, next = r[i], grab(r, i+1, end) + switch { + case c == '$' && next == '(': + vl, ok := readVariableReference(r, i+2, end) + if ok { + b.addVariableLocation(i, string(r[i+2:vl])) + i = vl + + } else { + return i, false, syntaxError(line) + } + case quote == '\'' && c == '\'' && next == '\'': + i++ + continue + case quote == '\'' && c == '\'' && prev != '\'', + quote == '"' && c == '"': + return i, true, nil + } + prev = c + } + return end, false, nil +} + +// addVariableLocation is called for each variable on the current line +func (b *Batch) addVariableLocation(i int, v string) { + if b.linevarmap == nil { + b.linevarmap = make(map[int]string) + } + b.linevarmap[i] = v +} diff --git a/pkg/sqlcmd/batch_test.go b/pkg/sqlcmd/batch_test.go index 4c4ddd8f..a00c85f7 100644 --- a/pkg/sqlcmd/batch_test.go +++ b/pkg/sqlcmd/batch_test.go @@ -1,223 +1,223 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "io" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBatchNext(t *testing.T) { - tests := []struct { - s string - stmts []string - cmds []string - state string - }{ - {"", nil, nil, "="}, - {"select 1", []string{"select 1"}, nil, "-"}, - {"select $(x)\nquit", []string{"select $(x)"}, []string{"QUIT"}, "-"}, - {"select '$ (X' \nquite", []string{"select '$ (X' " + SqlcmdEol + "quite"}, nil, "-"}, - {":list\n:reset\n", nil, []string{"LIST", "RESET"}, "="}, - {"select 1\n:list\nselect 2", []string{"select 1" + SqlcmdEol + "select 2"}, []string{"LIST"}, "-"}, - {"select '1\n", []string{"select '1" + SqlcmdEol + ""}, nil, "'"}, - {"select 1 /* comment\nGO", []string{"select 1 /* comment" + SqlcmdEol + "GO"}, nil, "*"}, - {"select '1\n00' \n/* comm\nent*/\nGO 4", []string{"select '1" + SqlcmdEol + "00' " + SqlcmdEol + "/* comm" + SqlcmdEol + "ent*/"}, []string{"GO"}, "-"}, - {"$(x) $(y) 100\nquit", []string{"$(x) $(y) 100"}, []string{"QUIT"}, "-"}, - {"select 1\n:list", []string{"select 1"}, []string{"LIST"}, "-"}, - {"select 1\n:reset", []string{"select 1"}, []string{"RESET"}, "-"}, - {"select 1\n:exit()", []string{"select 1"}, []string{"EXIT"}, "-"}, - {"select 1\n:exit (select 10)", []string{"select 1"}, []string{"EXIT"}, "-"}, - {"select 1\n:exit", []string{"select 1"}, []string{"EXIT"}, "-"}, - } - for _, test := range tests { - b := NewBatch(sp(test.s, "\n"), newCommands()) - var stmts, cmds []string - loop: - for { - cmd, _, err := b.Next() - switch { - case err == io.EOF: - // if we get EOF before a command we will try to run - // whatever is in the buffer - if s := b.String(); s != "" { - stmts = append(stmts, s) - } - break loop - case err != nil: - t.Fatalf("test %s did not expect error, got: %v", test.s, err) - } - if cmd != nil { - cmds = append(cmds, cmd.name) - } - } - assert.Equal(t, test.stmts, stmts, "Statements for %s", test.s) - assert.Equal(t, test.state, b.State(), "State for %s", test.s) - assert.Equal(t, test.cmds, cmds, "Commands for %s", test.s) - b.Reset(nil) - assert.Zero(t, b.Length, "Length after Reset") - assert.Zero(t, len(b.Buffer), "len(Buffer) after Reset") - assert.Zero(t, b.quote, "quote after Reset") - assert.False(t, b.comment, "comment after Reset") - assert.Equal(t, "=", b.State(), "State() after Reset") - } -} - -func sp(a, sep string) func() (string, error) { - s := strings.Split(a, sep) - return func() (string, error) { - if len(s) > 0 { - z := s[0] - s = s[1:] - return z, nil - } - return "", io.EOF - } -} - -func TestBatchNextErrOnInvalidVariable(t *testing.T) { - tests := []string{ - "select $(x", - "$((x", - "alter $( x)", - } - for _, test := range tests { - b := NewBatch(sp(test, "\n"), newCommands()) - cmd, _, err := b.Next() - assert.Nil(t, cmd, "cmd for "+test) - assert.Equal(t, uint(1), b.linecount, "linecount should increment on a variable syntax error") - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1.", "expected err for %s", test) - } -} - -func TestReadString(t *testing.T) { - tests := []struct { - // input string - s string - // index to start inside s - i int - // expected return string - exp string - // expected return bool - ok bool - }{ - {`'`, 0, ``, false}, - {` '`, 1, ``, false}, - {`'str' `, 0, `'str'`, true}, - {` 'str' `, 1, `'str'`, true}, - {`"str"`, 0, `"str"`, true}, - {`'str''str'`, 0, `'str''str'`, true}, - {` 'str''str' `, 1, `'str''str'`, true}, - {` "str''str" `, 1, `"str''str"`, true}, - // escaped \" aren't allowed in strings, so the second " would be next - // double quoted string - {`"str\""`, 0, `"str\"`, true}, - {` "str\"" `, 1, `"str\"`, true}, - {`'str\'`, 0, `'str\'`, true}, - {`''''`, 0, `''''`, true}, - {` '''' `, 1, `''''`, true}, - {`''''''`, 0, `''''''`, true}, - {` '''''' `, 1, `''''''`, true}, - {`'''`, 0, ``, false}, - {` ''' `, 1, ``, false}, - {`'''''`, 0, ``, false}, - {` ''''' `, 1, ``, false}, - {`"st'r"`, 0, `"st'r"`, true}, - {` "st'r" `, 1, `"st'r"`, true}, - {`"st''r"`, 0, `"st''r"`, true}, - {` "st''r" `, 1, `"st''r"`, true}, - {`'$(v)'`, 0, `'$(v)'`, true}, - {`'var $(var1) var2 $(var2)'`, 0, `'var $(var1) var2 $(var2)'`, true}, - {`'var $(var1) $`, 0, `'var $(var1) $`, false}, - } - b := NewBatch(nil, newCommands()) - - for _, test := range tests { - r := []rune(test.s) - c, end := rune(strings.TrimSpace(test.s)[0]), len(r) - if c != '\'' && c != '"' { - t.Fatalf("test %+v incorrect!", test) - } - pos, ok, err := b.readString(r, test.i+1, end, c, uint(0)) - assert.NoErrorf(t, err, "should be no error for %s", test) - assert.Equal(t, test.ok, ok, "test %+v ok", test) - if !ok { - continue - } - assert.Equal(t, c, r[pos], "test %+v last character") - v := string(r[test.i : pos+1]) - assert.Equal(t, test.exp, v, "test %+v returned string", test) - } -} - -func TestReadStringMalformedVariable(t *testing.T) { - tests := []string{ - "'select $(x'", - "' $((x'", - "'alter $( x)", - } - b := NewBatch(nil, newCommands()) - for _, test := range tests { - r := []rune(test) - _, ok, err := b.readString(r, 1, len(test), '\'', 10) - assert.Falsef(t, ok, "ok for %s", test) - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10.", "expected err for %s", test) - } -} - -func TestReadStringVarmap(t *testing.T) { - type mapTest struct { - s string - m map[int]string - } - tests := []mapTest{ - {`'var $(var1) var2 $(var2)'`, map[int]string{5: "var1", 18: "var2"}}, - {`'var $(va_1) var2 $(va-2)'`, map[int]string{5: "va_1", 18: "va-2"}}, - } - for _, test := range tests { - b := NewBatch(nil, newCommands()) - b.linevarmap = make(map[int]string) - i, ok, err := b.readString([]rune(test.s), 1, len(test.s), '\'', 0) - assert.Truef(t, ok, "ok returned by readString for %s", test.s) - assert.NoErrorf(t, err, "readString for %s", test.s) - assert.Equal(t, len(test.s)-1, i, "index returned by readString for %s", test.s) - assert.Equalf(t, test.m, b.linevarmap, "linevarmap after readString %s", test.s) - } -} - -func TestBatchNextVarMap(t *testing.T) { - type mapTest struct { - s string - m map[int]string - } - tests := []mapTest{ - {"'var $(var1)\nvar2 $(var2)\n'", map[int]string{5: "var1", 17 + len(SqlcmdEol): "var2"}}, - {"$(var1) select $(var2)\nselect 100\nselect '$(var3)'", map[int]string{ - 0: "var1", - 15: "var2", - 40 + 2*len(SqlcmdEol): "var3"}, - }, - } -loop: - for _, test := range tests { - var err error - b := NewBatch(sp(test.s, "\n"), newCommands()) - for { - _, _, err = b.Next() - if err == io.EOF { - assert.Equalf(t, test.m, b.varmap, "varmap after Next %s. Batch:%s", test.s, escapeeol(b.String())) - break loop - } else { - assert.NoErrorf(t, err, "Should have no error from Next") - } - } - } -} - -func escapeeol(s string) string { - return strings.Replace(strings.Replace(s, "\n", `\n`, -1), "\r", `\r`, -1) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBatchNext(t *testing.T) { + tests := []struct { + s string + stmts []string + cmds []string + state string + }{ + {"", nil, nil, "="}, + {"select 1", []string{"select 1"}, nil, "-"}, + {"select $(x)\nquit", []string{"select $(x)"}, []string{"QUIT"}, "-"}, + {"select '$ (X' \nquite", []string{"select '$ (X' " + SqlcmdEol + "quite"}, nil, "-"}, + {":list\n:reset\n", nil, []string{"LIST", "RESET"}, "="}, + {"select 1\n:list\nselect 2", []string{"select 1" + SqlcmdEol + "select 2"}, []string{"LIST"}, "-"}, + {"select '1\n", []string{"select '1" + SqlcmdEol + ""}, nil, "'"}, + {"select 1 /* comment\nGO", []string{"select 1 /* comment" + SqlcmdEol + "GO"}, nil, "*"}, + {"select '1\n00' \n/* comm\nent*/\nGO 4", []string{"select '1" + SqlcmdEol + "00' " + SqlcmdEol + "/* comm" + SqlcmdEol + "ent*/"}, []string{"GO"}, "-"}, + {"$(x) $(y) 100\nquit", []string{"$(x) $(y) 100"}, []string{"QUIT"}, "-"}, + {"select 1\n:list", []string{"select 1"}, []string{"LIST"}, "-"}, + {"select 1\n:reset", []string{"select 1"}, []string{"RESET"}, "-"}, + {"select 1\n:exit()", []string{"select 1"}, []string{"EXIT"}, "-"}, + {"select 1\n:exit (select 10)", []string{"select 1"}, []string{"EXIT"}, "-"}, + {"select 1\n:exit", []string{"select 1"}, []string{"EXIT"}, "-"}, + } + for _, test := range tests { + b := NewBatch(sp(test.s, "\n"), newCommands()) + var stmts, cmds []string + loop: + for { + cmd, _, err := b.Next() + switch { + case err == io.EOF: + // if we get EOF before a command we will try to run + // whatever is in the buffer + if s := b.String(); s != "" { + stmts = append(stmts, s) + } + break loop + case err != nil: + t.Fatalf("test %s did not expect error, got: %v", test.s, err) + } + if cmd != nil { + cmds = append(cmds, cmd.name) + } + } + assert.Equal(t, test.stmts, stmts, "Statements for %s", test.s) + assert.Equal(t, test.state, b.State(), "State for %s", test.s) + assert.Equal(t, test.cmds, cmds, "Commands for %s", test.s) + b.Reset(nil) + assert.Zero(t, b.Length, "Length after Reset") + assert.Zero(t, len(b.Buffer), "len(Buffer) after Reset") + assert.Zero(t, b.quote, "quote after Reset") + assert.False(t, b.comment, "comment after Reset") + assert.Equal(t, "=", b.State(), "State() after Reset") + } +} + +func sp(a, sep string) func() (string, error) { + s := strings.Split(a, sep) + return func() (string, error) { + if len(s) > 0 { + z := s[0] + s = s[1:] + return z, nil + } + return "", io.EOF + } +} + +func TestBatchNextErrOnInvalidVariable(t *testing.T) { + tests := []string{ + "select $(x", + "$((x", + "alter $( x)", + } + for _, test := range tests { + b := NewBatch(sp(test, "\n"), newCommands()) + cmd, _, err := b.Next() + assert.Nil(t, cmd, "cmd for "+test) + assert.Equal(t, uint(1), b.linecount, "linecount should increment on a variable syntax error") + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1.", "expected err for %s", test) + } +} + +func TestReadString(t *testing.T) { + tests := []struct { + // input string + s string + // index to start inside s + i int + // expected return string + exp string + // expected return bool + ok bool + }{ + {`'`, 0, ``, false}, + {` '`, 1, ``, false}, + {`'str' `, 0, `'str'`, true}, + {` 'str' `, 1, `'str'`, true}, + {`"str"`, 0, `"str"`, true}, + {`'str''str'`, 0, `'str''str'`, true}, + {` 'str''str' `, 1, `'str''str'`, true}, + {` "str''str" `, 1, `"str''str"`, true}, + // escaped \" aren't allowed in strings, so the second " would be next + // double quoted string + {`"str\""`, 0, `"str\"`, true}, + {` "str\"" `, 1, `"str\"`, true}, + {`'str\'`, 0, `'str\'`, true}, + {`''''`, 0, `''''`, true}, + {` '''' `, 1, `''''`, true}, + {`''''''`, 0, `''''''`, true}, + {` '''''' `, 1, `''''''`, true}, + {`'''`, 0, ``, false}, + {` ''' `, 1, ``, false}, + {`'''''`, 0, ``, false}, + {` ''''' `, 1, ``, false}, + {`"st'r"`, 0, `"st'r"`, true}, + {` "st'r" `, 1, `"st'r"`, true}, + {`"st''r"`, 0, `"st''r"`, true}, + {` "st''r" `, 1, `"st''r"`, true}, + {`'$(v)'`, 0, `'$(v)'`, true}, + {`'var $(var1) var2 $(var2)'`, 0, `'var $(var1) var2 $(var2)'`, true}, + {`'var $(var1) $`, 0, `'var $(var1) $`, false}, + } + b := NewBatch(nil, newCommands()) + + for _, test := range tests { + r := []rune(test.s) + c, end := rune(strings.TrimSpace(test.s)[0]), len(r) + if c != '\'' && c != '"' { + t.Fatalf("test %+v incorrect!", test) + } + pos, ok, err := b.readString(r, test.i+1, end, c, uint(0)) + assert.NoErrorf(t, err, "should be no error for %s", test) + assert.Equal(t, test.ok, ok, "test %+v ok", test) + if !ok { + continue + } + assert.Equal(t, c, r[pos], "test %+v last character") + v := string(r[test.i : pos+1]) + assert.Equal(t, test.exp, v, "test %+v returned string", test) + } +} + +func TestReadStringMalformedVariable(t *testing.T) { + tests := []string{ + "'select $(x'", + "' $((x'", + "'alter $( x)", + } + b := NewBatch(nil, newCommands()) + for _, test := range tests { + r := []rune(test) + _, ok, err := b.readString(r, 1, len(test), '\'', 10) + assert.Falsef(t, ok, "ok for %s", test) + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10.", "expected err for %s", test) + } +} + +func TestReadStringVarmap(t *testing.T) { + type mapTest struct { + s string + m map[int]string + } + tests := []mapTest{ + {`'var $(var1) var2 $(var2)'`, map[int]string{5: "var1", 18: "var2"}}, + {`'var $(va_1) var2 $(va-2)'`, map[int]string{5: "va_1", 18: "va-2"}}, + } + for _, test := range tests { + b := NewBatch(nil, newCommands()) + b.linevarmap = make(map[int]string) + i, ok, err := b.readString([]rune(test.s), 1, len(test.s), '\'', 0) + assert.Truef(t, ok, "ok returned by readString for %s", test.s) + assert.NoErrorf(t, err, "readString for %s", test.s) + assert.Equal(t, len(test.s)-1, i, "index returned by readString for %s", test.s) + assert.Equalf(t, test.m, b.linevarmap, "linevarmap after readString %s", test.s) + } +} + +func TestBatchNextVarMap(t *testing.T) { + type mapTest struct { + s string + m map[int]string + } + tests := []mapTest{ + {"'var $(var1)\nvar2 $(var2)\n'", map[int]string{5: "var1", 17 + len(SqlcmdEol): "var2"}}, + {"$(var1) select $(var2)\nselect 100\nselect '$(var3)'", map[int]string{ + 0: "var1", + 15: "var2", + 40 + 2*len(SqlcmdEol): "var3"}, + }, + } +loop: + for _, test := range tests { + var err error + b := NewBatch(sp(test.s, "\n"), newCommands()) + for { + _, _, err = b.Next() + if err == io.EOF { + assert.Equalf(t, test.m, b.varmap, "varmap after Next %s. Batch:%s", test.s, escapeeol(b.String())) + break loop + } else { + assert.NoErrorf(t, err, "Should have no error from Next") + } + } + } +} + +func escapeeol(s string) string { + return strings.Replace(strings.Replace(s, "\n", `\n`, -1), "\r", `\r`, -1) +} diff --git a/pkg/sqlcmd/commands.go b/pkg/sqlcmd/commands.go index 2a4c1b5b..9d2b3926 100644 --- a/pkg/sqlcmd/commands.go +++ b/pkg/sqlcmd/commands.go @@ -1,507 +1,507 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "fmt" - "os" - "regexp" - "sort" - "strconv" - "strings" - - "github.com/alecthomas/kong" - "golang.org/x/text/encoding/unicode" - "golang.org/x/text/transform" -) - -// Command defines a sqlcmd action which can be intermixed with the SQL batch -// Commands for sqlcmd are defined at https://docs.microsoft.com/sql/tools/sqlcmd-utility#sqlcmd-commands -type Command struct { - // regex must include at least one group if it has parameters - // Will be matched using FindStringSubmatch - regex *regexp.Regexp - // The function that implements the command. Third parameter is the line number - action func(*Sqlcmd, []string, uint) error - // Name of the command - name string - // whether the command is a system command - isSystem bool -} - -// Commands is the set of sqlcmd command implementations -type Commands map[string]*Command - -func newCommands() Commands { - // Commands is the set of Command implementations - return map[string]*Command{ - "EXIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?EXIT(?:[ \t]*(\(?.*\)?$)|$)`), - action: exitCommand, - name: "EXIT", - }, - "QUIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?QUIT(?:[ \t]+(.*$)|$)`), - action: quitCommand, - name: "QUIT", - }, - "GO": { - regex: regexp.MustCompile(batchTerminatorRegex("GO")), - action: goCommand, - name: "GO", - }, - "OUT": { - regex: regexp.MustCompile(`(?im)^[ \t]*:OUT(?:[ \t]+(.*$)|$)`), - action: outCommand, - name: "OUT", - }, - "ERROR": { - regex: regexp.MustCompile(`(?im)^[ \t]*:ERROR(?:[ \t]+(.*$)|$)`), - action: errorCommand, - name: "ERROR", - }, "READFILE": { - regex: regexp.MustCompile(`(?im)^[ \t]*:R(?:[ \t]+(.*$)|$)`), - action: readFileCommand, - name: "READFILE", - }, - "SETVAR": { - regex: regexp.MustCompile(`(?im)^[ \t]*:SETVAR(?:[ \t]+(.*$)|$)`), - action: setVarCommand, - name: "SETVAR", - }, - "LISTVAR": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:LISTVAR(?:[ \t]+(.*$)|$)`), - action: listVarCommand, - name: "LISTVAR", - }, - "RESET": { - regex: regexp.MustCompile(`(?im)^[ \t]*:RESET(?:[ \t]+(.*$)|$)`), - action: resetCommand, - name: "RESET", - }, - "LIST": { - regex: regexp.MustCompile(`(?im)^[ \t]*:LIST(?:[ \t]+(.*$)|$)`), - action: listCommand, - name: "LIST", - }, - "CONNECT": { - regex: regexp.MustCompile(`(?im)^[ \t]*:CONNECT(?:[ \t]+(.*$)|$)`), - action: connectCommand, - name: "CONNECT", - }, - "EXEC": { - regex: regexp.MustCompile(`(?im)^[ \t]*?:?!!(.*$)`), - action: execCommand, - name: "EXEC", - isSystem: true, - }, - "EDIT": { - regex: regexp.MustCompile(`(?im)^[\t ]*?:?ED(?:[ \t]+(.*$)|$)`), - action: editCommand, - name: "EDIT", - isSystem: true, - }, - } -} - -// DisableSysCommands disables the ED and :!! commands. -// When exitOnCall is true, running those commands will exit the process. -func (c Commands) DisableSysCommands(exitOnCall bool) { - f := warnDisabled - if exitOnCall { - f = errorDisabled - } - for _, cmd := range c { - if cmd.isSystem { - cmd.action = f - } - } -} - -func (c Commands) matchCommand(line string) (*Command, []string) { - for _, cmd := range c { - matchedCommand := cmd.regex.FindStringSubmatch(line) - if matchedCommand != nil { - return cmd, matchedCommand[1:] - } - } - return nil, nil -} - -func warnDisabled(s *Sqlcmd, args []string, line uint) error { - s.WriteError(s.GetError(), ErrCommandsDisabled) - return nil -} - -func errorDisabled(s *Sqlcmd, args []string, line uint) error { - s.WriteError(s.GetError(), ErrCommandsDisabled) - s.Exitcode = 1 - return ErrExitRequested -} - -func batchTerminatorRegex(terminator string) string { - return fmt.Sprintf(`(?im)^[\t ]*?%s(?:[ ]+(.*$)|$)`, regexp.QuoteMeta(terminator)) -} - -// SetBatchTerminator attempts to set the batch terminator to the given value -// Returns an error if the new value is not usable in the regex -func (c Commands) SetBatchTerminator(terminator string) error { - cmd := c["GO"] - regex, err := regexp.Compile(batchTerminatorRegex(terminator)) - if err != nil { - return err - } - cmd.regex = regex - return nil -} - -// exitCommand has 3 modes. -// With no (), it just exits without running any query -// With () it runs whatever batch is in the buffer then exits -// With any text between () it runs the text as a query then exits -func exitCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 { - return ErrExitRequested - } - params := strings.TrimSpace(args[0]) - if params == "" { - return ErrExitRequested - } - if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") { - return InvalidCommandError("EXIT", line) - } - // First we run the current batch - query := s.batch.String() - if query != "" { - query = s.getRunnableQuery(query) - if exitCode, err := s.runQuery(query); err != nil { - s.Exitcode = exitCode - return ErrExitRequested - } - } - query = strings.TrimSpace(params[1 : len(params)-1]) - s.batch.Reset([]rune(query)) - _, _, err := s.batch.Next() - if err != nil { - return err - } - query = s.batch.String() - if s.batch.String() != "" { - query = s.getRunnableQuery(query) - s.Exitcode, _ = s.runQuery(query) - } - return ErrExitRequested -} - -// quitCommand immediately exits the program without running any more batches -func quitCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("QUIT", line) - } - return ErrExitRequested -} - -// goCommand runs the current batch the number of times specified -func goCommand(s *Sqlcmd, args []string, line uint) error { - // default to 1 execution - n := 1 - var err error - if len(args) > 0 { - cnt := strings.TrimSpace(args[0]) - if cnt != "" { - if cnt, err = resolveArgumentVariables(s, []rune(cnt), true); err != nil { - return err - } - _, err = fmt.Sscanf(cnt, "%d", &n) - } - } - if err != nil || n < 1 { - return InvalidCommandError("GO", line) - } - query := s.batch.String() - if query == "" { - return nil - } - query = s.getRunnableQuery(query) - for i := 0; i < n; i++ { - if retcode, err := s.runQuery(query); err != nil { - s.Exitcode = retcode - return err - } - } - s.batch.Reset(nil) - return nil -} - -// outCommand changes the output writer to use a file -func outCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 || args[0] == "" { - return InvalidCommandError("OUT", line) - } - switch { - case strings.EqualFold(args[0], "stdout"): - s.SetOutput(os.Stdout) - case strings.EqualFold(args[0], "stderr"): - s.SetOutput(os.Stderr) - default: - o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return InvalidFileError(err, args[0]) - } - if s.UnicodeOutputFile { - // ODBC sqlcmd doesn't write a BOM but we will. - // Maybe the endian-ness should be configurable. - win16le := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) - encoder := transform.NewWriter(o, win16le.NewEncoder()) - s.SetOutput(encoder) - } else { - s.SetOutput(o) - } - } - return nil -} - -// errorCommand changes the error writer to use a file -func errorCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 || args[0] == "" { - return InvalidCommandError("OUT", line) - } - switch { - case strings.EqualFold(args[0], "stderr"): - s.SetError(os.Stderr) - case strings.EqualFold(args[0], "stdout"): - s.SetError(os.Stdout) - default: - o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - return InvalidFileError(err, args[0]) - } - s.SetError(o) - } - return nil -} - -func readFileCommand(s *Sqlcmd, args []string, line uint) error { - if args == nil || len(args) != 1 { - return InvalidCommandError(":R", line) - } - fileName, _ := resolveArgumentVariables(s, []rune(args[0]), false) - return s.IncludeFile(fileName, false) -} - -// setVarCommand parses a variable setting and applies it to the current Sqlcmd variables -func setVarCommand(s *Sqlcmd, args []string, line uint) error { - if args == nil || len(args) != 1 || args[0] == "" { - return InvalidCommandError(":SETVAR", line) - } - - varname := args[0] - val := "" - // The prior incarnation of sqlcmd doesn't require a space between the variable name and its value - // in some very unexpected cases. This version will require the space. - sp := strings.IndexRune(args[0], ' ') - if sp > -1 { - val = strings.TrimSpace(varname[sp:]) - varname = varname[:sp] - } - if err := s.vars.Setvar(varname, val); err != nil { - switch e := err.(type) { - case *VariableError: - return e - default: - return InvalidCommandError(":SETVAR", line) - } - } - return nil -} - -// listVarCommand prints the set of Sqlcmd scripting variables. -// Builtin values are printed first, followed by user-set values in sorted order. -func listVarCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("LISTVAR", line) - } - - vars := s.vars.All() - keys := make([]string, 0, len(vars)) - for k := range vars { - if !contains(builtinVariables, k) { - keys = append(keys, k) - } - } - sort.Strings(keys) - keys = append(builtinVariables, keys...) - for _, k := range keys { - fmt.Fprintf(s.GetOutput(), `%s = "%s"%s`, k, vars[k], SqlcmdEol) - } - return nil -} - -// resetCommand resets the statement cache -func resetCommand(s *Sqlcmd, args []string, line uint) error { - if s.batch != nil { - s.batch.Reset(nil) - } - - return nil -} - -// listCommand displays statements currently in the statement cache -func listCommand(s *Sqlcmd, args []string, line uint) error { - if s.batch != nil && s.batch.String() != "" { - fmt.Fprintf(s.GetOutput(), `%s%s`, []byte(s.batch.String()), SqlcmdEol) - } - - return nil -} - -type connectData struct { - Server string `arg:""` - Database string `short:"D"` - Username string `short:"U"` - Password string `short:"P"` - LoginTimeout string `short:"l"` - AuthenticationMethod string `short:"G"` -} - -func connectCommand(s *Sqlcmd, args []string, line uint) error { - - if len(args) == 0 { - return InvalidCommandError("CONNECT", line) - } - cmdLine := strings.TrimSpace(args[0]) - if cmdLine == "" { - return InvalidCommandError("CONNECT", line) - } - arguments := &connectData{} - parser, err := kong.New(arguments) - if err != nil { - return InvalidCommandError("CONNECT", line) - } - - // Fields removes extra whitespace. - // Note :connect doesn't support passwords with spaces - if _, err = parser.Parse(strings.Fields(cmdLine)); err != nil { - return InvalidCommandError("CONNECT", line) - } - - connect := *s.Connect - connect.UserName, _ = resolveArgumentVariables(s, []rune(arguments.Username), false) - connect.Password, _ = resolveArgumentVariables(s, []rune(arguments.Password), false) - connect.ServerName, _ = resolveArgumentVariables(s, []rune(arguments.Server), false) - timeout, _ := resolveArgumentVariables(s, []rune(arguments.LoginTimeout), false) - if timeout != "" { - if timeoutSeconds, err := strconv.ParseInt(timeout, 10, 32); err == nil { - if timeoutSeconds < 0 { - return InvalidCommandError("CONNECT", line) - } - connect.LoginTimeoutSeconds = int(timeoutSeconds) - } - } - connect.AuthenticationMethod = arguments.AuthenticationMethod - // If no user name is provided we switch to integrated auth - _ = s.ConnectDb(&connect, s.lineIo == nil) - // ConnectDb prints connection errors already, and failure to connect is not fatal even with -b option - return nil -} - -func execCommand(s *Sqlcmd, args []string, line uint) error { - if len(args) == 0 { - return InvalidCommandError("EXEC", line) - } - cmdLine := strings.TrimSpace(args[0]) - if cmdLine == "" { - return InvalidCommandError("EXEC", line) - } - if cmdLine, err := resolveArgumentVariables(s, []rune(cmdLine), true); err != nil { - return err - } else { - cmd := sysCommand(cmdLine) - cmd.Stderr = s.GetError() - cmd.Stdout = s.GetOutput() - _ = cmd.Run() - } - return nil -} - -func editCommand(s *Sqlcmd, args []string, line uint) error { - if args != nil && strings.TrimSpace(args[0]) != "" { - return InvalidCommandError("ED", line) - } - file, err := os.CreateTemp("", "sq*.sql") - if err != nil { - return err - } - fileName := file.Name() - defer os.Remove(fileName) - text := s.batch.String() - if s.batch.State() == "-" { - text = fmt.Sprintf("%s%s", text, SqlcmdEol) - } - _, err = file.WriteString(text) - if err != nil { - return err - } - file.Close() - cmd := sysCommand(s.vars.TextEditor() + " " + `"` + fileName + `"`) - cmd.Stderr = s.GetError() - cmd.Stdout = s.GetOutput() - err = cmd.Run() - if err != nil { - return err - } - wasEcho := s.echoFileLines - s.echoFileLines = true - s.batch.Reset(nil) - _ = s.IncludeFile(fileName, false) - s.echoFileLines = wasEcho - return nil -} - -func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) { - var b *strings.Builder - end := len(arg) - for i := 0; i < end; { - c, next := arg[i], grab(arg, i+1, end) - switch { - case c == '$' && next == '(': - vl, ok := readVariableReference(arg, i+2, end) - if ok { - varName := string(arg[i+2 : vl]) - val, ok := s.resolveVariable(varName) - if ok { - if b == nil { - b = new(strings.Builder) - b.Grow(len(arg)) - b.WriteString(string(arg[0:i])) - } - b.WriteString(val) - } else { - if failOnUnresolved { - return "", UndefinedVariable(varName) - } - s.WriteError(s.GetError(), UndefinedVariable(varName)) - if b != nil { - b.WriteString(string(arg[i : vl+1])) - } - } - i += ((vl - i) + 1) - } else { - if b != nil { - b.WriteString("$(") - } - i += 2 - } - default: - if b != nil { - b.WriteRune(c) - } - i++ - } - } - if b == nil { - return string(arg), nil - } - return b.String(), nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/alecthomas/kong" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +// Command defines a sqlcmd action which can be intermixed with the SQL batch +// Commands for sqlcmd are defined at https://docs.microsoft.com/sql/tools/sqlcmd-utility#sqlcmd-commands +type Command struct { + // regex must include at least one group if it has parameters + // Will be matched using FindStringSubmatch + regex *regexp.Regexp + // The function that implements the command. Third parameter is the line number + action func(*Sqlcmd, []string, uint) error + // Name of the command + name string + // whether the command is a system command + isSystem bool +} + +// Commands is the set of sqlcmd command implementations +type Commands map[string]*Command + +func newCommands() Commands { + // Commands is the set of Command implementations + return map[string]*Command{ + "EXIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?EXIT(?:[ \t]*(\(?.*\)?$)|$)`), + action: exitCommand, + name: "EXIT", + }, + "QUIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?QUIT(?:[ \t]+(.*$)|$)`), + action: quitCommand, + name: "QUIT", + }, + "GO": { + regex: regexp.MustCompile(batchTerminatorRegex("GO")), + action: goCommand, + name: "GO", + }, + "OUT": { + regex: regexp.MustCompile(`(?im)^[ \t]*:OUT(?:[ \t]+(.*$)|$)`), + action: outCommand, + name: "OUT", + }, + "ERROR": { + regex: regexp.MustCompile(`(?im)^[ \t]*:ERROR(?:[ \t]+(.*$)|$)`), + action: errorCommand, + name: "ERROR", + }, "READFILE": { + regex: regexp.MustCompile(`(?im)^[ \t]*:R(?:[ \t]+(.*$)|$)`), + action: readFileCommand, + name: "READFILE", + }, + "SETVAR": { + regex: regexp.MustCompile(`(?im)^[ \t]*:SETVAR(?:[ \t]+(.*$)|$)`), + action: setVarCommand, + name: "SETVAR", + }, + "LISTVAR": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:LISTVAR(?:[ \t]+(.*$)|$)`), + action: listVarCommand, + name: "LISTVAR", + }, + "RESET": { + regex: regexp.MustCompile(`(?im)^[ \t]*:RESET(?:[ \t]+(.*$)|$)`), + action: resetCommand, + name: "RESET", + }, + "LIST": { + regex: regexp.MustCompile(`(?im)^[ \t]*:LIST(?:[ \t]+(.*$)|$)`), + action: listCommand, + name: "LIST", + }, + "CONNECT": { + regex: regexp.MustCompile(`(?im)^[ \t]*:CONNECT(?:[ \t]+(.*$)|$)`), + action: connectCommand, + name: "CONNECT", + }, + "EXEC": { + regex: regexp.MustCompile(`(?im)^[ \t]*?:?!!(.*$)`), + action: execCommand, + name: "EXEC", + isSystem: true, + }, + "EDIT": { + regex: regexp.MustCompile(`(?im)^[\t ]*?:?ED(?:[ \t]+(.*$)|$)`), + action: editCommand, + name: "EDIT", + isSystem: true, + }, + } +} + +// DisableSysCommands disables the ED and :!! commands. +// When exitOnCall is true, running those commands will exit the process. +func (c Commands) DisableSysCommands(exitOnCall bool) { + f := warnDisabled + if exitOnCall { + f = errorDisabled + } + for _, cmd := range c { + if cmd.isSystem { + cmd.action = f + } + } +} + +func (c Commands) matchCommand(line string) (*Command, []string) { + for _, cmd := range c { + matchedCommand := cmd.regex.FindStringSubmatch(line) + if matchedCommand != nil { + return cmd, matchedCommand[1:] + } + } + return nil, nil +} + +func warnDisabled(s *Sqlcmd, args []string, line uint) error { + s.WriteError(s.GetError(), ErrCommandsDisabled) + return nil +} + +func errorDisabled(s *Sqlcmd, args []string, line uint) error { + s.WriteError(s.GetError(), ErrCommandsDisabled) + s.Exitcode = 1 + return ErrExitRequested +} + +func batchTerminatorRegex(terminator string) string { + return fmt.Sprintf(`(?im)^[\t ]*?%s(?:[ ]+(.*$)|$)`, regexp.QuoteMeta(terminator)) +} + +// SetBatchTerminator attempts to set the batch terminator to the given value +// Returns an error if the new value is not usable in the regex +func (c Commands) SetBatchTerminator(terminator string) error { + cmd := c["GO"] + regex, err := regexp.Compile(batchTerminatorRegex(terminator)) + if err != nil { + return err + } + cmd.regex = regex + return nil +} + +// exitCommand has 3 modes. +// With no (), it just exits without running any query +// With () it runs whatever batch is in the buffer then exits +// With any text between () it runs the text as a query then exits +func exitCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 { + return ErrExitRequested + } + params := strings.TrimSpace(args[0]) + if params == "" { + return ErrExitRequested + } + if !strings.HasPrefix(params, "(") || !strings.HasSuffix(params, ")") { + return InvalidCommandError("EXIT", line) + } + // First we run the current batch + query := s.batch.String() + if query != "" { + query = s.getRunnableQuery(query) + if exitCode, err := s.runQuery(query); err != nil { + s.Exitcode = exitCode + return ErrExitRequested + } + } + query = strings.TrimSpace(params[1 : len(params)-1]) + s.batch.Reset([]rune(query)) + _, _, err := s.batch.Next() + if err != nil { + return err + } + query = s.batch.String() + if s.batch.String() != "" { + query = s.getRunnableQuery(query) + s.Exitcode, _ = s.runQuery(query) + } + return ErrExitRequested +} + +// quitCommand immediately exits the program without running any more batches +func quitCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("QUIT", line) + } + return ErrExitRequested +} + +// goCommand runs the current batch the number of times specified +func goCommand(s *Sqlcmd, args []string, line uint) error { + // default to 1 execution + n := 1 + var err error + if len(args) > 0 { + cnt := strings.TrimSpace(args[0]) + if cnt != "" { + if cnt, err = resolveArgumentVariables(s, []rune(cnt), true); err != nil { + return err + } + _, err = fmt.Sscanf(cnt, "%d", &n) + } + } + if err != nil || n < 1 { + return InvalidCommandError("GO", line) + } + query := s.batch.String() + if query == "" { + return nil + } + query = s.getRunnableQuery(query) + for i := 0; i < n; i++ { + if retcode, err := s.runQuery(query); err != nil { + s.Exitcode = retcode + return err + } + } + s.batch.Reset(nil) + return nil +} + +// outCommand changes the output writer to use a file +func outCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 || args[0] == "" { + return InvalidCommandError("OUT", line) + } + switch { + case strings.EqualFold(args[0], "stdout"): + s.SetOutput(os.Stdout) + case strings.EqualFold(args[0], "stderr"): + s.SetOutput(os.Stderr) + default: + o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return InvalidFileError(err, args[0]) + } + if s.UnicodeOutputFile { + // ODBC sqlcmd doesn't write a BOM but we will. + // Maybe the endian-ness should be configurable. + win16le := unicode.UTF16(unicode.LittleEndian, unicode.UseBOM) + encoder := transform.NewWriter(o, win16le.NewEncoder()) + s.SetOutput(encoder) + } else { + s.SetOutput(o) + } + } + return nil +} + +// errorCommand changes the error writer to use a file +func errorCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 || args[0] == "" { + return InvalidCommandError("OUT", line) + } + switch { + case strings.EqualFold(args[0], "stderr"): + s.SetError(os.Stderr) + case strings.EqualFold(args[0], "stdout"): + s.SetError(os.Stdout) + default: + o, err := os.OpenFile(args[0], os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return InvalidFileError(err, args[0]) + } + s.SetError(o) + } + return nil +} + +func readFileCommand(s *Sqlcmd, args []string, line uint) error { + if args == nil || len(args) != 1 { + return InvalidCommandError(":R", line) + } + fileName, _ := resolveArgumentVariables(s, []rune(args[0]), false) + return s.IncludeFile(fileName, false) +} + +// setVarCommand parses a variable setting and applies it to the current Sqlcmd variables +func setVarCommand(s *Sqlcmd, args []string, line uint) error { + if args == nil || len(args) != 1 || args[0] == "" { + return InvalidCommandError(":SETVAR", line) + } + + varname := args[0] + val := "" + // The prior incarnation of sqlcmd doesn't require a space between the variable name and its value + // in some very unexpected cases. This version will require the space. + sp := strings.IndexRune(args[0], ' ') + if sp > -1 { + val = strings.TrimSpace(varname[sp:]) + varname = varname[:sp] + } + if err := s.vars.Setvar(varname, val); err != nil { + switch e := err.(type) { + case *VariableError: + return e + default: + return InvalidCommandError(":SETVAR", line) + } + } + return nil +} + +// listVarCommand prints the set of Sqlcmd scripting variables. +// Builtin values are printed first, followed by user-set values in sorted order. +func listVarCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("LISTVAR", line) + } + + vars := s.vars.All() + keys := make([]string, 0, len(vars)) + for k := range vars { + if !contains(builtinVariables, k) { + keys = append(keys, k) + } + } + sort.Strings(keys) + keys = append(builtinVariables, keys...) + for _, k := range keys { + fmt.Fprintf(s.GetOutput(), `%s = "%s"%s`, k, vars[k], SqlcmdEol) + } + return nil +} + +// resetCommand resets the statement cache +func resetCommand(s *Sqlcmd, args []string, line uint) error { + if s.batch != nil { + s.batch.Reset(nil) + } + + return nil +} + +// listCommand displays statements currently in the statement cache +func listCommand(s *Sqlcmd, args []string, line uint) error { + if s.batch != nil && s.batch.String() != "" { + fmt.Fprintf(s.GetOutput(), `%s%s`, []byte(s.batch.String()), SqlcmdEol) + } + + return nil +} + +type connectData struct { + Server string `arg:""` + Database string `short:"D"` + Username string `short:"U"` + Password string `short:"P"` + LoginTimeout string `short:"l"` + AuthenticationMethod string `short:"G"` +} + +func connectCommand(s *Sqlcmd, args []string, line uint) error { + + if len(args) == 0 { + return InvalidCommandError("CONNECT", line) + } + cmdLine := strings.TrimSpace(args[0]) + if cmdLine == "" { + return InvalidCommandError("CONNECT", line) + } + arguments := &connectData{} + parser, err := kong.New(arguments) + if err != nil { + return InvalidCommandError("CONNECT", line) + } + + // Fields removes extra whitespace. + // Note :connect doesn't support passwords with spaces + if _, err = parser.Parse(strings.Fields(cmdLine)); err != nil { + return InvalidCommandError("CONNECT", line) + } + + connect := *s.Connect + connect.UserName, _ = resolveArgumentVariables(s, []rune(arguments.Username), false) + connect.Password, _ = resolveArgumentVariables(s, []rune(arguments.Password), false) + connect.ServerName, _ = resolveArgumentVariables(s, []rune(arguments.Server), false) + timeout, _ := resolveArgumentVariables(s, []rune(arguments.LoginTimeout), false) + if timeout != "" { + if timeoutSeconds, err := strconv.ParseInt(timeout, 10, 32); err == nil { + if timeoutSeconds < 0 { + return InvalidCommandError("CONNECT", line) + } + connect.LoginTimeoutSeconds = int(timeoutSeconds) + } + } + connect.AuthenticationMethod = arguments.AuthenticationMethod + // If no user name is provided we switch to integrated auth + _ = s.ConnectDb(&connect, s.lineIo == nil) + // ConnectDb prints connection errors already, and failure to connect is not fatal even with -b option + return nil +} + +func execCommand(s *Sqlcmd, args []string, line uint) error { + if len(args) == 0 { + return InvalidCommandError("EXEC", line) + } + cmdLine := strings.TrimSpace(args[0]) + if cmdLine == "" { + return InvalidCommandError("EXEC", line) + } + if cmdLine, err := resolveArgumentVariables(s, []rune(cmdLine), true); err != nil { + return err + } else { + cmd := sysCommand(cmdLine) + cmd.Stderr = s.GetError() + cmd.Stdout = s.GetOutput() + _ = cmd.Run() + } + return nil +} + +func editCommand(s *Sqlcmd, args []string, line uint) error { + if args != nil && strings.TrimSpace(args[0]) != "" { + return InvalidCommandError("ED", line) + } + file, err := os.CreateTemp("", "sq*.sql") + if err != nil { + return err + } + fileName := file.Name() + defer os.Remove(fileName) + text := s.batch.String() + if s.batch.State() == "-" { + text = fmt.Sprintf("%s%s", text, SqlcmdEol) + } + _, err = file.WriteString(text) + if err != nil { + return err + } + file.Close() + cmd := sysCommand(s.vars.TextEditor() + " " + `"` + fileName + `"`) + cmd.Stderr = s.GetError() + cmd.Stdout = s.GetOutput() + err = cmd.Run() + if err != nil { + return err + } + wasEcho := s.echoFileLines + s.echoFileLines = true + s.batch.Reset(nil) + _ = s.IncludeFile(fileName, false) + s.echoFileLines = wasEcho + return nil +} + +func resolveArgumentVariables(s *Sqlcmd, arg []rune, failOnUnresolved bool) (string, error) { + var b *strings.Builder + end := len(arg) + for i := 0; i < end; { + c, next := arg[i], grab(arg, i+1, end) + switch { + case c == '$' && next == '(': + vl, ok := readVariableReference(arg, i+2, end) + if ok { + varName := string(arg[i+2 : vl]) + val, ok := s.resolveVariable(varName) + if ok { + if b == nil { + b = new(strings.Builder) + b.Grow(len(arg)) + b.WriteString(string(arg[0:i])) + } + b.WriteString(val) + } else { + if failOnUnresolved { + return "", UndefinedVariable(varName) + } + s.WriteError(s.GetError(), UndefinedVariable(varName)) + if b != nil { + b.WriteString(string(arg[i : vl+1])) + } + } + i += ((vl - i) + 1) + } else { + if b != nil { + b.WriteString("$(") + } + i += 2 + } + default: + if b != nil { + b.WriteRune(c) + } + i++ + } + } + if b == nil { + return string(arg), nil + } + return b.String(), nil +} diff --git a/pkg/sqlcmd/commands_test.go b/pkg/sqlcmd/commands_test.go index 584867ba..c5aa4feb 100644 --- a/pkg/sqlcmd/commands_test.go +++ b/pkg/sqlcmd/commands_test.go @@ -1,302 +1,302 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "bytes" - "fmt" - "os" - "strings" - "testing" - - "github.com/microsoft/go-mssqldb/azuread" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestQuitCommand(t *testing.T) { - s := &Sqlcmd{} - err := quitCommand(s, nil, 1) - require.ErrorIs(t, err, ErrExitRequested) - err = quitCommand(s, []string{"extra parameters"}, 2) - require.Error(t, err, "Quit should error out with extra parameters") - assert.NotErrorIs(t, err, ErrExitRequested, "Error with extra arguments") -} - -func TestCommandParsing(t *testing.T) { - type commandTest struct { - line string - cmd string - args []string - } - c := newCommands() - commands := []commandTest{ - {"quite", "", nil}, - {"quit", "QUIT", []string{""}}, - {":QUIT\n", "QUIT", []string{""}}, - {" QUIT \n", "QUIT", []string{""}}, - {"quit extra\n", "QUIT", []string{"extra"}}, - {`:Out c:\folder\file`, "OUT", []string{`c:\folder\file`}}, - {` :Error c:\folder\file`, "ERROR", []string{`c:\folder\file`}}, - {`:Setvar A1 "some value" `, "SETVAR", []string{`A1 "some value" `}}, - {` :Listvar`, "LISTVAR", []string{""}}, - {`:EXIT (select 100 as count)`, "EXIT", []string{"(select 100 as count)"}}, - {`:EXIT ( )`, "EXIT", []string{"( )"}}, - {`EXIT `, "EXIT", []string{""}}, - {`:Connect someserver -U someuser`, "CONNECT", []string{"someserver -U someuser"}}, - {`:r c:\$(var)\file.sql`, "READFILE", []string{`c:\$(var)\file.sql`}}, - {`:!! notepad`, "EXEC", []string{" notepad"}}, - {`:!!notepad`, "EXEC", []string{"notepad"}}, - {` !! dir c:\`, "EXEC", []string{` dir c:\`}}, - {`!!dir c:\`, "EXEC", []string{`dir c:\`}}, - } - - for _, test := range commands { - cmd, args := c.matchCommand(test.line) - if test.cmd != "" { - if assert.NotNil(t, cmd, "No command found for `%s`", test.line) { - assert.Equal(t, test.cmd, cmd.name, "Incorrect command for `%s`", test.line) - assert.Equal(t, test.args, args, "Incorrect arguments for `%s`", test.line) - } - } else { - assert.Nil(t, cmd, "Unexpected match for %s", test.line) - } - } -} - -func TestCustomBatchSeparator(t *testing.T) { - c := newCommands() - err := c.SetBatchTerminator("me!") - if assert.NoError(t, err, "SetBatchTerminator should succeed") { - cmd, args := c.matchCommand(" me! 5 \n") - if assert.NotNil(t, cmd, "matchCommand didn't find GO for custom batch separator") { - assert.Equal(t, "GO", cmd.name, "command name") - assert.Equal(t, "5", strings.TrimSpace(args[0]), "go argument") - } - } -} - -func TestVarCommands(t *testing.T) { - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - err := setVarCommand(s, []string{"ABC 100"}, 1) - assert.NoError(t, err, "setVarCommand ABC 100") - err = setVarCommand(s, []string{"XYZ 200"}, 2) - assert.NoError(t, err, "setVarCommand XYZ 200") - err = listVarCommand(s, []string{""}, 3) - assert.NoError(t, err, "listVarCommand") - s.SetOutput(nil) - varmap := s.vars.All() - o := buf.buf.String() - t.Logf("Listvar output:\n'%s'", o) - output := strings.Split(o, SqlcmdEol) - for i, v := range builtinVariables { - line := strings.Split(output[i], " = ") - assert.Equalf(t, v, line[0], "unexpected variable printed at index %d", i) - val := strings.Trim(line[1], `"`) - assert.Equalf(t, varmap[v], val, "Unexpected value for variable %s", v) - } - assert.Equalf(t, `ABC = "100"`, output[len(output)-3], "Penultimate non-empty line should be ABC") - assert.Equalf(t, `XYZ = "200"`, output[len(output)-2], "Last non-empty line should be XYZ") - assert.Equalf(t, "", output[len(output)-1], "Last line should be empty") - -} - -// memoryBuffer has both Write and Close methods for use as io.WriteCloser -type memoryBuffer struct { - buf *bytes.Buffer -} - -func (b *memoryBuffer) Write(p []byte) (n int, err error) { - return b.buf.Write(p) -} - -func (b *memoryBuffer) Close() error { - return nil -} - -func TestResetCommand(t *testing.T) { - var err error - - // setup a test sqlcmd - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - - // insert a test batch - s.batch.Reset([]rune("select 1")) - _, _, err = s.batch.Next() - assert.NoError(t, err, "Inserting test batch") - assert.Equal(t, s.batch.batchline, int(2), "Batch line updated after test batch insert") - - // execute reset command and validate results - err = resetCommand(s, nil, 1) - assert.Equal(t, s.batch.batchline, int(1), "Batch line not reset properly") - assert.NoError(t, err, "Executing :reset command") -} - -func TestListCommand(t *testing.T) { - var err error - - // setup a test sqlcmd - vars := InitializeVariables(false) - s := New(nil, "", vars) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - - // insert test batch - s.batch.Reset([]rune("select 1")) - _, _, err = s.batch.Next() - assert.NoError(t, err, "Inserting test batch") - - // execute list command and verify results - err = listCommand(s, nil, 1) - assert.NoError(t, err, "Executing :list command") - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, o, "select 1"+SqlcmdEol, ":list output not equal to batch") -} - -func TestConnectCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - prompted := false - s.lineIo = &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - prompted = true - return []byte{}, nil - }, - } - err := connectCommand(s, []string{"someserver -U someuser"}, 1) - assert.NoError(t, err, "connectCommand with valid arguments doesn't return an error on connect failure") - assert.True(t, prompted, "connectCommand with user name and no password should prompt for password") - assert.NotEqual(t, "someserver", s.Connect.ServerName, "On connection failure, sqlCmd.Connect does not copy inputs") - - err = connectCommand(s, []string{}, 2) - assert.EqualError(t, err, InvalidCommandError("CONNECT", 2).Error(), ":Connect with no arguments should return an error") - c := newConnect(t) - - authenticationMethod := "" - password := "" - username := "" - if canTestAzureAuth() { - authenticationMethod = "-G " + azuread.ActiveDirectoryDefault - } - if c.Password != "" { - password = "-P " + c.Password - } - if c.UserName != "" { - username = "-U " + c.UserName - } - s.vars.Set("servername", c.ServerName) - s.vars.Set("to", "111") - buf.buf.Reset() - err = connectCommand(s, []string{fmt.Sprintf("$(servername) %s %s %s -l $(to)", username, password, authenticationMethod)}, 3) - if assert.NoError(t, err, "connectCommand with valid parameters should not return an error") { - // not using assert to avoid printing passwords in the log - assert.NotContains(t, buf.buf.String(), "$(servername)", "ConnectDB should have succeeded") - if s.Connect.UserName != c.UserName || c.Password != s.Connect.Password || s.Connect.LoginTimeoutSeconds != 111 { - t.Fatalf("After connect, sqlCmd.Connect is not updated %+v", s.Connect) - } - } -} - -func TestErrorCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - file, err := os.CreateTemp("", "sqlcmderr") - assert.NoError(t, err, "os.CreateTemp") - defer os.Remove(file.Name()) - fileName := file.Name() - _ = file.Close() - err = errorCommand(s, []string{""}, 1) - assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name") - err = errorCommand(s, []string{fileName}, 1) - assert.NoError(t, err, "errorCommand") - // Only some error kinds go to the error output - err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"}) - assert.NoError(t, err, "runSqlCmd") - s.SetError(nil) - errText, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "ReadFile") { - assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents") - } -} - -func TestResolveArgumentVariables(t *testing.T) { - type argTest struct { - arg string - val string - err string - } - - args := []argTest{ - {"$(var1)", "var1val", ""}, - {"$(var1", "$(var1", ""}, - {`C:\folder\$(var1)\$(var2)\$(var1)\file.sql`, `C:\folder\var1val\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, - {`C:\folder\$(var1\$(var2)\$(var1)\file.sql`, `C:\folder\$(var1\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, - } - vars := InitializeVariables(false) - s := New(nil, "", vars) - s.vars.Set("var1", "var1val") - buf := &memoryBuffer{buf: new(bytes.Buffer)} - defer buf.Close() - s.SetError(buf) - for _, test := range args { - actual, _ := resolveArgumentVariables(s, []rune(test.arg), false) - assert.Equal(t, test.val, actual, "Incorrect argument parsing of "+test.arg) - assert.Contains(t, buf.buf.String(), test.err, "Error output mismatch for "+test.arg) - buf.buf.Reset() - } - actual, err := resolveArgumentVariables(s, []rune("$(var1)$(var2)"), true) - if assert.ErrorContains(t, err, UndefinedVariable("var2").Error(), "fail on unresolved variable") { - assert.Empty(t, actual, "fail on unresolved variable") - } -} - -func TestExecCommand(t *testing.T) { - vars := InitializeVariables(false) - s := New(nil, "", vars) - s.vars.Set("var1", "hello") - buf := &memoryBuffer{buf: new(bytes.Buffer)} - defer buf.Close() - s.SetOutput(buf) - err := execCommand(s, []string{`echo $(var1)`}, 1) - if assert.NoError(t, err, "execCommand with valid arguments") { - assert.Equal(t, buf.buf.String(), "hello"+SqlcmdEol, "echo output should be in sqlcmd output") - } -} - -func TestDisableSysCommandBlocksExec(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Cmd.DisableSysCommands(false) - c := []string{"set nocount on", ":!! echo hello", "select 100", "go"} - err := runSqlCmd(t, s, c) - if assert.NoError(t, err, ":!! with warning should not raise error") { - assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol+"100"+SqlcmdEol) - assert.Equal(t, 0, s.Exitcode, "ExitCode after warning") - } - buf.buf.Reset() - s.Cmd.DisableSysCommands(true) - err = runSqlCmd(t, s, c) - if assert.NoError(t, err, ":!! with error should not return error") { - assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol) - assert.NotContains(t, buf.buf.String(), "100", "query should not run when syscommand disabled") - assert.Equal(t, 1, s.Exitcode, "ExitCode after error") - } -} - -func TestEditCommand(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDEDITOR, "echo select 5000> ") - c := []string{"set nocount on", "go", "select 100", ":ed", "go"} - err := runSqlCmd(t, s, c) - if assert.NoError(t, err, ":ed should not raise error") { - assert.Equal(t, "1> select 5000"+SqlcmdEol+"5000"+SqlcmdEol+SqlcmdEol, buf.buf.String(), "Incorrect output from query after :ed command") - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" + + "github.com/microsoft/go-mssqldb/azuread" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQuitCommand(t *testing.T) { + s := &Sqlcmd{} + err := quitCommand(s, nil, 1) + require.ErrorIs(t, err, ErrExitRequested) + err = quitCommand(s, []string{"extra parameters"}, 2) + require.Error(t, err, "Quit should error out with extra parameters") + assert.NotErrorIs(t, err, ErrExitRequested, "Error with extra arguments") +} + +func TestCommandParsing(t *testing.T) { + type commandTest struct { + line string + cmd string + args []string + } + c := newCommands() + commands := []commandTest{ + {"quite", "", nil}, + {"quit", "QUIT", []string{""}}, + {":QUIT\n", "QUIT", []string{""}}, + {" QUIT \n", "QUIT", []string{""}}, + {"quit extra\n", "QUIT", []string{"extra"}}, + {`:Out c:\folder\file`, "OUT", []string{`c:\folder\file`}}, + {` :Error c:\folder\file`, "ERROR", []string{`c:\folder\file`}}, + {`:Setvar A1 "some value" `, "SETVAR", []string{`A1 "some value" `}}, + {` :Listvar`, "LISTVAR", []string{""}}, + {`:EXIT (select 100 as count)`, "EXIT", []string{"(select 100 as count)"}}, + {`:EXIT ( )`, "EXIT", []string{"( )"}}, + {`EXIT `, "EXIT", []string{""}}, + {`:Connect someserver -U someuser`, "CONNECT", []string{"someserver -U someuser"}}, + {`:r c:\$(var)\file.sql`, "READFILE", []string{`c:\$(var)\file.sql`}}, + {`:!! notepad`, "EXEC", []string{" notepad"}}, + {`:!!notepad`, "EXEC", []string{"notepad"}}, + {` !! dir c:\`, "EXEC", []string{` dir c:\`}}, + {`!!dir c:\`, "EXEC", []string{`dir c:\`}}, + } + + for _, test := range commands { + cmd, args := c.matchCommand(test.line) + if test.cmd != "" { + if assert.NotNil(t, cmd, "No command found for `%s`", test.line) { + assert.Equal(t, test.cmd, cmd.name, "Incorrect command for `%s`", test.line) + assert.Equal(t, test.args, args, "Incorrect arguments for `%s`", test.line) + } + } else { + assert.Nil(t, cmd, "Unexpected match for %s", test.line) + } + } +} + +func TestCustomBatchSeparator(t *testing.T) { + c := newCommands() + err := c.SetBatchTerminator("me!") + if assert.NoError(t, err, "SetBatchTerminator should succeed") { + cmd, args := c.matchCommand(" me! 5 \n") + if assert.NotNil(t, cmd, "matchCommand didn't find GO for custom batch separator") { + assert.Equal(t, "GO", cmd.name, "command name") + assert.Equal(t, "5", strings.TrimSpace(args[0]), "go argument") + } + } +} + +func TestVarCommands(t *testing.T) { + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + err := setVarCommand(s, []string{"ABC 100"}, 1) + assert.NoError(t, err, "setVarCommand ABC 100") + err = setVarCommand(s, []string{"XYZ 200"}, 2) + assert.NoError(t, err, "setVarCommand XYZ 200") + err = listVarCommand(s, []string{""}, 3) + assert.NoError(t, err, "listVarCommand") + s.SetOutput(nil) + varmap := s.vars.All() + o := buf.buf.String() + t.Logf("Listvar output:\n'%s'", o) + output := strings.Split(o, SqlcmdEol) + for i, v := range builtinVariables { + line := strings.Split(output[i], " = ") + assert.Equalf(t, v, line[0], "unexpected variable printed at index %d", i) + val := strings.Trim(line[1], `"`) + assert.Equalf(t, varmap[v], val, "Unexpected value for variable %s", v) + } + assert.Equalf(t, `ABC = "100"`, output[len(output)-3], "Penultimate non-empty line should be ABC") + assert.Equalf(t, `XYZ = "200"`, output[len(output)-2], "Last non-empty line should be XYZ") + assert.Equalf(t, "", output[len(output)-1], "Last line should be empty") + +} + +// memoryBuffer has both Write and Close methods for use as io.WriteCloser +type memoryBuffer struct { + buf *bytes.Buffer +} + +func (b *memoryBuffer) Write(p []byte) (n int, err error) { + return b.buf.Write(p) +} + +func (b *memoryBuffer) Close() error { + return nil +} + +func TestResetCommand(t *testing.T) { + var err error + + // setup a test sqlcmd + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + + // insert a test batch + s.batch.Reset([]rune("select 1")) + _, _, err = s.batch.Next() + assert.NoError(t, err, "Inserting test batch") + assert.Equal(t, s.batch.batchline, int(2), "Batch line updated after test batch insert") + + // execute reset command and validate results + err = resetCommand(s, nil, 1) + assert.Equal(t, s.batch.batchline, int(1), "Batch line not reset properly") + assert.NoError(t, err, "Executing :reset command") +} + +func TestListCommand(t *testing.T) { + var err error + + // setup a test sqlcmd + vars := InitializeVariables(false) + s := New(nil, "", vars) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + + // insert test batch + s.batch.Reset([]rune("select 1")) + _, _, err = s.batch.Next() + assert.NoError(t, err, "Inserting test batch") + + // execute list command and verify results + err = listCommand(s, nil, 1) + assert.NoError(t, err, "Executing :list command") + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, o, "select 1"+SqlcmdEol, ":list output not equal to batch") +} + +func TestConnectCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + prompted := false + s.lineIo = &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + prompted = true + return []byte{}, nil + }, + } + err := connectCommand(s, []string{"someserver -U someuser"}, 1) + assert.NoError(t, err, "connectCommand with valid arguments doesn't return an error on connect failure") + assert.True(t, prompted, "connectCommand with user name and no password should prompt for password") + assert.NotEqual(t, "someserver", s.Connect.ServerName, "On connection failure, sqlCmd.Connect does not copy inputs") + + err = connectCommand(s, []string{}, 2) + assert.EqualError(t, err, InvalidCommandError("CONNECT", 2).Error(), ":Connect with no arguments should return an error") + c := newConnect(t) + + authenticationMethod := "" + password := "" + username := "" + if canTestAzureAuth() { + authenticationMethod = "-G " + azuread.ActiveDirectoryDefault + } + if c.Password != "" { + password = "-P " + c.Password + } + if c.UserName != "" { + username = "-U " + c.UserName + } + s.vars.Set("servername", c.ServerName) + s.vars.Set("to", "111") + buf.buf.Reset() + err = connectCommand(s, []string{fmt.Sprintf("$(servername) %s %s %s -l $(to)", username, password, authenticationMethod)}, 3) + if assert.NoError(t, err, "connectCommand with valid parameters should not return an error") { + // not using assert to avoid printing passwords in the log + assert.NotContains(t, buf.buf.String(), "$(servername)", "ConnectDB should have succeeded") + if s.Connect.UserName != c.UserName || c.Password != s.Connect.Password || s.Connect.LoginTimeoutSeconds != 111 { + t.Fatalf("After connect, sqlCmd.Connect is not updated %+v", s.Connect) + } + } +} + +func TestErrorCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + file, err := os.CreateTemp("", "sqlcmderr") + assert.NoError(t, err, "os.CreateTemp") + defer os.Remove(file.Name()) + fileName := file.Name() + _ = file.Close() + err = errorCommand(s, []string{""}, 1) + assert.EqualError(t, err, InvalidCommandError("OUT", 1).Error(), "errorCommand with empty file name") + err = errorCommand(s, []string{fileName}, 1) + assert.NoError(t, err, "errorCommand") + // Only some error kinds go to the error output + err = runSqlCmd(t, s, []string{"print N'message'", "RAISERROR(N'Error', 16, 1)", "SELECT 1", ":SETVAR 1", "GO"}) + assert.NoError(t, err, "runSqlCmd") + s.SetError(nil) + errText, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "ReadFile") { + assert.Regexp(t, "Msg 50000, Level 16, State 1, Server .*, Line 2"+SqlcmdEol+"Error"+SqlcmdEol, string(errText), "Error file contents") + } +} + +func TestResolveArgumentVariables(t *testing.T) { + type argTest struct { + arg string + val string + err string + } + + args := []argTest{ + {"$(var1)", "var1val", ""}, + {"$(var1", "$(var1", ""}, + {`C:\folder\$(var1)\$(var2)\$(var1)\file.sql`, `C:\folder\var1val\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, + {`C:\folder\$(var1\$(var2)\$(var1)\file.sql`, `C:\folder\$(var1\$(var2)\var1val\file.sql`, "Sqlcmd: Error: 'var2' scripting variable not defined."}, + } + vars := InitializeVariables(false) + s := New(nil, "", vars) + s.vars.Set("var1", "var1val") + buf := &memoryBuffer{buf: new(bytes.Buffer)} + defer buf.Close() + s.SetError(buf) + for _, test := range args { + actual, _ := resolveArgumentVariables(s, []rune(test.arg), false) + assert.Equal(t, test.val, actual, "Incorrect argument parsing of "+test.arg) + assert.Contains(t, buf.buf.String(), test.err, "Error output mismatch for "+test.arg) + buf.buf.Reset() + } + actual, err := resolveArgumentVariables(s, []rune("$(var1)$(var2)"), true) + if assert.ErrorContains(t, err, UndefinedVariable("var2").Error(), "fail on unresolved variable") { + assert.Empty(t, actual, "fail on unresolved variable") + } +} + +func TestExecCommand(t *testing.T) { + vars := InitializeVariables(false) + s := New(nil, "", vars) + s.vars.Set("var1", "hello") + buf := &memoryBuffer{buf: new(bytes.Buffer)} + defer buf.Close() + s.SetOutput(buf) + err := execCommand(s, []string{`echo $(var1)`}, 1) + if assert.NoError(t, err, "execCommand with valid arguments") { + assert.Equal(t, buf.buf.String(), "hello"+SqlcmdEol, "echo output should be in sqlcmd output") + } +} + +func TestDisableSysCommandBlocksExec(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Cmd.DisableSysCommands(false) + c := []string{"set nocount on", ":!! echo hello", "select 100", "go"} + err := runSqlCmd(t, s, c) + if assert.NoError(t, err, ":!! with warning should not raise error") { + assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol+"100"+SqlcmdEol) + assert.Equal(t, 0, s.Exitcode, "ExitCode after warning") + } + buf.buf.Reset() + s.Cmd.DisableSysCommands(true) + err = runSqlCmd(t, s, c) + if assert.NoError(t, err, ":!! with error should not return error") { + assert.Contains(t, buf.buf.String(), ErrCommandsDisabled.Error()+SqlcmdEol) + assert.NotContains(t, buf.buf.String(), "100", "query should not run when syscommand disabled") + assert.Equal(t, 1, s.Exitcode, "ExitCode after error") + } +} + +func TestEditCommand(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDEDITOR, "echo select 5000> ") + c := []string{"set nocount on", "go", "select 100", ":ed", "go"} + err := runSqlCmd(t, s, c) + if assert.NoError(t, err, ":ed should not raise error") { + assert.Equal(t, "1> select 5000"+SqlcmdEol+"5000"+SqlcmdEol+SqlcmdEol, buf.buf.String(), "Incorrect output from query after :ed command") + } +} diff --git a/pkg/sqlcmd/errors.go b/pkg/sqlcmd/errors.go index 0a977543..391bf416 100644 --- a/pkg/sqlcmd/errors.go +++ b/pkg/sqlcmd/errors.go @@ -1,94 +1,94 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "errors" - "fmt" - "strings" -) - -// ErrorPrefix is the prefix for all sqlcmd-generated errors -const ErrorPrefix = "Sqlcmd: Error: " - -// WarningPrefix is the prefix for all sqlcmd-generated warnings -const WarningPrefix = "Sqlcmd: Warning: " - -// ArgumentError is related to command line switch validation not handled by kong -type ArgumentError struct { - Parameter string - Rule string -} - -func (e *ArgumentError) Error() string { - return ErrorPrefix + e.Rule -} - -// InvalidServerName indicates the SQLCMDSERVER variable has an incorrect format -var InvalidServerName = ArgumentError{ - Parameter: "server", - Rule: "server must be of the form [tcp]:server[[/instance]|[,port]]", -} - -// VariableError is an error about scripting variables -type VariableError struct { - Variable string - MessageFormat string -} - -func (e *VariableError) Error() string { - return ErrorPrefix + fmt.Sprintf(e.MessageFormat, e.Variable) -} - -// ReadOnlyVariable indicates the user tried to set a value to a read-only variable -func ReadOnlyVariable(variable string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "The scripting variable: '%s' is read-only", - } -} - -// UndefinedVariable indicates the user tried to reference an undefined variable -func UndefinedVariable(variable string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "'%s' scripting variable not defined.", - } -} - -// InvalidVariableValue indicates the variable was set to an invalid value -func InvalidVariableValue(variable string, value string) *VariableError { - return &VariableError{ - Variable: variable, - MessageFormat: "The environment variable: '%s' has invalid value: '" + strings.ReplaceAll(value, `%`, `%%`) + "'.", - } -} - -// CommandError indicates syntax errors for specific sqlcmd commands -type CommandError struct { - Command string - LineNumber uint -} - -func (e *CommandError) Error() string { - return ErrorPrefix + fmt.Sprintf("Syntax error at line %d near command '%s'.", e.LineNumber, e.Command) -} - -// InvalidCommandError creates a SQLCmdCommandError -func InvalidCommandError(command string, lineNumber uint) *CommandError { - return &CommandError{ - Command: command, - LineNumber: lineNumber, - } -} - -// InvalidFileError indicates a file could not be opened -func InvalidFileError(err error, path string) error { - return errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + path + " (Reason: " + err.Error() + ").") -} - -// SyntaxError indicates a malformed sqlcmd statement -func syntaxError(lineNumber uint) error { - return fmt.Errorf("%sSyntax error at line %d.", ErrorPrefix, lineNumber) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "errors" + "fmt" + "strings" +) + +// ErrorPrefix is the prefix for all sqlcmd-generated errors +const ErrorPrefix = "Sqlcmd: Error: " + +// WarningPrefix is the prefix for all sqlcmd-generated warnings +const WarningPrefix = "Sqlcmd: Warning: " + +// ArgumentError is related to command line switch validation not handled by kong +type ArgumentError struct { + Parameter string + Rule string +} + +func (e *ArgumentError) Error() string { + return ErrorPrefix + e.Rule +} + +// InvalidServerName indicates the SQLCMDSERVER variable has an incorrect format +var InvalidServerName = ArgumentError{ + Parameter: "server", + Rule: "server must be of the form [tcp]:server[[/instance]|[,port]]", +} + +// VariableError is an error about scripting variables +type VariableError struct { + Variable string + MessageFormat string +} + +func (e *VariableError) Error() string { + return ErrorPrefix + fmt.Sprintf(e.MessageFormat, e.Variable) +} + +// ReadOnlyVariable indicates the user tried to set a value to a read-only variable +func ReadOnlyVariable(variable string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "The scripting variable: '%s' is read-only", + } +} + +// UndefinedVariable indicates the user tried to reference an undefined variable +func UndefinedVariable(variable string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "'%s' scripting variable not defined.", + } +} + +// InvalidVariableValue indicates the variable was set to an invalid value +func InvalidVariableValue(variable string, value string) *VariableError { + return &VariableError{ + Variable: variable, + MessageFormat: "The environment variable: '%s' has invalid value: '" + strings.ReplaceAll(value, `%`, `%%`) + "'.", + } +} + +// CommandError indicates syntax errors for specific sqlcmd commands +type CommandError struct { + Command string + LineNumber uint +} + +func (e *CommandError) Error() string { + return ErrorPrefix + fmt.Sprintf("Syntax error at line %d near command '%s'.", e.LineNumber, e.Command) +} + +// InvalidCommandError creates a SQLCmdCommandError +func InvalidCommandError(command string, lineNumber uint) *CommandError { + return &CommandError{ + Command: command, + LineNumber: lineNumber, + } +} + +// InvalidFileError indicates a file could not be opened +func InvalidFileError(err error, path string) error { + return errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + path + " (Reason: " + err.Error() + ").") +} + +// SyntaxError indicates a malformed sqlcmd statement +func syntaxError(lineNumber uint) error { + return fmt.Errorf("%sSyntax error at line %d.", ErrorPrefix, lineNumber) +} diff --git a/pkg/sqlcmd/format.go b/pkg/sqlcmd/format.go index 9de7430a..b44e07e9 100644 --- a/pkg/sqlcmd/format.go +++ b/pkg/sqlcmd/format.go @@ -1,663 +1,663 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "database/sql" - "fmt" - "io" - "strings" - "time" - - "github.com/google/uuid" - mssql "github.com/microsoft/go-mssqldb" -) - -const ( - defaultMaxDisplayWidth = 1024 * 1024 - maxPadWidth = 8000 -) - -// Formatter defines methods to process query output -type Formatter interface { - // BeginBatch is called before the query runs - BeginBatch(query string, vars *Variables, out io.Writer, err io.Writer) - // EndBatch is the last function called during batch execution and signals the end of the batch - EndBatch() - // BeginResultSet is called when a new result set is encountered - BeginResultSet([]*sql.ColumnType) - // EndResultSet is called after all rows in a result set have been processed - EndResultSet() - // AddRow is called for each row in a result set. It returns the value of the first column - AddRow(*sql.Rows) string - // AddMessage is called for every information message returned by the server during the batch - AddMessage(string) - // AddError is called for each error encountered during batch execution - AddError(err error) -} - -// ControlCharacterBehavior specifies the text handling required for control characters in the output -type ControlCharacterBehavior int - -const ( - // ControlIgnore preserves control characters in the output - ControlIgnore ControlCharacterBehavior = iota - // ControlReplace replaces control characters with spaces, 1 space per character - ControlReplace - // ControlRemove removes control characters from the output - ControlRemove - // ControlReplaceConsecutive replaces multiple consecutive control characters with a single space - ControlReplaceConsecutive -) - -type columnDetail struct { - displayWidth int64 - leftJustify bool - zeroesAfterDecimal bool - col sql.ColumnType - precision int - scale int -} - -// The default formatter based on the native sqlcmd style -// It supports both horizontal (default) and vertical layout for results. -// Both vertical and horizontal layouts respect column widths set by SQLCMD variables. -type sqlCmdFormatterType struct { - out io.Writer - err io.Writer - vars *Variables - colsep string - removeTrailingSpaces bool - ccb ControlCharacterBehavior - columnDetails []columnDetail - rowcount int - writepos int64 - format string - maxColNameLen int -} - -// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter -func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool) Formatter { - return &sqlCmdFormatterType{ - removeTrailingSpaces: removeTrailingSpaces, - format: "horizontal", - } -} - -// Adds the given string to the current line, wrapping it based on the screen width setting -func (f *sqlCmdFormatterType) writeOut(s string) { - w := f.vars.ScreenWidth() - if w == 0 { - f.mustWriteOut(s) - return - } - - r := []rune(s) - for i := 0; true; { - if i == len(r) { - f.mustWriteOut(string(r)) - return - } else if f.writepos == w { - f.mustWriteOut(string(r[:i])) - f.mustWriteOut(SqlcmdEol) - r = []rune(string(r[i:])) - f.writepos = 0 - i = 0 - } else { - c := r[i] - if c != '\r' && c != '\n' { - f.writepos++ - } else { - f.writepos = 0 - } - i++ - } - } -} - -// Stores the settings to use for processing the current batch -// TODO: add a third io.Writer for messages when we add -r support -func (f *sqlCmdFormatterType) BeginBatch(_ string, vars *Variables, out io.Writer, err io.Writer) { - f.out = out - f.err = err - f.vars = vars - f.colsep = vars.ColumnSeparator() - f.format = vars.Format() -} - -func (f *sqlCmdFormatterType) EndBatch() { -} - -// Calculate the widths for each column and print the column names -// Since sql.ColumnType only provides sizes for variable length types we will -// base our numbers for most types on https://docs.microsoft.com/sql/odbc/reference/appendixes/column-size -func (f *sqlCmdFormatterType) BeginResultSet(cols []*sql.ColumnType) { - f.rowcount = 0 - f.columnDetails, f.maxColNameLen = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth()) - if f.vars.RowsBetweenHeaders() > -1 && f.format == "horizontal" { - f.printColumnHeadings() - } -} - -// Writes a blank line to the designated output writer -func (f *sqlCmdFormatterType) EndResultSet() { - f.writeOut(SqlcmdEol) -} - -// Writes the current row to the designated output writer -func (f *sqlCmdFormatterType) AddRow(row *sql.Rows) string { - retval := "" - values, err := f.scanRow(row) - if err != nil { - f.mustWriteErr(err.Error()) - return retval - } - retval = values[0] - if f.format == "horizontal" { - // values are the full values, look at the displaywidth of each column and truncate accordingly - for i, v := range values { - if i > 0 { - f.writeOut(f.vars.ColumnSeparator()) - } - f.printColumnValue(v, i) - } - f.rowcount++ - gap := f.vars.RowsBetweenHeaders() - if gap > 0 && (int64(f.rowcount)%gap == 0) { - f.writeOut(SqlcmdEol) - f.printColumnHeadings() - } - } else { - f.addVerticalRow(values) - } - f.writeOut(SqlcmdEol) - return retval - -} - -func (f *sqlCmdFormatterType) addVerticalRow(values []string) { - for i, v := range values { - if f.vars.RowsBetweenHeaders() > -1 { - builder := new(strings.Builder) - name := f.columnDetails[i].col.Name() - builder.WriteString(name) - builder = padRight(builder, int64(f.maxColNameLen-len(name)+1), " ") - f.writeOut(builder.String()) - } - f.printColumnValue(v, i) - f.writeOut(SqlcmdEol) - } -} - -// Writes a non-error message to the designated message writer -func (f *sqlCmdFormatterType) AddMessage(msg string) { - f.mustWriteOut(msg + SqlcmdEol) -} - -// Writes an error to the designated err Writer -func (f *sqlCmdFormatterType) AddError(err error) { - print := true - b := new(strings.Builder) - msg := err.Error() - switch e := (err).(type) { - case mssql.Error: - if print = f.vars.ErrorLevel() <= 0 || e.Class >= uint8(f.vars.ErrorLevel()); print { - b.WriteString(fmt.Sprintf("Msg %d, Level %d, State %d, Server %s, Line %d%s", e.Number, e.Class, e.State, e.ServerName, e.LineNo, SqlcmdEol)) - msg = strings.TrimPrefix(msg, "mssql: ") - } - } - if print { - b.WriteString(msg) - b.WriteString(SqlcmdEol) - f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String()) - } -} - -// Prints column headings based on columnDetail, variables, and command line arguments -func (f *sqlCmdFormatterType) printColumnHeadings() { - names := new(strings.Builder) - sep := new(strings.Builder) - - var leftPad, rightPad int64 - for i, c := range f.columnDetails { - rightPad = 0 - nameLen := int64(len([]rune(c.col.Name()))) - if f.removeTrailingSpaces { - if nameLen == 0 { - // special case for unnamed columns when using -W - // print a single - - rightPad = 1 - sep = padRight(sep, 1, "-") - } else { - sep = padRight(sep, nameLen, "-") - } - } else { - length := min64(c.displayWidth, maxPadWidth) - if nameLen < length { - rightPad = length - nameLen - } - sep = padRight(sep, length, "-") - } - names = padRight(names, leftPad, " ") - names.WriteString(c.col.Name()[:min64(nameLen, c.displayWidth)]) - names = padRight(names, rightPad, " ") - if i != len(f.columnDetails)-1 { - names.WriteString(f.colsep) - sep.WriteString(f.colsep) - } - } - names.WriteString(SqlcmdEol) - sep.WriteString(SqlcmdEol) - names = fitToScreen(names, f.vars.ScreenWidth()) - sep = fitToScreen(sep, f.vars.ScreenWidth()) - f.mustWriteOut(names.String()) - f.mustWriteOut(sep.String()) -} - -// Wraps the input string every width characters when width > 0 -// When width == 0 returns the input Builder -// When width > 0 returns a new Builder containing the wrapped string -func fitToScreen(s *strings.Builder, width int64) *strings.Builder { - str := s.String() - runes := []rune(str) - if width == 0 || int64(len(runes)) < width { - return s - } - - line := new(strings.Builder) - line.Grow(len(str)) - var c int64 - for i, r := range runes { - if c == width { - // We have printed a line's worth - // if the next character is not part of a carriage return write our Eol - if (SqlcmdEol == "\r\n" && (i == len(runes)-1 || (i < len(runes)-1 && string(runes[i:i+2]) != SqlcmdEol))) || (SqlcmdEol == "\n" && r != '\n') { - line.WriteString(SqlcmdEol) - c = 0 - } - } - line.WriteRune(r) - if r == '\n' { - c = 0 - // we are assuming \r is a non-printed character - // The likelihood of a \r not being followed by \n is low - } else if r == '\r' && SqlcmdEol == "\r\n" { - c = 0 - } else { - c++ - } - } - return line -} - -// Given the array of driver-provided columnType values and the sqlcmd size limits, -// Return an array of columnDetail objects describing the output format for each column. -// Return the length of the longest column name. -func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) ([]columnDetail, int) { - columnDetails := make([]columnDetail, len(cols)) - maxNameLen := 0 - for i, c := range cols { - length, _ := c.Length() - nameLen := int64(len([]rune(c.Name()))) - if nameLen > int64(maxNameLen) { - maxNameLen = int(nameLen) - } - columnDetails[i].col = *c - columnDetails[i].leftJustify = true - columnDetails[i].zeroesAfterDecimal = false - p, s, ok := c.DecimalSize() - if ok { - columnDetails[i].precision = int(p) - columnDetails[i].scale = int(s) - } - if length == 0 { - columnDetails[i].displayWidth = defaultMaxDisplayWidth - } else { - columnDetails[i].displayWidth = length - } - typeName := c.DatabaseTypeName() - - switch typeName { - // Types with 0 size from sql.ColumnType - case "BIT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(1, nameLen) - case "TINYINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(3, nameLen) - case "SMALLINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(6, nameLen) - case "INT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(11, nameLen) - case "BIGINT": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(21, nameLen) - case "REAL", "SMALLMONEY": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(14, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "FLOAT", "MONEY": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(24, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DECIMAL": - columnDetails[i].leftJustify = false - d, _, ok := c.DecimalSize() - // maybe panic on !ok? - if !ok { - d = 24 - } - columnDetails[i].displayWidth = max64(d+2, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DATE": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(16, nameLen) - case "DATETIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(23, nameLen) - case "SMALLDATETIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(19, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "DATETIME2": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(38, nameLen) - columnDetails[i].zeroesAfterDecimal = true - case "TIME": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(16, nameLen) - case "DATETIMEOFFSET": - columnDetails[i].leftJustify = false - columnDetails[i].displayWidth = max64(45, nameLen) - case "UNIQUEIDENTIFIER": - columnDetails[i].displayWidth = max64(36, nameLen) - // Types that can be fixed or variable - case "VARCHAR": - if length > 8000 { - columnDetails[i].displayWidth = variable - } else { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } - case "NVARCHAR": - if length > 4000 { - columnDetails[i].displayWidth = variable - } else { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } - case "VARBINARY": - if length <= 8000 { - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - } else { - columnDetails[i].displayWidth = variable - } - case "SQL_VARIANT": - if fixed > 0 { - columnDetails[i].displayWidth = min64(fixed, 8000) - } else { - columnDetails[i].displayWidth = 8000 - } - // Fixed length types - case "CHAR", "NCHAR": - if fixed > 0 { - length = min64(fixed, length) - } - columnDetails[i].displayWidth = max64(length, nameLen) - // Variable length types - // TODO: Fix BINARY once we have a driver with fix for https://github.com/denisenkom/go-mssqldb/issues/685 - case "XML", "TEXT", "NTEXT", "IMAGE", "BINARY": - columnDetails[i].displayWidth = variable - default: - columnDetails[i].displayWidth = length - } - // When max var length is 0 we don't print column headers and print every value with unlimited width - if variable == 0 { - columnDetails[i].displayWidth = 0 - } - } - return columnDetails, maxNameLen -} - -// scanRow fetches the next row and converts each value to the appropriate string representation -func (f *sqlCmdFormatterType) scanRow(rows *sql.Rows) ([]string, error) { - r := make([]interface{}, len(f.columnDetails)) - for i := range r { - r[i] = new(interface{}) - } - if err := rows.Scan(r...); err != nil { - return nil, err - } - row := make([]string, len(f.columnDetails)) - for n, z := range r { - j := z.(*interface{}) - if *j == nil { - row[n] = "NULL" - } else { - switch x := (*j).(type) { - case []byte: - if isBinaryDataType(&f.columnDetails[n].col) { - row[n] = decodeBinary(x) - } else if f.columnDetails[n].col.DatabaseTypeName() == "UNIQUEIDENTIFIER" { - // Unscramble the guid - // see https://github.com/denisenkom/go-mssqldb/issues/56 - x[0], x[1], x[2], x[3] = x[3], x[2], x[1], x[0] - x[4], x[5] = x[5], x[4] - x[6], x[7] = x[7], x[6] - if guid, err := uuid.FromBytes(x); err == nil { - row[n] = guid.String() - } else { - // this should never happen - row[n] = uuid.New().String() - } - } else { - row[n] = string(x) - } - case string: - row[n] = x - case time.Time: - // Go lacks any way to get the user's preferred time format or even the system default - switch f.columnDetails[n].col.DatabaseTypeName() { - case "DATE": - row[n] = x.Format("2006-01-02") - case "DATETIME": - row[n] = x.Format(dateTimeFormatString(3, false)) - case "DATETIME2": - row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, false)) - case "SMALLDATETIME": - row[n] = x.Format(dateTimeFormatString(0, false)) - case "DATETIMEOFFSET": - row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, true)) - case "TIME": - format := "15:04:05" - if f.columnDetails[n].scale > 0 { - format = fmt.Sprintf("%s.%0*d", format, f.columnDetails[n].scale, 0) - } - row[n] = x.Format(format) - default: - row[n] = x.Format(time.RFC3339) - } - case fmt.Stringer: - row[n] = x.String() - // not sure why go-mssql reports bit as bool - case bool: - if x { - row[n] = "1" - } else { - row[n] = "0" - } - default: - var err error - if row[n], err = fmt.Sprintf("%v", x), nil; err != nil { - return nil, err - } - } - } - } - return row, nil -} - -func dateTimeFormatString(scale int, addOffset bool) string { - format := `2006-01-02 15:04:05` - if scale > 0 { - format = fmt.Sprintf("%s.%0*d", format, scale, 0) - } - if addOffset { - format += " -07:00" - } - return format -} - -// Prints the final version of a cell based on formatting variables and command line parameters -func (f *sqlCmdFormatterType) printColumnValue(val string, col int) { - c := f.columnDetails[col] - s := new(strings.Builder) - if isNeedingControlCharacterTreatment(&c.col) { - val = applyControlCharacterBehavior(val, f.ccb) - } - - if isNeedingHexPrefix(&c.col) { - val = "0x" + val - } - - s.WriteString(val) - r := []rune(val) - if f.format == "horizontal" { - if !f.removeTrailingSpaces { - if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) { - padding := c.displayWidth - min64(c.displayWidth, int64(len(r))) - if padding > 0 { - if c.leftJustify { - s = padRight(s, padding, " ") - } else { - s = padLeft(s, padding, " ") - } - } - } - } - - r = []rune(s.String()) - } - if c.displayWidth > 0 && int64(len(r)) > c.displayWidth { - s.Reset() - s.WriteString(string(r[:c.displayWidth])) - } - f.writeOut(s.String()) -} - -func (f *sqlCmdFormatterType) mustWriteOut(s string) { - _, err := f.out.Write([]byte(s)) - if err != nil { - panic(err) - } -} - -func (f *sqlCmdFormatterType) mustWriteErr(s string) { - _, err := f.err.Write([]byte(s)) - if err != nil { - panic(err) - } -} - -func isLargeVariableType(col *sql.ColumnType) bool { - l, _ := col.Length() - switch col.DatabaseTypeName() { - - case "VARCHAR", "VARBINARY": - return l > 8000 - case "NVARCHAR": - return l > 4000 - case "XML", "TEXT", "NTEXT", "IMAGE": - return true - } - return false -} - -func isNeedingControlCharacterTreatment(col *sql.ColumnType) bool { - switch col.DatabaseTypeName() { - case "CHAR", "VARCHAR", "TEXT", "NTEXT", "NCHAR", "NVARCHAR", "XML": - return true - } - return false -} -func isBinaryDataType(col *sql.ColumnType) bool { - switch col.DatabaseTypeName() { - case "BINARY", "VARBINARY": - return true - } - return false -} - -func isNeedingHexPrefix(col *sql.ColumnType) bool { - return isBinaryDataType(col) // || col.DatabaseTypeName() == "UDT" -} - -func isControlChar(r rune) bool { - c := int(r) - return c == 0x7f || (c >= 0 && c <= 0x1f) -} - -func applyControlCharacterBehavior(val string, ccb ControlCharacterBehavior) string { - if ccb == ControlIgnore { - return val - } - b := new(strings.Builder) - r := []rune(val) - if ccb == ControlReplace { - for _, l := range r { - if isControlChar(l) { - b.WriteRune(' ') - } else { - b.WriteRune(l) - } - } - } else { - for i := 0; i < len(r); { - if !isControlChar(r[i]) { - b.WriteRune(r[i]) - i++ - } else { - for ; i < len(r) && isControlChar(r[i]); i++ { - } - if ccb == ControlReplaceConsecutive { - b.WriteRune(' ') - } - } - } - } - return b.String() -} - -// Per https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-binary -var hexDigits = []rune{'A', 'B', 'C', 'D', 'E', 'F'} - -func decodeBinary(b []byte) string { - - s := new(strings.Builder) - s.Grow(len(b) * 2) - for _, ch := range b { - b1 := ch >> 4 - b2 := ch & 0x0f - if b1 >= 10 { - s.WriteRune(hexDigits[b1-10]) - } else { - s.WriteRune(rune('0' + b1)) - } - if b2 >= 10 { - s.WriteRune(hexDigits[b2-10]) - } else { - s.WriteRune(rune('0' + b2)) - } - } - return s.String() -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "database/sql" + "fmt" + "io" + "strings" + "time" + + "github.com/google/uuid" + mssql "github.com/microsoft/go-mssqldb" +) + +const ( + defaultMaxDisplayWidth = 1024 * 1024 + maxPadWidth = 8000 +) + +// Formatter defines methods to process query output +type Formatter interface { + // BeginBatch is called before the query runs + BeginBatch(query string, vars *Variables, out io.Writer, err io.Writer) + // EndBatch is the last function called during batch execution and signals the end of the batch + EndBatch() + // BeginResultSet is called when a new result set is encountered + BeginResultSet([]*sql.ColumnType) + // EndResultSet is called after all rows in a result set have been processed + EndResultSet() + // AddRow is called for each row in a result set. It returns the value of the first column + AddRow(*sql.Rows) string + // AddMessage is called for every information message returned by the server during the batch + AddMessage(string) + // AddError is called for each error encountered during batch execution + AddError(err error) +} + +// ControlCharacterBehavior specifies the text handling required for control characters in the output +type ControlCharacterBehavior int + +const ( + // ControlIgnore preserves control characters in the output + ControlIgnore ControlCharacterBehavior = iota + // ControlReplace replaces control characters with spaces, 1 space per character + ControlReplace + // ControlRemove removes control characters from the output + ControlRemove + // ControlReplaceConsecutive replaces multiple consecutive control characters with a single space + ControlReplaceConsecutive +) + +type columnDetail struct { + displayWidth int64 + leftJustify bool + zeroesAfterDecimal bool + col sql.ColumnType + precision int + scale int +} + +// The default formatter based on the native sqlcmd style +// It supports both horizontal (default) and vertical layout for results. +// Both vertical and horizontal layouts respect column widths set by SQLCMD variables. +type sqlCmdFormatterType struct { + out io.Writer + err io.Writer + vars *Variables + colsep string + removeTrailingSpaces bool + ccb ControlCharacterBehavior + columnDetails []columnDetail + rowcount int + writepos int64 + format string + maxColNameLen int +} + +// NewSQLCmdDefaultFormatter returns a Formatter that mimics the original ODBC-based sqlcmd formatter +func NewSQLCmdDefaultFormatter(removeTrailingSpaces bool) Formatter { + return &sqlCmdFormatterType{ + removeTrailingSpaces: removeTrailingSpaces, + format: "horizontal", + } +} + +// Adds the given string to the current line, wrapping it based on the screen width setting +func (f *sqlCmdFormatterType) writeOut(s string) { + w := f.vars.ScreenWidth() + if w == 0 { + f.mustWriteOut(s) + return + } + + r := []rune(s) + for i := 0; true; { + if i == len(r) { + f.mustWriteOut(string(r)) + return + } else if f.writepos == w { + f.mustWriteOut(string(r[:i])) + f.mustWriteOut(SqlcmdEol) + r = []rune(string(r[i:])) + f.writepos = 0 + i = 0 + } else { + c := r[i] + if c != '\r' && c != '\n' { + f.writepos++ + } else { + f.writepos = 0 + } + i++ + } + } +} + +// Stores the settings to use for processing the current batch +// TODO: add a third io.Writer for messages when we add -r support +func (f *sqlCmdFormatterType) BeginBatch(_ string, vars *Variables, out io.Writer, err io.Writer) { + f.out = out + f.err = err + f.vars = vars + f.colsep = vars.ColumnSeparator() + f.format = vars.Format() +} + +func (f *sqlCmdFormatterType) EndBatch() { +} + +// Calculate the widths for each column and print the column names +// Since sql.ColumnType only provides sizes for variable length types we will +// base our numbers for most types on https://docs.microsoft.com/sql/odbc/reference/appendixes/column-size +func (f *sqlCmdFormatterType) BeginResultSet(cols []*sql.ColumnType) { + f.rowcount = 0 + f.columnDetails, f.maxColNameLen = calcColumnDetails(cols, f.vars.MaxFixedColumnWidth(), f.vars.MaxVarColumnWidth()) + if f.vars.RowsBetweenHeaders() > -1 && f.format == "horizontal" { + f.printColumnHeadings() + } +} + +// Writes a blank line to the designated output writer +func (f *sqlCmdFormatterType) EndResultSet() { + f.writeOut(SqlcmdEol) +} + +// Writes the current row to the designated output writer +func (f *sqlCmdFormatterType) AddRow(row *sql.Rows) string { + retval := "" + values, err := f.scanRow(row) + if err != nil { + f.mustWriteErr(err.Error()) + return retval + } + retval = values[0] + if f.format == "horizontal" { + // values are the full values, look at the displaywidth of each column and truncate accordingly + for i, v := range values { + if i > 0 { + f.writeOut(f.vars.ColumnSeparator()) + } + f.printColumnValue(v, i) + } + f.rowcount++ + gap := f.vars.RowsBetweenHeaders() + if gap > 0 && (int64(f.rowcount)%gap == 0) { + f.writeOut(SqlcmdEol) + f.printColumnHeadings() + } + } else { + f.addVerticalRow(values) + } + f.writeOut(SqlcmdEol) + return retval + +} + +func (f *sqlCmdFormatterType) addVerticalRow(values []string) { + for i, v := range values { + if f.vars.RowsBetweenHeaders() > -1 { + builder := new(strings.Builder) + name := f.columnDetails[i].col.Name() + builder.WriteString(name) + builder = padRight(builder, int64(f.maxColNameLen-len(name)+1), " ") + f.writeOut(builder.String()) + } + f.printColumnValue(v, i) + f.writeOut(SqlcmdEol) + } +} + +// Writes a non-error message to the designated message writer +func (f *sqlCmdFormatterType) AddMessage(msg string) { + f.mustWriteOut(msg + SqlcmdEol) +} + +// Writes an error to the designated err Writer +func (f *sqlCmdFormatterType) AddError(err error) { + print := true + b := new(strings.Builder) + msg := err.Error() + switch e := (err).(type) { + case mssql.Error: + if print = f.vars.ErrorLevel() <= 0 || e.Class >= uint8(f.vars.ErrorLevel()); print { + b.WriteString(fmt.Sprintf("Msg %d, Level %d, State %d, Server %s, Line %d%s", e.Number, e.Class, e.State, e.ServerName, e.LineNo, SqlcmdEol)) + msg = strings.TrimPrefix(msg, "mssql: ") + } + } + if print { + b.WriteString(msg) + b.WriteString(SqlcmdEol) + f.mustWriteErr(fitToScreen(b, f.vars.ScreenWidth()).String()) + } +} + +// Prints column headings based on columnDetail, variables, and command line arguments +func (f *sqlCmdFormatterType) printColumnHeadings() { + names := new(strings.Builder) + sep := new(strings.Builder) + + var leftPad, rightPad int64 + for i, c := range f.columnDetails { + rightPad = 0 + nameLen := int64(len([]rune(c.col.Name()))) + if f.removeTrailingSpaces { + if nameLen == 0 { + // special case for unnamed columns when using -W + // print a single - + rightPad = 1 + sep = padRight(sep, 1, "-") + } else { + sep = padRight(sep, nameLen, "-") + } + } else { + length := min64(c.displayWidth, maxPadWidth) + if nameLen < length { + rightPad = length - nameLen + } + sep = padRight(sep, length, "-") + } + names = padRight(names, leftPad, " ") + names.WriteString(c.col.Name()[:min64(nameLen, c.displayWidth)]) + names = padRight(names, rightPad, " ") + if i != len(f.columnDetails)-1 { + names.WriteString(f.colsep) + sep.WriteString(f.colsep) + } + } + names.WriteString(SqlcmdEol) + sep.WriteString(SqlcmdEol) + names = fitToScreen(names, f.vars.ScreenWidth()) + sep = fitToScreen(sep, f.vars.ScreenWidth()) + f.mustWriteOut(names.String()) + f.mustWriteOut(sep.String()) +} + +// Wraps the input string every width characters when width > 0 +// When width == 0 returns the input Builder +// When width > 0 returns a new Builder containing the wrapped string +func fitToScreen(s *strings.Builder, width int64) *strings.Builder { + str := s.String() + runes := []rune(str) + if width == 0 || int64(len(runes)) < width { + return s + } + + line := new(strings.Builder) + line.Grow(len(str)) + var c int64 + for i, r := range runes { + if c == width { + // We have printed a line's worth + // if the next character is not part of a carriage return write our Eol + if (SqlcmdEol == "\r\n" && (i == len(runes)-1 || (i < len(runes)-1 && string(runes[i:i+2]) != SqlcmdEol))) || (SqlcmdEol == "\n" && r != '\n') { + line.WriteString(SqlcmdEol) + c = 0 + } + } + line.WriteRune(r) + if r == '\n' { + c = 0 + // we are assuming \r is a non-printed character + // The likelihood of a \r not being followed by \n is low + } else if r == '\r' && SqlcmdEol == "\r\n" { + c = 0 + } else { + c++ + } + } + return line +} + +// Given the array of driver-provided columnType values and the sqlcmd size limits, +// Return an array of columnDetail objects describing the output format for each column. +// Return the length of the longest column name. +func calcColumnDetails(cols []*sql.ColumnType, fixed int64, variable int64) ([]columnDetail, int) { + columnDetails := make([]columnDetail, len(cols)) + maxNameLen := 0 + for i, c := range cols { + length, _ := c.Length() + nameLen := int64(len([]rune(c.Name()))) + if nameLen > int64(maxNameLen) { + maxNameLen = int(nameLen) + } + columnDetails[i].col = *c + columnDetails[i].leftJustify = true + columnDetails[i].zeroesAfterDecimal = false + p, s, ok := c.DecimalSize() + if ok { + columnDetails[i].precision = int(p) + columnDetails[i].scale = int(s) + } + if length == 0 { + columnDetails[i].displayWidth = defaultMaxDisplayWidth + } else { + columnDetails[i].displayWidth = length + } + typeName := c.DatabaseTypeName() + + switch typeName { + // Types with 0 size from sql.ColumnType + case "BIT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(1, nameLen) + case "TINYINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(3, nameLen) + case "SMALLINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(6, nameLen) + case "INT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(11, nameLen) + case "BIGINT": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(21, nameLen) + case "REAL", "SMALLMONEY": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(14, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "FLOAT", "MONEY": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(24, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DECIMAL": + columnDetails[i].leftJustify = false + d, _, ok := c.DecimalSize() + // maybe panic on !ok? + if !ok { + d = 24 + } + columnDetails[i].displayWidth = max64(d+2, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DATE": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(16, nameLen) + case "DATETIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(23, nameLen) + case "SMALLDATETIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(19, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "DATETIME2": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(38, nameLen) + columnDetails[i].zeroesAfterDecimal = true + case "TIME": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(16, nameLen) + case "DATETIMEOFFSET": + columnDetails[i].leftJustify = false + columnDetails[i].displayWidth = max64(45, nameLen) + case "UNIQUEIDENTIFIER": + columnDetails[i].displayWidth = max64(36, nameLen) + // Types that can be fixed or variable + case "VARCHAR": + if length > 8000 { + columnDetails[i].displayWidth = variable + } else { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } + case "NVARCHAR": + if length > 4000 { + columnDetails[i].displayWidth = variable + } else { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } + case "VARBINARY": + if length <= 8000 { + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + } else { + columnDetails[i].displayWidth = variable + } + case "SQL_VARIANT": + if fixed > 0 { + columnDetails[i].displayWidth = min64(fixed, 8000) + } else { + columnDetails[i].displayWidth = 8000 + } + // Fixed length types + case "CHAR", "NCHAR": + if fixed > 0 { + length = min64(fixed, length) + } + columnDetails[i].displayWidth = max64(length, nameLen) + // Variable length types + // TODO: Fix BINARY once we have a driver with fix for https://github.com/denisenkom/go-mssqldb/issues/685 + case "XML", "TEXT", "NTEXT", "IMAGE", "BINARY": + columnDetails[i].displayWidth = variable + default: + columnDetails[i].displayWidth = length + } + // When max var length is 0 we don't print column headers and print every value with unlimited width + if variable == 0 { + columnDetails[i].displayWidth = 0 + } + } + return columnDetails, maxNameLen +} + +// scanRow fetches the next row and converts each value to the appropriate string representation +func (f *sqlCmdFormatterType) scanRow(rows *sql.Rows) ([]string, error) { + r := make([]interface{}, len(f.columnDetails)) + for i := range r { + r[i] = new(interface{}) + } + if err := rows.Scan(r...); err != nil { + return nil, err + } + row := make([]string, len(f.columnDetails)) + for n, z := range r { + j := z.(*interface{}) + if *j == nil { + row[n] = "NULL" + } else { + switch x := (*j).(type) { + case []byte: + if isBinaryDataType(&f.columnDetails[n].col) { + row[n] = decodeBinary(x) + } else if f.columnDetails[n].col.DatabaseTypeName() == "UNIQUEIDENTIFIER" { + // Unscramble the guid + // see https://github.com/denisenkom/go-mssqldb/issues/56 + x[0], x[1], x[2], x[3] = x[3], x[2], x[1], x[0] + x[4], x[5] = x[5], x[4] + x[6], x[7] = x[7], x[6] + if guid, err := uuid.FromBytes(x); err == nil { + row[n] = guid.String() + } else { + // this should never happen + row[n] = uuid.New().String() + } + } else { + row[n] = string(x) + } + case string: + row[n] = x + case time.Time: + // Go lacks any way to get the user's preferred time format or even the system default + switch f.columnDetails[n].col.DatabaseTypeName() { + case "DATE": + row[n] = x.Format("2006-01-02") + case "DATETIME": + row[n] = x.Format(dateTimeFormatString(3, false)) + case "DATETIME2": + row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, false)) + case "SMALLDATETIME": + row[n] = x.Format(dateTimeFormatString(0, false)) + case "DATETIMEOFFSET": + row[n] = x.Format(dateTimeFormatString(f.columnDetails[n].scale, true)) + case "TIME": + format := "15:04:05" + if f.columnDetails[n].scale > 0 { + format = fmt.Sprintf("%s.%0*d", format, f.columnDetails[n].scale, 0) + } + row[n] = x.Format(format) + default: + row[n] = x.Format(time.RFC3339) + } + case fmt.Stringer: + row[n] = x.String() + // not sure why go-mssql reports bit as bool + case bool: + if x { + row[n] = "1" + } else { + row[n] = "0" + } + default: + var err error + if row[n], err = fmt.Sprintf("%v", x), nil; err != nil { + return nil, err + } + } + } + } + return row, nil +} + +func dateTimeFormatString(scale int, addOffset bool) string { + format := `2006-01-02 15:04:05` + if scale > 0 { + format = fmt.Sprintf("%s.%0*d", format, scale, 0) + } + if addOffset { + format += " -07:00" + } + return format +} + +// Prints the final version of a cell based on formatting variables and command line parameters +func (f *sqlCmdFormatterType) printColumnValue(val string, col int) { + c := f.columnDetails[col] + s := new(strings.Builder) + if isNeedingControlCharacterTreatment(&c.col) { + val = applyControlCharacterBehavior(val, f.ccb) + } + + if isNeedingHexPrefix(&c.col) { + val = "0x" + val + } + + s.WriteString(val) + r := []rune(val) + if f.format == "horizontal" { + if !f.removeTrailingSpaces { + if f.vars.MaxVarColumnWidth() != 0 || !isLargeVariableType(&c.col) { + padding := c.displayWidth - min64(c.displayWidth, int64(len(r))) + if padding > 0 { + if c.leftJustify { + s = padRight(s, padding, " ") + } else { + s = padLeft(s, padding, " ") + } + } + } + } + + r = []rune(s.String()) + } + if c.displayWidth > 0 && int64(len(r)) > c.displayWidth { + s.Reset() + s.WriteString(string(r[:c.displayWidth])) + } + f.writeOut(s.String()) +} + +func (f *sqlCmdFormatterType) mustWriteOut(s string) { + _, err := f.out.Write([]byte(s)) + if err != nil { + panic(err) + } +} + +func (f *sqlCmdFormatterType) mustWriteErr(s string) { + _, err := f.err.Write([]byte(s)) + if err != nil { + panic(err) + } +} + +func isLargeVariableType(col *sql.ColumnType) bool { + l, _ := col.Length() + switch col.DatabaseTypeName() { + + case "VARCHAR", "VARBINARY": + return l > 8000 + case "NVARCHAR": + return l > 4000 + case "XML", "TEXT", "NTEXT", "IMAGE": + return true + } + return false +} + +func isNeedingControlCharacterTreatment(col *sql.ColumnType) bool { + switch col.DatabaseTypeName() { + case "CHAR", "VARCHAR", "TEXT", "NTEXT", "NCHAR", "NVARCHAR", "XML": + return true + } + return false +} +func isBinaryDataType(col *sql.ColumnType) bool { + switch col.DatabaseTypeName() { + case "BINARY", "VARBINARY": + return true + } + return false +} + +func isNeedingHexPrefix(col *sql.ColumnType) bool { + return isBinaryDataType(col) // || col.DatabaseTypeName() == "UDT" +} + +func isControlChar(r rune) bool { + c := int(r) + return c == 0x7f || (c >= 0 && c <= 0x1f) +} + +func applyControlCharacterBehavior(val string, ccb ControlCharacterBehavior) string { + if ccb == ControlIgnore { + return val + } + b := new(strings.Builder) + r := []rune(val) + if ccb == ControlReplace { + for _, l := range r { + if isControlChar(l) { + b.WriteRune(' ') + } else { + b.WriteRune(l) + } + } + } else { + for i := 0; i < len(r); { + if !isControlChar(r[i]) { + b.WriteRune(r[i]) + i++ + } else { + for ; i < len(r) && isControlChar(r[i]); i++ { + } + if ccb == ControlReplaceConsecutive { + b.WriteRune(' ') + } + } + } + } + return b.String() +} + +// Per https://docs.microsoft.com/sql/odbc/reference/appendixes/sql-to-c-binary +var hexDigits = []rune{'A', 'B', 'C', 'D', 'E', 'F'} + +func decodeBinary(b []byte) string { + + s := new(strings.Builder) + s.Grow(len(b) * 2) + for _, ch := range b { + b1 := ch >> 4 + b2 := ch & 0x0f + if b1 >= 10 { + s.WriteRune(hexDigits[b1-10]) + } else { + s.WriteRune(rune('0' + b1)) + } + if b2 >= 10 { + s.WriteRune(hexDigits[b2-10]) + } else { + s.WriteRune(rune('0' + b2)) + } + } + return s.String() +} diff --git a/pkg/sqlcmd/format_darwin.go b/pkg/sqlcmd/format_darwin.go index fc16bf8c..f9bc89cf 100644 --- a/pkg/sqlcmd/format_darwin.go +++ b/pkg/sqlcmd/format_darwin.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\n" diff --git a/pkg/sqlcmd/format_linux.go b/pkg/sqlcmd/format_linux.go index fc16bf8c..f9bc89cf 100644 --- a/pkg/sqlcmd/format_linux.go +++ b/pkg/sqlcmd/format_linux.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\n" diff --git a/pkg/sqlcmd/format_test.go b/pkg/sqlcmd/format_test.go index 39224500..48387ebc 100644 --- a/pkg/sqlcmd/format_test.go +++ b/pkg/sqlcmd/format_test.go @@ -1,140 +1,140 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "context" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFitToScreen(t *testing.T) { - type fitTest struct { - width int64 - raw string - fit string - } - - tests := []fitTest{ - {0, "this is a string", "this is a string"}, - {9, "12345678", "12345678"}, - {9, "123456789", "123456789"}, - {9, "123456789A", "123456789" + SqlcmdEol + "A"}, - {9, "123456789" + SqlcmdEol, "123456789" + SqlcmdEol}, - {9, "12345678" + SqlcmdEol + "9A", "12345678" + SqlcmdEol + "9A"}, - {9, "123456789\rA", "123456789" + SqlcmdEol + "\rA"}, - } - - for _, test := range tests { - - line := new(strings.Builder) - line.WriteString(test.raw) - t.Log(test.raw) - f := fitToScreen(line, test.width).String() - assert.Equal(t, test.fit, f, "Mismatched fit for raw string: '%s'", test.raw) - } -} - -func TestCalcColumnDetails(t *testing.T) { - type colTest struct { - fixed int64 - variable int64 - query string - details []columnDetail - max int - } - - tests := []colTest{ - {8, 8, - "select 100 as '123456789ABC', getdate() as '987654321', 'string' as col1", - []columnDetail{ - {leftJustify: false, displayWidth: 12}, - {leftJustify: false, displayWidth: 23}, - {leftJustify: true, displayWidth: 6}, - }, - 12, - }, - } - - db, err := ConnectDb(t) - if assert.NoError(t, err, "ConnectDB failed") { - defer db.Close() - for x, test := range tests { - rows, err := db.QueryContext(context.Background(), test.query) - if assert.NoError(t, err, "Query failed: %s", test.query) { - defer rows.Close() - cols, err := rows.ColumnTypes() - if assert.NoError(t, err, "ColumnTypes failed:%s", test.query) { - actual, max := calcColumnDetails(cols, test.fixed, test.variable) - for i, a := range actual { - if test.details[i].displayWidth != a.displayWidth || - test.details[i].leftJustify != a.leftJustify || - test.details[i].zeroesAfterDecimal != a.zeroesAfterDecimal { - assert.Failf(t, "", "[%d] Incorrect test details for column [%s] in query '%s':%+v", x, cols[i].Name(), test.query, a) - } - assert.Equal(t, test.max, max, "[%d] Max column name length incorrect", x) - } - } - } - } - } -} - -func TestControlCharacterBehavior(t *testing.T) { - type ccbTest struct { - raw string - replaced string - removed string - consecutivereplaced string - } - - tests := []ccbTest{ - {"no control", "no control", "no control", "no control"}, - {string(rune(1)) + "tabs\t\treturns\r\n\r\n", " tabs returns ", "tabsreturns", " tabs returns "}, - } - - for _, test := range tests { - s := applyControlCharacterBehavior(test.raw, ControlReplace) - assert.Equalf(t, test.replaced, s, "Incorrect Replaced for '%s'", test.raw) - s = applyControlCharacterBehavior(test.raw, ControlRemove) - assert.Equalf(t, test.removed, s, "Incorrect Remove for '%s'", test.raw) - s = applyControlCharacterBehavior(test.raw, ControlReplaceConsecutive) - assert.Equalf(t, test.consecutivereplaced, s, "Incorrect ReplaceConsecutive for '%s'", test.raw) - } -} - -func TestDecodeBinary(t *testing.T) { - type decodeTest struct { - b []byte - s string - } - - tests := []decodeTest{ - {[]byte("123456ABCDEF"), "313233343536414243444546"}, - {[]byte{0x12, 0x34, 0x56}, "123456"}, - } - for _, test := range tests { - a := decodeBinary(test.b) - assert.Equalf(t, test.s, a, "Incorrect decoded binary string for %v", test.b) - } -} - -func BenchmarkDecodeBinary(b *testing.B) { - b.ReportAllocs() - bytes := make([]byte, 10000) - for i := 0; i < 10000; i++ { - bytes[i] = byte(i % 0xff) - } - b.ResetTimer() - - for i := 0; i < b.N; i++ { - s := decodeBinary(bytes) - if len(s) != 20000 { - b.Fatalf("Incorrect length of returned string. Should be 20k, was %d", len(s)) - } - } - -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFitToScreen(t *testing.T) { + type fitTest struct { + width int64 + raw string + fit string + } + + tests := []fitTest{ + {0, "this is a string", "this is a string"}, + {9, "12345678", "12345678"}, + {9, "123456789", "123456789"}, + {9, "123456789A", "123456789" + SqlcmdEol + "A"}, + {9, "123456789" + SqlcmdEol, "123456789" + SqlcmdEol}, + {9, "12345678" + SqlcmdEol + "9A", "12345678" + SqlcmdEol + "9A"}, + {9, "123456789\rA", "123456789" + SqlcmdEol + "\rA"}, + } + + for _, test := range tests { + + line := new(strings.Builder) + line.WriteString(test.raw) + t.Log(test.raw) + f := fitToScreen(line, test.width).String() + assert.Equal(t, test.fit, f, "Mismatched fit for raw string: '%s'", test.raw) + } +} + +func TestCalcColumnDetails(t *testing.T) { + type colTest struct { + fixed int64 + variable int64 + query string + details []columnDetail + max int + } + + tests := []colTest{ + {8, 8, + "select 100 as '123456789ABC', getdate() as '987654321', 'string' as col1", + []columnDetail{ + {leftJustify: false, displayWidth: 12}, + {leftJustify: false, displayWidth: 23}, + {leftJustify: true, displayWidth: 6}, + }, + 12, + }, + } + + db, err := ConnectDb(t) + if assert.NoError(t, err, "ConnectDB failed") { + defer db.Close() + for x, test := range tests { + rows, err := db.QueryContext(context.Background(), test.query) + if assert.NoError(t, err, "Query failed: %s", test.query) { + defer rows.Close() + cols, err := rows.ColumnTypes() + if assert.NoError(t, err, "ColumnTypes failed:%s", test.query) { + actual, max := calcColumnDetails(cols, test.fixed, test.variable) + for i, a := range actual { + if test.details[i].displayWidth != a.displayWidth || + test.details[i].leftJustify != a.leftJustify || + test.details[i].zeroesAfterDecimal != a.zeroesAfterDecimal { + assert.Failf(t, "", "[%d] Incorrect test details for column [%s] in query '%s':%+v", x, cols[i].Name(), test.query, a) + } + assert.Equal(t, test.max, max, "[%d] Max column name length incorrect", x) + } + } + } + } + } +} + +func TestControlCharacterBehavior(t *testing.T) { + type ccbTest struct { + raw string + replaced string + removed string + consecutivereplaced string + } + + tests := []ccbTest{ + {"no control", "no control", "no control", "no control"}, + {string(rune(1)) + "tabs\t\treturns\r\n\r\n", " tabs returns ", "tabsreturns", " tabs returns "}, + } + + for _, test := range tests { + s := applyControlCharacterBehavior(test.raw, ControlReplace) + assert.Equalf(t, test.replaced, s, "Incorrect Replaced for '%s'", test.raw) + s = applyControlCharacterBehavior(test.raw, ControlRemove) + assert.Equalf(t, test.removed, s, "Incorrect Remove for '%s'", test.raw) + s = applyControlCharacterBehavior(test.raw, ControlReplaceConsecutive) + assert.Equalf(t, test.consecutivereplaced, s, "Incorrect ReplaceConsecutive for '%s'", test.raw) + } +} + +func TestDecodeBinary(t *testing.T) { + type decodeTest struct { + b []byte + s string + } + + tests := []decodeTest{ + {[]byte("123456ABCDEF"), "313233343536414243444546"}, + {[]byte{0x12, 0x34, 0x56}, "123456"}, + } + for _, test := range tests { + a := decodeBinary(test.b) + assert.Equalf(t, test.s, a, "Incorrect decoded binary string for %v", test.b) + } +} + +func BenchmarkDecodeBinary(b *testing.B) { + b.ReportAllocs() + bytes := make([]byte, 10000) + for i := 0; i < 10000; i++ { + bytes[i] = byte(i % 0xff) + } + b.ResetTimer() + + for i := 0; i < b.N; i++ { + s := decodeBinary(bytes) + if len(s) != 20000 { + b.Fatalf("Incorrect length of returned string. Should be 20k, was %d", len(s)) + } + } + +} diff --git a/pkg/sqlcmd/format_windows.go b/pkg/sqlcmd/format_windows.go index 9d7b6f32..7d22ae36 100644 --- a/pkg/sqlcmd/format_windows.go +++ b/pkg/sqlcmd/format_windows.go @@ -1,7 +1,7 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -// SqlcmdEol is the end-of-line marker for sqlcmd output -const SqlcmdEol = "\r\n" +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +// SqlcmdEol is the end-of-line marker for sqlcmd output +const SqlcmdEol = "\r\n" diff --git a/pkg/sqlcmd/parse.go b/pkg/sqlcmd/parse.go index e9192c78..f2549c7a 100644 --- a/pkg/sqlcmd/parse.go +++ b/pkg/sqlcmd/parse.go @@ -1,101 +1,101 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "strings" - "unicode" -) - -// grab grabs i from r, or returns 0 if i >= end. -func grab(r []rune, i, end int) rune { - if i < end { - return r[i] - } - return 0 -} - -// findNonSpace finds first non space rune in r, returning end if not found. -func findNonSpace(r []rune, i, end int) (int, bool) { - for ; i < end; i++ { - if !isSpaceOrControl(r[i]) { - return i, true - } - } - return i, false -} - -// isEmptyLine returns true when r is empty or composed of only whitespace. -func isEmptyLine(r []rune, i, end int) bool { - _, ok := findNonSpace(r, i, end) - return !ok -} - -// readMultilineComment finds the end of a multiline comment (ie, '*/'). -func readMultilineComment(r []rune, i, end int) (int, bool) { - i++ - for ; i < end; i++ { - if r[i-1] == '*' && r[i] == '/' { - return i, true - } - } - return end, false -} - -// readCommand reads to the next control character to find -// a command in the string. Command regexes constrain matches -// to the beginning of the string, and all commands consume -// an entire line. -func readCommand(c Commands, r []rune, i, end int) (*Command, []string, int) { - for ; i < end; i++ { - next := grab(r, i, end) - if next == 0 || unicode.IsControl(next) { - break - } - } - cmd, args := c.matchCommand(string(r[:i])) - return cmd, args, i -} - -// readVariableReference returns the index of the end of the variable reference or false if it's not a valid identifier -func readVariableReference(r []rune, i int, end int) (int, bool) { - for ; i < end; i++ { - if r[i] == ')' { - return i, true - } - if (r[i] >= 'a' && r[i] <= 'z') || (r[i] >= 'A' && r[i] <= 'Z') || (r[i] >= '0' && r[i] <= '9') || strings.ContainsRune(validVariableRunes, r[i]) { - continue - } - break - } - return 0, false -} - -func max64(a, b int64) int64 { - if a > b { - return a - } - return b -} - -// min returns the minimum of a, b. -func min(a, b int) int { - if a < b { - return a - } - return b -} - -func min64(a, b int64) int64 { - if a < b { - return a - } - return b -} - -// isSpaceOrControl is a special test for either a space or a control (ie, \b) -// characters. -func isSpaceOrControl(r rune) bool { - return unicode.IsSpace(r) || unicode.IsControl(r) -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "strings" + "unicode" +) + +// grab grabs i from r, or returns 0 if i >= end. +func grab(r []rune, i, end int) rune { + if i < end { + return r[i] + } + return 0 +} + +// findNonSpace finds first non space rune in r, returning end if not found. +func findNonSpace(r []rune, i, end int) (int, bool) { + for ; i < end; i++ { + if !isSpaceOrControl(r[i]) { + return i, true + } + } + return i, false +} + +// isEmptyLine returns true when r is empty or composed of only whitespace. +func isEmptyLine(r []rune, i, end int) bool { + _, ok := findNonSpace(r, i, end) + return !ok +} + +// readMultilineComment finds the end of a multiline comment (ie, '*/'). +func readMultilineComment(r []rune, i, end int) (int, bool) { + i++ + for ; i < end; i++ { + if r[i-1] == '*' && r[i] == '/' { + return i, true + } + } + return end, false +} + +// readCommand reads to the next control character to find +// a command in the string. Command regexes constrain matches +// to the beginning of the string, and all commands consume +// an entire line. +func readCommand(c Commands, r []rune, i, end int) (*Command, []string, int) { + for ; i < end; i++ { + next := grab(r, i, end) + if next == 0 || unicode.IsControl(next) { + break + } + } + cmd, args := c.matchCommand(string(r[:i])) + return cmd, args, i +} + +// readVariableReference returns the index of the end of the variable reference or false if it's not a valid identifier +func readVariableReference(r []rune, i int, end int) (int, bool) { + for ; i < end; i++ { + if r[i] == ')' { + return i, true + } + if (r[i] >= 'a' && r[i] <= 'z') || (r[i] >= 'A' && r[i] <= 'Z') || (r[i] >= '0' && r[i] <= '9') || strings.ContainsRune(validVariableRunes, r[i]) { + continue + } + break + } + return 0, false +} + +func max64(a, b int64) int64 { + if a > b { + return a + } + return b +} + +// min returns the minimum of a, b. +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func min64(a, b int64) int64 { + if a < b { + return a + } + return b +} + +// isSpaceOrControl is a special test for either a space or a control (ie, \b) +// characters. +func isSpaceOrControl(r rune) bool { + return unicode.IsSpace(r) || unicode.IsControl(r) +} diff --git a/pkg/sqlcmd/parse_test.go b/pkg/sqlcmd/parse_test.go index 143d5f85..9b809c52 100644 --- a/pkg/sqlcmd/parse_test.go +++ b/pkg/sqlcmd/parse_test.go @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index 0ae6605e..46cfa746 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -1,592 +1,592 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "bytes" - "database/sql" - "fmt" - "io" - "os" - "os/user" - "strings" - "testing" - - "github.com/microsoft/go-mssqldb/azuread" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" -) - -const oneRowAffected = "(1 row affected)" - -func TestConnectionStringFromSqlCmd(t *testing.T) { - type connectionStringTest struct { - settings *ConnectSettings - connectionString string - } - - pwd := uuid.New().String() - - commands := []connectionStringTest{ - - {&ConnectSettings{}, "sqlserver://."}, - { - &ConnectSettings{TrustServerCertificate: true, WorkstationName: "mystation", Database: "somedatabase"}, - "sqlserver://.?database=somedatabase&trustservercertificate=true&workstation+id=mystation", - }, - { - &ConnectSettings{WorkstationName: "mystation", Encrypt: "false", Database: "somedatabase"}, - "sqlserver://.?database=somedatabase&encrypt=false&workstation+id=mystation", - }, - { - &ConnectSettings{TrustServerCertificate: true, Password: pwd, ServerName: `someserver\instance`, Database: "somedatabase", UserName: "someuser"}, - fmt.Sprintf("sqlserver://someuser:%s@someserver/instance?database=somedatabase&trustservercertificate=true", pwd), - }, - { - &ConnectSettings{TrustServerCertificate: true, UseTrustedConnection: true, Password: pwd, ServerName: `tcp:someserver,1045`, UserName: "someuser"}, - "sqlserver://someserver:1045?trustservercertificate=true", - }, - { - &ConnectSettings{ServerName: `tcp:someserver,1045`}, - "sqlserver://someserver:1045", - }, - { - &ConnectSettings{ServerName: "someserver", AuthenticationMethod: azuread.ActiveDirectoryServicePrincipal, UserName: "myapp@mytenant", Password: pwd}, - fmt.Sprintf("sqlserver://myapp%%40mytenant:%s@someserver", pwd), - }, - } - - for i, test := range commands { - - connectionString, err := test.settings.ConnectionString() - if assert.NoError(t, err, "Unexpected error from [%d] %+v", i, test.settings) { - assert.Equal(t, test.connectionString, connectionString, "Wrong connection string from [%d]: %+v", i, test.settings) - } - } -} - -/* The following tests require a working SQL instance and rely on SqlCmd environment variables -to manage the initial connection string. The default connection when no environment variables are -set will be to localhost using Windows auth. - -*/ -func TestSqlCmdConnectDb(t *testing.T) { - v := InitializeVariables(true) - s := &Sqlcmd{vars: v} - s.Connect = newConnect(t) - err := s.ConnectDb(nil, false) - if assert.NoError(t, err, "ConnectDb should succeed") { - sqlcmduser := os.Getenv(SQLCMDUSER) - if sqlcmduser == "" { - u, _ := user.Current() - sqlcmduser = u.Username - } - assert.Equal(t, sqlcmduser, s.vars.SQLCmdUser(), "SQLCMDUSER variable should match connected user") - } -} - -func ConnectDb(t testing.TB) (*sql.Conn, error) { - v := InitializeVariables(true) - s := &Sqlcmd{vars: v} - s.Connect = newConnect(t) - err := s.ConnectDb(nil, false) - return s.db, err -} - -func TestSqlCmdQueryAndExit(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - s.Query = "select $(X" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - s.SetOutput(nil) - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } -} - -// Simulate :r command -func TestIncludeFileNoExecutions(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"singlebatchnogo.sql", false) - s.SetOutput(nil) - if assert.NoError(t, err, "IncludeFile singlebatchnogo.sql false") { - assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile singlebatchnogo.sql false") - assert.Equal(t, "select 100 as num"+SqlcmdEol+"select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile singlebatchnogo.sql false") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "", string(bytes), "Incorrect output from Run") - } - file, err = os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - defer os.Remove(file.Name()) - s.SetOutput(file) - // The second file has a go so it will execute all statements before it - err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", false) - if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql false") { - assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql false") - assert.Equal(t, "select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql false") - s.SetOutput(nil) - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } - } -} - -// Simulate -i command line usage -func TestIncludeFileProcessAll(t *testing.T) { - s, file := setupSqlcmdWithFileOutput(t) - defer os.Remove(file.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"twobatchwithgo.sql", true) - s.SetOutput(nil) - if assert.NoError(t, err, "IncludeFile twobatchwithgo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchwithgo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchwithgo.sql true") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - file, err = os.CreateTemp("", "sqlcmdout") - defer os.Remove(file.Name()) - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(file) - err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", true) - if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql true") - bytes, err := os.ReadFile(file.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") - } - } - } -} - -func TestIncludeFileWithVariables(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - dataPath := "testdata" + string(os.PathSeparator) - err := s.IncludeFile(dataPath+"variablesnogo.sql", true) - if assert.NoError(t, err, "IncludeFile variablesnogo.sql true") { - assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile variablesnogo.sql true") - assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile variablesnogo.sql true") - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol, o) - } -} - -func TestGetRunnableQuery(t *testing.T) { - v := InitializeVariables(false) - v.Set("var1", "v1") - v.Set("var2", "variable2") - - type test struct { - raw string - q string - } - tests := []test{ - {"$(var1)", "v1"}, - {"$ (var2)", "$ (var2)"}, - {"select '$(VAR1) $(VAR2)' as c", "select 'v1 variable2' as c"}, - {" $(VAR1) ' $(VAR2) ' as $(VAR1)", " v1 ' variable2 ' as v1"}, - } - s := New(nil, "", v) - for _, test := range tests { - s.batch.Reset([]rune(test.raw)) - _, _, _ = s.batch.Next() - s.Connect.DisableVariableSubstitution = false - t.Log(test.raw) - r := s.getRunnableQuery(test.raw) - assert.Equalf(t, test.q, r, `runnableQuery for "%s"`, test.raw) - s.Connect.DisableVariableSubstitution = true - r = s.getRunnableQuery(test.raw) - assert.Equalf(t, test.raw, r, `runnableQuery without variable subs for "%s"`, test.raw) - } -} - -func TestExitInitialQuery(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _ = s.vars.Setvar("var1", "1200") - s.Query = "EXIT(SELECT '$(var1)', 2100)" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - s.SetOutput(nil) - o := buf.buf.String() - assert.Equal(t, "1200 2100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Output") - assert.Equal(t, 1200, s.Exitcode, "ExitCode") - } - -} - -func TestExitCodeSetOnError(t *testing.T) { - s, _ := setupSqlCmdWithMemoryOutput(t) - s.Connect.ErrorSeverityLevel = 12 - retcode, err := s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.NoError(t, err, "!ExitOnError 11") - assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") - assert.NoError(t, err, "!ExitOnError 14") - assert.Equal(t, 14, retcode, "Raiserror above ErrorSeverityLevel") - s.Connect.ExitOnError = true - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.NoError(t, err, "ExitOnError and Raiserror below ErrorSeverityLevel") - assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") - assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and Raiserror above ErrorSeverityLevel") - assert.Equal(t, 14, retcode, "ExitOnError and Raiserror above ErrorSeverityLevel") - s.Connect.ErrorSeverityLevel = 0 - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") - assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") - assert.Equal(t, 1, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") - retcode, err = s.runQuery("RAISERROR (N'Testing!' , 5, 1)") - assert.NoError(t, err, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") - assert.Equal(t, -101, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") - retcode, err = s.runQuery("RAISERROR (15001, 10, 127)") - assert.ErrorIs(t, err, ErrExitRequested, "RAISERROR with state 127") - assert.Equal(t, 15001, retcode, "RAISERROR (15001, 10, 127)") -} - -func TestSqlCmdExitOnError(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Connect.ExitOnError = true - err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"}) - o := buf.buf.String() - assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error") - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'."+SqlcmdEol, o, "Only first select should run") - assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error") - - s, buf = setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.Connect.ExitOnError = true - s.Connect.ErrorSeverityLevel = 15 - s.vars.Set(SQLCMDERRORLEVEL, "14") - err = runSqlCmd(t, s, []string{"raiserror(N'13', 13, 1)", "GO", "raiserror(N'14', 14, 1)", "GO", "raiserror(N'15', 15, 1)", "GO", "SELECT 'nope'", "GO"}) - o = buf.buf.String() - assert.NotContains(t, o, "Level 13", "Level 13 should be filtered from the output") - assert.NotContains(t, o, "nope", "Last select should not be run") - assert.Contains(t, o, "Level 14", "Level 14 should be in the output") - assert.Contains(t, o, "Level 15", "Level 15 should be in the output") - assert.Equal(t, 15, s.Exitcode, "s.ExitCode for a syntax error") - assert.NoError(t, err, "Run should not return an error for a SQL error") -} - -func TestSqlCmdSetErrorLevel(t *testing.T) { - s, _ := setupSqlCmdWithMemoryOutput(t) - s.Connect.ErrorSeverityLevel = 15 - err := runSqlCmd(t, s, []string{"select bad as bad", "GO", "select 1", "GO"}) - assert.NoError(t, err, "runSqlCmd should have no error") - assert.Equal(t, 16, s.Exitcode, "Select error should be the exit code") -} - -type testConsole struct { - PromptText string - OnPasswordPrompt func(prompt string) ([]byte, error) - OnReadLine func() (string, error) -} - -func (tc *testConsole) Readline() (string, error) { - return tc.OnReadLine() -} - -func (tc *testConsole) ReadPassword(prompt string) ([]byte, error) { - return tc.OnPasswordPrompt(prompt) -} - -func (tc *testConsole) SetPrompt(s string) { - tc.PromptText = s -} - -func (tc *testConsole) Close() { - -} - -func TestPromptForPasswordNegative(t *testing.T) { - prompted := false - console := &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - assert.Equal(t, "Password:", prompt, "Incorrect password prompt") - prompted = true - return []byte{}, nil - }, - OnReadLine: func() (string, error) { - assert.Fail(t, "ReadLine should not be called") - return "", nil - }, - } - v := InitializeVariables(true) - s := New(console, "", v) - s.Connect.UserName = "someuser" - err := s.ConnectDb(nil, false) - assert.True(t, prompted, "Password prompt not shown for SQL auth") - assert.Error(t, err, "ConnectDb") - prompted = false - s.Connect.AuthenticationMethod = azuread.ActiveDirectoryPassword - err = s.ConnectDb(nil, false) - assert.True(t, prompted, "Password prompt not shown for AD Password auth") - assert.Error(t, err, "ConnectDb") - prompted = false -} - -func TestPromptForPasswordPositive(t *testing.T) { - prompted := false - c := newConnect(t) - if c.Password == "" { - // See if azure variables are set for activedirectoryserviceprincipal - c.UserName = os.Getenv("AZURE_CLIENT_ID") + "@" + os.Getenv("AZURE_TENANT_ID") - c.Password = os.Getenv("AZURE_CLIENT_SECRET") - c.AuthenticationMethod = azuread.ActiveDirectoryServicePrincipal - if c.Password == "" { - t.Skip("No password available") - } - } - password := c.Password - c.Password = "" - console := &testConsole{ - OnPasswordPrompt: func(prompt string) ([]byte, error) { - assert.Equal(t, "Password:", prompt, "Incorrect password prompt") - prompted = true - return []byte(password), nil - }, - OnReadLine: func() (string, error) { - assert.Fail(t, "ReadLine should not be called") - return "", nil - }, - } - v := InitializeVariables(true) - s := New(console, "", v) - // attempt without password prompt - err := s.ConnectDb(c, true) - assert.False(t, prompted, "ConnectDb with nopw=true should not prompt for password") - assert.Error(t, err, "ConnectDb with nopw==true and no password provided") - err = s.ConnectDb(c, false) - assert.True(t, prompted, "ConnectDb with !nopw should prompt for password") - assert.NoError(t, err, "ConnectDb with !nopw and valid password returned from prompt") - if s.Connect.Password != password { - t.Fatal(t, err, "Password not stored in the connection") - } -} - -func TestVerticalLayoutNoColumns(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDFORMAT, "vert") - _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, - "100"+SqlcmdEol+"2000"+SqlcmdEol+"300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, - buf.buf.String(), "Query without column headers") -} - -func TestSelectGuidColumn(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _, err := s.runQuery("select convert(uniqueidentifier, N'3ddba21e-ff0f-4d24-90b4-f355864d7865')") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, "3ddba21e-ff0f-4d24-90b4-f355864d7865"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a uniqueidentifier should work") -} - -func TestSelectNullGuidColumn(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - _, err := s.runQuery("select convert(uniqueidentifier,null)") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, "NULL"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a null uniqueidentifier should work") -} - -func TestVerticalLayoutWithColumns(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.vars.Set(SQLCMDFORMAT, "vert") - s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "256") - _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") - assert.NoError(t, err, "runQuery failed") - assert.Equal(t, - "column1 100"+SqlcmdEol+"col2 2000"+SqlcmdEol+" 300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, - buf.buf.String(), "Query without column headers") - -} - -func TestSqlCmdDefersToPrintError(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - s.PrintError = func(msg string, severity uint8) bool { - return severity > 10 - } - err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"}) - if assert.NoError(t, err, "runSqlCmd failed") { - assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError") - } -} - -func TestSqlCmdMaintainsConnectionBetweenBatches(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - err := runSqlCmd(t, s, []string{"CREATE TABLE #tmp1 (col1 int)", "insert into #tmp1 values (1)", "GO", "select * from #tmp1", "drop table #tmp1", "GO"}) - if assert.NoError(t, err, "runSqlCmd failed") { - assert.Equal(t, oneRowAffected+SqlcmdEol+"1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "Sqlcmd uses the same connection for all queries") - } -} - -func TestDateTimeFormats(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - defer buf.Close() - err := s.IncludeFile(`testdata/selectdates.sql`, true) - if assert.NoError(t, err, "selectdates.sql") { - assert.Equal(t, - `2022-03-05 14:01:02.000 2021-01-02 11:06:02.2000 2021-05-05 00:00:00.000000 +00:00 2019-01-11 13:00:00 14:01:02.0000000 2011-02-03`+SqlcmdEol+SqlcmdEol, - buf.buf.String(), - "Unexpected date format output") - - } -} - -func TestQueryServerPropertyReturnsColumnName(t *testing.T) { - s, buf := setupSqlCmdWithMemoryOutput(t) - s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "100") - defer buf.Close() - err := runSqlCmd(t, s, []string{"select SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition", "GO"}) - if assert.NoError(t, err, "select should succeed") { - assert.Contains(t, buf.buf.String(), "DatabaseEngineEdition", "Column name missing from output") - } -} - -func TestSqlCmdOutputAndError(t *testing.T) { - s, outfile, errfile := setupSqlcmdWithFileErrorOutput(t) - defer os.Remove(outfile.Name()) - defer os.Remove(errfile.Name()) - s.Query = "select $(X" - err := s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - bytes, err := os.ReadFile(errfile.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") - } - } - s.Query = "select '1'" - err = s.Run(true, false) - if assert.NoError(t, err, "s.Run(once = true)") { - bytes, err := os.ReadFile(outfile.Name()) - if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for query execution") - } - } - - s, outfile, errfile = setupSqlcmdWithFileErrorOutput(t) - defer os.Remove(outfile.Name()) - defer os.Remove(errfile.Name()) - dataPath := "testdata" + string(os.PathSeparator) - err = s.IncludeFile(dataPath+"testerrorredirection.sql", false) - if assert.NoError(t, err, "IncludeFile testerrorredirection.sql false") { - bytes, err := os.ReadFile(outfile.Name()) - if assert.NoError(t, err, "os.ReadFile outfile") { - assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for sql file execution in outfile") - } - bytes, err = os.ReadFile(errfile.Name()) - if assert.NoError(t, err, "os.ReadFile errfile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") - } - } -} - -// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input -func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error { - t.Helper() - i := 0 - s.batch.read = func() (string, error) { - if i < len(lines) { - index := i - i++ - return lines[index], nil - } - return "", io.EOF - } - return s.Run(false, false) -} - -func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - buf := &memoryBuffer{buf: new(bytes.Buffer)} - s.SetOutput(buf) - err := s.ConnectDb(nil, true) - assert.NoError(t, err, "s.ConnectDB") - return s, buf -} - -func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - file, err := os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(file) - err = s.ConnectDb(nil, true) - if err != nil { - os.Remove(file.Name()) - } - assert.NoError(t, err, "s.ConnectDB") - return s, file -} - -func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File) { - t.Helper() - v := InitializeVariables(true) - v.Set(SQLCMDMAXVARTYPEWIDTH, "0") - s := New(nil, "", v) - s.Connect = newConnect(t) - s.Format = NewSQLCmdDefaultFormatter(true) - outfile, err := os.CreateTemp("", "sqlcmdout") - assert.NoError(t, err, "os.CreateTemp") - errfile, err := os.CreateTemp("", "sqlcmderr") - assert.NoError(t, err, "os.CreateTemp") - s.SetOutput(outfile) - s.SetError(errfile) - err = s.ConnectDb(nil, true) - if err != nil { - os.Remove(outfile.Name()) - os.Remove(errfile.Name()) - } - assert.NoError(t, err, "s.ConnectDB") - return s, outfile, errfile -} - -// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set -func canTestAzureAuth() bool { - server := os.Getenv(SQLCMDSERVER) - userName := os.Getenv(SQLCMDUSER) - return strings.Contains(server, ".database.windows.net") && userName == "" -} - -func newConnect(t testing.TB) *ConnectSettings { - t.Helper() - connect := ConnectSettings{ - UserName: os.Getenv(SQLCMDUSER), - Database: os.Getenv(SQLCMDDBNAME), - ServerName: os.Getenv(SQLCMDSERVER), - Password: os.Getenv(SQLCMDPASSWORD), - } - if canTestAzureAuth() { - t.Log("Using ActiveDirectoryDefault") - connect.AuthenticationMethod = azuread.ActiveDirectoryDefault - } - return &connect -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "bytes" + "database/sql" + "fmt" + "io" + "os" + "os/user" + "strings" + "testing" + + "github.com/microsoft/go-mssqldb/azuread" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +const oneRowAffected = "(1 row affected)" + +func TestConnectionStringFromSqlCmd(t *testing.T) { + type connectionStringTest struct { + settings *ConnectSettings + connectionString string + } + + pwd := uuid.New().String() + + commands := []connectionStringTest{ + + {&ConnectSettings{}, "sqlserver://."}, + { + &ConnectSettings{TrustServerCertificate: true, WorkstationName: "mystation", Database: "somedatabase"}, + "sqlserver://.?database=somedatabase&trustservercertificate=true&workstation+id=mystation", + }, + { + &ConnectSettings{WorkstationName: "mystation", Encrypt: "false", Database: "somedatabase"}, + "sqlserver://.?database=somedatabase&encrypt=false&workstation+id=mystation", + }, + { + &ConnectSettings{TrustServerCertificate: true, Password: pwd, ServerName: `someserver\instance`, Database: "somedatabase", UserName: "someuser"}, + fmt.Sprintf("sqlserver://someuser:%s@someserver/instance?database=somedatabase&trustservercertificate=true", pwd), + }, + { + &ConnectSettings{TrustServerCertificate: true, UseTrustedConnection: true, Password: pwd, ServerName: `tcp:someserver,1045`, UserName: "someuser"}, + "sqlserver://someserver:1045?trustservercertificate=true", + }, + { + &ConnectSettings{ServerName: `tcp:someserver,1045`}, + "sqlserver://someserver:1045", + }, + { + &ConnectSettings{ServerName: "someserver", AuthenticationMethod: azuread.ActiveDirectoryServicePrincipal, UserName: "myapp@mytenant", Password: pwd}, + fmt.Sprintf("sqlserver://myapp%%40mytenant:%s@someserver", pwd), + }, + } + + for i, test := range commands { + + connectionString, err := test.settings.ConnectionString() + if assert.NoError(t, err, "Unexpected error from [%d] %+v", i, test.settings) { + assert.Equal(t, test.connectionString, connectionString, "Wrong connection string from [%d]: %+v", i, test.settings) + } + } +} + +/* The following tests require a working SQL instance and rely on SqlCmd environment variables +to manage the initial connection string. The default connection when no environment variables are +set will be to localhost using Windows auth. + +*/ +func TestSqlCmdConnectDb(t *testing.T) { + v := InitializeVariables(true) + s := &Sqlcmd{vars: v} + s.Connect = newConnect(t) + err := s.ConnectDb(nil, false) + if assert.NoError(t, err, "ConnectDb should succeed") { + sqlcmduser := os.Getenv(SQLCMDUSER) + if sqlcmduser == "" { + u, _ := user.Current() + sqlcmduser = u.Username + } + assert.Equal(t, sqlcmduser, s.vars.SQLCmdUser(), "SQLCMDUSER variable should match connected user") + } +} + +func ConnectDb(t testing.TB) (*sql.Conn, error) { + v := InitializeVariables(true) + s := &Sqlcmd{vars: v} + s.Connect = newConnect(t) + err := s.ConnectDb(nil, false) + return s.db, err +} + +func TestSqlCmdQueryAndExit(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + s.Query = "select $(X" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + s.SetOutput(nil) + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } +} + +// Simulate :r command +func TestIncludeFileNoExecutions(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"singlebatchnogo.sql", false) + s.SetOutput(nil) + if assert.NoError(t, err, "IncludeFile singlebatchnogo.sql false") { + assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile singlebatchnogo.sql false") + assert.Equal(t, "select 100 as num"+SqlcmdEol+"select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile singlebatchnogo.sql false") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "", string(bytes), "Incorrect output from Run") + } + file, err = os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + defer os.Remove(file.Name()) + s.SetOutput(file) + // The second file has a go so it will execute all statements before it + err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", false) + if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql false") { + assert.Equal(t, "-", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql false") + assert.Equal(t, "select 'string' as title", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql false") + s.SetOutput(nil) + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } + } +} + +// Simulate -i command line usage +func TestIncludeFileProcessAll(t *testing.T) { + s, file := setupSqlcmdWithFileOutput(t) + defer os.Remove(file.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"twobatchwithgo.sql", true) + s.SetOutput(nil) + if assert.NoError(t, err, "IncludeFile twobatchwithgo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchwithgo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchwithgo.sql true") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + file, err = os.CreateTemp("", "sqlcmdout") + defer os.Remove(file.Name()) + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(file) + err = s.IncludeFile(dataPath+"twobatchnoendinggo.sql", true) + if assert.NoError(t, err, "IncludeFile twobatchnoendinggo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile twobatchnoendinggo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile twobatchnoendinggo.sql true") + bytes, err := os.ReadFile(file.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"string"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, string(bytes), "Incorrect output from Run") + } + } + } +} + +func TestIncludeFileWithVariables(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + dataPath := "testdata" + string(os.PathSeparator) + err := s.IncludeFile(dataPath+"variablesnogo.sql", true) + if assert.NoError(t, err, "IncludeFile variablesnogo.sql true") { + assert.Equal(t, "=", s.batch.State(), "s.batch.State() after IncludeFile variablesnogo.sql true") + assert.Equal(t, "", s.batch.String(), "s.batch.String() after IncludeFile variablesnogo.sql true") + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, "100"+SqlcmdEol+SqlcmdEol, o) + } +} + +func TestGetRunnableQuery(t *testing.T) { + v := InitializeVariables(false) + v.Set("var1", "v1") + v.Set("var2", "variable2") + + type test struct { + raw string + q string + } + tests := []test{ + {"$(var1)", "v1"}, + {"$ (var2)", "$ (var2)"}, + {"select '$(VAR1) $(VAR2)' as c", "select 'v1 variable2' as c"}, + {" $(VAR1) ' $(VAR2) ' as $(VAR1)", " v1 ' variable2 ' as v1"}, + } + s := New(nil, "", v) + for _, test := range tests { + s.batch.Reset([]rune(test.raw)) + _, _, _ = s.batch.Next() + s.Connect.DisableVariableSubstitution = false + t.Log(test.raw) + r := s.getRunnableQuery(test.raw) + assert.Equalf(t, test.q, r, `runnableQuery for "%s"`, test.raw) + s.Connect.DisableVariableSubstitution = true + r = s.getRunnableQuery(test.raw) + assert.Equalf(t, test.raw, r, `runnableQuery without variable subs for "%s"`, test.raw) + } +} + +func TestExitInitialQuery(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _ = s.vars.Setvar("var1", "1200") + s.Query = "EXIT(SELECT '$(var1)', 2100)" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + s.SetOutput(nil) + o := buf.buf.String() + assert.Equal(t, "1200 2100"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, o, "Output") + assert.Equal(t, 1200, s.Exitcode, "ExitCode") + } + +} + +func TestExitCodeSetOnError(t *testing.T) { + s, _ := setupSqlCmdWithMemoryOutput(t) + s.Connect.ErrorSeverityLevel = 12 + retcode, err := s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.NoError(t, err, "!ExitOnError 11") + assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") + assert.NoError(t, err, "!ExitOnError 14") + assert.Equal(t, 14, retcode, "Raiserror above ErrorSeverityLevel") + s.Connect.ExitOnError = true + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.NoError(t, err, "ExitOnError and Raiserror below ErrorSeverityLevel") + assert.Equal(t, -101, retcode, "Raiserror below ErrorSeverityLevel") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 14, 1)") + assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and Raiserror above ErrorSeverityLevel") + assert.Equal(t, 14, retcode, "ExitOnError and Raiserror above ErrorSeverityLevel") + s.Connect.ErrorSeverityLevel = 0 + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 11, 1)") + assert.ErrorIs(t, err, ErrExitRequested, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") + assert.Equal(t, 1, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror above 10") + retcode, err = s.runQuery("RAISERROR (N'Testing!' , 5, 1)") + assert.NoError(t, err, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") + assert.Equal(t, -101, retcode, "ExitOnError and ErrorSeverityLevel = 0, Raiserror below 10") + retcode, err = s.runQuery("RAISERROR (15001, 10, 127)") + assert.ErrorIs(t, err, ErrExitRequested, "RAISERROR with state 127") + assert.Equal(t, 15001, retcode, "RAISERROR (15001, 10, 127)") +} + +func TestSqlCmdExitOnError(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Connect.ExitOnError = true + err := runSqlCmd(t, s, []string{"select 1", "GO", ":setvar", "select 2", "GO"}) + o := buf.buf.String() + assert.EqualError(t, err, "Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'.", "Run should return an error") + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol+"Sqlcmd: Error: Syntax error at line 3 near command ':SETVAR'."+SqlcmdEol, o, "Only first select should run") + assert.Equal(t, 1, s.Exitcode, "s.ExitCode for a syntax error") + + s, buf = setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.Connect.ExitOnError = true + s.Connect.ErrorSeverityLevel = 15 + s.vars.Set(SQLCMDERRORLEVEL, "14") + err = runSqlCmd(t, s, []string{"raiserror(N'13', 13, 1)", "GO", "raiserror(N'14', 14, 1)", "GO", "raiserror(N'15', 15, 1)", "GO", "SELECT 'nope'", "GO"}) + o = buf.buf.String() + assert.NotContains(t, o, "Level 13", "Level 13 should be filtered from the output") + assert.NotContains(t, o, "nope", "Last select should not be run") + assert.Contains(t, o, "Level 14", "Level 14 should be in the output") + assert.Contains(t, o, "Level 15", "Level 15 should be in the output") + assert.Equal(t, 15, s.Exitcode, "s.ExitCode for a syntax error") + assert.NoError(t, err, "Run should not return an error for a SQL error") +} + +func TestSqlCmdSetErrorLevel(t *testing.T) { + s, _ := setupSqlCmdWithMemoryOutput(t) + s.Connect.ErrorSeverityLevel = 15 + err := runSqlCmd(t, s, []string{"select bad as bad", "GO", "select 1", "GO"}) + assert.NoError(t, err, "runSqlCmd should have no error") + assert.Equal(t, 16, s.Exitcode, "Select error should be the exit code") +} + +type testConsole struct { + PromptText string + OnPasswordPrompt func(prompt string) ([]byte, error) + OnReadLine func() (string, error) +} + +func (tc *testConsole) Readline() (string, error) { + return tc.OnReadLine() +} + +func (tc *testConsole) ReadPassword(prompt string) ([]byte, error) { + return tc.OnPasswordPrompt(prompt) +} + +func (tc *testConsole) SetPrompt(s string) { + tc.PromptText = s +} + +func (tc *testConsole) Close() { + +} + +func TestPromptForPasswordNegative(t *testing.T) { + prompted := false + console := &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + assert.Equal(t, "Password:", prompt, "Incorrect password prompt") + prompted = true + return []byte{}, nil + }, + OnReadLine: func() (string, error) { + assert.Fail(t, "ReadLine should not be called") + return "", nil + }, + } + v := InitializeVariables(true) + s := New(console, "", v) + s.Connect.UserName = "someuser" + err := s.ConnectDb(nil, false) + assert.True(t, prompted, "Password prompt not shown for SQL auth") + assert.Error(t, err, "ConnectDb") + prompted = false + s.Connect.AuthenticationMethod = azuread.ActiveDirectoryPassword + err = s.ConnectDb(nil, false) + assert.True(t, prompted, "Password prompt not shown for AD Password auth") + assert.Error(t, err, "ConnectDb") + prompted = false +} + +func TestPromptForPasswordPositive(t *testing.T) { + prompted := false + c := newConnect(t) + if c.Password == "" { + // See if azure variables are set for activedirectoryserviceprincipal + c.UserName = os.Getenv("AZURE_CLIENT_ID") + "@" + os.Getenv("AZURE_TENANT_ID") + c.Password = os.Getenv("AZURE_CLIENT_SECRET") + c.AuthenticationMethod = azuread.ActiveDirectoryServicePrincipal + if c.Password == "" { + t.Skip("No password available") + } + } + password := c.Password + c.Password = "" + console := &testConsole{ + OnPasswordPrompt: func(prompt string) ([]byte, error) { + assert.Equal(t, "Password:", prompt, "Incorrect password prompt") + prompted = true + return []byte(password), nil + }, + OnReadLine: func() (string, error) { + assert.Fail(t, "ReadLine should not be called") + return "", nil + }, + } + v := InitializeVariables(true) + s := New(console, "", v) + // attempt without password prompt + err := s.ConnectDb(c, true) + assert.False(t, prompted, "ConnectDb with nopw=true should not prompt for password") + assert.Error(t, err, "ConnectDb with nopw==true and no password provided") + err = s.ConnectDb(c, false) + assert.True(t, prompted, "ConnectDb with !nopw should prompt for password") + assert.NoError(t, err, "ConnectDb with !nopw and valid password returned from prompt") + if s.Connect.Password != password { + t.Fatal(t, err, "Password not stored in the connection") + } +} + +func TestVerticalLayoutNoColumns(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDFORMAT, "vert") + _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, + "100"+SqlcmdEol+"2000"+SqlcmdEol+"300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, + buf.buf.String(), "Query without column headers") +} + +func TestSelectGuidColumn(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _, err := s.runQuery("select convert(uniqueidentifier, N'3ddba21e-ff0f-4d24-90b4-f355864d7865')") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, "3ddba21e-ff0f-4d24-90b4-f355864d7865"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a uniqueidentifier should work") +} + +func TestSelectNullGuidColumn(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + _, err := s.runQuery("select convert(uniqueidentifier,null)") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, "NULL"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "select a null uniqueidentifier should work") +} + +func TestVerticalLayoutWithColumns(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.vars.Set(SQLCMDFORMAT, "vert") + s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "256") + _, err := s.runQuery("SELECT 100 as 'column1', 2000 as 'col2', 300") + assert.NoError(t, err, "runQuery failed") + assert.Equal(t, + "column1 100"+SqlcmdEol+"col2 2000"+SqlcmdEol+" 300"+SqlcmdEol+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, + buf.buf.String(), "Query without column headers") + +} + +func TestSqlCmdDefersToPrintError(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + s.PrintError = func(msg string, severity uint8) bool { + return severity > 10 + } + err := runSqlCmd(t, s, []string{"PRINT 'this has severity 10'", "RAISERROR (N'Testing!' , 11, 1)", "GO"}) + if assert.NoError(t, err, "runSqlCmd failed") { + assert.Equal(t, "this has severity 10"+SqlcmdEol, buf.buf.String(), "Errors should be filtered by s.PrintError") + } +} + +func TestSqlCmdMaintainsConnectionBetweenBatches(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + err := runSqlCmd(t, s, []string{"CREATE TABLE #tmp1 (col1 int)", "insert into #tmp1 values (1)", "GO", "select * from #tmp1", "drop table #tmp1", "GO"}) + if assert.NoError(t, err, "runSqlCmd failed") { + assert.Equal(t, oneRowAffected+SqlcmdEol+"1"+SqlcmdEol+SqlcmdEol+oneRowAffected+SqlcmdEol, buf.buf.String(), "Sqlcmd uses the same connection for all queries") + } +} + +func TestDateTimeFormats(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + defer buf.Close() + err := s.IncludeFile(`testdata/selectdates.sql`, true) + if assert.NoError(t, err, "selectdates.sql") { + assert.Equal(t, + `2022-03-05 14:01:02.000 2021-01-02 11:06:02.2000 2021-05-05 00:00:00.000000 +00:00 2019-01-11 13:00:00 14:01:02.0000000 2011-02-03`+SqlcmdEol+SqlcmdEol, + buf.buf.String(), + "Unexpected date format output") + + } +} + +func TestQueryServerPropertyReturnsColumnName(t *testing.T) { + s, buf := setupSqlCmdWithMemoryOutput(t) + s.vars.Set(SQLCMDMAXVARTYPEWIDTH, "100") + defer buf.Close() + err := runSqlCmd(t, s, []string{"select SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition", "GO"}) + if assert.NoError(t, err, "select should succeed") { + assert.Contains(t, buf.buf.String(), "DatabaseEngineEdition", "Column name missing from output") + } +} + +func TestSqlCmdOutputAndError(t *testing.T) { + s, outfile, errfile := setupSqlcmdWithFileErrorOutput(t) + defer os.Remove(outfile.Name()) + defer os.Remove(errfile.Name()) + s.Query = "select $(X" + err := s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + bytes, err := os.ReadFile(errfile.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") + } + } + s.Query = "select '1'" + err = s.Run(true, false) + if assert.NoError(t, err, "s.Run(once = true)") { + bytes, err := os.ReadFile(outfile.Name()) + if assert.NoError(t, err, "os.ReadFile") { + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for query execution") + } + } + + s, outfile, errfile = setupSqlcmdWithFileErrorOutput(t) + defer os.Remove(outfile.Name()) + defer os.Remove(errfile.Name()) + dataPath := "testdata" + string(os.PathSeparator) + err = s.IncludeFile(dataPath+"testerrorredirection.sql", false) + if assert.NoError(t, err, "IncludeFile testerrorredirection.sql false") { + bytes, err := os.ReadFile(outfile.Name()) + if assert.NoError(t, err, "os.ReadFile outfile") { + assert.Equal(t, "1"+SqlcmdEol+SqlcmdEol+"(1 row affected)"+SqlcmdEol, string(bytes), "Unexpected output for sql file execution in outfile") + } + bytes, err = os.ReadFile(errfile.Name()) + if assert.NoError(t, err, "os.ReadFile errfile") { + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") + } + } +} + +// runSqlCmd uses lines as input for sqlcmd instead of relying on file or console input +func runSqlCmd(t testing.TB, s *Sqlcmd, lines []string) error { + t.Helper() + i := 0 + s.batch.read = func() (string, error) { + if i < len(lines) { + index := i + i++ + return lines[index], nil + } + return "", io.EOF + } + return s.Run(false, false) +} + +func setupSqlCmdWithMemoryOutput(t testing.TB) (*Sqlcmd, *memoryBuffer) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + buf := &memoryBuffer{buf: new(bytes.Buffer)} + s.SetOutput(buf) + err := s.ConnectDb(nil, true) + assert.NoError(t, err, "s.ConnectDB") + return s, buf +} + +func setupSqlcmdWithFileOutput(t testing.TB) (*Sqlcmd, *os.File) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + file, err := os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(file) + err = s.ConnectDb(nil, true) + if err != nil { + os.Remove(file.Name()) + } + assert.NoError(t, err, "s.ConnectDB") + return s, file +} + +func setupSqlcmdWithFileErrorOutput(t testing.TB) (*Sqlcmd, *os.File, *os.File) { + t.Helper() + v := InitializeVariables(true) + v.Set(SQLCMDMAXVARTYPEWIDTH, "0") + s := New(nil, "", v) + s.Connect = newConnect(t) + s.Format = NewSQLCmdDefaultFormatter(true) + outfile, err := os.CreateTemp("", "sqlcmdout") + assert.NoError(t, err, "os.CreateTemp") + errfile, err := os.CreateTemp("", "sqlcmderr") + assert.NoError(t, err, "os.CreateTemp") + s.SetOutput(outfile) + s.SetError(errfile) + err = s.ConnectDb(nil, true) + if err != nil { + os.Remove(outfile.Name()) + os.Remove(errfile.Name()) + } + assert.NoError(t, err, "s.ConnectDB") + return s, outfile, errfile +} + +// Assuming public Azure, use AAD when SQLCMDUSER environment variable is not set +func canTestAzureAuth() bool { + server := os.Getenv(SQLCMDSERVER) + userName := os.Getenv(SQLCMDUSER) + return strings.Contains(server, ".database.windows.net") && userName == "" +} + +func newConnect(t testing.TB) *ConnectSettings { + t.Helper() + connect := ConnectSettings{ + UserName: os.Getenv(SQLCMDUSER), + Database: os.Getenv(SQLCMDDBNAME), + ServerName: os.Getenv(SQLCMDSERVER), + Password: os.Getenv(SQLCMDPASSWORD), + } + if canTestAzureAuth() { + t.Log("Using ActiveDirectoryDefault") + connect.AuthenticationMethod = azuread.ActiveDirectoryDefault + } + return &connect +} diff --git a/pkg/sqlcmd/testdata/singlebatchnogo.sql b/pkg/sqlcmd/testdata/singlebatchnogo.sql index 8d4c9bf2..29b68f16 100644 --- a/pkg/sqlcmd/testdata/singlebatchnogo.sql +++ b/pkg/sqlcmd/testdata/singlebatchnogo.sql @@ -1,2 +1,2 @@ -select 100 as num -select 'string' as title +select 100 as num +select 'string' as title diff --git a/pkg/sqlcmd/testdata/twobatchnoendinggo.sql b/pkg/sqlcmd/testdata/twobatchnoendinggo.sql index 0be51209..90c4d289 100644 --- a/pkg/sqlcmd/testdata/twobatchnoendinggo.sql +++ b/pkg/sqlcmd/testdata/twobatchnoendinggo.sql @@ -1,3 +1,3 @@ -select 100 as num -go -select 'string' as title +select 100 as num +go +select 'string' as title diff --git a/pkg/sqlcmd/testdata/twobatchwithgo.sql b/pkg/sqlcmd/testdata/twobatchwithgo.sql index 26554bc9..58439168 100644 --- a/pkg/sqlcmd/testdata/twobatchwithgo.sql +++ b/pkg/sqlcmd/testdata/twobatchwithgo.sql @@ -1,4 +1,4 @@ -select 100 as num -GO -select 'string' as title -GO +select 100 as num +GO +select 'string' as title +GO diff --git a/pkg/sqlcmd/util.go b/pkg/sqlcmd/util.go index fc0fb08a..79f75b67 100644 --- a/pkg/sqlcmd/util.go +++ b/pkg/sqlcmd/util.go @@ -1,73 +1,73 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "strconv" - "strings" -) - -// splitServer extracts connection parameters from a server name input -func splitServer(serverName string) (string, string, uint64, error) { - instance := "" - port := uint64(0) - if strings.HasPrefix(serverName, "tcp:") { - if len(serverName) == 4 { - return "", "", 0, &InvalidServerName - } - serverName = serverName[4:] - } - serverNameParts := strings.Split(serverName, ",") - if len(serverNameParts) > 2 { - return "", "", 0, &InvalidServerName - } - if len(serverNameParts) == 2 { - var err error - port, err = strconv.ParseUint(serverNameParts[1], 10, 16) - if err != nil { - return "", "", 0, &InvalidServerName - } - serverName = serverNameParts[0] - } else { - serverNameParts = strings.Split(serverName, "\\") - if len(serverNameParts) > 2 { - return "", "", 0, &InvalidServerName - } - if len(serverNameParts) == 2 { - instance = serverNameParts[1] - serverName = serverNameParts[0] - } - } - return serverName, instance, port, nil -} - -// padRight appends c instances of s to builder -func padRight(builder *strings.Builder, c int64, s string) *strings.Builder { - var i int64 - for ; i < c; i++ { - builder.WriteString(s) - } - return builder -} - -// padLeft prepends c instances of s to builder -func padLeft(builder *strings.Builder, c int64, s string) *strings.Builder { - newBuilder := new(strings.Builder) - newBuilder.Grow(builder.Len()) - var i int64 - for ; i < c; i++ { - newBuilder.WriteString(s) - } - newBuilder.WriteString(builder.String()) - return newBuilder -} - -func contains(arr []string, s string) bool { - for _, a := range arr { - if a == s { - return true - } - } - return false -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "strconv" + "strings" +) + +// splitServer extracts connection parameters from a server name input +func splitServer(serverName string) (string, string, uint64, error) { + instance := "" + port := uint64(0) + if strings.HasPrefix(serverName, "tcp:") { + if len(serverName) == 4 { + return "", "", 0, &InvalidServerName + } + serverName = serverName[4:] + } + serverNameParts := strings.Split(serverName, ",") + if len(serverNameParts) > 2 { + return "", "", 0, &InvalidServerName + } + if len(serverNameParts) == 2 { + var err error + port, err = strconv.ParseUint(serverNameParts[1], 10, 16) + if err != nil { + return "", "", 0, &InvalidServerName + } + serverName = serverNameParts[0] + } else { + serverNameParts = strings.Split(serverName, "\\") + if len(serverNameParts) > 2 { + return "", "", 0, &InvalidServerName + } + if len(serverNameParts) == 2 { + instance = serverNameParts[1] + serverName = serverNameParts[0] + } + } + return serverName, instance, port, nil +} + +// padRight appends c instances of s to builder +func padRight(builder *strings.Builder, c int64, s string) *strings.Builder { + var i int64 + for ; i < c; i++ { + builder.WriteString(s) + } + return builder +} + +// padLeft prepends c instances of s to builder +func padLeft(builder *strings.Builder, c int64, s string) *strings.Builder { + newBuilder := new(strings.Builder) + newBuilder.Grow(builder.Len()) + var i int64 + for ; i < c; i++ { + newBuilder.WriteString(s) + } + newBuilder.WriteString(builder.String()) + return newBuilder +} + +func contains(arr []string, s string) bool { + for _, a := range arr { + if a == s { + return true + } + } + return false +} diff --git a/pkg/sqlcmd/variables.go b/pkg/sqlcmd/variables.go index ebf6b1b5..e86a5b5e 100644 --- a/pkg/sqlcmd/variables.go +++ b/pkg/sqlcmd/variables.go @@ -1,335 +1,335 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "fmt" - "os" - "strings" - "unicode" -) - -// Variables provides set and get of sqlcmd scripting variables -type Variables map[string]string - -// Built-in scripting variables -const ( - SQLCMDDBNAME = "SQLCMDDBNAME" - SQLCMDINI = "SQLCMDINI" - SQLCMDPACKETSIZE = "SQLCMDPACKETSIZE" - SQLCMDPASSWORD = "SQLCMDPASSWORD" - SQLCMDSERVER = "SQLCMDSERVER" - SQLCMDUSER = "SQLCMDUSER" - SQLCMDWORKSTATION = "SQLCMDWORKSTATION" - SQLCMDLOGINTIMEOUT = "SQLCMDLOGINTIMEOUT" - SQLCMDSTATTIMEOUT = "SQLCMDSTATTIMEOUT" - SQLCMDHEADERS = "SQLCMDHEADERS" - SQLCMDCOLSEP = "SQLCMDCOLSEP" - SQLCMDCOLWIDTH = "SQLCMDCOLWIDTH" - SQLCMDERRORLEVEL = "SQLCMDERRORLEVEL" - SQLCMDFORMAT = "SQLCMDFORMAT" - SQLCMDMAXVARTYPEWIDTH = "SQLCMDMAXVARTYPEWIDTH" - SQLCMDMAXFIXEDTYPEWIDTH = "SQLCMDMAXFIXEDTYPEWIDTH" - SQLCMDEDITOR = "SQLCMDEDITOR" - SQLCMDUSEAAD = "SQLCMDUSEAAD" -) - -// builtinVariables are the predefined SQLCMD variables. Their values are printed first by :listvar -var builtinVariables = []string{ - SQLCMDCOLSEP, - SQLCMDCOLWIDTH, - SQLCMDDBNAME, - SQLCMDEDITOR, - SQLCMDERRORLEVEL, - SQLCMDFORMAT, - SQLCMDHEADERS, - SQLCMDINI, - SQLCMDLOGINTIMEOUT, - SQLCMDMAXFIXEDTYPEWIDTH, - SQLCMDMAXVARTYPEWIDTH, - SQLCMDPACKETSIZE, - SQLCMDSERVER, - SQLCMDSTATTIMEOUT, - SQLCMDUSEAAD, - SQLCMDUSER, - SQLCMDWORKSTATION, -} - -// readonlyVariables are variables that can't be changed via :setvar -var readOnlyVariables = []string{ - SQLCMDDBNAME, - SQLCMDINI, - SQLCMDPACKETSIZE, - SQLCMDSERVER, - SQLCMDUSER, - SQLCMDWORKSTATION, -} - -func (v Variables) checkReadOnly(key string) error { - currentValue, hasValue := v[key] - if hasValue { - for _, variable := range readOnlyVariables { - if variable == key && currentValue != "" { - return ReadOnlyVariable(key) - } - } - } - return nil -} - -// Set sets or adds the value in the map. -func (v Variables) Set(name, value string) { - key := strings.ToUpper(name) - v[key] = value -} - -// Get returns the value of the named variable -// To distinguish an empty value from an unset value use the bool return value -func (v Variables) Get(name string) (string, bool) { - key := strings.ToUpper(name) - s, ok := v[key] - return s, ok -} - -// Unset removes the value from the map -func (v Variables) Unset(name string) { - key := strings.ToUpper(name) - delete(v, key) -} - -// All returns a copy of the current variables -func (v Variables) All() map[string]string { - return map[string]string(v) -} - -// SQLCmdUser returns the SQLCMDUSER variable value -func (v Variables) SQLCmdUser() string { - return v[SQLCMDUSER] -} - -// SQLCmdServer returns the server connection parameters derived from the SQLCMDSERVER variable value -func (v Variables) SQLCmdServer() (serverName string, instance string, port uint64, err error) { - serverName = v[SQLCMDSERVER] - return splitServer(serverName) -} - -// SQLCmdDatabase returns the SQLCMDDBNAME variable value -func (v Variables) SQLCmdDatabase() string { - return v[SQLCMDDBNAME] -} - -// UseAad returns whether the SQLCMDUSEAAD variable value is set to "true" -func (v Variables) UseAad() bool { - return strings.EqualFold(v[SQLCMDUSEAAD], "true") -} - -// ColumnSeparator is the value of SQLCMDCOLSEP variable. It can have 0 or 1 characters -func (v Variables) ColumnSeparator() string { - sep := v[SQLCMDCOLSEP] - if len(sep) > 1 { - return sep[:1] - } - return sep -} - -// MaxFixedColumnWidth is the value of SQLCMDMAXFIXEDTYPEWIDTH variable. -// When non-zero, it limits the width of columns for types CHAR, NCHAR, NVARCHAR, VARCHAR, VARBINARY, VARIANT -func (v Variables) MaxFixedColumnWidth() int64 { - w := v[SQLCMDMAXFIXEDTYPEWIDTH] - return mustValue(w) -} - -// MaxVarColumnWidth is the value of SQLCMDMAXVARTYPEWIDTH variable. -// When non-zero, it limits the width of columns for (max) versions of CHAR, NCHAR, VARBINARY. -// It also limits the width of xml, UDT, text, ntext, and image -func (v Variables) MaxVarColumnWidth() int64 { - w := v[SQLCMDMAXVARTYPEWIDTH] - return mustValue(w) -} - -// ScreenWidth is the value of SQLCMDCOLWIDTH variable. -// It tells the formatter how many characters wide to limit all screen output. -func (v Variables) ScreenWidth() int64 { - w := v[SQLCMDCOLWIDTH] - return mustValue(w) -} - -// RowsBetweenHeaders is the value of SQLCMDHEADERS variable. -// When MaxVarColumnWidth() is 0, it returns -1 -func (v Variables) RowsBetweenHeaders() int64 { - if v.MaxVarColumnWidth() == 0 { - return -1 - } - h := mustValue(v[SQLCMDHEADERS]) - return h -} - -// ErrorLevel controls the minimum level of errors that are printed -func (v Variables) ErrorLevel() int64 { - return mustValue(v[SQLCMDERRORLEVEL]) -} - -// Format is the name of the results format -func (v Variables) Format() string { - switch v[SQLCMDFORMAT] { - case "vert", "vertical": - return "vertical" - } - return "horizontal" -} - -// StartupScriptFile is the path to the file that contains the startup script -func (v Variables) StartupScriptFile() string { - return v[SQLCMDINI] -} - -// TextEditor is the query editor application launched by the :ED command -func (v Variables) TextEditor() string { - return v[SQLCMDEDITOR] -} - -func mustValue(val string) int64 { - var n int64 - _, err := fmt.Sscanf(val, "%d", &n) - if err == nil { - return n - } - panic(err) -} - -// defaultVariables defines variables that cannot be removed from the map, only reset -// to their default values. -var defaultVariables = Variables{ - SQLCMDCOLSEP: " ", - SQLCMDCOLWIDTH: "0", - SQLCMDEDITOR: defaultEditor, - SQLCMDERRORLEVEL: "0", - SQLCMDHEADERS: "0", - SQLCMDLOGINTIMEOUT: "30", - SQLCMDMAXFIXEDTYPEWIDTH: "0", - SQLCMDMAXVARTYPEWIDTH: "256", - SQLCMDSTATTIMEOUT: "0", -} - -// InitializeVariables initializes variables with default values. -// When fromEnvironment is true, then loads from the runtime environment -func InitializeVariables(fromEnvironment bool) *Variables { - variables := Variables{ - SQLCMDCOLSEP: defaultVariables[SQLCMDCOLSEP], - SQLCMDCOLWIDTH: defaultVariables[SQLCMDCOLWIDTH], - SQLCMDDBNAME: "", - SQLCMDEDITOR: defaultVariables[SQLCMDEDITOR], - SQLCMDERRORLEVEL: defaultVariables[SQLCMDERRORLEVEL], - SQLCMDHEADERS: defaultVariables[SQLCMDHEADERS], - SQLCMDINI: "", - SQLCMDLOGINTIMEOUT: defaultVariables[SQLCMDLOGINTIMEOUT], - SQLCMDMAXFIXEDTYPEWIDTH: defaultVariables[SQLCMDMAXFIXEDTYPEWIDTH], - SQLCMDMAXVARTYPEWIDTH: defaultVariables[SQLCMDMAXVARTYPEWIDTH], - SQLCMDPACKETSIZE: "4096", - SQLCMDSERVER: "", - SQLCMDSTATTIMEOUT: defaultVariables[SQLCMDSTATTIMEOUT], - SQLCMDUSER: "", - SQLCMDUSEAAD: "", - } - hostname, _ := os.Hostname() - variables.Set(SQLCMDWORKSTATION, hostname) - - if fromEnvironment { - for v := range variables.All() { - envVar, ok := os.LookupEnv(v) - if ok { - variables.Set(v, envVar) - } - } - } - return &variables -} - -// Setvar implements the :Setvar command -// TODO: Add validation functions for the variables. -func (variables *Variables) Setvar(name, value string) error { - err := ValidIdentifier(name) - if err == nil { - if err = variables.checkReadOnly(name); err != nil { - err = ReadOnlyVariable(name) - } - } - if err != nil { - return err - } - if value == "" { - if _, ok := variables.Get(name); !ok { - return UndefinedVariable(name) - } - if def, ok := defaultVariables.Get(name); ok { - value = def - } else { - variables.Unset(name) - return nil - } - } else { - value, err = ParseValue(value) - } - if err != nil { - return err - } - variables.Set(name, value) - return nil -} - -const validVariableRunes = "_-" - -// ValidIdentifier determines if a given string can be used as a variable name -func ValidIdentifier(name string) error { - - first := true - for _, c := range name { - if !unicode.IsLetter(c) && (first || (!unicode.IsDigit(c) && !strings.ContainsRune(validVariableRunes, c))) { - return fmt.Errorf("Invalid variable identifier %s", name) - } - first = false - } - return nil -} - -// ParseValue returns the string to use as the variable value -// If the string contains a space or a quote, it must be delimited by quotes and literal quotes -// within the value must be escaped by another quote -// "this has a quote "" in it" is valid -// "this has a quote" in it" is not valid -func ParseValue(val string) (string, error) { - quoted := val[0] == '"' - err := fmt.Errorf("Invalid variable value %s", val) - if !quoted { - if strings.ContainsAny(val, "\t\n\r ") { - return "", err - } - return val, nil - } - if len(val) == 1 || val[len(val)-1] != '"' { - return "", err - } - - b := new(strings.Builder) - quoted = false - r := []rune(val) -loop: - for i := 1; i < len(r)-1; i++ { - switch { - case quoted && r[i] == '"': - b.WriteRune('"') - quoted = false - case quoted && r[i] != '"': - break loop - case !quoted && r[i] == '"': - quoted = true - default: - b.WriteRune(r[i]) - } - } - if quoted { - return "", err - } - return b.String(), nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "fmt" + "os" + "strings" + "unicode" +) + +// Variables provides set and get of sqlcmd scripting variables +type Variables map[string]string + +// Built-in scripting variables +const ( + SQLCMDDBNAME = "SQLCMDDBNAME" + SQLCMDINI = "SQLCMDINI" + SQLCMDPACKETSIZE = "SQLCMDPACKETSIZE" + SQLCMDPASSWORD = "SQLCMDPASSWORD" + SQLCMDSERVER = "SQLCMDSERVER" + SQLCMDUSER = "SQLCMDUSER" + SQLCMDWORKSTATION = "SQLCMDWORKSTATION" + SQLCMDLOGINTIMEOUT = "SQLCMDLOGINTIMEOUT" + SQLCMDSTATTIMEOUT = "SQLCMDSTATTIMEOUT" + SQLCMDHEADERS = "SQLCMDHEADERS" + SQLCMDCOLSEP = "SQLCMDCOLSEP" + SQLCMDCOLWIDTH = "SQLCMDCOLWIDTH" + SQLCMDERRORLEVEL = "SQLCMDERRORLEVEL" + SQLCMDFORMAT = "SQLCMDFORMAT" + SQLCMDMAXVARTYPEWIDTH = "SQLCMDMAXVARTYPEWIDTH" + SQLCMDMAXFIXEDTYPEWIDTH = "SQLCMDMAXFIXEDTYPEWIDTH" + SQLCMDEDITOR = "SQLCMDEDITOR" + SQLCMDUSEAAD = "SQLCMDUSEAAD" +) + +// builtinVariables are the predefined SQLCMD variables. Their values are printed first by :listvar +var builtinVariables = []string{ + SQLCMDCOLSEP, + SQLCMDCOLWIDTH, + SQLCMDDBNAME, + SQLCMDEDITOR, + SQLCMDERRORLEVEL, + SQLCMDFORMAT, + SQLCMDHEADERS, + SQLCMDINI, + SQLCMDLOGINTIMEOUT, + SQLCMDMAXFIXEDTYPEWIDTH, + SQLCMDMAXVARTYPEWIDTH, + SQLCMDPACKETSIZE, + SQLCMDSERVER, + SQLCMDSTATTIMEOUT, + SQLCMDUSEAAD, + SQLCMDUSER, + SQLCMDWORKSTATION, +} + +// readonlyVariables are variables that can't be changed via :setvar +var readOnlyVariables = []string{ + SQLCMDDBNAME, + SQLCMDINI, + SQLCMDPACKETSIZE, + SQLCMDSERVER, + SQLCMDUSER, + SQLCMDWORKSTATION, +} + +func (v Variables) checkReadOnly(key string) error { + currentValue, hasValue := v[key] + if hasValue { + for _, variable := range readOnlyVariables { + if variable == key && currentValue != "" { + return ReadOnlyVariable(key) + } + } + } + return nil +} + +// Set sets or adds the value in the map. +func (v Variables) Set(name, value string) { + key := strings.ToUpper(name) + v[key] = value +} + +// Get returns the value of the named variable +// To distinguish an empty value from an unset value use the bool return value +func (v Variables) Get(name string) (string, bool) { + key := strings.ToUpper(name) + s, ok := v[key] + return s, ok +} + +// Unset removes the value from the map +func (v Variables) Unset(name string) { + key := strings.ToUpper(name) + delete(v, key) +} + +// All returns a copy of the current variables +func (v Variables) All() map[string]string { + return map[string]string(v) +} + +// SQLCmdUser returns the SQLCMDUSER variable value +func (v Variables) SQLCmdUser() string { + return v[SQLCMDUSER] +} + +// SQLCmdServer returns the server connection parameters derived from the SQLCMDSERVER variable value +func (v Variables) SQLCmdServer() (serverName string, instance string, port uint64, err error) { + serverName = v[SQLCMDSERVER] + return splitServer(serverName) +} + +// SQLCmdDatabase returns the SQLCMDDBNAME variable value +func (v Variables) SQLCmdDatabase() string { + return v[SQLCMDDBNAME] +} + +// UseAad returns whether the SQLCMDUSEAAD variable value is set to "true" +func (v Variables) UseAad() bool { + return strings.EqualFold(v[SQLCMDUSEAAD], "true") +} + +// ColumnSeparator is the value of SQLCMDCOLSEP variable. It can have 0 or 1 characters +func (v Variables) ColumnSeparator() string { + sep := v[SQLCMDCOLSEP] + if len(sep) > 1 { + return sep[:1] + } + return sep +} + +// MaxFixedColumnWidth is the value of SQLCMDMAXFIXEDTYPEWIDTH variable. +// When non-zero, it limits the width of columns for types CHAR, NCHAR, NVARCHAR, VARCHAR, VARBINARY, VARIANT +func (v Variables) MaxFixedColumnWidth() int64 { + w := v[SQLCMDMAXFIXEDTYPEWIDTH] + return mustValue(w) +} + +// MaxVarColumnWidth is the value of SQLCMDMAXVARTYPEWIDTH variable. +// When non-zero, it limits the width of columns for (max) versions of CHAR, NCHAR, VARBINARY. +// It also limits the width of xml, UDT, text, ntext, and image +func (v Variables) MaxVarColumnWidth() int64 { + w := v[SQLCMDMAXVARTYPEWIDTH] + return mustValue(w) +} + +// ScreenWidth is the value of SQLCMDCOLWIDTH variable. +// It tells the formatter how many characters wide to limit all screen output. +func (v Variables) ScreenWidth() int64 { + w := v[SQLCMDCOLWIDTH] + return mustValue(w) +} + +// RowsBetweenHeaders is the value of SQLCMDHEADERS variable. +// When MaxVarColumnWidth() is 0, it returns -1 +func (v Variables) RowsBetweenHeaders() int64 { + if v.MaxVarColumnWidth() == 0 { + return -1 + } + h := mustValue(v[SQLCMDHEADERS]) + return h +} + +// ErrorLevel controls the minimum level of errors that are printed +func (v Variables) ErrorLevel() int64 { + return mustValue(v[SQLCMDERRORLEVEL]) +} + +// Format is the name of the results format +func (v Variables) Format() string { + switch v[SQLCMDFORMAT] { + case "vert", "vertical": + return "vertical" + } + return "horizontal" +} + +// StartupScriptFile is the path to the file that contains the startup script +func (v Variables) StartupScriptFile() string { + return v[SQLCMDINI] +} + +// TextEditor is the query editor application launched by the :ED command +func (v Variables) TextEditor() string { + return v[SQLCMDEDITOR] +} + +func mustValue(val string) int64 { + var n int64 + _, err := fmt.Sscanf(val, "%d", &n) + if err == nil { + return n + } + panic(err) +} + +// defaultVariables defines variables that cannot be removed from the map, only reset +// to their default values. +var defaultVariables = Variables{ + SQLCMDCOLSEP: " ", + SQLCMDCOLWIDTH: "0", + SQLCMDEDITOR: defaultEditor, + SQLCMDERRORLEVEL: "0", + SQLCMDHEADERS: "0", + SQLCMDLOGINTIMEOUT: "30", + SQLCMDMAXFIXEDTYPEWIDTH: "0", + SQLCMDMAXVARTYPEWIDTH: "256", + SQLCMDSTATTIMEOUT: "0", +} + +// InitializeVariables initializes variables with default values. +// When fromEnvironment is true, then loads from the runtime environment +func InitializeVariables(fromEnvironment bool) *Variables { + variables := Variables{ + SQLCMDCOLSEP: defaultVariables[SQLCMDCOLSEP], + SQLCMDCOLWIDTH: defaultVariables[SQLCMDCOLWIDTH], + SQLCMDDBNAME: "", + SQLCMDEDITOR: defaultVariables[SQLCMDEDITOR], + SQLCMDERRORLEVEL: defaultVariables[SQLCMDERRORLEVEL], + SQLCMDHEADERS: defaultVariables[SQLCMDHEADERS], + SQLCMDINI: "", + SQLCMDLOGINTIMEOUT: defaultVariables[SQLCMDLOGINTIMEOUT], + SQLCMDMAXFIXEDTYPEWIDTH: defaultVariables[SQLCMDMAXFIXEDTYPEWIDTH], + SQLCMDMAXVARTYPEWIDTH: defaultVariables[SQLCMDMAXVARTYPEWIDTH], + SQLCMDPACKETSIZE: "4096", + SQLCMDSERVER: "", + SQLCMDSTATTIMEOUT: defaultVariables[SQLCMDSTATTIMEOUT], + SQLCMDUSER: "", + SQLCMDUSEAAD: "", + } + hostname, _ := os.Hostname() + variables.Set(SQLCMDWORKSTATION, hostname) + + if fromEnvironment { + for v := range variables.All() { + envVar, ok := os.LookupEnv(v) + if ok { + variables.Set(v, envVar) + } + } + } + return &variables +} + +// Setvar implements the :Setvar command +// TODO: Add validation functions for the variables. +func (variables *Variables) Setvar(name, value string) error { + err := ValidIdentifier(name) + if err == nil { + if err = variables.checkReadOnly(name); err != nil { + err = ReadOnlyVariable(name) + } + } + if err != nil { + return err + } + if value == "" { + if _, ok := variables.Get(name); !ok { + return UndefinedVariable(name) + } + if def, ok := defaultVariables.Get(name); ok { + value = def + } else { + variables.Unset(name) + return nil + } + } else { + value, err = ParseValue(value) + } + if err != nil { + return err + } + variables.Set(name, value) + return nil +} + +const validVariableRunes = "_-" + +// ValidIdentifier determines if a given string can be used as a variable name +func ValidIdentifier(name string) error { + + first := true + for _, c := range name { + if !unicode.IsLetter(c) && (first || (!unicode.IsDigit(c) && !strings.ContainsRune(validVariableRunes, c))) { + return fmt.Errorf("Invalid variable identifier %s", name) + } + first = false + } + return nil +} + +// ParseValue returns the string to use as the variable value +// If the string contains a space or a quote, it must be delimited by quotes and literal quotes +// within the value must be escaped by another quote +// "this has a quote "" in it" is valid +// "this has a quote" in it" is not valid +func ParseValue(val string) (string, error) { + quoted := val[0] == '"' + err := fmt.Errorf("Invalid variable value %s", val) + if !quoted { + if strings.ContainsAny(val, "\t\n\r ") { + return "", err + } + return val, nil + } + if len(val) == 1 || val[len(val)-1] != '"' { + return "", err + } + + b := new(strings.Builder) + quoted = false + r := []rune(val) +loop: + for i := 1; i < len(r)-1; i++ { + switch { + case quoted && r[i] == '"': + b.WriteRune('"') + quoted = false + case quoted && r[i] != '"': + break loop + case !quoted && r[i] == '"': + quoted = true + default: + b.WriteRune(r[i]) + } + } + if quoted { + return "", err + } + return b.String(), nil +} diff --git a/pkg/sqlcmd/variables_test.go b/pkg/sqlcmd/variables_test.go index 45b8ae5b..cf4de871 100644 --- a/pkg/sqlcmd/variables_test.go +++ b/pkg/sqlcmd/variables_test.go @@ -1,116 +1,116 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBasicVariableOperations(t *testing.T) { - variables := Variables{ - "var1": "val1", - } - variables.Set("var2", "val2") - assert.Contains(t, variables, "VAR2", "Set should add a capitalized key") - all := variables.All() - keys := make([]string, 0, len(all)) - for k := range all { - keys = append(keys, k) - } - assert.ElementsMatch(t, []string{"var1", "VAR2"}, keys, "All returns every key") - assert.Equal(t, "val2", all["VAR2"], "VAR2 set value") - -} - -func TestSetvarFailsForReadOnlyVariables(t *testing.T) { - variables := Variables{} - variables.Set("SQLCMDDBNAME", "somedatabase") - err := variables.Setvar("SQLCMDDBNAME", "newdatabase") - assert.Error(t, err, "setting a readonly variable fails") - assert.Equal(t, "somedatabase", variables.SQLCmdDatabase(), "readonly variable shouldn't be changed by Setvar") -} - -func TestEnvironmentVariablesAsInput(t *testing.T) { - os.Setenv("SQLCMDSERVER", "someserver") - defer os.Unsetenv("SQLCMDSERVER") - os.Setenv("x", "somevalue") - defer os.Unsetenv("x") - vars := InitializeVariables(true).All() - assert.Equal(t, "someserver", vars["SQLCMDSERVER"], "InitializeVariables should read a valid environment variable from the known list") - _, ok := vars["x"] - assert.False(t, ok, "InitializeVariables should skip variables not in the known list") -} - -func TestSqlServerSplitsName(t *testing.T) { - vars := Variables{ - SQLCMDSERVER: `tcp:someserver\someinstance`, - } - serverName, instance, port, err := vars.SQLCmdServer() - if assert.NoError(t, err, "tcp:server\\someinstance") { - assert.Equal(t, "someserver", serverName, "server name for instance") - assert.Equal(t, uint64(0), port, "port for instance") - assert.Equal(t, "someinstance", instance, "instance for instance") - } - vars = Variables{ - SQLCMDSERVER: `tcp:someserver,1111`, - } - serverName, instance, port, err = vars.SQLCmdServer() - if assert.NoError(t, err, "tcp:server,1111") { - assert.Equal(t, "someserver", serverName, "server name for port number") - assert.Equal(t, uint64(1111), port, "port for port number") - assert.Equal(t, "", instance, "instance for port number") - } -} - -func TestParseValue(t *testing.T) { - type test struct { - raw string - val string - valid bool - } - tests := []test{ - {`""`, "", true}, - {`"`, "", false}, - {`"""`, "", false}, - {`no quotes`, "", false}, - {`"is quoted"`, "is quoted", true}, - {`" " single quote "`, "", false}, - {`" "" escaped quotes "" "`, ` " escaped quotes " `, true}, - } - - for _, tst := range tests { - v, err := ParseValue(tst.raw) - if tst.valid { - if assert.NoErrorf(t, err, "Unexpected error for value %s", tst.raw) { - assert.Equalf(t, tst.val, v, "Incorrect parsed value for %s", tst.raw) - } - } else { - assert.Errorf(t, err, "Expected error for %s", tst.raw) - } - } -} - -func TestValidIdentifier(t *testing.T) { - type test struct { - raw string - valid bool - } - tests := []test{ - {"1A", false}, - {"A1", true}, - {"A+", false}, - {"A-_b", true}, - } - for _, tst := range tests { - err := ValidIdentifier(tst.raw) - if tst.valid { - assert.NoErrorf(t, err, "%s is valid", tst.raw) - } else { - assert.Errorf(t, err, "%s is invalid", tst.raw) - } - } -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBasicVariableOperations(t *testing.T) { + variables := Variables{ + "var1": "val1", + } + variables.Set("var2", "val2") + assert.Contains(t, variables, "VAR2", "Set should add a capitalized key") + all := variables.All() + keys := make([]string, 0, len(all)) + for k := range all { + keys = append(keys, k) + } + assert.ElementsMatch(t, []string{"var1", "VAR2"}, keys, "All returns every key") + assert.Equal(t, "val2", all["VAR2"], "VAR2 set value") + +} + +func TestSetvarFailsForReadOnlyVariables(t *testing.T) { + variables := Variables{} + variables.Set("SQLCMDDBNAME", "somedatabase") + err := variables.Setvar("SQLCMDDBNAME", "newdatabase") + assert.Error(t, err, "setting a readonly variable fails") + assert.Equal(t, "somedatabase", variables.SQLCmdDatabase(), "readonly variable shouldn't be changed by Setvar") +} + +func TestEnvironmentVariablesAsInput(t *testing.T) { + os.Setenv("SQLCMDSERVER", "someserver") + defer os.Unsetenv("SQLCMDSERVER") + os.Setenv("x", "somevalue") + defer os.Unsetenv("x") + vars := InitializeVariables(true).All() + assert.Equal(t, "someserver", vars["SQLCMDSERVER"], "InitializeVariables should read a valid environment variable from the known list") + _, ok := vars["x"] + assert.False(t, ok, "InitializeVariables should skip variables not in the known list") +} + +func TestSqlServerSplitsName(t *testing.T) { + vars := Variables{ + SQLCMDSERVER: `tcp:someserver\someinstance`, + } + serverName, instance, port, err := vars.SQLCmdServer() + if assert.NoError(t, err, "tcp:server\\someinstance") { + assert.Equal(t, "someserver", serverName, "server name for instance") + assert.Equal(t, uint64(0), port, "port for instance") + assert.Equal(t, "someinstance", instance, "instance for instance") + } + vars = Variables{ + SQLCMDSERVER: `tcp:someserver,1111`, + } + serverName, instance, port, err = vars.SQLCmdServer() + if assert.NoError(t, err, "tcp:server,1111") { + assert.Equal(t, "someserver", serverName, "server name for port number") + assert.Equal(t, uint64(1111), port, "port for port number") + assert.Equal(t, "", instance, "instance for port number") + } +} + +func TestParseValue(t *testing.T) { + type test struct { + raw string + val string + valid bool + } + tests := []test{ + {`""`, "", true}, + {`"`, "", false}, + {`"""`, "", false}, + {`no quotes`, "", false}, + {`"is quoted"`, "is quoted", true}, + {`" " single quote "`, "", false}, + {`" "" escaped quotes "" "`, ` " escaped quotes " `, true}, + } + + for _, tst := range tests { + v, err := ParseValue(tst.raw) + if tst.valid { + if assert.NoErrorf(t, err, "Unexpected error for value %s", tst.raw) { + assert.Equalf(t, tst.val, v, "Incorrect parsed value for %s", tst.raw) + } + } else { + assert.Errorf(t, err, "Expected error for %s", tst.raw) + } + } +} + +func TestValidIdentifier(t *testing.T) { + type test struct { + raw string + valid bool + } + tests := []test{ + {"1A", false}, + {"A1", true}, + {"A+", false}, + {"A-_b", true}, + } + for _, tst := range tests { + err := ValidIdentifier(tst.raw) + if tst.valid { + assert.NoErrorf(t, err, "%s is valid", tst.raw) + } else { + assert.Errorf(t, err, "%s is invalid", tst.raw) + } + } +} diff --git a/testdata/sql.txt b/testdata/sql.txt index b98b2681..7519b04f 100644 --- a/testdata/sql.txt +++ b/testdata/sql.txt @@ -1,3 +1,3 @@ -select 1 as col1 -go - +select 1 as col1 +go + From bbc1dea0a6cd72e48b7f70fa7fc87d3fe2178426 Mon Sep 17 00:00:00 2001 From: stuartpa Date: Mon, 21 Nov 2022 04:29:23 -0800 Subject: [PATCH 3/5] Make filename cross plat, and normalize EOLs --- cmd/sqlcmd/main_test.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/cmd/sqlcmd/main_test.go b/cmd/sqlcmd/main_test.go index a6aa2a38..6a42ecfe 100644 --- a/cmd/sqlcmd/main_test.go +++ b/cmd/sqlcmd/main_test.go @@ -4,6 +4,7 @@ package main import ( "os" + "path/filepath" "runtime" "strings" "testing" @@ -189,24 +190,24 @@ func TestUnicodeOutput(t *testing.T) { func TestUnicodeInput(t *testing.T) { testfiles := []string{ - `testdata/selectutf8.txt`, - `testdata/selectutf8_bom.txt`, - `testdata/selectunicode_BE.txt`, - `testdata/selectunicode_LE.txt`, + filepath.Join(`testdata`, `selectutf8.txt`), + filepath.Join(`testdata`, `selectutf8_bom.txt`), + filepath.Join(`testdata`, `selectunicode_BE.txt`), + filepath.Join(`testdata`, `selectunicode_LE.txt`), } for _, test := range testfiles { for _, unicodeOutput := range []bool{true, false} { var outfile string if unicodeOutput { - outfile = `testdata/unicodeout_linux.txt` + outfile = filepath.Join(`testdata`, `unicodeout_linux.txt`) if runtime.GOOS == "windows" { - outfile = `testdata/unicodeout.txt` + outfile = filepath.Join(`testdata`, `unicodeout.txt`) } } else { outfile = `testdata/utf8out_linux.txt` if runtime.GOOS == "windows" { - outfile = `testdata/utf8out.txt` + outfile = filepath.Join(`testdata`, `utf8out.txt`) } } o, err := os.CreateTemp("", "sqlcmdmain") @@ -226,10 +227,12 @@ func TestUnicodeInput(t *testing.T) { assert.NoError(t, err, "run") assert.Equal(t, 0, exitCode, "exitCode") bytes, err := os.ReadFile(o.Name()) + s := strings.ReplaceAll(string(bytes), sqlcmd.SqlcmdEol, "\n") // Normalize Eols for cross plat if assert.NoError(t, err, "os.ReadFile") { expectedBytes, err := os.ReadFile(outfile) + expectedS := strings.ReplaceAll(string(expectedBytes), sqlcmd.SqlcmdEol, "\n") // Normalize Eols for cross plat if assert.NoErrorf(t, err, "Unable to open %s", outfile) { - assert.Equalf(t, expectedBytes, bytes, "input file: <%s> output bytes should match <%s>", test, outfile) + assert.Equalf(t, expectedS, s, "input file: <%s> output bytes should match <%s>", test, outfile) } } } @@ -263,8 +266,8 @@ func TestQueryAndExit(t *testing.T) { } // Test to verify fix for issue: https://github.com/microsoft/go-sqlcmd/issues/98 -// 1. Verify when -b is passed in (ExitOnError), we don't always get an error (even when input is good) -// 2, Verify when the input is actually bad, we do get an error +// 1. Verify when -b is passed in (ExitOnError), we don't always get an error (even when input is good) +// 2, Verify when the input is actually bad, we do get an error func TestExitOnError(t *testing.T) { args = newArguments() args.InputFile = []string{"testdata/select100.sql"} @@ -320,7 +323,7 @@ func TestAzureAuth(t *testing.T) { func TestMissingInputFile(t *testing.T) { args = newArguments() - args.InputFile = []string{"testdata/missingFile.sql"} + args.InputFile = []string{filepath.Join("testdata", "missingFile.sql")} if canTestAzureAuth() { args.UseAad = true From fa66fe4729bf45c33602684973b115611428dcaa Mon Sep 17 00:00:00 2001 From: stuartpa Date: Mon, 21 Nov 2022 04:43:47 -0800 Subject: [PATCH 4/5] Merge conflicts --- pkg/sqlcmd/batch_test.go | 4 +-- pkg/sqlcmd/errors.go | 70 ++++++++++++++++++++++++++++++++++++--- pkg/sqlcmd/sqlcmd_test.go | 6 ++-- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/pkg/sqlcmd/batch_test.go b/pkg/sqlcmd/batch_test.go index a00c85f7..a323c1fb 100644 --- a/pkg/sqlcmd/batch_test.go +++ b/pkg/sqlcmd/batch_test.go @@ -90,7 +90,7 @@ func TestBatchNextErrOnInvalidVariable(t *testing.T) { cmd, _, err := b.Next() assert.Nil(t, cmd, "cmd for "+test) assert.Equal(t, uint(1), b.linecount, "linecount should increment on a variable syntax error") - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1.", "expected err for %s", test) + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 1", "expected err for %s", test) } } @@ -165,7 +165,7 @@ func TestReadStringMalformedVariable(t *testing.T) { r := []rune(test) _, ok, err := b.readString(r, 1, len(test), '\'', 10) assert.Falsef(t, ok, "ok for %s", test) - assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10.", "expected err for %s", test) + assert.EqualErrorf(t, err, "Sqlcmd: Error: Syntax error at line 10", "expected err for %s", test) } } diff --git a/pkg/sqlcmd/errors.go b/pkg/sqlcmd/errors.go index 391bf416..f6bc1c01 100644 --- a/pkg/sqlcmd/errors.go +++ b/pkg/sqlcmd/errors.go @@ -15,6 +15,26 @@ const ErrorPrefix = "Sqlcmd: Error: " // WarningPrefix is the prefix for all sqlcmd-generated warnings const WarningPrefix = "Sqlcmd: Warning: " +// Common Sqlcmd error messages +const ErrCmdDisabled = "ED and !! commands, startup script, and environment variables are disabled" + +type SqlcmdError interface { + error + IsSqlcmdErr() bool +} + +type CommonSqlcmdErr struct { + message string +} + +func (e *CommonSqlcmdErr) Error() string { + return e.message +} + +func (e *CommonSqlcmdErr) IsSqlcmdErr() bool { + return true +} + // ArgumentError is related to command line switch validation not handled by kong type ArgumentError struct { Parameter string @@ -25,6 +45,10 @@ func (e *ArgumentError) Error() string { return ErrorPrefix + e.Rule } +func (e *ArgumentError) IsSqlcmdErr() bool { + return true +} + // InvalidServerName indicates the SQLCMDSERVER variable has an incorrect format var InvalidServerName = ArgumentError{ Parameter: "server", @@ -41,6 +65,10 @@ func (e *VariableError) Error() string { return ErrorPrefix + fmt.Sprintf(e.MessageFormat, e.Variable) } +func (e *VariableError) IsSqlcmdErr() bool { + return true +} + // ReadOnlyVariable indicates the user tried to set a value to a read-only variable func ReadOnlyVariable(variable string) *VariableError { return &VariableError{ @@ -75,6 +103,10 @@ func (e *CommandError) Error() string { return ErrorPrefix + fmt.Sprintf("Syntax error at line %d near command '%s'.", e.LineNumber, e.Command) } +func (e *CommandError) IsSqlcmdErr() bool { + return true +} + // InvalidCommandError creates a SQLCmdCommandError func InvalidCommandError(command string, lineNumber uint) *CommandError { return &CommandError{ @@ -83,12 +115,42 @@ func InvalidCommandError(command string, lineNumber uint) *CommandError { } } +type FileError struct { + err error + path string +} + +func (e *FileError) Error() string { + return e.err.Error() +} + +func (e *FileError) IsSqlcmdErr() bool { + return true +} + // InvalidFileError indicates a file could not be opened -func InvalidFileError(err error, path string) error { - return errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + path + " (Reason: " + err.Error() + ").") +func InvalidFileError(err error, filepath string) error { + return &FileError{ + err: errors.New(ErrorPrefix + " Error occurred while opening or operating on file " + filepath + " (Reason: " + err.Error() + ")."), + path: filepath, + } +} + +type SyntaxError struct { + err error +} + +func (e *SyntaxError) Error() string { + return e.err.Error() +} + +func (e *SyntaxError) IsSqlcmdErr() bool { + return true } // SyntaxError indicates a malformed sqlcmd statement -func syntaxError(lineNumber uint) error { - return fmt.Errorf("%sSyntax error at line %d.", ErrorPrefix, lineNumber) +func syntaxError(lineNumber uint) SqlcmdError { + return &SyntaxError{ + err: fmt.Errorf("%sSyntax error at line %d", ErrorPrefix, lineNumber), + } } diff --git a/pkg/sqlcmd/sqlcmd_test.go b/pkg/sqlcmd/sqlcmd_test.go index 46cfa746..81b66633 100644 --- a/pkg/sqlcmd/sqlcmd_test.go +++ b/pkg/sqlcmd/sqlcmd_test.go @@ -104,7 +104,7 @@ func TestSqlCmdQueryAndExit(t *testing.T) { s.SetOutput(nil) bytes, err := os.ReadFile(file.Name()) if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Incorrect output from Run") + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1"+SqlcmdEol, string(bytes), "Incorrect output from Run") } } } @@ -471,7 +471,7 @@ func TestSqlCmdOutputAndError(t *testing.T) { if assert.NoError(t, err, "s.Run(once = true)") { bytes, err := os.ReadFile(errfile.Name()) if assert.NoError(t, err, "os.ReadFile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1."+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 1"+SqlcmdEol, string(bytes), "Expected syntax error not received for query execution") } } s.Query = "select '1'" @@ -495,7 +495,7 @@ func TestSqlCmdOutputAndError(t *testing.T) { } bytes, err = os.ReadFile(errfile.Name()) if assert.NoError(t, err, "os.ReadFile errfile") { - assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3."+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") + assert.Equal(t, "Sqlcmd: Error: Syntax error at line 3"+SqlcmdEol, string(bytes), "Expected syntax error not found in errfile") } } } From 4276145b82ae1e9e6e23d85d13298a357cc96a85 Mon Sep 17 00:00:00 2001 From: stuartpa Date: Mon, 21 Nov 2022 04:44:21 -0800 Subject: [PATCH 5/5] Merge conflicts --- .github/workflows/codeql.yml | 158 ++--- .github/workflows/pr-validation.yml | 54 +- .gitignore | 48 +- .vscode/launch.json | 76 +- .vscode/tasks.json | 12 +- CODE_OF_CONDUCT.md | 18 +- Dockerfile | 56 +- LICENSE | 42 +- README.md | 316 ++++----- SECURITY.md | 80 +-- build/azure-pipelines/build-tag.yml | 22 +- .../azure-pipelines/package-common-create.yml | 276 ++++---- build/azure-pipelines/package-common-test.yml | 70 +- build/azure-pipelines/package-product.yml | 88 +-- cmd/sqlcmd/main.go | 668 +++++++++--------- cmd/sqlcmd/testdata/create100db.sql | 60 +- cmd/sqlcmd/testdata/drop100db.txt | 60 +- cmd/sqlcmd/testdata/select,100.sql | 2 +- cmd/sqlcmd/testdata/selectutf8.txt | 4 +- cmd/sqlcmd/testdata/selectutf8_bom.txt | 4 +- cmd/sqlcmd/testdata/utf8out.txt | 10 +- cmd/sqlcmd/testdata/utf8out_linux.txt | 10 +- cmd/sqlcmd/winres/winres.json | 64 +- go.mod | 62 +- go.sum | 198 +++--- pkg/sqlcmd/connect.go | 262 +++---- pkg/sqlcmd/exec_darwin.go | 36 +- pkg/sqlcmd/exec_linux.go | 36 +- pkg/sqlcmd/exec_windows.go | 54 +- pkg/sqlcmd/testdata/selectdates.sql | 18 +- pkg/sqlcmd/testdata/testerrorredirection.sql | 6 +- pkg/sqlcmd/testdata/variablesnogo.sql | 10 +- release/linux/deb/README.md | 168 ++--- release/linux/deb/build-pkg.sh | 114 +-- release/linux/deb/pipeline-test.sh | 128 ++-- release/linux/deb/pipeline.sh | 144 ++-- release/linux/deb/prepare-rules.sh | 250 +++---- release/linux/docker/README.md | 34 +- release/linux/docker/pipeline-test.sh | 76 +- release/linux/docker/pipeline.sh | 122 ++-- release/linux/rpm/README.md | 34 +- release/linux/rpm/build-rpm.sh | 54 +- release/linux/rpm/pipeline-test.sh | 212 +++--- release/linux/rpm/pipeline.sh | 114 +-- release/linux/rpm/sqlcmd.spec | 74 +- release/windows/choco/sqlcmd.nuspec | 52 +- release/windows/choco/tools/LICENSE.txt | 50 +- release/windows/choco/tools/VERIFICATION.txt | 18 +- .../windows/choco/tools/chocolateyinstall.ps1 | 90 +-- release/windows/msi/README.md | 82 +-- release/windows/msi/eula/eulatext_1033.rtf | 546 +++++++------- release/windows/msi/product.wxs | 148 ++-- release/windows/msi/scripts/pipeline-test.ps1 | 90 +-- release/windows/msi/scripts/pipeline.cmd | 152 ++-- release/windows/msi/sqlcmd.sln | 62 +- release/windows/msi/sqlcmd.wixproj | 94 +-- 56 files changed, 2894 insertions(+), 2894 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a411a1cb..f719b3fe 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,79 +1,79 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - paths-ignore: - - '**/*.md' - - '**/*.txt' - - '**/*.sql' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Setup go - uses: actions/setup-go@v2 - with: - go-version: '1.18' - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - - # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs - # queries: security-extended,security-and-quality - - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. - - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + paths-ignore: + - '**/*.md' + - '**/*.txt' + - '**/*.sql' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: '1.18' + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 6878f945..d6812940 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -1,27 +1,27 @@ -name: pr-validation - -on: - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Setup go - uses: actions/setup-go@v2 - with: - go-version: '1.18' - - name: Run tests against Linux SQL - run: | - go version - cd cmd/sqlcmd - go get -d - go build . - export SQLCMDPASSWORD=$(date +%s|sha256sum|base64|head -c 32) - export SQLCMDUSER=sa - docker run -m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$SQLCMDPASSWORD mcr.microsoft.com/mssql/server:2017-latest - cd ../.. - go test -v ./... +name: pr-validation + +on: + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup go + uses: actions/setup-go@v2 + with: + go-version: '1.18' + - name: Run tests against Linux SQL + run: | + go version + cd cmd/sqlcmd + go get -d + go build . + export SQLCMDPASSWORD=$(date +%s|sha256sum|base64|head -c 32) + export SQLCMDUSER=sa + docker run -m 2GB -e ACCEPT_EULA=1 -d --name sql2017 -p:1433:1433 -e SA_PASSWORD=$SQLCMDPASSWORD mcr.microsoft.com/mssql/server:2017-latest + cd ../.. + go test -v ./... diff --git a/.gitignore b/.gitignore index cbee714f..01374202 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,24 @@ -# Binaries for programs and plugins -output -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, built with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ - -coverage.json -coverage.txt -coverage.xml -testresults.xml - -# .syso is generated by go-winres. Only needed for official builds -*.syso +# Binaries for programs and plugins +output +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +coverage.json +coverage.txt +coverage.xml +testresults.xml + +# .syso is generated by go-winres. Only needed for official builds +*.syso diff --git a/.vscode/launch.json b/.vscode/launch.json index 2e634553..3d13d998 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,39 +1,39 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - - - { - - "name": "Attach to Process", - - "type": "go", - - "request": "attach", - - "mode": "local", - - "processId": 0 - - }, - { - "name" : "Run query and exit", - "type" : "go", - "request": "launch", - "mode" : "auto", - "program": "${workspaceFolder}/cmd/sqlcmd", - "args" : ["-Q", "EXIT(select 100 as Count)"], - }, - { - "name" : "Run file query", - "type" : "go", - "request": "launch", - "mode" : "auto", - "program": "${workspaceFolder}/cmd/sqlcmd", - "args" : ["-i", "testdata\\select100.sql"], - }, - ] +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + + { + + "name": "Attach to Process", + + "type": "go", + + "request": "attach", + + "mode": "local", + + "processId": 0 + + }, + { + "name" : "Run query and exit", + "type" : "go", + "request": "launch", + "mode" : "auto", + "program": "${workspaceFolder}/cmd/sqlcmd", + "args" : ["-Q", "EXIT(select 100 as Count)"], + }, + { + "name" : "Run file query", + "type" : "go", + "request": "launch", + "mode" : "auto", + "program": "${workspaceFolder}/cmd/sqlcmd", + "args" : ["-i", "testdata\\select100.sql"], + }, + ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6c16501f..a6ac821c 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,7 +1,7 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - ] +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + ] } \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c72a5749..f9ba8cf6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,9 +1,9 @@ -# Microsoft Open Source Code of Conduct - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). - -Resources: - -- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) -- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/Dockerfile b/Dockerfile index 2faa30d5..57344106 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,28 @@ -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Example: -# docker run --rm microsoft/sqlcmd sqlcmd --help -# - -FROM scratch -ARG BUILD_DATE -ARG PACKAGE_VERSION - -LABEL maintainer="Microsoft" \ - org.label-schema.schema-version="1.0" \ - org.label-schema.vendor="Microsoft" \ - org.label-schema.name="SQLCMD CLI" \ - org.label-schema.version=$PACKAGE_VERSION \ - org.label-schema.license="https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE" \ - org.label-schema.description="The MSSQL SQLCMD CLI tool" \ - org.label-schema.url="https://github.com/microsoft/go-sqlcmd" \ - org.label-schema.usage="https://docs.microsoft.com/sql/tools/sqlcmd-utility" \ - org.label-schema.build-date=$BUILD_DATE \ - org.label-schema.docker.cmd="docker run -it microsoft/sqlcmd:$PACKAGE_VERSION" - -COPY ./sqlcmd /usr/bin/sqlcmd - -CMD ["sqlcmd"] +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Example: +# docker run --rm microsoft/sqlcmd sqlcmd --help +# + +FROM scratch +ARG BUILD_DATE +ARG PACKAGE_VERSION + +LABEL maintainer="Microsoft" \ + org.label-schema.schema-version="1.0" \ + org.label-schema.vendor="Microsoft" \ + org.label-schema.name="SQLCMD CLI" \ + org.label-schema.version=$PACKAGE_VERSION \ + org.label-schema.license="https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE" \ + org.label-schema.description="The MSSQL SQLCMD CLI tool" \ + org.label-schema.url="https://github.com/microsoft/go-sqlcmd" \ + org.label-schema.usage="https://docs.microsoft.com/sql/tools/sqlcmd-utility" \ + org.label-schema.build-date=$BUILD_DATE \ + org.label-schema.docker.cmd="docker run -it microsoft/sqlcmd:$PACKAGE_VERSION" + +COPY ./sqlcmd /usr/bin/sqlcmd + +CMD ["sqlcmd"] diff --git a/LICENSE b/LICENSE index 3d8b93bc..9e841e7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ - MIT License - - Copyright (c) Microsoft Corporation. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/README.md b/README.md index afdb0857..73ff4325 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,158 @@ -# Go-based SQL Utilities - Preview - -This repo contains command line tools and go packages for working with Microsoft SQL Server, Azure SQL Database, and Azure Synapse. - -## Sqlcmd - -The `sqlcmd` project aims to be a complete port of the native sqlcmd to the `go` language, utilizing the [go-mssqldb](https://github.com/microsoft/go-mssqldb) driver. For full documentation of the tool and installation instructions, see https://docs.microsoft.com/sql/tools/go-sqlcmd-utility - -### Breaking changes - -We will be implementing command line switches and behaviors over time. Several switches and behaviors are expected to change in this implementation. - -- `-P` switch will be removed. Passwords for SQL authentication can only be provided through these mechanisms: - - - The `SQLCMDPASSWORD` environment variable - - The `:CONNECT` command - - When prompted, the user can type the password to complete a connection -- `-r` requires a 0 or 1 argument -- `-R` switch will be removed. The go runtime does not provide access to user locale information, and it's not readily available through syscall on all supported platforms. -- `-I` switch will be removed. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts. -- `-N` now takes a string value that can be one of `true`, `false`, or `disable` to specify the encryption choice. (`default` is the same as omitting the parameter) - - If `-N` and `-C` are not provided, sqlcmd will negotiate authentication with the server without validating the server certificate. - - If `-N` is provided but `-C` is not, sqlcmd will require validation of the server certificate. Note that a `false` value for encryption could still lead to encryption of the login packet. - - If both `-N` and `-C` are provided, sqlcmd will use their values for encryption negotiation. - - More information about client/server encryption negotiation can be found at -- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it. -- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types. -- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The ODBC sqlcmd allows the query run by `EXIT(query)` to span multiple lines. -- `-i` now requires multiple arguments for the switch to be separated by `,`. - -### Miscellaneous enhancements - -- `:Connect` now has an optional `-G` parameter to select one of the authentication methods for Azure SQL Database - `SqlAuthentication`, `ActiveDirectoryDefault`, `ActiveDirectoryIntegrated`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, `ActiveDirectoryPassword`. If `-G` is not provided, either Integrated security or SQL Authentication will be used, dependent on the presence of a `-U` user name parameter. -- The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces. -- Sqlcmd can now print results using a vertical format. Use the new `-F vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable. - -### Azure Active Directory Authentication - -This version of sqlcmd supports a broader range of AAD authentication models, based on the [azidentity package](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity). The implementation relies on an AAD Connector in the [driver](https://github.com/microsoft/go-mssqldb). - -#### Command line - -To use AAD auth, you can use one of two command line switches - -`-G` is (mostly) compatible with its usage in the prior version of sqlcmd. If a user name and password are provided, it will authenticate using AAD Password authentication. If a user name is provided it will use AAD Interactive authentication which may display a web browser. If no user name or password is provided, it will use a DefaultAzureCredential which attempts to authenticate through a variety of mechanisms. - -`--authentication-method=` can be used to specify one of the following authentication types. - -`ActiveDirectoryDefault` - -- For an overview of the types of authentication this mode will use, see (). -- Choose this method if your database automation scripts are intended to run in both local development environments and in a production deployment in Azure. You'll be able to use a client secret or an Azure CLI login on your development environment and a managed identity or client secret on your production deployment without changing the script. -- Setting environment variables AZURE_TENANT_ID, and AZURE_CLIENT_ID are necessary for DefaultAzureCredential to begin checking the environment configuration and look for one of the following additional environment variables in order to authenticate: - - - Setting environment variable AZURE_CLIENT_SECRET configures the DefaultAzureCredential to choose ClientSecretCredential. - - Setting environment variable AZURE_CLIENT_CERTIFICATE_PATH configures the DefaultAzureCredential to choose ClientCertificateCredential if AZURE_CLIENT_SECRET is not set. - - Setting environment variable AZURE_USERNAME configures the DefaultAzureCredential to choose UsernamePasswordCredential if AZURE_CLIENT_SECRET and AZURE_CLIENT_CERTIFICATE_PATH are not set. - -`ActiveDirectoryIntegrated` - -This method is currently not implemented and will fall back to `ActiveDirectoryDefault` - -`ActiveDirectoryPassword` - -This method will authenticate using a user name and password. It will not work if MFA is required. -You provide the user name and password using the usual command line switches or SQLCMD environment variables. -Set `AZURE_TENANT_ID` environment variable to the tenant id of the server if not using the default tenant of the user. - -`ActiveDirectoryInteractive` - -This method will launch a web browser to authenticate the user. - -`ActiveDirectoryManagedIdentity` - -Use this method when running sqlcmd on an Azure VM that has either a system-assigned or user-assigned managed identity. If using a user-assigned managed identity, set the user name to the ID of the managed identity. If using a system-assigned identity, leave user name empty. - -`ActiveDirectoryServicePrincipal` - -This method authenticates the provided user name as a service principal id and the password as the client secret for the service principal. Provide a user name in the form `@`. Set `SQLCMDPASSWORD` variable to the client secret. If using a certificate instead of a client secret, set `AZURE_CLIENT_CERTIFICATE_PATH` environment variable to the path of the certificate file. - -### Environment variables for AAD auth - -Some settings for AAD auth do not have command line inputs, and some environment variables are consumed directly by the `azidentity` package used by `sqlcmd`. -These environment variables can be set to configure some aspects of AAD auth and to bypass default behaviors. In addition to the variables listed above, the following are sqlcmd-specific and apply to multiple methods. - -`SQLCMDCLIENTID` - set this to the identifier of an application registered in your AAD which is authorized to authenticate to Azure SQL Database. Applies to `ActiveDirectoryInteractive` and `ActiveDirectoryPassword` methods. - -### Packages - -#### sqlcmd executable - -Build [sqlcmd](cmd/sqlcmd) - -```sh - -go build ./cmd/sqlcmd - -``` - -#### sqlcmd package - -pkg/sqlcmd is consumable by other hosts. Go docs for the package are forthcoming. See the test code and [main.go](cmd/sqlcmd/main.go) for examples of initializing and running sqlcmd. - -## Building - -To add version data to your build using `go-winres`, add `GOBIN` to your `PATH` then use `go generate` -The version on the binary will match the version tag of the branch. - -```sh - -go install github.com/tc-hib/go-winres@latest -cd cmd/sqlcmd -go generate - -``` - -Scripts to build the binaries and package them for release will be added in a build folder off the root. We will also add Azure Devops pipeline yml files there to initiate builds and releases. Until then just use `go build ./cmd/sqlcmd` to create a sqlcmd binary. - -## Testing - -The tests rely on SQLCMD scripting variables to provide the connection string parameters. Set SQLCMDSERVER, SQLCMDDATABASE, SQLCMDUSER, SQLCMDPASSWORD variables appropriately then - -```sh - -go test ./... - -``` - -If you are developing on Windows, you can use docker or WSL to run the tests on Linux. `docker run` lets you pass the environment variables. For example, if your code is in `i:\git\go-sqlcmd` you can run tests in a docker container: - -```cmd - -docker run -rm -e SQLCMDSERVER= -e SQLCMDUSER= -e SQLCMDPASSWORD= -v i:\git\go-sqlcmd:/go-sqlcmd -w /go-sqlcmd golang:1.16 go test ./... - -``` - -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Trademarks - -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. - +# Go-based SQL Utilities - Preview + +This repo contains command line tools and go packages for working with Microsoft SQL Server, Azure SQL Database, and Azure Synapse. + +## Sqlcmd + +The `sqlcmd` project aims to be a complete port of the native sqlcmd to the `go` language, utilizing the [go-mssqldb](https://github.com/microsoft/go-mssqldb) driver. For full documentation of the tool and installation instructions, see https://docs.microsoft.com/sql/tools/go-sqlcmd-utility + +### Breaking changes + +We will be implementing command line switches and behaviors over time. Several switches and behaviors are expected to change in this implementation. + +- `-P` switch will be removed. Passwords for SQL authentication can only be provided through these mechanisms: + + - The `SQLCMDPASSWORD` environment variable + - The `:CONNECT` command + - When prompted, the user can type the password to complete a connection +- `-r` requires a 0 or 1 argument +- `-R` switch will be removed. The go runtime does not provide access to user locale information, and it's not readily available through syscall on all supported platforms. +- `-I` switch will be removed. To disable quoted identifier behavior, add `SET QUOTED IDENTIFIER OFF` in your scripts. +- `-N` now takes a string value that can be one of `true`, `false`, or `disable` to specify the encryption choice. (`default` is the same as omitting the parameter) + - If `-N` and `-C` are not provided, sqlcmd will negotiate authentication with the server without validating the server certificate. + - If `-N` is provided but `-C` is not, sqlcmd will require validation of the server certificate. Note that a `false` value for encryption could still lead to encryption of the login packet. + - If both `-N` and `-C` are provided, sqlcmd will use their values for encryption negotiation. + - More information about client/server encryption negotiation can be found at +- `-u` The generated Unicode output file will have the UTF16 Little-Endian Byte-order mark (BOM) written to it. +- Some behaviors that were kept to maintain compatibility with `OSQL` may be changed, such as alignment of column headers for some data types. +- All commands must fit on one line, even `EXIT`. Interactive mode will not check for open parentheses or quotes for commands and prompt for successive lines. The ODBC sqlcmd allows the query run by `EXIT(query)` to span multiple lines. +- `-i` now requires multiple arguments for the switch to be separated by `,`. + +### Miscellaneous enhancements + +- `:Connect` now has an optional `-G` parameter to select one of the authentication methods for Azure SQL Database - `SqlAuthentication`, `ActiveDirectoryDefault`, `ActiveDirectoryIntegrated`, `ActiveDirectoryServicePrincipal`, `ActiveDirectoryManagedIdentity`, `ActiveDirectoryPassword`. If `-G` is not provided, either Integrated security or SQL Authentication will be used, dependent on the presence of a `-U` user name parameter. +- The new `--driver-logging-level` command line parameter allows you to see traces from the `go-mssqldb` client driver. Use `64` to see all traces. +- Sqlcmd can now print results using a vertical format. Use the new `-F vertical` command line option to set it. It's also controlled by the `SQLCMDFORMAT` scripting variable. + +### Azure Active Directory Authentication + +This version of sqlcmd supports a broader range of AAD authentication models, based on the [azidentity package](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity). The implementation relies on an AAD Connector in the [driver](https://github.com/microsoft/go-mssqldb). + +#### Command line + +To use AAD auth, you can use one of two command line switches + +`-G` is (mostly) compatible with its usage in the prior version of sqlcmd. If a user name and password are provided, it will authenticate using AAD Password authentication. If a user name is provided it will use AAD Interactive authentication which may display a web browser. If no user name or password is provided, it will use a DefaultAzureCredential which attempts to authenticate through a variety of mechanisms. + +`--authentication-method=` can be used to specify one of the following authentication types. + +`ActiveDirectoryDefault` + +- For an overview of the types of authentication this mode will use, see (). +- Choose this method if your database automation scripts are intended to run in both local development environments and in a production deployment in Azure. You'll be able to use a client secret or an Azure CLI login on your development environment and a managed identity or client secret on your production deployment without changing the script. +- Setting environment variables AZURE_TENANT_ID, and AZURE_CLIENT_ID are necessary for DefaultAzureCredential to begin checking the environment configuration and look for one of the following additional environment variables in order to authenticate: + + - Setting environment variable AZURE_CLIENT_SECRET configures the DefaultAzureCredential to choose ClientSecretCredential. + - Setting environment variable AZURE_CLIENT_CERTIFICATE_PATH configures the DefaultAzureCredential to choose ClientCertificateCredential if AZURE_CLIENT_SECRET is not set. + - Setting environment variable AZURE_USERNAME configures the DefaultAzureCredential to choose UsernamePasswordCredential if AZURE_CLIENT_SECRET and AZURE_CLIENT_CERTIFICATE_PATH are not set. + +`ActiveDirectoryIntegrated` + +This method is currently not implemented and will fall back to `ActiveDirectoryDefault` + +`ActiveDirectoryPassword` + +This method will authenticate using a user name and password. It will not work if MFA is required. +You provide the user name and password using the usual command line switches or SQLCMD environment variables. +Set `AZURE_TENANT_ID` environment variable to the tenant id of the server if not using the default tenant of the user. + +`ActiveDirectoryInteractive` + +This method will launch a web browser to authenticate the user. + +`ActiveDirectoryManagedIdentity` + +Use this method when running sqlcmd on an Azure VM that has either a system-assigned or user-assigned managed identity. If using a user-assigned managed identity, set the user name to the ID of the managed identity. If using a system-assigned identity, leave user name empty. + +`ActiveDirectoryServicePrincipal` + +This method authenticates the provided user name as a service principal id and the password as the client secret for the service principal. Provide a user name in the form `@`. Set `SQLCMDPASSWORD` variable to the client secret. If using a certificate instead of a client secret, set `AZURE_CLIENT_CERTIFICATE_PATH` environment variable to the path of the certificate file. + +### Environment variables for AAD auth + +Some settings for AAD auth do not have command line inputs, and some environment variables are consumed directly by the `azidentity` package used by `sqlcmd`. +These environment variables can be set to configure some aspects of AAD auth and to bypass default behaviors. In addition to the variables listed above, the following are sqlcmd-specific and apply to multiple methods. + +`SQLCMDCLIENTID` - set this to the identifier of an application registered in your AAD which is authorized to authenticate to Azure SQL Database. Applies to `ActiveDirectoryInteractive` and `ActiveDirectoryPassword` methods. + +### Packages + +#### sqlcmd executable + +Build [sqlcmd](cmd/sqlcmd) + +```sh + +go build ./cmd/sqlcmd + +``` + +#### sqlcmd package + +pkg/sqlcmd is consumable by other hosts. Go docs for the package are forthcoming. See the test code and [main.go](cmd/sqlcmd/main.go) for examples of initializing and running sqlcmd. + +## Building + +To add version data to your build using `go-winres`, add `GOBIN` to your `PATH` then use `go generate` +The version on the binary will match the version tag of the branch. + +```sh + +go install github.com/tc-hib/go-winres@latest +cd cmd/sqlcmd +go generate + +``` + +Scripts to build the binaries and package them for release will be added in a build folder off the root. We will also add Azure Devops pipeline yml files there to initiate builds and releases. Until then just use `go build ./cmd/sqlcmd` to create a sqlcmd binary. + +## Testing + +The tests rely on SQLCMD scripting variables to provide the connection string parameters. Set SQLCMDSERVER, SQLCMDDATABASE, SQLCMDUSER, SQLCMDPASSWORD variables appropriately then + +```sh + +go test ./... + +``` + +If you are developing on Windows, you can use docker or WSL to run the tests on Linux. `docker run` lets you pass the environment variables. For example, if your code is in `i:\git\go-sqlcmd` you can run tests in a docker container: + +```cmd + +docker run -rm -e SQLCMDSERVER= -e SQLCMDUSER= -e SQLCMDPASSWORD= -v i:\git\go-sqlcmd:/go-sqlcmd -w /go-sqlcmd golang:1.16 go test ./... + +``` + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. + diff --git a/SECURITY.md b/SECURITY.md index 12fbd833..f7b89984 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,41 +1,41 @@ - - -## Security - -Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). - -If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. - -## Reporting Security Issues - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). - -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). - -Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: - - * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) - * Full paths of source file(s) related to the manifestation of the issue - * The location of the affected source code (tag/branch/commit or direct URL) - * Any special configuration required to reproduce the issue - * Step-by-step instructions to reproduce the issue - * Proof-of-concept or exploit code (if possible) - * Impact of the issue, including how an attacker might exploit the issue - -This information will help us triage your report more quickly. - -If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. - -## Preferred Languages - -We prefer all communications to be in English. - -## Policy - -Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). - + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + \ No newline at end of file diff --git a/build/azure-pipelines/build-tag.yml b/build/azure-pipelines/build-tag.yml index 4526bd5a..38796860 100644 --- a/build/azure-pipelines/build-tag.yml +++ b/build/azure-pipelines/build-tag.yml @@ -1,11 +1,11 @@ - -steps: -- task: PowerShell@2 - displayName: Set last tag to variable - name: getVersion - inputs: - targetType: 'inline' - script: | - $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) - Write-Host("##vso[task.setvariable variable=VERSION_TAG;isoutput=true]$VERSION_TAG") - Write-Host($VERSION_TAG) + +steps: +- task: PowerShell@2 + displayName: Set last tag to variable + name: getVersion + inputs: + targetType: 'inline' + script: | + $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) + Write-Host("##vso[task.setvariable variable=VERSION_TAG;isoutput=true]$VERSION_TAG") + Write-Host($VERSION_TAG) diff --git a/build/azure-pipelines/package-common-create.yml b/build/azure-pipelines/package-common-create.yml index ec359bc2..070ed65b 100644 --- a/build/azure-pipelines/package-common-create.yml +++ b/build/azure-pipelines/package-common-create.yml @@ -1,138 +1,138 @@ -parameters: -- name: OS - type: string - default: -- name: Type - type: string - -steps: -- task: DownloadPipelineArtifact@2 - inputs: - source: 'specific' - project: 'ae14e11c-7eb2-46af-b588-471e6116d635' - pipeline: 500 - runVersion: 'latest' - targetPath: '$(Pipeline.Workspace)' -- task: PowerShell@2 - displayName: Set last tag to variable - inputs: - targetType: 'inline' - script: | - $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) - $VERSION_TAG = $VERSION_TAG.substring(1) # Trim initial 'v' - Write-Host("##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG") - Write-Host($VERSION_TAG) -- task: CmdLine@2 - condition: ne(variables.OS, 'windows') - displayName: 'Build ${{ parameters.OS }}/${{ parameters.Type }} distribution' - inputs: - script: release/${{ parameters.OS }}/${{ parameters.Type }}/pipeline.sh - workingDirectory: $(Build.SourcesCliDirectory) - env: - CLI_VERSION: $(VERSION_TAG) - BUILD_OUTPUT: $(Pipeline.Workspace) - BUILD_STAGINGDIRECTORY: $(Build.ArtifactStagingDirectory) -- task: VSBuild@1 - condition: eq(variables.OS, 'windows') - displayName: 'Build Windows MSI distribution' - inputs: - solution: '$(Build.SourcesCliDirectory)\release\windows\msi\sqlcmd.wixproj' - msbuildArchitecture: 'x86' - msbuildArgs: '/p:Configuration=Release' - env: - CLI_VERSION: $(VERSION_TAG) -- task: EsrpCodeSigning@1 - condition: eq(variables.OS, 'windows') - inputs: - ConnectedServiceName: 'Code Signing' - FolderPath: '$(Build.SourcesDirectory)/release/windows/msi/bin/Release' - Pattern: '*.msi' - signConfigType: 'inlineSignParams' - inlineOperation: | - [ - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolSign", - "parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "Sqlcmd Tools" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "https://github.com/microsoft/go-sqlcmd" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd sha256" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - }, - { - "keyCode": "CP-230012", - "operationSetCode": "SigntoolVerify", - "parameters": [ - { - "parameterName": "VerifyAll", - "parameterValue": "/all" - } - ], - "toolName": "signtool.exe", - "toolVersion": "6.2.9304.0" - } - ] - SessionTimeout: '600' - MaxConcurrency: '50' - MaxRetryAttempts: '20' -- task: Bash@3 - condition: eq(variables.OS, 'windows') - displayName: 'Rename MSI' - inputs: - targetType: 'inline' - script: | - mv "$(Build.SourcesDirectory)/release/windows/msi/bin/Release/sqlcmd.msi" "$(Build.SourcesDirectory)/release/windows/msi/bin/Release/sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION}.msi" - env: - CLI_VERSION: $(VERSION_TAG) - CLI_VERSION_REVISION: 1 -- task: CopyFiles@2 - condition: eq(variables.OS, 'windows') - displayName: 'Copy MSI to: $(Build.ArtifactStagingDirectory)' - inputs: - sourceFolder: $(Build.SourcesDirectory)/release/windows/msi/bin/Release - contents: '?(*.msi|*.md)' - TargetFolder: $(Build.ArtifactStagingDirectory) -- task: EsrpCodeSigning@1 - condition: and(eq(variables.OS, 'linux'), ne(variables.Type, 'docker')) - inputs: - ConnectedServiceName: 'Code Signing' - FolderPath: $(Build.ArtifactStagingDirectory) - Pattern: '*.${{ parameters.Type }}' - signConfigType: 'inlineSignParams' - inlineOperation: | - [ - { - "KeyCode" : "CP-450779-Pgp", - "OperationCode" : "LinuxSign", - "Parameters" : {}, - "ToolName" : "sign", - "ToolVersion" : "1.0" - } - ] - SessionTimeout: '600' - MaxConcurrency: '50' - MaxRetryAttempts: '20' -- task: PublishPipelineArtifact@0 - displayName: 'Publish Artifact: ${{ parameters.Type }}' - inputs: - targetPath: $(Build.ArtifactStagingDirectory) - artifactName: ${{ parameters.Type }} +parameters: +- name: OS + type: string + default: +- name: Type + type: string + +steps: +- task: DownloadPipelineArtifact@2 + inputs: + source: 'specific' + project: 'ae14e11c-7eb2-46af-b588-471e6116d635' + pipeline: 500 + runVersion: 'latest' + targetPath: '$(Pipeline.Workspace)' +- task: PowerShell@2 + displayName: Set last tag to variable + inputs: + targetType: 'inline' + script: | + $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) + $VERSION_TAG = $VERSION_TAG.substring(1) # Trim initial 'v' + Write-Host("##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG") + Write-Host($VERSION_TAG) +- task: CmdLine@2 + condition: ne(variables.OS, 'windows') + displayName: 'Build ${{ parameters.OS }}/${{ parameters.Type }} distribution' + inputs: + script: release/${{ parameters.OS }}/${{ parameters.Type }}/pipeline.sh + workingDirectory: $(Build.SourcesCliDirectory) + env: + CLI_VERSION: $(VERSION_TAG) + BUILD_OUTPUT: $(Pipeline.Workspace) + BUILD_STAGINGDIRECTORY: $(Build.ArtifactStagingDirectory) +- task: VSBuild@1 + condition: eq(variables.OS, 'windows') + displayName: 'Build Windows MSI distribution' + inputs: + solution: '$(Build.SourcesCliDirectory)\release\windows\msi\sqlcmd.wixproj' + msbuildArchitecture: 'x86' + msbuildArgs: '/p:Configuration=Release' + env: + CLI_VERSION: $(VERSION_TAG) +- task: EsrpCodeSigning@1 + condition: eq(variables.OS, 'windows') + inputs: + ConnectedServiceName: 'Code Signing' + FolderPath: '$(Build.SourcesDirectory)/release/windows/msi/bin/Release' + Pattern: '*.msi' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "Sqlcmd Tools" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://github.com/microsoft/go-sqlcmd" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd sha256" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "signtool.exe", + "toolVersion": "6.2.9304.0" + } + ] + SessionTimeout: '600' + MaxConcurrency: '50' + MaxRetryAttempts: '20' +- task: Bash@3 + condition: eq(variables.OS, 'windows') + displayName: 'Rename MSI' + inputs: + targetType: 'inline' + script: | + mv "$(Build.SourcesDirectory)/release/windows/msi/bin/Release/sqlcmd.msi" "$(Build.SourcesDirectory)/release/windows/msi/bin/Release/sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION}.msi" + env: + CLI_VERSION: $(VERSION_TAG) + CLI_VERSION_REVISION: 1 +- task: CopyFiles@2 + condition: eq(variables.OS, 'windows') + displayName: 'Copy MSI to: $(Build.ArtifactStagingDirectory)' + inputs: + sourceFolder: $(Build.SourcesDirectory)/release/windows/msi/bin/Release + contents: '?(*.msi|*.md)' + TargetFolder: $(Build.ArtifactStagingDirectory) +- task: EsrpCodeSigning@1 + condition: and(eq(variables.OS, 'linux'), ne(variables.Type, 'docker')) + inputs: + ConnectedServiceName: 'Code Signing' + FolderPath: $(Build.ArtifactStagingDirectory) + Pattern: '*.${{ parameters.Type }}' + signConfigType: 'inlineSignParams' + inlineOperation: | + [ + { + "KeyCode" : "CP-450779-Pgp", + "OperationCode" : "LinuxSign", + "Parameters" : {}, + "ToolName" : "sign", + "ToolVersion" : "1.0" + } + ] + SessionTimeout: '600' + MaxConcurrency: '50' + MaxRetryAttempts: '20' +- task: PublishPipelineArtifact@0 + displayName: 'Publish Artifact: ${{ parameters.Type }}' + inputs: + targetPath: $(Build.ArtifactStagingDirectory) + artifactName: ${{ parameters.Type }} diff --git a/build/azure-pipelines/package-common-test.yml b/build/azure-pipelines/package-common-test.yml index 71770b3f..14026264 100644 --- a/build/azure-pipelines/package-common-test.yml +++ b/build/azure-pipelines/package-common-test.yml @@ -1,36 +1,36 @@ -parameters: -- name: OS - type: string - default: -- name: Type - type: string - -steps: -- task: PowerShell@2 - displayName: Set last tag to variable - inputs: - targetType: 'inline' - script: | - $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) - $VERSION_TAG = $VERSION_TAG.substring(1) # Trim initial 'v' - Write-Host("##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG") - Write-Host($VERSION_TAG) -- task: CmdLine@2 - condition: ne(variables.OS, 'windows') - displayName: 'Test ${{ parameters.OS }}/${{ parameters.Type }} package' - inputs: - script: release/${{ parameters.OS }}/${{ parameters.Type }}/pipeline-test.sh - workingDirectory: $(Build.SourcesCliDirectory) - env: - CLI_VERSION: $(VERSION_TAG) - BUILD_STAGINGDIRECTORY: $(Build.ArtifactStagingDirectory) -- task: PowerShell@2 - condition: eq(variables.OS, 'windows') - displayName: 'Test ${{ parameters.OS }}/${{ parameters.Type }} package' - inputs: - targetType: 'filePath' - filePath: '$(Build.SourcesDirectory)\release\windows\msi\scripts\pipeline-test.ps1' - failOnStderr: true - env: - CLI_VERSION: $(VERSION_TAG) +parameters: +- name: OS + type: string + default: +- name: Type + type: string + +steps: +- task: PowerShell@2 + displayName: Set last tag to variable + inputs: + targetType: 'inline' + script: | + $VERSION_TAG = git describe --tags (git rev-list --tags --max-count=1) + $VERSION_TAG = $VERSION_TAG.substring(1) # Trim initial 'v' + Write-Host("##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG") + Write-Host($VERSION_TAG) +- task: CmdLine@2 + condition: ne(variables.OS, 'windows') + displayName: 'Test ${{ parameters.OS }}/${{ parameters.Type }} package' + inputs: + script: release/${{ parameters.OS }}/${{ parameters.Type }}/pipeline-test.sh + workingDirectory: $(Build.SourcesCliDirectory) + env: + CLI_VERSION: $(VERSION_TAG) + BUILD_STAGINGDIRECTORY: $(Build.ArtifactStagingDirectory) +- task: PowerShell@2 + condition: eq(variables.OS, 'windows') + displayName: 'Test ${{ parameters.OS }}/${{ parameters.Type }} package' + inputs: + targetType: 'filePath' + filePath: '$(Build.SourcesDirectory)\release\windows\msi\scripts\pipeline-test.ps1' + failOnStderr: true + env: + CLI_VERSION: $(VERSION_TAG) \ No newline at end of file diff --git a/build/azure-pipelines/package-product.yml b/build/azure-pipelines/package-product.yml index 3d18e391..762c82ee 100644 --- a/build/azure-pipelines/package-product.yml +++ b/build/azure-pipelines/package-product.yml @@ -1,44 +1,44 @@ -# sqlcmd package pipeline - -trigger: none - -pr: none - -variables: - Build.SourcesCliDirectory: '$(Build.SourcesDirectory)/' - MSI_OUTPUT_DIR: '$(Build.SourcesCliDirectory)/output' - -stages: - - stage: CreatePackages - displayName: Create and Test Package Matrix - jobs: - - job: Package - strategy: - matrix: - rpm: - imageName: 'ubuntu-latest' - os: linux - type: rpm - debian: - imageName: 'ubuntu-latest' - os: linux - type: deb - docker: - imageName: 'ubuntu-latest' - os: linux - type: docker - windows: - imageName: 'windows-2019' - os: windows - type: msi - pool: - vmImage: $(imageName) - steps: - - template: package-common-create.yml - parameters: - OS: $(os) - Type: $(type) - - template: package-common-test.yml - parameters: - OS: $(os) - Type: $(type) +# sqlcmd package pipeline + +trigger: none + +pr: none + +variables: + Build.SourcesCliDirectory: '$(Build.SourcesDirectory)/' + MSI_OUTPUT_DIR: '$(Build.SourcesCliDirectory)/output' + +stages: + - stage: CreatePackages + displayName: Create and Test Package Matrix + jobs: + - job: Package + strategy: + matrix: + rpm: + imageName: 'ubuntu-latest' + os: linux + type: rpm + debian: + imageName: 'ubuntu-latest' + os: linux + type: deb + docker: + imageName: 'ubuntu-latest' + os: linux + type: docker + windows: + imageName: 'windows-2019' + os: windows + type: msi + pool: + vmImage: $(imageName) + steps: + - template: package-common-create.yml + parameters: + OS: $(os) + Type: $(type) + - template: package-common-test.yml + parameters: + OS: $(os) + Type: $(type) diff --git a/cmd/sqlcmd/main.go b/cmd/sqlcmd/main.go index 8e4f7cc3..765ad425 100644 --- a/cmd/sqlcmd/main.go +++ b/cmd/sqlcmd/main.go @@ -1,334 +1,334 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -//go:generate go-winres make --file-version=git-tag --product-version=git-tag -package main - -import ( - "errors" - "fmt" - "os" - - "github.com/alecthomas/kong" - "github.com/microsoft/go-mssqldb/azuread" - "github.com/microsoft/go-sqlcmd/pkg/console" - "github.com/microsoft/go-sqlcmd/pkg/sqlcmd" -) - -var version = "Local-build" // overridden in pipeline builds with: -ldflags="-X main.version=$(VersionTag)" - -// SQLCmdArguments defines the command line arguments for sqlcmd -// The exhaustive list is at https://docs.microsoft.com/sql/tools/sqlcmd-utility?view=sql-server-ver15 -type SQLCmdArguments struct { - // Which batch terminator to use. Default is GO - BatchTerminator string `short:"c" default:"GO" arghelp:"Specifies the batch terminator. The default value is GO."` - // Whether to trust the server certificate on an encrypted connection - TrustServerCertificate bool `short:"C" help:"Implicitly trust the server certificate without validation."` - DatabaseName string `short:"d" help:"This option sets the sqlcmd scripting variable SQLCMDDBNAME. This parameter specifies the initial database. The default is your login's default-database property. If the database does not exist, an error message is generated and sqlcmd exits."` - UseTrustedConnection bool `short:"E" xor:"uid, auth" help:"Uses a trusted connection instead of using a user name and password to sign in to SQL Server, ignoring any environment variables that define user name and password."` - UserName string `short:"U" xor:"uid" help:"The login name or contained database user name. For contained database users, you must provide the database name option"` - // Files from which to read query text - InputFile []string `short:"i" xor:"input1, input2" type:"existingFile" help:"Identifies one or more files that contain batches of SQL statements. If one or more files do not exist, sqlcmd will exit. Mutually exclusive with -Q/-q."` - OutputFile string `short:"o" type:"path" help:"Identifies the file that receives output from sqlcmd."` - // First query to run in interactive mode - InitialQuery string `short:"q" xor:"input1" help:"Executes a query when sqlcmd starts, but does not exit sqlcmd when the query has finished running. Multiple-semicolon-delimited queries can be executed."` - // Query to run then exit - Query string `short:"Q" xor:"input2" help:"Executes a query when sqlcmd starts and then immediately exits sqlcmd. Multiple-semicolon-delimited queries can be executed."` - Server string `short:"S" help:"[tcp:]server[\\instance_name][,port]Specifies the instance of SQL Server to which to connect. It sets the sqlcmd scripting variable SQLCMDSERVER."` - // Disable syscommands with a warning - DisableCmdAndWarn bool `short:"X" xor:"syscmd" help:"Disables commands that might compromise system security. Sqlcmd issues a warning and continues."` - // AuthenticationMethod is new for go-sqlcmd - AuthenticationMethod string `xor:"auth" help:"Specifies the SQL authentication method to use to connect to Azure SQL Database. One of:ActiveDirectoryDefault,ActiveDirectoryIntegrated,ActiveDirectoryPassword,ActiveDirectoryInteractive,ActiveDirectoryManagedIdentity,ActiveDirectoryServicePrincipal,SqlPassword"` - UseAad bool `short:"G" xor:"auth" help:"Tells sqlcmd to use Active Directory authentication. If no user name is provided, authentication method ActiveDirectoryDefault is used. If a password is provided, ActiveDirectoryPassword is used. Otherwise ActiveDirectoryInteractive is used."` - DisableVariableSubstitution bool `short:"x" help:"Causes sqlcmd to ignore scripting variables. This parameter is useful when a script contains many INSERT statements that may contain strings that have the same format as regular variables, such as $(variable_name)."` - Variables map[string]string `short:"v" help:"Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits"` - PacketSize int `short:"a" help:"Requests a packet of a different size. This option sets the sqlcmd scripting variable SQLCMDPACKETSIZE. packet_size must be a value between 512 and 32767. The default = 4096. A larger packet size can enhance performance for execution of scripts that have lots of SQL statements between GO commands. You can request a larger packet size. However, if the request is denied, sqlcmd uses the server default for packet size."` - LoginTimeout int `short:"l" default:"-1" help:"Specifies the number of seconds before a sqlcmd login to the go-mssqldb driver times out when you try to connect to a server. This option sets the sqlcmd scripting variable SQLCMDLOGINTIMEOUT. The default value is 30. 0 means infinite."` - WorkstationName string `short:"H" help:"This option sets the sqlcmd scripting variable SQLCMDWORKSTATION. The workstation name is listed in the hostname column of the sys.sysprocesses catalog view and can be returned using the stored procedure sp_who. If this option is not specified, the default is the current computer name. This name can be used to identify different sqlcmd sessions."` - ApplicationIntent string `short:"K" default:"default" enum:"default,ReadOnly" help:"Declares the application workload type when connecting to a server. The only currently supported value is ReadOnly. If -K is not specified, the sqlcmd utility will not support connectivity to a secondary replica in an Always On availability group."` - EncryptConnection string `short:"N" default:"default" enum:"default,false,true,disable" help:"This switch is used by the client to request an encrypted connection."` - DriverLoggingLevel int `help:"Level of mssql driver messages to print."` - ExitOnError bool `short:"b" help:"Specifies that sqlcmd exits and returns a DOS ERRORLEVEL value when an error occurs."` - ErrorSeverityLevel uint8 `short:"V" help:"Controls the severity level that is used to set the ERRORLEVEL variable on exit."` - ErrorLevel int `short:"m" help:"Controls which error messages are sent to stdout. Messages that have severity level greater than or equal to this level are sent."` - Format string `short:"F" help:"Specifies the formatting for results." default:"horiz" enum:"horiz,horizontal,vert,vertical"` - ErrorsToStderr int `short:"r" help:"Redirects the error message output to the screen (stderr). A value of 0 means messages with severity >= 11 will b redirected. A value of 1 means all error message output including PRINT is redirected." enum:"-1,0,1" default:"-1"` - Headers int `short:"h" help:"Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed."` - UnicodeOutputFile bool `short:"u" help:"Specifies that all output files are encoded with little-endian Unicode"` - Version bool `help:"Show the sqlcmd version information"` - ColumnSeparator string `short:"s" help:"Specifies the column separator character. Sets the SQLCMDCOLSEP variable."` - ScreenWidth *int `short:"w" help:"Specifies the screen width for output. Sets the SQLCMDCOLWIDTH variable."` - TrimSpaces bool `short:"W" help:"Remove trailing spaces from a column."` - MultiSubnetFailover bool `short:"M" help:"Provided for backward compatibility. Sqlcmd always optimizes detection of the active replica of a SQL Failover Cluster."` - Password string `short:"P" help:"Obsolete. The initial passwords must be set using the SQLCMDPASSWORD environment variable or entered at the password prompt."` - // Keep Help at the end of the list - Help bool `short:"?" help:"Show syntax summary."` -} - -// Validate accounts for settings not described by Kong attributes -func (a *SQLCmdArguments) Validate() error { - if a.PacketSize != 0 && (a.PacketSize < 512 || a.PacketSize > 32767) { - return fmt.Errorf(`'-a %d': Packet size has to be a number between 512 and 32767.`, a.PacketSize) - } - // Ignore 0 even though it's technically an invalid input - if a.Headers < -1 { - return fmt.Errorf(`'-h %d': header value must be either -1 or a value between 1 and 2147483647`, a.Headers) - } - if a.ScreenWidth != nil && (*a.ScreenWidth < 9 || *a.ScreenWidth > 65535) { - return fmt.Errorf(`'-w %d': value must be greater than 8 and less than 65536.`, *a.ScreenWidth) - } - if a.Password != "" { - return fmt.Errorf(`'-P' is obsolete. The initial passwords must be set using the SQLCMDPASSWORD environment variable or entered at the password prompt.`) - } - return nil -} - -// newArguments constructs a SQLCmdArguments instance with default values -// Any parameter with a "default" Kong attribute should have an assignment here -func newArguments() SQLCmdArguments { - return SQLCmdArguments{ - BatchTerminator: "GO", - } -} - -// Breaking changes in command line are listed here. -// Any switch not listed in breaking changes and not also included in SqlCmdArguments just has not been implemented yet -// 1. -P: Passwords have to be provided through SQLCMDPASSWORD environment variable or typed when prompted -// 2. -R: Go runtime doesn't expose user locale information and syscall would only enable it on Windows, so we won't try to implement it - -var args SQLCmdArguments - -func (a SQLCmdArguments) authenticationMethod(hasPassword bool) string { - if a.UseTrustedConnection { - return sqlcmd.NotSpecified - } - if a.UseAad { - switch { - case a.UserName == "": - return azuread.ActiveDirectoryIntegrated - case hasPassword: - return azuread.ActiveDirectoryPassword - default: - return azuread.ActiveDirectoryInteractive - } - } - if a.AuthenticationMethod == "" { - return sqlcmd.NotSpecified - } - return a.AuthenticationMethod -} - -func main() { - ctx := kong.Parse(&args, kong.NoDefaultHelp()) - if args.Version { - ctx.Printf("%v", version) - os.Exit(0) - } - if args.Help { - _ = ctx.PrintUsage(false) - os.Exit(0) - } - vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn) - setVars(vars, &args) - - // so far sqlcmd prints all the errors itself so ignore it - exitCode, _ := run(vars, &args) - os.Exit(exitCode) -} - -// setVars initializes scripting variables from command line arguments -func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) { - varmap := map[string]func(*SQLCmdArguments) string{ - sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName }, - sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string { - if a.LoginTimeout > -1 { - return fmt.Sprint(a.LoginTimeout) - } - return "" - }, - sqlcmd.SQLCMDUSEAAD: func(a *SQLCmdArguments) string { - if a.UseAad { - return "true" - } - switch a.AuthenticationMethod { - case azuread.ActiveDirectoryIntegrated: - case azuread.ActiveDirectoryInteractive: - case azuread.ActiveDirectoryPassword: - return "true" - } - return "" - }, - sqlcmd.SQLCMDWORKSTATION: func(a *SQLCmdArguments) string { return args.WorkstationName }, - sqlcmd.SQLCMDSERVER: func(a *SQLCmdArguments) string { return a.Server }, - sqlcmd.SQLCMDERRORLEVEL: func(a *SQLCmdArguments) string { return fmt.Sprint(a.ErrorLevel) }, - sqlcmd.SQLCMDPACKETSIZE: func(a *SQLCmdArguments) string { - if args.PacketSize > 0 { - return fmt.Sprint(args.PacketSize) - } - return "" - }, - sqlcmd.SQLCMDUSER: func(a *SQLCmdArguments) string { return a.UserName }, - sqlcmd.SQLCMDSTATTIMEOUT: func(a *SQLCmdArguments) string { return "" }, - sqlcmd.SQLCMDHEADERS: func(a *SQLCmdArguments) string { return fmt.Sprint(a.Headers) }, - sqlcmd.SQLCMDCOLSEP: func(a *SQLCmdArguments) string { - if a.ColumnSeparator != "" { - return string(a.ColumnSeparator[0]) - } - return "" - }, - sqlcmd.SQLCMDCOLWIDTH: func(a *SQLCmdArguments) string { - if a.ScreenWidth != nil { - return fmt.Sprint(*a.ScreenWidth) - } - return "" - }, - sqlcmd.SQLCMDMAXVARTYPEWIDTH: func(a *SQLCmdArguments) string { return "" }, - sqlcmd.SQLCMDMAXFIXEDTYPEWIDTH: func(a *SQLCmdArguments) string { return "" }, - sqlcmd.SQLCMDFORMAT: func(a *SQLCmdArguments) string { return a.Format }, - } - for varname, set := range varmap { - val := set(args) - if val != "" { - vars.Set(varname, val) - } - } - - // Following sqlcmd tradition there's no validation of -v kvps - for v := range args.Variables { - vars.Set(v, args.Variables[v]) - } - -} - -func setConnect(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments, vars *sqlcmd.Variables) { - if !args.DisableCmdAndWarn { - connect.Password = os.Getenv(sqlcmd.SQLCMDPASSWORD) - } - connect.ServerName = args.Server - if connect.ServerName == "" { - connect.ServerName, _ = vars.Get(sqlcmd.SQLCMDSERVER) - } - connect.Database = args.DatabaseName - if connect.Database == "" { - connect.Database, _ = vars.Get(sqlcmd.SQLCMDDBNAME) - } - connect.UserName = args.UserName - if connect.UserName == "" { - connect.UserName, _ = vars.Get(sqlcmd.SQLCMDUSER) - } - connect.UseTrustedConnection = args.UseTrustedConnection - connect.TrustServerCertificate = args.TrustServerCertificate - connect.AuthenticationMethod = args.authenticationMethod(connect.Password != "") - connect.DisableEnvironmentVariables = args.DisableCmdAndWarn - connect.DisableVariableSubstitution = args.DisableVariableSubstitution - connect.ApplicationIntent = args.ApplicationIntent - connect.LoginTimeoutSeconds = args.LoginTimeout - connect.Encrypt = args.EncryptConnection - connect.PacketSize = args.PacketSize - connect.WorkstationName = args.WorkstationName - connect.LogLevel = args.DriverLoggingLevel - connect.ExitOnError = args.ExitOnError - connect.ErrorSeverityLevel = args.ErrorSeverityLevel -} - -func isConsoleInitializationRequired(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments) bool { - iactive := args.InputFile == nil && args.Query == "" - return iactive || connect.RequiresPassword() -} - -func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { - wd, err := os.Getwd() - if err != nil { - return 1, err - } - - var connectConfig sqlcmd.ConnectSettings - setConnect(&connectConfig, args, vars) - var line sqlcmd.Console = nil - if isConsoleInitializationRequired(&connectConfig, args) { - line = console.NewConsole("") - defer line.Close() - } - - s := sqlcmd.New(line, wd, vars) - s.UnicodeOutputFile = args.UnicodeOutputFile - - if args.DisableCmdAndWarn { - s.Cmd.DisableSysCommands(false) - } - - if args.BatchTerminator != "GO" { - err = s.Cmd.SetBatchTerminator(args.BatchTerminator) - if err != nil { - err = fmt.Errorf("invalid batch terminator '%s'", args.BatchTerminator) - } - } - if err != nil { - return 1, err - } - - s.Connect = &connectConfig - s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces) - if args.OutputFile != "" { - err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile}) - if err != nil { - return 1, err - } - } else { - var stderrSeverity uint8 = 11 - if args.ErrorsToStderr == 1 { - stderrSeverity = 0 - } - if args.ErrorsToStderr >= 0 { - s.PrintError = func(msg string, severity uint8) bool { - if severity >= stderrSeverity { - s.WriteError(os.Stderr, errors.New(msg+sqlcmd.SqlcmdEol)) - return true - } - return false - } - } - } - - // connect using no overrides - err = s.ConnectDb(nil, line == nil) - if err != nil { - return 1, err - } - - script := vars.StartupScriptFile() - if !args.DisableCmdAndWarn && len(script) > 0 { - f, fileErr := os.Open(script) - if fileErr != nil { - s.WriteError(s.GetError(), sqlcmd.InvalidVariableValue(sqlcmd.SQLCMDINI, script)) - } else { - _ = f.Close() - // IncludeFile won't return an error for a SQL error, but ExitCode will be non-zero if -b was passed on the commandline - err = s.IncludeFile(script, true) - } - } - - if err == nil && s.Exitcode == 0 { - once := false - if args.InitialQuery != "" { - s.Query = args.InitialQuery - } else if args.Query != "" { - once = true - s.Query = args.Query - } - iactive := args.InputFile == nil && args.Query == "" - if iactive || s.Query != "" { - err = s.Run(once, false) - } else { - for f := range args.InputFile { - if err = s.IncludeFile(args.InputFile[f], true); err != nil { - s.WriteError(s.GetError(), err) - s.Exitcode = 1 - break - } - } - } - } - s.SetOutput(nil) - s.SetError(nil) - return s.Exitcode, err -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +//go:generate go-winres make --file-version=git-tag --product-version=git-tag +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/alecthomas/kong" + "github.com/microsoft/go-mssqldb/azuread" + "github.com/microsoft/go-sqlcmd/pkg/console" + "github.com/microsoft/go-sqlcmd/pkg/sqlcmd" +) + +var version = "Local-build" // overridden in pipeline builds with: -ldflags="-X main.version=$(VersionTag)" + +// SQLCmdArguments defines the command line arguments for sqlcmd +// The exhaustive list is at https://docs.microsoft.com/sql/tools/sqlcmd-utility?view=sql-server-ver15 +type SQLCmdArguments struct { + // Which batch terminator to use. Default is GO + BatchTerminator string `short:"c" default:"GO" arghelp:"Specifies the batch terminator. The default value is GO."` + // Whether to trust the server certificate on an encrypted connection + TrustServerCertificate bool `short:"C" help:"Implicitly trust the server certificate without validation."` + DatabaseName string `short:"d" help:"This option sets the sqlcmd scripting variable SQLCMDDBNAME. This parameter specifies the initial database. The default is your login's default-database property. If the database does not exist, an error message is generated and sqlcmd exits."` + UseTrustedConnection bool `short:"E" xor:"uid, auth" help:"Uses a trusted connection instead of using a user name and password to sign in to SQL Server, ignoring any environment variables that define user name and password."` + UserName string `short:"U" xor:"uid" help:"The login name or contained database user name. For contained database users, you must provide the database name option"` + // Files from which to read query text + InputFile []string `short:"i" xor:"input1, input2" type:"existingFile" help:"Identifies one or more files that contain batches of SQL statements. If one or more files do not exist, sqlcmd will exit. Mutually exclusive with -Q/-q."` + OutputFile string `short:"o" type:"path" help:"Identifies the file that receives output from sqlcmd."` + // First query to run in interactive mode + InitialQuery string `short:"q" xor:"input1" help:"Executes a query when sqlcmd starts, but does not exit sqlcmd when the query has finished running. Multiple-semicolon-delimited queries can be executed."` + // Query to run then exit + Query string `short:"Q" xor:"input2" help:"Executes a query when sqlcmd starts and then immediately exits sqlcmd. Multiple-semicolon-delimited queries can be executed."` + Server string `short:"S" help:"[tcp:]server[\\instance_name][,port]Specifies the instance of SQL Server to which to connect. It sets the sqlcmd scripting variable SQLCMDSERVER."` + // Disable syscommands with a warning + DisableCmdAndWarn bool `short:"X" xor:"syscmd" help:"Disables commands that might compromise system security. Sqlcmd issues a warning and continues."` + // AuthenticationMethod is new for go-sqlcmd + AuthenticationMethod string `xor:"auth" help:"Specifies the SQL authentication method to use to connect to Azure SQL Database. One of:ActiveDirectoryDefault,ActiveDirectoryIntegrated,ActiveDirectoryPassword,ActiveDirectoryInteractive,ActiveDirectoryManagedIdentity,ActiveDirectoryServicePrincipal,SqlPassword"` + UseAad bool `short:"G" xor:"auth" help:"Tells sqlcmd to use Active Directory authentication. If no user name is provided, authentication method ActiveDirectoryDefault is used. If a password is provided, ActiveDirectoryPassword is used. Otherwise ActiveDirectoryInteractive is used."` + DisableVariableSubstitution bool `short:"x" help:"Causes sqlcmd to ignore scripting variables. This parameter is useful when a script contains many INSERT statements that may contain strings that have the same format as regular variables, such as $(variable_name)."` + Variables map[string]string `short:"v" help:"Creates a sqlcmd scripting variable that can be used in a sqlcmd script. Enclose the value in quotation marks if the value contains spaces. You can specify multiple var=values values. If there are errors in any of the values specified, sqlcmd generates an error message and then exits"` + PacketSize int `short:"a" help:"Requests a packet of a different size. This option sets the sqlcmd scripting variable SQLCMDPACKETSIZE. packet_size must be a value between 512 and 32767. The default = 4096. A larger packet size can enhance performance for execution of scripts that have lots of SQL statements between GO commands. You can request a larger packet size. However, if the request is denied, sqlcmd uses the server default for packet size."` + LoginTimeout int `short:"l" default:"-1" help:"Specifies the number of seconds before a sqlcmd login to the go-mssqldb driver times out when you try to connect to a server. This option sets the sqlcmd scripting variable SQLCMDLOGINTIMEOUT. The default value is 30. 0 means infinite."` + WorkstationName string `short:"H" help:"This option sets the sqlcmd scripting variable SQLCMDWORKSTATION. The workstation name is listed in the hostname column of the sys.sysprocesses catalog view and can be returned using the stored procedure sp_who. If this option is not specified, the default is the current computer name. This name can be used to identify different sqlcmd sessions."` + ApplicationIntent string `short:"K" default:"default" enum:"default,ReadOnly" help:"Declares the application workload type when connecting to a server. The only currently supported value is ReadOnly. If -K is not specified, the sqlcmd utility will not support connectivity to a secondary replica in an Always On availability group."` + EncryptConnection string `short:"N" default:"default" enum:"default,false,true,disable" help:"This switch is used by the client to request an encrypted connection."` + DriverLoggingLevel int `help:"Level of mssql driver messages to print."` + ExitOnError bool `short:"b" help:"Specifies that sqlcmd exits and returns a DOS ERRORLEVEL value when an error occurs."` + ErrorSeverityLevel uint8 `short:"V" help:"Controls the severity level that is used to set the ERRORLEVEL variable on exit."` + ErrorLevel int `short:"m" help:"Controls which error messages are sent to stdout. Messages that have severity level greater than or equal to this level are sent."` + Format string `short:"F" help:"Specifies the formatting for results." default:"horiz" enum:"horiz,horizontal,vert,vertical"` + ErrorsToStderr int `short:"r" help:"Redirects the error message output to the screen (stderr). A value of 0 means messages with severity >= 11 will b redirected. A value of 1 means all error message output including PRINT is redirected." enum:"-1,0,1" default:"-1"` + Headers int `short:"h" help:"Specifies the number of rows to print between the column headings. Use -h-1 to specify that headers not be printed."` + UnicodeOutputFile bool `short:"u" help:"Specifies that all output files are encoded with little-endian Unicode"` + Version bool `help:"Show the sqlcmd version information"` + ColumnSeparator string `short:"s" help:"Specifies the column separator character. Sets the SQLCMDCOLSEP variable."` + ScreenWidth *int `short:"w" help:"Specifies the screen width for output. Sets the SQLCMDCOLWIDTH variable."` + TrimSpaces bool `short:"W" help:"Remove trailing spaces from a column."` + MultiSubnetFailover bool `short:"M" help:"Provided for backward compatibility. Sqlcmd always optimizes detection of the active replica of a SQL Failover Cluster."` + Password string `short:"P" help:"Obsolete. The initial passwords must be set using the SQLCMDPASSWORD environment variable or entered at the password prompt."` + // Keep Help at the end of the list + Help bool `short:"?" help:"Show syntax summary."` +} + +// Validate accounts for settings not described by Kong attributes +func (a *SQLCmdArguments) Validate() error { + if a.PacketSize != 0 && (a.PacketSize < 512 || a.PacketSize > 32767) { + return fmt.Errorf(`'-a %d': Packet size has to be a number between 512 and 32767.`, a.PacketSize) + } + // Ignore 0 even though it's technically an invalid input + if a.Headers < -1 { + return fmt.Errorf(`'-h %d': header value must be either -1 or a value between 1 and 2147483647`, a.Headers) + } + if a.ScreenWidth != nil && (*a.ScreenWidth < 9 || *a.ScreenWidth > 65535) { + return fmt.Errorf(`'-w %d': value must be greater than 8 and less than 65536.`, *a.ScreenWidth) + } + if a.Password != "" { + return fmt.Errorf(`'-P' is obsolete. The initial passwords must be set using the SQLCMDPASSWORD environment variable or entered at the password prompt.`) + } + return nil +} + +// newArguments constructs a SQLCmdArguments instance with default values +// Any parameter with a "default" Kong attribute should have an assignment here +func newArguments() SQLCmdArguments { + return SQLCmdArguments{ + BatchTerminator: "GO", + } +} + +// Breaking changes in command line are listed here. +// Any switch not listed in breaking changes and not also included in SqlCmdArguments just has not been implemented yet +// 1. -P: Passwords have to be provided through SQLCMDPASSWORD environment variable or typed when prompted +// 2. -R: Go runtime doesn't expose user locale information and syscall would only enable it on Windows, so we won't try to implement it + +var args SQLCmdArguments + +func (a SQLCmdArguments) authenticationMethod(hasPassword bool) string { + if a.UseTrustedConnection { + return sqlcmd.NotSpecified + } + if a.UseAad { + switch { + case a.UserName == "": + return azuread.ActiveDirectoryIntegrated + case hasPassword: + return azuread.ActiveDirectoryPassword + default: + return azuread.ActiveDirectoryInteractive + } + } + if a.AuthenticationMethod == "" { + return sqlcmd.NotSpecified + } + return a.AuthenticationMethod +} + +func main() { + ctx := kong.Parse(&args, kong.NoDefaultHelp()) + if args.Version { + ctx.Printf("%v", version) + os.Exit(0) + } + if args.Help { + _ = ctx.PrintUsage(false) + os.Exit(0) + } + vars := sqlcmd.InitializeVariables(!args.DisableCmdAndWarn) + setVars(vars, &args) + + // so far sqlcmd prints all the errors itself so ignore it + exitCode, _ := run(vars, &args) + os.Exit(exitCode) +} + +// setVars initializes scripting variables from command line arguments +func setVars(vars *sqlcmd.Variables, args *SQLCmdArguments) { + varmap := map[string]func(*SQLCmdArguments) string{ + sqlcmd.SQLCMDDBNAME: func(a *SQLCmdArguments) string { return a.DatabaseName }, + sqlcmd.SQLCMDLOGINTIMEOUT: func(a *SQLCmdArguments) string { + if a.LoginTimeout > -1 { + return fmt.Sprint(a.LoginTimeout) + } + return "" + }, + sqlcmd.SQLCMDUSEAAD: func(a *SQLCmdArguments) string { + if a.UseAad { + return "true" + } + switch a.AuthenticationMethod { + case azuread.ActiveDirectoryIntegrated: + case azuread.ActiveDirectoryInteractive: + case azuread.ActiveDirectoryPassword: + return "true" + } + return "" + }, + sqlcmd.SQLCMDWORKSTATION: func(a *SQLCmdArguments) string { return args.WorkstationName }, + sqlcmd.SQLCMDSERVER: func(a *SQLCmdArguments) string { return a.Server }, + sqlcmd.SQLCMDERRORLEVEL: func(a *SQLCmdArguments) string { return fmt.Sprint(a.ErrorLevel) }, + sqlcmd.SQLCMDPACKETSIZE: func(a *SQLCmdArguments) string { + if args.PacketSize > 0 { + return fmt.Sprint(args.PacketSize) + } + return "" + }, + sqlcmd.SQLCMDUSER: func(a *SQLCmdArguments) string { return a.UserName }, + sqlcmd.SQLCMDSTATTIMEOUT: func(a *SQLCmdArguments) string { return "" }, + sqlcmd.SQLCMDHEADERS: func(a *SQLCmdArguments) string { return fmt.Sprint(a.Headers) }, + sqlcmd.SQLCMDCOLSEP: func(a *SQLCmdArguments) string { + if a.ColumnSeparator != "" { + return string(a.ColumnSeparator[0]) + } + return "" + }, + sqlcmd.SQLCMDCOLWIDTH: func(a *SQLCmdArguments) string { + if a.ScreenWidth != nil { + return fmt.Sprint(*a.ScreenWidth) + } + return "" + }, + sqlcmd.SQLCMDMAXVARTYPEWIDTH: func(a *SQLCmdArguments) string { return "" }, + sqlcmd.SQLCMDMAXFIXEDTYPEWIDTH: func(a *SQLCmdArguments) string { return "" }, + sqlcmd.SQLCMDFORMAT: func(a *SQLCmdArguments) string { return a.Format }, + } + for varname, set := range varmap { + val := set(args) + if val != "" { + vars.Set(varname, val) + } + } + + // Following sqlcmd tradition there's no validation of -v kvps + for v := range args.Variables { + vars.Set(v, args.Variables[v]) + } + +} + +func setConnect(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments, vars *sqlcmd.Variables) { + if !args.DisableCmdAndWarn { + connect.Password = os.Getenv(sqlcmd.SQLCMDPASSWORD) + } + connect.ServerName = args.Server + if connect.ServerName == "" { + connect.ServerName, _ = vars.Get(sqlcmd.SQLCMDSERVER) + } + connect.Database = args.DatabaseName + if connect.Database == "" { + connect.Database, _ = vars.Get(sqlcmd.SQLCMDDBNAME) + } + connect.UserName = args.UserName + if connect.UserName == "" { + connect.UserName, _ = vars.Get(sqlcmd.SQLCMDUSER) + } + connect.UseTrustedConnection = args.UseTrustedConnection + connect.TrustServerCertificate = args.TrustServerCertificate + connect.AuthenticationMethod = args.authenticationMethod(connect.Password != "") + connect.DisableEnvironmentVariables = args.DisableCmdAndWarn + connect.DisableVariableSubstitution = args.DisableVariableSubstitution + connect.ApplicationIntent = args.ApplicationIntent + connect.LoginTimeoutSeconds = args.LoginTimeout + connect.Encrypt = args.EncryptConnection + connect.PacketSize = args.PacketSize + connect.WorkstationName = args.WorkstationName + connect.LogLevel = args.DriverLoggingLevel + connect.ExitOnError = args.ExitOnError + connect.ErrorSeverityLevel = args.ErrorSeverityLevel +} + +func isConsoleInitializationRequired(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments) bool { + iactive := args.InputFile == nil && args.Query == "" + return iactive || connect.RequiresPassword() +} + +func run(vars *sqlcmd.Variables, args *SQLCmdArguments) (int, error) { + wd, err := os.Getwd() + if err != nil { + return 1, err + } + + var connectConfig sqlcmd.ConnectSettings + setConnect(&connectConfig, args, vars) + var line sqlcmd.Console = nil + if isConsoleInitializationRequired(&connectConfig, args) { + line = console.NewConsole("") + defer line.Close() + } + + s := sqlcmd.New(line, wd, vars) + s.UnicodeOutputFile = args.UnicodeOutputFile + + if args.DisableCmdAndWarn { + s.Cmd.DisableSysCommands(false) + } + + if args.BatchTerminator != "GO" { + err = s.Cmd.SetBatchTerminator(args.BatchTerminator) + if err != nil { + err = fmt.Errorf("invalid batch terminator '%s'", args.BatchTerminator) + } + } + if err != nil { + return 1, err + } + + s.Connect = &connectConfig + s.Format = sqlcmd.NewSQLCmdDefaultFormatter(args.TrimSpaces) + if args.OutputFile != "" { + err = s.RunCommand(s.Cmd["OUT"], []string{args.OutputFile}) + if err != nil { + return 1, err + } + } else { + var stderrSeverity uint8 = 11 + if args.ErrorsToStderr == 1 { + stderrSeverity = 0 + } + if args.ErrorsToStderr >= 0 { + s.PrintError = func(msg string, severity uint8) bool { + if severity >= stderrSeverity { + s.WriteError(os.Stderr, errors.New(msg+sqlcmd.SqlcmdEol)) + return true + } + return false + } + } + } + + // connect using no overrides + err = s.ConnectDb(nil, line == nil) + if err != nil { + return 1, err + } + + script := vars.StartupScriptFile() + if !args.DisableCmdAndWarn && len(script) > 0 { + f, fileErr := os.Open(script) + if fileErr != nil { + s.WriteError(s.GetError(), sqlcmd.InvalidVariableValue(sqlcmd.SQLCMDINI, script)) + } else { + _ = f.Close() + // IncludeFile won't return an error for a SQL error, but ExitCode will be non-zero if -b was passed on the commandline + err = s.IncludeFile(script, true) + } + } + + if err == nil && s.Exitcode == 0 { + once := false + if args.InitialQuery != "" { + s.Query = args.InitialQuery + } else if args.Query != "" { + once = true + s.Query = args.Query + } + iactive := args.InputFile == nil && args.Query == "" + if iactive || s.Query != "" { + err = s.Run(once, false) + } else { + for f := range args.InputFile { + if err = s.IncludeFile(args.InputFile[f], true); err != nil { + s.WriteError(s.GetError(), err) + s.Exitcode = 1 + break + } + } + } + } + s.SetOutput(nil) + s.SetError(nil) + return s.Exitcode, err +} diff --git a/cmd/sqlcmd/testdata/create100db.sql b/cmd/sqlcmd/testdata/create100db.sql index 49f888fc..1b6c669a 100644 --- a/cmd/sqlcmd/testdata/create100db.sql +++ b/cmd/sqlcmd/testdata/create100db.sql @@ -1,30 +1,30 @@ -create database db100 -go - -create database db101 -go - -create database db102 -go -create database db103 -go -create database db104 -go -create database db105 -go -create database db106 -go -create database db107 -go -create database db108 -go -create database db109 -go -create database db110 -go -create database db111 -go -create database db112 -go -create database db113 -go +create database db100 +go + +create database db101 +go + +create database db102 +go +create database db103 +go +create database db104 +go +create database db105 +go +create database db106 +go +create database db107 +go +create database db108 +go +create database db109 +go +create database db110 +go +create database db111 +go +create database db112 +go +create database db113 +go diff --git a/cmd/sqlcmd/testdata/drop100db.txt b/cmd/sqlcmd/testdata/drop100db.txt index b7d49c34..e64e5905 100644 --- a/cmd/sqlcmd/testdata/drop100db.txt +++ b/cmd/sqlcmd/testdata/drop100db.txt @@ -1,30 +1,30 @@ -drop database db100 -go - -drop database db101 -go - -drop database db102 -go -drop database db103 -go -drop database db104 -go -drop database db105 -go -drop database db106 -go -drop database db107 -go -drop database db108 -go -drop database db109 -go -drop database db110 -go -drop database db111 -go -drop database db112 -go -drop database db113 -go +drop database db100 +go + +drop database db101 +go + +drop database db102 +go +drop database db103 +go +drop database db104 +go +drop database db105 +go +drop database db106 +go +drop database db107 +go +drop database db108 +go +drop database db109 +go +drop database db110 +go +drop database db111 +go +drop database db112 +go +drop database db113 +go diff --git a/cmd/sqlcmd/testdata/select,100.sql b/cmd/sqlcmd/testdata/select,100.sql index 1b87fa39..718c071f 100644 --- a/cmd/sqlcmd/testdata/select,100.sql +++ b/cmd/sqlcmd/testdata/select,100.sql @@ -1 +1 @@ -select 100 +select 100 diff --git a/cmd/sqlcmd/testdata/selectutf8.txt b/cmd/sqlcmd/testdata/selectutf8.txt index ec57d6b5..6f5fd3e3 100644 --- a/cmd/sqlcmd/testdata/selectutf8.txt +++ b/cmd/sqlcmd/testdata/selectutf8.txt @@ -1,2 +1,2 @@ -select N'挨挨唉哀皑癌蔼矮' as SimplifiedChinese - +select N'挨挨唉哀皑癌蔼矮' as SimplifiedChinese + diff --git a/cmd/sqlcmd/testdata/selectutf8_bom.txt b/cmd/sqlcmd/testdata/selectutf8_bom.txt index 27a91bf1..5bcbebdc 100644 --- a/cmd/sqlcmd/testdata/selectutf8_bom.txt +++ b/cmd/sqlcmd/testdata/selectutf8_bom.txt @@ -1,2 +1,2 @@ -select N'挨挨唉哀皑癌蔼矮' as SimplifiedChinese - +select N'挨挨唉哀皑癌蔼矮' as SimplifiedChinese + diff --git a/cmd/sqlcmd/testdata/utf8out.txt b/cmd/sqlcmd/testdata/utf8out.txt index 780da073..3046d4de 100644 --- a/cmd/sqlcmd/testdata/utf8out.txt +++ b/cmd/sqlcmd/testdata/utf8out.txt @@ -1,5 +1,5 @@ -SimplifiedChinese ------------------ -挨挨唉哀皑癌蔼矮 - -(1 row affected) +SimplifiedChinese +----------------- +挨挨唉哀皑癌蔼矮 + +(1 row affected) diff --git a/cmd/sqlcmd/testdata/utf8out_linux.txt b/cmd/sqlcmd/testdata/utf8out_linux.txt index 780da073..3046d4de 100644 --- a/cmd/sqlcmd/testdata/utf8out_linux.txt +++ b/cmd/sqlcmd/testdata/utf8out_linux.txt @@ -1,5 +1,5 @@ -SimplifiedChinese ------------------ -挨挨唉哀皑癌蔼矮 - -(1 row affected) +SimplifiedChinese +----------------- +挨挨唉哀皑癌蔼矮 + +(1 row affected) diff --git a/cmd/sqlcmd/winres/winres.json b/cmd/sqlcmd/winres/winres.json index 03f0edf5..c9eb4c06 100644 --- a/cmd/sqlcmd/winres/winres.json +++ b/cmd/sqlcmd/winres/winres.json @@ -1,33 +1,33 @@ -{ - "RT_GROUP_ICON": { - "APP": { - "0000" : "../../../release/windows/msi/resources/sqlcmd.ico" - } - }, - "RT_VERSION": { - "#1": { - "0000": { - "fixed": { - "file_version": "0.0.0.0", - "product_version": "0.0.0.0" - }, - "info": { - "0409": { - "Comments": "SQL", - "CompanyName": "Microsoft Corporation", - "FileDescription": "T-SQL execution command line utility", - "FileVersion": "", - "InternalName": "go-sqlcmd", - "LegalCopyright": "Microsoft Corporation", - "LegalTrademarks": "Microsoft SQL Server is a registered trademark of Microsoft Corporation", - "OriginalFilename": "sqlcmd.exe", - "PrivateBuild": "", - "ProductName": "Microsoft SQL Server", - "ProductVersion": "", - "SpecialBuild": "" - } - } - } - } - } +{ + "RT_GROUP_ICON": { + "APP": { + "0000" : "../../../release/windows/msi/resources/sqlcmd.ico" + } + }, + "RT_VERSION": { + "#1": { + "0000": { + "fixed": { + "file_version": "0.0.0.0", + "product_version": "0.0.0.0" + }, + "info": { + "0409": { + "Comments": "SQL", + "CompanyName": "Microsoft Corporation", + "FileDescription": "T-SQL execution command line utility", + "FileVersion": "", + "InternalName": "go-sqlcmd", + "LegalCopyright": "Microsoft Corporation", + "LegalTrademarks": "Microsoft SQL Server is a registered trademark of Microsoft Corporation", + "OriginalFilename": "sqlcmd.exe", + "PrivateBuild": "", + "ProductName": "Microsoft SQL Server", + "ProductVersion": "", + "SpecialBuild": "" + } + } + } + } + } } \ No newline at end of file diff --git a/go.mod b/go.mod index 92ff3afb..e9b839b9 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,31 @@ -module github.com/microsoft/go-sqlcmd - -go 1.18 - -require ( - github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0 - github.com/golang-sql/sqlexp v0.1.0 - github.com/google/uuid v1.3.0 - github.com/microsoft/go-mssqldb v0.17.0 - github.com/peterh/liner v1.2.2 - github.com/stretchr/testify v1.8.0 - golang.org/x/text v0.3.7 -) - -require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-runewidth v0.0.3 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect - golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect - golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +module github.com/microsoft/go-sqlcmd + +go 1.18 + +require ( + github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0 + github.com/golang-sql/sqlexp v0.1.0 + github.com/google/uuid v1.3.0 + github.com/microsoft/go-mssqldb v0.17.0 + github.com/peterh/liner v1.2.2 + github.com/stretchr/testify v1.8.0 + golang.org/x/text v0.3.7 +) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.3 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index ab4e572f..3a6ec387 100644 --- a/go.sum +++ b/go.sum @@ -1,99 +1,99 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 h1:sVPhtT2qjO86rTUaWMr4WoES4TkjGnzcioXcnHV9s5k= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= -github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= -github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= -github.com/alecthomas/kong v0.5.1-0.20220516223738-0aaa4c11997b h1:QF7Hdi3ReQRAST66vU7bqkHODmcVJIUZyTGo9gLHluk= -github.com/alecthomas/kong v0.5.1-0.20220516223738-0aaa4c11997b/go.mod h1:GaAkr/DV/nSKftP7snQLewFh9pZqrm+OEn3HqkvWU7c= -github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA= -github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I= -github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0 h1:HQ3WlFsqBcr4qsiHtfA7UdFSrChglOcQa8q/tbXJFBI= -github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= -github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= -github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= -github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= -github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= -github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= -github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= -github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= -github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw= -golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= -golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 h1:sVPhtT2qjO86rTUaWMr4WoES4TkjGnzcioXcnHV9s5k= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= +github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/alecthomas/kong v0.5.1-0.20220516223738-0aaa4c11997b h1:QF7Hdi3ReQRAST66vU7bqkHODmcVJIUZyTGo9gLHluk= +github.com/alecthomas/kong v0.5.1-0.20220516223738-0aaa4c11997b/go.mod h1:GaAkr/DV/nSKftP7snQLewFh9pZqrm+OEn3HqkvWU7c= +github.com/alecthomas/kong v0.6.1 h1:1kNhcFepkR+HmasQpbiKDLylIL8yh5B5y1zPp5bJimA= +github.com/alecthomas/kong v0.6.1/go.mod h1:JfHWDzLmbh/puW6I3V7uWenoh56YNVONW+w8eKeUr9I= +github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0 h1:HQ3WlFsqBcr4qsiHtfA7UdFSrChglOcQa8q/tbXJFBI= +github.com/alecthomas/kong v0.6.2-0.20220922001058-c62bf25854a0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= +github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= +github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= +github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 h1:Tgea0cVUD0ivh5ADBX4WwuI12DUd2to3nCYe2eayMIw= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7 h1:BXxu8t6QN0G1uff4bzZzSkpsax8+ALqTGUtz08QrV00= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/sqlcmd/connect.go b/pkg/sqlcmd/connect.go index ec099fe0..01dac1ec 100644 --- a/pkg/sqlcmd/connect.go +++ b/pkg/sqlcmd/connect.go @@ -1,131 +1,131 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -package sqlcmd - -import ( - "fmt" - "net/url" - - "github.com/microsoft/go-mssqldb/azuread" -) - -// ConnectSettings specifies the settings for connections -type ConnectSettings struct { - // ServerName is the full name including instance and port - ServerName string - // UseTrustedConnection indicates integrated auth is used when no user name is provided - UseTrustedConnection bool - // TrustServerCertificate sets the TrustServerCertificate setting on the connection string - TrustServerCertificate bool - // AuthenticationMethod defines the authentication method for connecting to Azure SQL Database - AuthenticationMethod string - // DisableEnvironmentVariables determines if sqlcmd resolves scripting variables from the process environment - DisableEnvironmentVariables bool - // DisableVariableSubstitution determines if scripting variables should be evaluated - DisableVariableSubstitution bool - // UserName is the username for the SQL connection - UserName string - // Password is the password used with SQL authentication or AAD authentications that require a password - Password string - // Encrypt is the choice of encryption - Encrypt string - // PacketSize is the size of the packet for TDS communication - PacketSize int - // LoginTimeoutSeconds specifies the timeout for establishing a connection - LoginTimeoutSeconds int - // WorkstationName is the string to use to identify the host in server DMVs - WorkstationName string - // ApplicationIntent can only be empty or "ReadOnly" - ApplicationIntent string - // LogLevel is the mssql driver log level - LogLevel int - // ExitOnError specifies whether to exit the app on an error - ExitOnError bool - // ErrorSeverityLevel sets the minimum SQL severity level to treat as an error - ErrorSeverityLevel uint8 - // Database is the name of the database for the connection - Database string -} - -func (c ConnectSettings) authenticationMethod() string { - if c.AuthenticationMethod == "" { - return NotSpecified - } - return c.AuthenticationMethod -} - -func (connect ConnectSettings) integratedAuthentication() bool { - return connect.UseTrustedConnection || (connect.UserName == "" && connect.authenticationMethod() == NotSpecified) -} - -func (connect ConnectSettings) sqlAuthentication() bool { - return connect.authenticationMethod() == SqlPassword || - (!connect.UseTrustedConnection && connect.authenticationMethod() == NotSpecified && connect.UserName != "") -} - -func (connect ConnectSettings) RequiresPassword() bool { - requiresPassword := connect.sqlAuthentication() - if !requiresPassword { - switch connect.authenticationMethod() { - case azuread.ActiveDirectoryApplication, azuread.ActiveDirectoryPassword, azuread.ActiveDirectoryServicePrincipal: - requiresPassword = true - } - } - return requiresPassword -} - -// ConnectionString returns the go-mssql connection string to use for queries -func (connect ConnectSettings) ConnectionString() (connectionString string, err error) { - serverName, instance, port, err := splitServer(connect.ServerName) - if serverName == "" { - serverName = "." - } - if err != nil { - return "", err - } - query := url.Values{} - connectionURL := &url.URL{ - Scheme: "sqlserver", - Path: instance, - } - - if connect.sqlAuthentication() || connect.authenticationMethod() == azuread.ActiveDirectoryPassword || connect.authenticationMethod() == azuread.ActiveDirectoryServicePrincipal || connect.authenticationMethod() == azuread.ActiveDirectoryApplication { - connectionURL.User = url.UserPassword(connect.UserName, connect.Password) - } - if (connect.authenticationMethod() == azuread.ActiveDirectoryMSI || connect.authenticationMethod() == azuread.ActiveDirectoryManagedIdentity) && connect.UserName != "" { - connectionURL.User = url.UserPassword(connect.UserName, connect.Password) - } - if port > 0 { - connectionURL.Host = fmt.Sprintf("%s:%d", serverName, port) - } else { - connectionURL.Host = serverName - } - if connect.Database != "" { - query.Add("database", connect.Database) - } - - if connect.TrustServerCertificate { - query.Add("trustservercertificate", "true") - } - if connect.ApplicationIntent != "" && connect.ApplicationIntent != "default" { - query.Add("applicationintent", connect.ApplicationIntent) - } - if connect.LoginTimeoutSeconds > 0 { - query.Add("connection timeout", fmt.Sprint(connect.LoginTimeoutSeconds)) - } - if connect.PacketSize > 0 { - query.Add("packet size", fmt.Sprint(connect.PacketSize)) - } - if connect.WorkstationName != "" { - query.Add("workstation id", connect.WorkstationName) - } - if connect.Encrypt != "" && connect.Encrypt != "default" { - query.Add("encrypt", connect.Encrypt) - } - if connect.LogLevel > 0 { - query.Add("log", fmt.Sprint(connect.LogLevel)) - } - connectionURL.RawQuery = query.Encode() - return connectionURL.String(), nil -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package sqlcmd + +import ( + "fmt" + "net/url" + + "github.com/microsoft/go-mssqldb/azuread" +) + +// ConnectSettings specifies the settings for connections +type ConnectSettings struct { + // ServerName is the full name including instance and port + ServerName string + // UseTrustedConnection indicates integrated auth is used when no user name is provided + UseTrustedConnection bool + // TrustServerCertificate sets the TrustServerCertificate setting on the connection string + TrustServerCertificate bool + // AuthenticationMethod defines the authentication method for connecting to Azure SQL Database + AuthenticationMethod string + // DisableEnvironmentVariables determines if sqlcmd resolves scripting variables from the process environment + DisableEnvironmentVariables bool + // DisableVariableSubstitution determines if scripting variables should be evaluated + DisableVariableSubstitution bool + // UserName is the username for the SQL connection + UserName string + // Password is the password used with SQL authentication or AAD authentications that require a password + Password string + // Encrypt is the choice of encryption + Encrypt string + // PacketSize is the size of the packet for TDS communication + PacketSize int + // LoginTimeoutSeconds specifies the timeout for establishing a connection + LoginTimeoutSeconds int + // WorkstationName is the string to use to identify the host in server DMVs + WorkstationName string + // ApplicationIntent can only be empty or "ReadOnly" + ApplicationIntent string + // LogLevel is the mssql driver log level + LogLevel int + // ExitOnError specifies whether to exit the app on an error + ExitOnError bool + // ErrorSeverityLevel sets the minimum SQL severity level to treat as an error + ErrorSeverityLevel uint8 + // Database is the name of the database for the connection + Database string +} + +func (c ConnectSettings) authenticationMethod() string { + if c.AuthenticationMethod == "" { + return NotSpecified + } + return c.AuthenticationMethod +} + +func (connect ConnectSettings) integratedAuthentication() bool { + return connect.UseTrustedConnection || (connect.UserName == "" && connect.authenticationMethod() == NotSpecified) +} + +func (connect ConnectSettings) sqlAuthentication() bool { + return connect.authenticationMethod() == SqlPassword || + (!connect.UseTrustedConnection && connect.authenticationMethod() == NotSpecified && connect.UserName != "") +} + +func (connect ConnectSettings) RequiresPassword() bool { + requiresPassword := connect.sqlAuthentication() + if !requiresPassword { + switch connect.authenticationMethod() { + case azuread.ActiveDirectoryApplication, azuread.ActiveDirectoryPassword, azuread.ActiveDirectoryServicePrincipal: + requiresPassword = true + } + } + return requiresPassword +} + +// ConnectionString returns the go-mssql connection string to use for queries +func (connect ConnectSettings) ConnectionString() (connectionString string, err error) { + serverName, instance, port, err := splitServer(connect.ServerName) + if serverName == "" { + serverName = "." + } + if err != nil { + return "", err + } + query := url.Values{} + connectionURL := &url.URL{ + Scheme: "sqlserver", + Path: instance, + } + + if connect.sqlAuthentication() || connect.authenticationMethod() == azuread.ActiveDirectoryPassword || connect.authenticationMethod() == azuread.ActiveDirectoryServicePrincipal || connect.authenticationMethod() == azuread.ActiveDirectoryApplication { + connectionURL.User = url.UserPassword(connect.UserName, connect.Password) + } + if (connect.authenticationMethod() == azuread.ActiveDirectoryMSI || connect.authenticationMethod() == azuread.ActiveDirectoryManagedIdentity) && connect.UserName != "" { + connectionURL.User = url.UserPassword(connect.UserName, connect.Password) + } + if port > 0 { + connectionURL.Host = fmt.Sprintf("%s:%d", serverName, port) + } else { + connectionURL.Host = serverName + } + if connect.Database != "" { + query.Add("database", connect.Database) + } + + if connect.TrustServerCertificate { + query.Add("trustservercertificate", "true") + } + if connect.ApplicationIntent != "" && connect.ApplicationIntent != "default" { + query.Add("applicationintent", connect.ApplicationIntent) + } + if connect.LoginTimeoutSeconds > 0 { + query.Add("connection timeout", fmt.Sprint(connect.LoginTimeoutSeconds)) + } + if connect.PacketSize > 0 { + query.Add("packet size", fmt.Sprint(connect.PacketSize)) + } + if connect.WorkstationName != "" { + query.Add("workstation id", connect.WorkstationName) + } + if connect.Encrypt != "" && connect.Encrypt != "default" { + query.Add("encrypt", connect.Encrypt) + } + if connect.LogLevel > 0 { + query.Add("log", fmt.Sprint(connect.LogLevel)) + } + connectionURL.RawQuery = query.Encode() + return connectionURL.String(), nil +} diff --git a/pkg/sqlcmd/exec_darwin.go b/pkg/sqlcmd/exec_darwin.go index 31bd6a41..458a7123 100644 --- a/pkg/sqlcmd/exec_darwin.go +++ b/pkg/sqlcmd/exec_darwin.go @@ -1,18 +1,18 @@ -package sqlcmd - -import ( - "os/exec" -) - -func sysCommand(arg string) *exec.Cmd { - cmd := exec.Command(comSpec(), "-c", arg) - return cmd -} - -// comSpec returns the path of the command shell executable -func comSpec() string { - // /bin/sh will be a link to the shell - return `/bin/sh` -} - -const defaultEditor = "vi" +package sqlcmd + +import ( + "os/exec" +) + +func sysCommand(arg string) *exec.Cmd { + cmd := exec.Command(comSpec(), "-c", arg) + return cmd +} + +// comSpec returns the path of the command shell executable +func comSpec() string { + // /bin/sh will be a link to the shell + return `/bin/sh` +} + +const defaultEditor = "vi" diff --git a/pkg/sqlcmd/exec_linux.go b/pkg/sqlcmd/exec_linux.go index 31bd6a41..458a7123 100644 --- a/pkg/sqlcmd/exec_linux.go +++ b/pkg/sqlcmd/exec_linux.go @@ -1,18 +1,18 @@ -package sqlcmd - -import ( - "os/exec" -) - -func sysCommand(arg string) *exec.Cmd { - cmd := exec.Command(comSpec(), "-c", arg) - return cmd -} - -// comSpec returns the path of the command shell executable -func comSpec() string { - // /bin/sh will be a link to the shell - return `/bin/sh` -} - -const defaultEditor = "vi" +package sqlcmd + +import ( + "os/exec" +) + +func sysCommand(arg string) *exec.Cmd { + cmd := exec.Command(comSpec(), "-c", arg) + return cmd +} + +// comSpec returns the path of the command shell executable +func comSpec() string { + // /bin/sh will be a link to the shell + return `/bin/sh` +} + +const defaultEditor = "vi" diff --git a/pkg/sqlcmd/exec_windows.go b/pkg/sqlcmd/exec_windows.go index 5e56afa0..bf81d352 100644 --- a/pkg/sqlcmd/exec_windows.go +++ b/pkg/sqlcmd/exec_windows.go @@ -1,27 +1,27 @@ -package sqlcmd - -import ( - "os" - "os/exec" - "syscall" -) - -func sysCommand(arg string) *exec.Cmd { - cmd := exec.Command(comSpec()) - cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: cmd.Path + " " + comArgs(arg)} - return cmd -} - -// comSpec returns the path of the command shell executable -func comSpec() string { - if cmd, ok := os.LookupEnv("COMSPEC"); ok { - return cmd - } - return `C:\Windows\System32\cmd.exe` -} - -func comArgs(args string) string { - return `/c ` + args -} - -const defaultEditor = "notepad.exe" +package sqlcmd + +import ( + "os" + "os/exec" + "syscall" +) + +func sysCommand(arg string) *exec.Cmd { + cmd := exec.Command(comSpec()) + cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: cmd.Path + " " + comArgs(arg)} + return cmd +} + +// comSpec returns the path of the command shell executable +func comSpec() string { + if cmd, ok := os.LookupEnv("COMSPEC"); ok { + return cmd + } + return `C:\Windows\System32\cmd.exe` +} + +func comArgs(args string) string { + return `/c ` + args +} + +const defaultEditor = "notepad.exe" diff --git a/pkg/sqlcmd/testdata/selectdates.sql b/pkg/sqlcmd/testdata/selectdates.sql index 36e82da3..551e414c 100644 --- a/pkg/sqlcmd/testdata/selectdates.sql +++ b/pkg/sqlcmd/testdata/selectdates.sql @@ -1,9 +1,9 @@ -set nocount on -declare @d1 datetime = '2022-03-05 14:01:02' -declare @d2 datetime2(4) = '2021-1-2 11:06:02.2' -declare @d3 datetimeoffset(6) = '2021-5-5' -declare @d4 smalldatetime = '2019-01-11 13:00:00' -declare @d5 time = '14:01:02' -declare @d6 date = '2011-02-03' - -select @d1, @d2, @d3, @d4, @d5, @d6 +set nocount on +declare @d1 datetime = '2022-03-05 14:01:02' +declare @d2 datetime2(4) = '2021-1-2 11:06:02.2' +declare @d3 datetimeoffset(6) = '2021-5-5' +declare @d4 smalldatetime = '2019-01-11 13:00:00' +declare @d5 time = '14:01:02' +declare @d6 date = '2011-02-03' + +select @d1, @d2, @d3, @d4, @d5, @d6 diff --git a/pkg/sqlcmd/testdata/testerrorredirection.sql b/pkg/sqlcmd/testdata/testerrorredirection.sql index 2e5f0854..ef8ff9f2 100644 --- a/pkg/sqlcmd/testdata/testerrorredirection.sql +++ b/pkg/sqlcmd/testdata/testerrorredirection.sql @@ -1,4 +1,4 @@ -select '1' -go -select $(var +select '1' +go +select $(var go \ No newline at end of file diff --git a/pkg/sqlcmd/testdata/variablesnogo.sql b/pkg/sqlcmd/testdata/variablesnogo.sql index 45023a93..bea1d010 100644 --- a/pkg/sqlcmd/testdata/variablesnogo.sql +++ b/pkg/sqlcmd/testdata/variablesnogo.sql @@ -1,5 +1,5 @@ -set nocount on -:setvar hundred 100 --- comment -select $(hundred) - +set nocount on +:setvar hundred 100 +-- comment +select $(hundred) + diff --git a/release/linux/deb/README.md b/release/linux/deb/README.md index 40d7db6a..8e715622 100644 --- a/release/linux/deb/README.md +++ b/release/linux/deb/README.md @@ -1,84 +1,84 @@ -# Debian Packaging Release - -## Building the Debian package - -Execute the following command from the root directory of this repository: - -``` bash -./release/linux/debian/pipeline.sh -``` - -Output will be sent to `./output/debian` - -## Dev Installation and Verification - -``` bash -./release/linux/debian/pipeline-test.sh -``` - -## Release Install/Update/Uninstall Steps - -> **Note:** Replace `{{HOST}}` and `{{CLI_VERSION}}` with the appropriate values. - -### Install sqlcmd with apt (Ubuntu or Debian) - -1. Download and install the signing key: - -```bash -sudo curl -sL http://{{HOST}}/browse/repo/ubuntu/dpgswdist.v1.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/dpgswdist.v1.asc.gpg > /dev/null -``` - -2. Add the sqlcmd repository information: - -```bash -sudo echo "deb [trusted=yes arch=amd64] http://{{HOST}}/browse/repo/ubuntu/sqlcmd mssql main" | tee /etc/apt/sources.list.d/sqlcmd.list -``` - -3. Update repository information and install sqlcmd: - -```bash -sudo apt-get update -sudo apt-get install sqlcmd -``` - -5. Verify installation success: - -```bash -sqlcmd --help -``` - -### Update - -1. Upgrade sqlcmd only: - -```bash -sudo apt-get update && sudo apt-get install --only-upgrade -y sqlcmd -``` - -### Uninstall - -1. Uninstall with apt-get remove: - -```bash -sudo apt-get remove -y sqlcmd -``` - -2. Remove the sqlcmd repository information: - -> Note: This step is not needed if you plan on installing sqlcmd in the future - -```bash -sudo rm /etc/apt/sources.list.d/sqlcmd.list -``` - -3. Remove the signing key: - -```bash -sudo rm /etc/apt/trusted.gpg.d/dpgswdist.v1.asc.gpg -``` - -4. Remove any unneeded dependencies that were installed with sqlcmd: - -```bash -sudo apt autoremove -``` +# Debian Packaging Release + +## Building the Debian package + +Execute the following command from the root directory of this repository: + +``` bash +./release/linux/debian/pipeline.sh +``` + +Output will be sent to `./output/debian` + +## Dev Installation and Verification + +``` bash +./release/linux/debian/pipeline-test.sh +``` + +## Release Install/Update/Uninstall Steps + +> **Note:** Replace `{{HOST}}` and `{{CLI_VERSION}}` with the appropriate values. + +### Install sqlcmd with apt (Ubuntu or Debian) + +1. Download and install the signing key: + +```bash +sudo curl -sL http://{{HOST}}/browse/repo/ubuntu/dpgswdist.v1.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/dpgswdist.v1.asc.gpg > /dev/null +``` + +2. Add the sqlcmd repository information: + +```bash +sudo echo "deb [trusted=yes arch=amd64] http://{{HOST}}/browse/repo/ubuntu/sqlcmd mssql main" | tee /etc/apt/sources.list.d/sqlcmd.list +``` + +3. Update repository information and install sqlcmd: + +```bash +sudo apt-get update +sudo apt-get install sqlcmd +``` + +5. Verify installation success: + +```bash +sqlcmd --help +``` + +### Update + +1. Upgrade sqlcmd only: + +```bash +sudo apt-get update && sudo apt-get install --only-upgrade -y sqlcmd +``` + +### Uninstall + +1. Uninstall with apt-get remove: + +```bash +sudo apt-get remove -y sqlcmd +``` + +2. Remove the sqlcmd repository information: + +> Note: This step is not needed if you plan on installing sqlcmd in the future + +```bash +sudo rm /etc/apt/sources.list.d/sqlcmd.list +``` + +3. Remove the signing key: + +```bash +sudo rm /etc/apt/trusted.gpg.d/dpgswdist.v1.asc.gpg +``` + +4. Remove any unneeded dependencies that were installed with sqlcmd: + +```bash +sudo apt autoremove +``` diff --git a/release/linux/deb/build-pkg.sh b/release/linux/deb/build-pkg.sh index dc996f3b..eaec3e7c 100755 --- a/release/linux/deb/build-pkg.sh +++ b/release/linux/deb/build-pkg.sh @@ -1,57 +1,57 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Build a debian/ubuntu `sqlcmd` package. This script is intended to be ran in a -# container with the respective disto/image laid down. -# -# Usage: -# $ build-pkg.sh - -set -exv - -: "${CLI_VERSION:?CLI_VERSION environment variable not set.}" -: "${CLI_VERSION_REVISION:?CLI_VERSION_REVISION environment variable not set.}" - -WORKDIR=`cd $(dirname $0); cd ../../../; pwd` - -ls -la ${WORKDIR} - -apt-get -y update || exit 1 -export DEBIAN_FRONTEND=noninteractive -apt-get install -y \ - debhelper \ - dpkg-dev \ - locales || exit 1 - -# Locale -sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ - dpkg-reconfigure --frontend=noninteractive locales && \ - update-locale LANG=en_US.UTF-8 - -export LANG=en_US.UTF-8 -export PATH=$PATH - -# Verify -chmod u+x /mnt/workspace/sqlcmd -/mnt/workspace/sqlcmd --help - -mkdir /opt/stage -cp /mnt/workspace/sqlcmd /opt/stage/sqlcmd - -# Create create directory for debian build -mkdir -p ${WORKDIR}/debian -${WORKDIR}/release/linux/deb/prepare-rules.sh ${WORKDIR}/debian ${WORKDIR} - -cd ${WORKDIR} -dpkg-buildpackage -us -uc - -ls ${WORKDIR} -R - -debPkg=${WORKDIR}/../sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION:=1}_all.deb -cp ${debPkg} /mnt/output/ +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Build a debian/ubuntu `sqlcmd` package. This script is intended to be ran in a +# container with the respective disto/image laid down. +# +# Usage: +# $ build-pkg.sh + +set -exv + +: "${CLI_VERSION:?CLI_VERSION environment variable not set.}" +: "${CLI_VERSION_REVISION:?CLI_VERSION_REVISION environment variable not set.}" + +WORKDIR=`cd $(dirname $0); cd ../../../; pwd` + +ls -la ${WORKDIR} + +apt-get -y update || exit 1 +export DEBIAN_FRONTEND=noninteractive +apt-get install -y \ + debhelper \ + dpkg-dev \ + locales || exit 1 + +# Locale +sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=en_US.UTF-8 + +export LANG=en_US.UTF-8 +export PATH=$PATH + +# Verify +chmod u+x /mnt/workspace/sqlcmd +/mnt/workspace/sqlcmd --help + +mkdir /opt/stage +cp /mnt/workspace/sqlcmd /opt/stage/sqlcmd + +# Create create directory for debian build +mkdir -p ${WORKDIR}/debian +${WORKDIR}/release/linux/deb/prepare-rules.sh ${WORKDIR}/debian ${WORKDIR} + +cd ${WORKDIR} +dpkg-buildpackage -us -uc + +ls ${WORKDIR} -R + +debPkg=${WORKDIR}/../sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION:=1}_all.deb +cp ${debPkg} /mnt/output/ diff --git a/release/linux/deb/pipeline-test.sh b/release/linux/deb/pipeline-test.sh index ff00c8bd..17ced4fc 100755 --- a/release/linux/deb/pipeline-test.sh +++ b/release/linux/deb/pipeline-test.sh @@ -1,64 +1,64 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff Debian package tests against versions: -# -# Usage: -# ----------------------------------- -# buster - Debian 10 -# stretch - Debian 9 -# jessie - Debian 8 -# ----------------------------------- -# focal - Ubuntu 20.04 -# bionic - Ubuntu 18.04 -# xenial - Ubuntu 16.04 -# ----------------------------------- -# -# Usage: -# $ pipeline-test.sh - -set -e #xv - -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" - -CLI_VERSION=${CLI_VERSION:=0.0.1} -CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} -BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/debian} - -DISTROS=( buster buster ) # TODO: Should we validate against more distros?: buster stretch jessie bionic xenial focal -BASE_IMAGES=( debian:buster ubuntu:focal ) # TODO: debian:buster debian:stretch debian:jessie ubuntu:bionic ubuntu:xenial ubuntu:focal - -echo "==========================================================" -echo "CLI_VERSION: ${CLI_VERSION}" -echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION}" -echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" -echo "Distribution: ${DISTROS}" -echo "==========================================================" - -for i in ${!DISTROS[@]}; do - echo "==========================================================" - echo "Test debian package on ${DISTROS[$i]}" - echo "==========================================================" - - debPkg=sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION}~${DISTROS[$i]}_all.deb - - script="apt-get update && \ - dpkg -i /mnt/artifacts/${debPkg} && \ - apt-get -f install && \ - sqlcmd --help" - - docker pull ${BASE_IMAGES[$i]} - docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ - ${BASE_IMAGES[$i]} \ - /bin/bash -c "${script}" - - echo "" -done +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff Debian package tests against versions: +# +# Usage: +# ----------------------------------- +# buster - Debian 10 +# stretch - Debian 9 +# jessie - Debian 8 +# ----------------------------------- +# focal - Ubuntu 20.04 +# bionic - Ubuntu 18.04 +# xenial - Ubuntu 16.04 +# ----------------------------------- +# +# Usage: +# $ pipeline-test.sh + +set -e #xv + +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" + +CLI_VERSION=${CLI_VERSION:=0.0.1} +CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} +BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/debian} + +DISTROS=( buster buster ) # TODO: Should we validate against more distros?: buster stretch jessie bionic xenial focal +BASE_IMAGES=( debian:buster ubuntu:focal ) # TODO: debian:buster debian:stretch debian:jessie ubuntu:bionic ubuntu:xenial ubuntu:focal + +echo "==========================================================" +echo "CLI_VERSION: ${CLI_VERSION}" +echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION}" +echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" +echo "Distribution: ${DISTROS}" +echo "==========================================================" + +for i in ${!DISTROS[@]}; do + echo "==========================================================" + echo "Test debian package on ${DISTROS[$i]}" + echo "==========================================================" + + debPkg=sqlcmd_${CLI_VERSION}-${CLI_VERSION_REVISION}~${DISTROS[$i]}_all.deb + + script="apt-get update && \ + dpkg -i /mnt/artifacts/${debPkg} && \ + apt-get -f install && \ + sqlcmd --help" + + docker pull ${BASE_IMAGES[$i]} + docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ + ${BASE_IMAGES[$i]} \ + /bin/bash -c "${script}" + + echo "" +done diff --git a/release/linux/deb/pipeline.sh b/release/linux/deb/pipeline.sh index 70010ab4..60886d6d 100755 --- a/release/linux/deb/pipeline.sh +++ b/release/linux/deb/pipeline.sh @@ -1,72 +1,72 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff debian package build in docker and copy the .deb package artifact -# back to the local filesystem. The build pipeline can then save it as an -# artifact as it sees fit. -# -# Note: Intended to be ran under ubuntu. -# -# Usage: -# ----------------------------------- -# buster - Debian 10 -# stretch - Debian 9 -# jessie - Debian 8 -# ----------------------------------- -# focal - Ubuntu 20.04 -# bionic - Ubuntu 18.04 -# xenial - Ubuntu 16.04 -# ----------------------------------- -# -# Example: -# -# export DISTRO=xenial -# export DISTRO_BASE_IMAGE=ubuntu:xenial -# -# $ pipeline.sh - -set -exv - -DISTRO=${DISTRO:=buster} -DISTRO_BASE_IMAGE=${DISTRO_BASE_IMAGE:=debian:buster} - -: "${DISTRO:?DISTRO environment variable not set.}" -: "${DISTRO_BASE_IMAGE:?DISTRO_BASE_IMAGE environment variable not set.}" -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" -DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/debian} - -PIPELINE_WORKSPACE=${REPO_ROOT_DIR} - -if [[ "${BUILD_OUTPUT}" != "" ]]; then - cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd -fi - -CLI_VERSION=${CLI_VERSION:=0.0.1} - -echo "==========================================================" -echo "CLI_VERSION: ${CLI_VERSION}" -echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION:=1}" -echo "Distribution: ${DISTRO}" -echo "Distribution Image: ${DISTRO_BASE_IMAGE}" -echo "==========================================================" - -mkdir -p ${DIST_DIR} || exit 1 - -echo ${REPO_ROOT_DIR} - -docker run --rm \ - -v "${REPO_ROOT_DIR}":/mnt/repo \ - -v "${DIST_DIR}":/mnt/output \ - -v "${PIPELINE_WORKSPACE}":/mnt/workspace \ - -e CLI_VERSION=${CLI_VERSION} \ - -e CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1}~${DISTRO} \ - "${DISTRO_BASE_IMAGE}" \ - /mnt/repo/release/linux/deb/build-pkg.sh +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff debian package build in docker and copy the .deb package artifact +# back to the local filesystem. The build pipeline can then save it as an +# artifact as it sees fit. +# +# Note: Intended to be ran under ubuntu. +# +# Usage: +# ----------------------------------- +# buster - Debian 10 +# stretch - Debian 9 +# jessie - Debian 8 +# ----------------------------------- +# focal - Ubuntu 20.04 +# bionic - Ubuntu 18.04 +# xenial - Ubuntu 16.04 +# ----------------------------------- +# +# Example: +# +# export DISTRO=xenial +# export DISTRO_BASE_IMAGE=ubuntu:xenial +# +# $ pipeline.sh + +set -exv + +DISTRO=${DISTRO:=buster} +DISTRO_BASE_IMAGE=${DISTRO_BASE_IMAGE:=debian:buster} + +: "${DISTRO:?DISTRO environment variable not set.}" +: "${DISTRO_BASE_IMAGE:?DISTRO_BASE_IMAGE environment variable not set.}" +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" +DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/debian} + +PIPELINE_WORKSPACE=${REPO_ROOT_DIR} + +if [[ "${BUILD_OUTPUT}" != "" ]]; then + cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd +fi + +CLI_VERSION=${CLI_VERSION:=0.0.1} + +echo "==========================================================" +echo "CLI_VERSION: ${CLI_VERSION}" +echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION:=1}" +echo "Distribution: ${DISTRO}" +echo "Distribution Image: ${DISTRO_BASE_IMAGE}" +echo "==========================================================" + +mkdir -p ${DIST_DIR} || exit 1 + +echo ${REPO_ROOT_DIR} + +docker run --rm \ + -v "${REPO_ROOT_DIR}":/mnt/repo \ + -v "${DIST_DIR}":/mnt/output \ + -v "${PIPELINE_WORKSPACE}":/mnt/workspace \ + -e CLI_VERSION=${CLI_VERSION} \ + -e CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1}~${DISTRO} \ + "${DISTRO_BASE_IMAGE}" \ + /mnt/repo/release/linux/deb/build-pkg.sh diff --git a/release/linux/deb/prepare-rules.sh b/release/linux/deb/prepare-rules.sh index 0129f5c7..ab38a1bf 100755 --- a/release/linux/deb/prepare-rules.sh +++ b/release/linux/deb/prepare-rules.sh @@ -1,125 +1,125 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Create the debian/directory for building the `sqlcmd` Debian package and -# the package rules. -# -# Usage: -# -# prepare-rules.sh DEBIAN-DIR SRC_DIR -# - -set -evx - -if [[ -z "$1" ]] - then - echo "No argument supplied for debian directory." - exit 1 -fi - -if [[ -z "$2" ]] - then - echo "No argument supplied for source directory." - exit 1 -fi - -TAB=$'\t' - -debian_dir=$1 -source_dir=$2 - -mkdir -p $debian_dir/source || exit 1 - -echo '1.0' > $debian_dir/source/format -echo '9' > $debian_dir/compat - -cat > $debian_dir/changelog <<- EOM -sqlcmd (${CLI_VERSION}-${CLI_VERSION_REVISION:=1}) stable; urgency=low - - * Debian package release. - - -- sqlcmd tools team $(date -R) - -EOM - -cat > $debian_dir/control <<- EOM -Source: sqlcmd -Section: sql -Priority: extra -Maintainer: sqlcmd tools team -Build-Depends: debhelper (>= 9) -Standards-Version: 3.9.5 -Homepage: https://github.com/microsoft/go-sqlcmd - -Package: sqlcmd -Architecture: all -Depends: \${shlibs:Depends}, \${misc:Depends} -Description: SQLCMD TOOLS CLI - SQLCMD TOOLS CLI, a multi-platform command line experience for Microsoft SQL Server and Azure SQL. - -EOM - -cat > $debian_dir/copyright <<- EOM -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: sqlcmd -Upstream-Contact: sqlcmd tools team -Source: PUBLIC - -Files: * -Copyright: Copyright (c) Microsoft Corporation -License: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE - -MIT License - -Copyright (c) Microsoft Corporation. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE - -EOM - -cat > $debian_dir/rules << EOM -#!/usr/bin/make -f - -# Uncomment this to turn on verbose mode. -export DH_VERBOSE=1 -export DH_OPTIONS=-v - -%: -${TAB}dh \$@ --sourcedirectory $source_dir - -override_dh_install: -${TAB}mkdir -p debian/sqlcmd/usr/bin/ -${TAB}cp -r /opt/stage/sqlcmd debian/sqlcmd/usr/bin/sqlcmd -${TAB}chmod 0755 debian/sqlcmd/usr/bin/sqlcmd - -override_dh_strip: -${TAB}dh_strip --exclude=_cffi_backend - -EOM - -cat $debian_dir/rules - -# Debian rules should be executable -chmod 0755 $debian_dir/rules +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Create the debian/directory for building the `sqlcmd` Debian package and +# the package rules. +# +# Usage: +# +# prepare-rules.sh DEBIAN-DIR SRC_DIR +# + +set -evx + +if [[ -z "$1" ]] + then + echo "No argument supplied for debian directory." + exit 1 +fi + +if [[ -z "$2" ]] + then + echo "No argument supplied for source directory." + exit 1 +fi + +TAB=$'\t' + +debian_dir=$1 +source_dir=$2 + +mkdir -p $debian_dir/source || exit 1 + +echo '1.0' > $debian_dir/source/format +echo '9' > $debian_dir/compat + +cat > $debian_dir/changelog <<- EOM +sqlcmd (${CLI_VERSION}-${CLI_VERSION_REVISION:=1}) stable; urgency=low + + * Debian package release. + + -- sqlcmd tools team $(date -R) + +EOM + +cat > $debian_dir/control <<- EOM +Source: sqlcmd +Section: sql +Priority: extra +Maintainer: sqlcmd tools team +Build-Depends: debhelper (>= 9) +Standards-Version: 3.9.5 +Homepage: https://github.com/microsoft/go-sqlcmd + +Package: sqlcmd +Architecture: all +Depends: \${shlibs:Depends}, \${misc:Depends} +Description: SQLCMD TOOLS CLI + SQLCMD TOOLS CLI, a multi-platform command line experience for Microsoft SQL Server and Azure SQL. + +EOM + +cat > $debian_dir/copyright <<- EOM +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: sqlcmd +Upstream-Contact: sqlcmd tools team +Source: PUBLIC + +Files: * +Copyright: Copyright (c) Microsoft Corporation +License: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE + +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE + +EOM + +cat > $debian_dir/rules << EOM +#!/usr/bin/make -f + +# Uncomment this to turn on verbose mode. +export DH_VERBOSE=1 +export DH_OPTIONS=-v + +%: +${TAB}dh \$@ --sourcedirectory $source_dir + +override_dh_install: +${TAB}mkdir -p debian/sqlcmd/usr/bin/ +${TAB}cp -r /opt/stage/sqlcmd debian/sqlcmd/usr/bin/sqlcmd +${TAB}chmod 0755 debian/sqlcmd/usr/bin/sqlcmd + +override_dh_strip: +${TAB}dh_strip --exclude=_cffi_backend + +EOM + +cat $debian_dir/rules + +# Debian rules should be executable +chmod 0755 $debian_dir/rules diff --git a/release/linux/docker/README.md b/release/linux/docker/README.md index e8854dd0..1d1f150b 100644 --- a/release/linux/docker/README.md +++ b/release/linux/docker/README.md @@ -1,17 +1,17 @@ -# Docker Release - -## Building Docker in CI/CD pipeline - -Execute the following command from the root directory of this repository: - -```bash -./release/linux/docker/pipeline.sh -``` - -Output will be sent to `./output/docker` - -## Verify - -```bash -./release/linux/docker/pipeline-test.sh -``` +# Docker Release + +## Building Docker in CI/CD pipeline + +Execute the following command from the root directory of this repository: + +```bash +./release/linux/docker/pipeline.sh +``` + +Output will be sent to `./output/docker` + +## Verify + +```bash +./release/linux/docker/pipeline-test.sh +``` diff --git a/release/linux/docker/pipeline-test.sh b/release/linux/docker/pipeline-test.sh index 1653f5dd..2a04b240 100755 --- a/release/linux/docker/pipeline-test.sh +++ b/release/linux/docker/pipeline-test.sh @@ -1,38 +1,38 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff docker image test: -# -# Usage: -# -# $ pipeline-test.sh - -set -exv - -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" - -PACKAGE_VERSION=${CLI_VERSION:=0.0.1} -PACKAGE_VERSION_REVISION=${CLI_VERSION_REVISION:=1} - -BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/docker} -IMAGE_NAME=microsoft/sqlcmd${BUILD_BUILDNUMBER:=''}:latest -TAR_FILE=${BUILD_ARTIFACTSTAGINGDIRECTORY}/sqlcmd-docker-${PACKAGE_VERSION}-${PACKAGE_VERSION_REVISION}.tar - -echo "==========================================================" -echo "PACKAGE_VERSION: ${PACKAGE_VERSION}" -echo "PACKAGE_VERSION_REVISION: ${PACKAGE_VERSION_REVISION}" -echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" -echo "Image name: ${IMAGE_NAME}" -echo "Docker image file: ${TAR_FILE}" -echo "==========================================================" - -docker load < ${TAR_FILE} -docker run ${IMAGE_NAME} sqlcmd --help || exit 1 +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff docker image test: +# +# Usage: +# +# $ pipeline-test.sh + +set -exv + +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" + +PACKAGE_VERSION=${CLI_VERSION:=0.0.1} +PACKAGE_VERSION_REVISION=${CLI_VERSION_REVISION:=1} + +BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/docker} +IMAGE_NAME=microsoft/sqlcmd${BUILD_BUILDNUMBER:=''}:latest +TAR_FILE=${BUILD_ARTIFACTSTAGINGDIRECTORY}/sqlcmd-docker-${PACKAGE_VERSION}-${PACKAGE_VERSION_REVISION}.tar + +echo "==========================================================" +echo "PACKAGE_VERSION: ${PACKAGE_VERSION}" +echo "PACKAGE_VERSION_REVISION: ${PACKAGE_VERSION_REVISION}" +echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" +echo "Image name: ${IMAGE_NAME}" +echo "Docker image file: ${TAR_FILE}" +echo "==========================================================" + +docker load < ${TAR_FILE} +docker run ${IMAGE_NAME} sqlcmd --help || exit 1 diff --git a/release/linux/docker/pipeline.sh b/release/linux/docker/pipeline.sh index c402bdcd..efba61da 100755 --- a/release/linux/docker/pipeline.sh +++ b/release/linux/docker/pipeline.sh @@ -1,61 +1,61 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Build and save the `sqlcmd` image into the bundle: -# `sqlcmd-docker-${PACKAGE_VERSION}.tar` -# -# Usage: -# -# export BUILD_NUMBER=12345 (optional - used to identify the IMAGE_NAME) -# $ pipeline.sh - -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" -DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/docker} -IMAGE_NAME=microsoft/sqlcmd${BUILD_BUILDNUMBER:=''} - -if [[ "${BUILD_OUTPUT}" != "" ]]; then - cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd -fi - -chmod u+x ${REPO_ROOT_DIR}/sqlcmd - -PACKAGE_VERSION=${CLI_VERSION:=0.0.1} -PACKAGE_VERSION_REVISION=${CLI_VERSION_REVISION:=1} - -echo "==========================================================" -echo "PACKAGE_VERSION: ${PACKAGE_VERSION}" -echo "PACKAGE_VERSION_REVISION: ${PACKAGE_VERSION_REVISION}" -echo "IMAGE_NAME: ${IMAGE_NAME}" -echo "Output location: ${DIST_DIR}" -echo "==========================================================" - -docker build --no-cache \ - --build-arg BUILD_DATE="`date -u +"%Y-%m-%dT%H:%M:%SZ"`" \ - --build-arg PACKAGE_VERSION=${PACKAGE_VERSION} \ - --build-arg PACKAGE_VERSION_REVISION=${PACKAGE_VERSION_REVISION} \ - --tag ${IMAGE_NAME}:latest \ - ${REPO_ROOT_DIR} - -echo "==========================================================" -echo "Done - docker build" -echo "==========================================================" - -mkdir -p ${DIST_DIR} || exit 1 -docker save -o "${DIST_DIR}/sqlcmd-docker-${PACKAGE_VERSION}-${PACKAGE_VERSION_REVISION}.tar" ${IMAGE_NAME}:latest - -echo "==========================================================" -echo "Done - docker save" -echo "==========================================================" - -echo "=== Done =================================================" -docker rmi -f ${IMAGE_NAME}:latest -ls ${DIST_DIR} -echo "==========================================================" +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Build and save the `sqlcmd` image into the bundle: +# `sqlcmd-docker-${PACKAGE_VERSION}.tar` +# +# Usage: +# +# export BUILD_NUMBER=12345 (optional - used to identify the IMAGE_NAME) +# $ pipeline.sh + +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" +DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/docker} +IMAGE_NAME=microsoft/sqlcmd${BUILD_BUILDNUMBER:=''} + +if [[ "${BUILD_OUTPUT}" != "" ]]; then + cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd +fi + +chmod u+x ${REPO_ROOT_DIR}/sqlcmd + +PACKAGE_VERSION=${CLI_VERSION:=0.0.1} +PACKAGE_VERSION_REVISION=${CLI_VERSION_REVISION:=1} + +echo "==========================================================" +echo "PACKAGE_VERSION: ${PACKAGE_VERSION}" +echo "PACKAGE_VERSION_REVISION: ${PACKAGE_VERSION_REVISION}" +echo "IMAGE_NAME: ${IMAGE_NAME}" +echo "Output location: ${DIST_DIR}" +echo "==========================================================" + +docker build --no-cache \ + --build-arg BUILD_DATE="`date -u +"%Y-%m-%dT%H:%M:%SZ"`" \ + --build-arg PACKAGE_VERSION=${PACKAGE_VERSION} \ + --build-arg PACKAGE_VERSION_REVISION=${PACKAGE_VERSION_REVISION} \ + --tag ${IMAGE_NAME}:latest \ + ${REPO_ROOT_DIR} + +echo "==========================================================" +echo "Done - docker build" +echo "==========================================================" + +mkdir -p ${DIST_DIR} || exit 1 +docker save -o "${DIST_DIR}/sqlcmd-docker-${PACKAGE_VERSION}-${PACKAGE_VERSION_REVISION}.tar" ${IMAGE_NAME}:latest + +echo "==========================================================" +echo "Done - docker save" +echo "==========================================================" + +echo "=== Done =================================================" +docker rmi -f ${IMAGE_NAME}:latest +ls ${DIST_DIR} +echo "==========================================================" diff --git a/release/linux/rpm/README.md b/release/linux/rpm/README.md index de168402..33b15210 100644 --- a/release/linux/rpm/README.md +++ b/release/linux/rpm/README.md @@ -1,17 +1,17 @@ -RPM Release - -## Building RPM in CI/CD pipeline - -Execute the following command from the root directory of this repository: - -``` bash -./release/linux/rpm/pipeline.sh -``` -Output will be sent to `./output/rpm` - -To test the packages: - -``` bash -./release/linux/rpm/pipeline-test.sh -``` - +RPM Release + +## Building RPM in CI/CD pipeline + +Execute the following command from the root directory of this repository: + +``` bash +./release/linux/rpm/pipeline.sh +``` +Output will be sent to `./output/rpm` + +To test the packages: + +``` bash +./release/linux/rpm/pipeline-test.sh +``` + diff --git a/release/linux/rpm/build-rpm.sh b/release/linux/rpm/build-rpm.sh index fd41b385..54ac11bd 100755 --- a/release/linux/rpm/build-rpm.sh +++ b/release/linux/rpm/build-rpm.sh @@ -1,27 +1,27 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Build a rpm `sqlcmd` package. This script is intended to be run in a -# container with the respective distro/image laid down. -# -# Usage: -# $ build-rpm.sh - -set -exv - -: "${CLI_VERSION:?CLI_VERSION environment variable not set.}" -: "${CLI_VERSION_REVISION:?CLI_VERSION_REVISION environment variable not set.}" - -yum update -y -yum install -y rpm-build - -export LC_ALL=en_US.UTF-8 -export REPO_ROOT_DIR=`cd $(dirname $0); cd ../../../; pwd` - -rpmbuild -v -bb --clean ${REPO_ROOT_DIR}/release/linux/rpm/sqlcmd.spec && cp /root/rpmbuild/RPMS/x86_64/* /mnt/output +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Build a rpm `sqlcmd` package. This script is intended to be run in a +# container with the respective distro/image laid down. +# +# Usage: +# $ build-rpm.sh + +set -exv + +: "${CLI_VERSION:?CLI_VERSION environment variable not set.}" +: "${CLI_VERSION_REVISION:?CLI_VERSION_REVISION environment variable not set.}" + +yum update -y +yum install -y rpm-build + +export LC_ALL=en_US.UTF-8 +export REPO_ROOT_DIR=`cd $(dirname $0); cd ../../../; pwd` + +rpmbuild -v -bb --clean ${REPO_ROOT_DIR}/release/linux/rpm/sqlcmd.spec && cp /root/rpmbuild/RPMS/x86_64/* /mnt/output diff --git a/release/linux/rpm/pipeline-test.sh b/release/linux/rpm/pipeline-test.sh index 297973e2..49c04ab4 100755 --- a/release/linux/rpm/pipeline-test.sh +++ b/release/linux/rpm/pipeline-test.sh @@ -1,106 +1,106 @@ -#!/usr/bin/env bash - -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff rpm package tests against versions: -# -# ----------------------------------- -# centos:centos8 -# centos:centos7 -# ----------------------------------- -# fedora:31 -# fedora:30 -# fedora:29 -# ----------------------------------- -# opensuse/leap:latest -# ----------------------------------- -# -# Usage: -# $ pipeline-test.sh - -set -exv - -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" - -CLI_VERSION=${CLI_VERSION:=0.0.1} -CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} - -BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/rpm} - -YUM_DISTRO_BASE_IMAGE=( centos:centos7 centos:centos8 fedora:29 fedora:30 fedora:31 ) -YUM_DISTRO_SUFFIX=( el7 el7 fc29 fc29 fc29 ) - -ZYPPER_DISTRO_BASE_IMAGE=( opensuse/leap:latest ) -ZYPPER_DISTRO_SUFFIX=( el7 ) - -echo "==========================================================" -echo "CLI_VERSION: ${CLI_VERSION}" -echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION}" -echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" -echo "Distribution: ${YUM_DISTRO_BASE_IMAGE} ${ZYPPER_DISTRO_BASE_IMAGE}" -echo "==========================================================" - -# -- yum installs -- -for i in ${!YUM_DISTRO_BASE_IMAGE[@]}; do - image=${YUM_DISTRO_BASE_IMAGE[$i]} - suffix=${YUM_DISTRO_SUFFIX[$i]} - - echo "==========================================================" - echo "Test rpm package on ${image} .${suffix}" - echo "==========================================================" - rpmPkg=sqlcmd-${CLI_VERSION}-${CLI_VERSION_REVISION}.${suffix}.x86_64.rpm - - # Per: https://techglimpse.com/failed-metadata-repo-appstream-centos-8/ - # change the mirrors to vault.centos.org where they will be archived permanently - mirrors="" - if [[ "${image}" == "centos:centos8" ]]; then - mirrors="cd /etc/yum.repos.d/ && \ - sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \ - sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* && \ " - fi - - script="${mirrors} - rpm --import https://packages.microsoft.com/keys/microsoft.asc && \ - yum update -y && \ - yum localinstall /mnt/artifacts/${rpmPkg} -y && \ - sqlcmd --help" - - docker pull ${image} - docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ - ${image} \ - /bin/bash -c "${script}" - - echo "" -done - -# -- zypper installs -- -for i in ${!ZYPPER_DISTRO_BASE_IMAGE[@]}; do - image=${ZYPPER_DISTRO_BASE_IMAGE[$i]} - suffix=${ZYPPER_DISTRO_SUFFIX[$i]} - - echo "==========================================================" - echo "Test rpm package on ${image} .${suffix}" - echo "==========================================================" - rpmPkg=sqlcmd-${CLI_VERSION}-${CLI_VERSION_REVISION}.${suffix}.x86_64.rpm - # If testing locally w/o signing, use `--allow-unsigned-rpm` but do not commit: - # zypper --non-interactive install --allow-unsigned-rpm /mnt/artifacts/${rpmPkg} && \ - - script="zypper --non-interactive install curl && \ - rpm -v --import https://packages.microsoft.com/keys/microsoft.asc && \ - zypper --non-interactive install /mnt/artifacts/${rpmPkg} && \ - sqlcmd --help" - - docker pull ${image} - docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ - ${image} \ - /bin/bash -c "${script}" - - echo "" -done +#!/usr/bin/env bash + +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff rpm package tests against versions: +# +# ----------------------------------- +# centos:centos8 +# centos:centos7 +# ----------------------------------- +# fedora:31 +# fedora:30 +# fedora:29 +# ----------------------------------- +# opensuse/leap:latest +# ----------------------------------- +# +# Usage: +# $ pipeline-test.sh + +set -exv + +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" + +CLI_VERSION=${CLI_VERSION:=0.0.1} +CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} + +BUILD_ARTIFACTSTAGINGDIRECTORY=${BUILD_ARTIFACTSTAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/rpm} + +YUM_DISTRO_BASE_IMAGE=( centos:centos7 centos:centos8 fedora:29 fedora:30 fedora:31 ) +YUM_DISTRO_SUFFIX=( el7 el7 fc29 fc29 fc29 ) + +ZYPPER_DISTRO_BASE_IMAGE=( opensuse/leap:latest ) +ZYPPER_DISTRO_SUFFIX=( el7 ) + +echo "==========================================================" +echo "CLI_VERSION: ${CLI_VERSION}" +echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION}" +echo "BUILD_ARTIFACTSTAGINGDIRECTORY: ${BUILD_ARTIFACTSTAGINGDIRECTORY}" +echo "Distribution: ${YUM_DISTRO_BASE_IMAGE} ${ZYPPER_DISTRO_BASE_IMAGE}" +echo "==========================================================" + +# -- yum installs -- +for i in ${!YUM_DISTRO_BASE_IMAGE[@]}; do + image=${YUM_DISTRO_BASE_IMAGE[$i]} + suffix=${YUM_DISTRO_SUFFIX[$i]} + + echo "==========================================================" + echo "Test rpm package on ${image} .${suffix}" + echo "==========================================================" + rpmPkg=sqlcmd-${CLI_VERSION}-${CLI_VERSION_REVISION}.${suffix}.x86_64.rpm + + # Per: https://techglimpse.com/failed-metadata-repo-appstream-centos-8/ + # change the mirrors to vault.centos.org where they will be archived permanently + mirrors="" + if [[ "${image}" == "centos:centos8" ]]; then + mirrors="cd /etc/yum.repos.d/ && \ + sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* && \ + sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* && \ " + fi + + script="${mirrors} + rpm --import https://packages.microsoft.com/keys/microsoft.asc && \ + yum update -y && \ + yum localinstall /mnt/artifacts/${rpmPkg} -y && \ + sqlcmd --help" + + docker pull ${image} + docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ + ${image} \ + /bin/bash -c "${script}" + + echo "" +done + +# -- zypper installs -- +for i in ${!ZYPPER_DISTRO_BASE_IMAGE[@]}; do + image=${ZYPPER_DISTRO_BASE_IMAGE[$i]} + suffix=${ZYPPER_DISTRO_SUFFIX[$i]} + + echo "==========================================================" + echo "Test rpm package on ${image} .${suffix}" + echo "==========================================================" + rpmPkg=sqlcmd-${CLI_VERSION}-${CLI_VERSION_REVISION}.${suffix}.x86_64.rpm + # If testing locally w/o signing, use `--allow-unsigned-rpm` but do not commit: + # zypper --non-interactive install --allow-unsigned-rpm /mnt/artifacts/${rpmPkg} && \ + + script="zypper --non-interactive install curl && \ + rpm -v --import https://packages.microsoft.com/keys/microsoft.asc && \ + zypper --non-interactive install /mnt/artifacts/${rpmPkg} && \ + sqlcmd --help" + + docker pull ${image} + docker run --rm -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}:/mnt/artifacts \ + ${image} \ + /bin/bash -c "${script}" + + echo "" +done diff --git a/release/linux/rpm/pipeline.sh b/release/linux/rpm/pipeline.sh index 1866df0f..7d11aa6a 100755 --- a/release/linux/rpm/pipeline.sh +++ b/release/linux/rpm/pipeline.sh @@ -1,57 +1,57 @@ -#!/usr/bin/env bash -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff rpm package build. The build pipeline can then save it as an -# artifact as it sees fit. -# -# Usage: -# -# foundation images: `centos:centos7|fedora:29` -# -# $ pipeline.sh - -set -exv - -: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" - -if [[ "${BUILD_OUTPUT}" != "" ]]; then - cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd -fi - -DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/rpm} -DISTRO_BASE_IMAGE=( centos:centos7 fedora:29 ) - -CLI_VERSION=${CLI_VERSION:=0.0.1} - -echo "==========================================================" -echo "CLI_VERSION: ${CLI_VERSION}" -echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION:=1}" -echo "Distribution Image: ${DISTRO_BASE_IMAGE}" -echo "Output location: ${DIST_DIR}" -echo "==========================================================" - -mkdir -p ${DIST_DIR} || exit 1 - -for i in ${!DISTRO_BASE_IMAGE[@]}; do - image=${DISTRO_BASE_IMAGE[$i]} - - echo "==========================================================" - echo "Build rpm on ${image}" - echo "==========================================================" - - docker run --rm \ - -v "${REPO_ROOT_DIR}":/mnt/repo \ - -v "${DIST_DIR}":/mnt/output \ - -v "${PIPELINE_WORKSPACE}":/mnt/workspace \ - -e CLI_VERSION=${CLI_VERSION} \ - -e CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} \ - "${image}" \ - /mnt/repo/release/linux/rpm/build-rpm.sh -done +#!/usr/bin/env bash +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff rpm package build. The build pipeline can then save it as an +# artifact as it sees fit. +# +# Usage: +# +# foundation images: `centos:centos7|fedora:29` +# +# $ pipeline.sh + +set -exv + +: "${REPO_ROOT_DIR:=`cd $(dirname $0); cd ../../../; pwd`}" + +if [[ "${BUILD_OUTPUT}" != "" ]]; then + cp ${BUILD_OUTPUT}/SqlcmdLinuxAmd64/sqlcmd ${REPO_ROOT_DIR}/sqlcmd +fi + +DIST_DIR=${BUILD_STAGINGDIRECTORY:=${REPO_ROOT_DIR}/output/rpm} +DISTRO_BASE_IMAGE=( centos:centos7 fedora:29 ) + +CLI_VERSION=${CLI_VERSION:=0.0.1} + +echo "==========================================================" +echo "CLI_VERSION: ${CLI_VERSION}" +echo "CLI_VERSION_REVISION: ${CLI_VERSION_REVISION:=1}" +echo "Distribution Image: ${DISTRO_BASE_IMAGE}" +echo "Output location: ${DIST_DIR}" +echo "==========================================================" + +mkdir -p ${DIST_DIR} || exit 1 + +for i in ${!DISTRO_BASE_IMAGE[@]}; do + image=${DISTRO_BASE_IMAGE[$i]} + + echo "==========================================================" + echo "Build rpm on ${image}" + echo "==========================================================" + + docker run --rm \ + -v "${REPO_ROOT_DIR}":/mnt/repo \ + -v "${DIST_DIR}":/mnt/output \ + -v "${PIPELINE_WORKSPACE}":/mnt/workspace \ + -e CLI_VERSION=${CLI_VERSION} \ + -e CLI_VERSION_REVISION=${CLI_VERSION_REVISION:=1} \ + "${image}" \ + /mnt/repo/release/linux/rpm/build-rpm.sh +done diff --git a/release/linux/rpm/sqlcmd.spec b/release/linux/rpm/sqlcmd.spec index 23384680..9a5da82a 100644 --- a/release/linux/rpm/sqlcmd.spec +++ b/release/linux/rpm/sqlcmd.spec @@ -1,37 +1,37 @@ -# RPM spec file for sqlcmd -# Definition of macros used - https://fedoraproject.org/wiki/Packaging:RPMMacros?rd=Packaging/RPMMacros - -# .el7.centos -> .el7 -%if 0%{?rhel} - %define dist .el%{?rhel} -%endif - -%define name sqlcmd -%define release 1%{?dist} -%define version %{getenv:CLI_VERSION} -%define repo_path %{getenv:REPO_ROOT_DIR} -%define cli_lib_dir %{_libdir}/sqlcmd - -%undefine _missing_build_ids_terminate_build -%global _missing_build_ids_terminate_build 0 - -Summary: MSSQL SQLCMD CLI Tools -License: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE -Name: %{name} -Version: %{version} -Release: %{release} -Url: https://github.com/microsoft/go-sqlcmd -BuildArch: x86_64 - -%description -SQLCMD CLI, a multi-platform command line experience for Microsoft SQL Server and Azure SQL. - -%prep -%install - -# Create executable -mkdir -p %{buildroot}%{_bindir} -cp %{repo_path}/sqlcmd %{buildroot}%{_bindir} - -%files -%attr(0755,root,root) %{_bindir}/sqlcmd +# RPM spec file for sqlcmd +# Definition of macros used - https://fedoraproject.org/wiki/Packaging:RPMMacros?rd=Packaging/RPMMacros + +# .el7.centos -> .el7 +%if 0%{?rhel} + %define dist .el%{?rhel} +%endif + +%define name sqlcmd +%define release 1%{?dist} +%define version %{getenv:CLI_VERSION} +%define repo_path %{getenv:REPO_ROOT_DIR} +%define cli_lib_dir %{_libdir}/sqlcmd + +%undefine _missing_build_ids_terminate_build +%global _missing_build_ids_terminate_build 0 + +Summary: MSSQL SQLCMD CLI Tools +License: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE +Name: %{name} +Version: %{version} +Release: %{release} +Url: https://github.com/microsoft/go-sqlcmd +BuildArch: x86_64 + +%description +SQLCMD CLI, a multi-platform command line experience for Microsoft SQL Server and Azure SQL. + +%prep +%install + +# Create executable +mkdir -p %{buildroot}%{_bindir} +cp %{repo_path}/sqlcmd %{buildroot}%{_bindir} + +%files +%attr(0755,root,root) %{_bindir}/sqlcmd diff --git a/release/windows/choco/sqlcmd.nuspec b/release/windows/choco/sqlcmd.nuspec index 98c97d3c..a5e1682d 100644 --- a/release/windows/choco/sqlcmd.nuspec +++ b/release/windows/choco/sqlcmd.nuspec @@ -1,26 +1,26 @@ - - - - - sqlcmd - 0.8.1 - sqlcmd (Install) - Microsoft - https://docs.microsoft.com/en-us/sql/tools/go-sqlcmd-utility - https://github.com/microsoft/go-sqlcmd/blob/main/release/windows/msi/resources/go-sqlcmd_256.png - Copyright (c) Microsoft Corporation - https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE - false - https://github.com/microsoft/go-sqlcmd - https://github.com/microsoft/go-sqlcmd/tree/main/release/windows/choco - https://docs.microsoft.com/en-us/sql/tools/go-sqlcmd-utility - https://github.com/microsoft/go-sqlcmd/issues - sqlcmd mssql sqlserver - sqlcmd CLI for Microsoft SQL Server and Azure SQL - sqlcmd is a multi-platform command line experience for Microsoft SQL Server and Azure SQL - https://github.com/microsoft/go-sqlcmd/releases/tag/v0.8.1 - - - - - + + + + + sqlcmd + 0.8.1 + sqlcmd (Install) + Microsoft + https://docs.microsoft.com/en-us/sql/tools/go-sqlcmd-utility + https://github.com/microsoft/go-sqlcmd/blob/main/release/windows/msi/resources/go-sqlcmd_256.png + Copyright (c) Microsoft Corporation + https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE + false + https://github.com/microsoft/go-sqlcmd + https://github.com/microsoft/go-sqlcmd/tree/main/release/windows/choco + https://docs.microsoft.com/en-us/sql/tools/go-sqlcmd-utility + https://github.com/microsoft/go-sqlcmd/issues + sqlcmd mssql sqlserver + sqlcmd CLI for Microsoft SQL Server and Azure SQL + sqlcmd is a multi-platform command line experience for Microsoft SQL Server and Azure SQL + https://github.com/microsoft/go-sqlcmd/releases/tag/v0.8.1 + + + + + diff --git a/release/windows/choco/tools/LICENSE.txt b/release/windows/choco/tools/LICENSE.txt index a59f683e..9b20e534 100644 --- a/release/windows/choco/tools/LICENSE.txt +++ b/release/windows/choco/tools/LICENSE.txt @@ -1,25 +1,25 @@ -From: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE - -LICENSE - -MIT License - -Copyright (c) Microsoft Corporation. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE +From: https://github.com/microsoft/go-sqlcmd/blob/main/LICENSE + +LICENSE + +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/release/windows/choco/tools/VERIFICATION.txt b/release/windows/choco/tools/VERIFICATION.txt index fc3bed0d..7a1a6530 100644 --- a/release/windows/choco/tools/VERIFICATION.txt +++ b/release/windows/choco/tools/VERIFICATION.txt @@ -1,10 +1,10 @@ -VERIFICATION -Verification is intended to assist the Chocolatey moderators and community -in verifying that this package's contents are trustworthy. - -Download .msi from Microsoft Download Center -https://download.microsoft.com/download/d/4/4/d4403a51-2ab7-4ea8-b850-d2710c5e1323/sqlcmd_0.8.1-1.msi - -Run checksum -t sha256 -f sqlcmd_0.8.1-1.msi - +VERIFICATION +Verification is intended to assist the Chocolatey moderators and community +in verifying that this package's contents are trustworthy. + +Download .msi from Microsoft Download Center +https://download.microsoft.com/download/d/4/4/d4403a51-2ab7-4ea8-b850-d2710c5e1323/sqlcmd_0.8.1-1.msi + +Run checksum -t sha256 -f sqlcmd_0.8.1-1.msi + We are the software vendor (Microsoft) \ No newline at end of file diff --git a/release/windows/choco/tools/chocolateyinstall.ps1 b/release/windows/choco/tools/chocolateyinstall.ps1 index 52eb228e..168fa5ff 100644 --- a/release/windows/choco/tools/chocolateyinstall.ps1 +++ b/release/windows/choco/tools/chocolateyinstall.ps1 @@ -1,45 +1,45 @@ -$ErrorActionPreference = 'Stop'; - -$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" -$url = '{{DownloadUrl}}' -$url64 = 'https://download.microsoft.com/download/d/4/4/d4403a51-2ab7-4ea8-b850-d2710c5e1323/sqlcmd_0.8.1-1.msi' - -$packageArgs = @{ - packageName = $env:ChocolateyPackageName - unzipLocation = $toolsDir - fileType = 'MSI' - url = $url - url64bit = $url64 - - softwareName = 'sqlcmd*' - - checksum = '{{Checksum}}' - checksumType = '{{ChecksumType}}' - checksum64 = '03587762932D5A66ACFE15D306FE14645D53BC61162B4DA0D9AF29B4A8A1550D' - checksumType64= 'sha256' - - silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" - validExitCodes= @(0, 3010, 1641) -} - -Install-ChocolateyPackage @packageArgs - - - - - - - - - - - - - - - - - - - - +$ErrorActionPreference = 'Stop'; + +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +$url = '{{DownloadUrl}}' +$url64 = 'https://download.microsoft.com/download/d/4/4/d4403a51-2ab7-4ea8-b850-d2710c5e1323/sqlcmd_0.8.1-1.msi' + +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + unzipLocation = $toolsDir + fileType = 'MSI' + url = $url + url64bit = $url64 + + softwareName = 'sqlcmd*' + + checksum = '{{Checksum}}' + checksumType = '{{ChecksumType}}' + checksum64 = '03587762932D5A66ACFE15D306FE14645D53BC61162B4DA0D9AF29B4A8A1550D' + checksumType64= 'sha256' + + silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" + validExitCodes= @(0, 3010, 1641) +} + +Install-ChocolateyPackage @packageArgs + + + + + + + + + + + + + + + + + + + + diff --git a/release/windows/msi/README.md b/release/windows/msi/README.md index 9b87eee7..ba2daf8a 100644 --- a/release/windows/msi/README.md +++ b/release/windows/msi/README.md @@ -1,41 +1,41 @@ -# Windows MSI - -This document provides instructions on creating the MSI. - -## Prerequisites - -1. WIX Toolset -2. Turn on the '.NET Framework 3.5' Windows Feature (required for WIX Toolset) -3. Install [WIX Toolset build tools](http://wixtoolset.org/releases/) if not already installed -4. Install [Microsoft Build Tools](https://www.microsoft.com/en-us/download/details.aspx?id=48159) - -## Building - -1. Set the `CLI_VERSION` environment variable -2. Run `release\windows\msi\scripts\pipeline.cmd` -3. The unsigned MSI will be in the `.\output\msi` folder - -> **Note:** For `building step 1.` above set both env-vars to the same version-tag for the immediate, this will consolidated in the future. - -## Release Install/Update/Uninstall Steps - -> **Note:** Replace `{{HOST}}` and `{{CLI_VERSION}}` with the appropriate values. - -### Install `Sqlcmd Tools` on Windows - -The MSI distributable is used for installing or updating the `Sqlcmd Tools` CLI on Windows. - -[Download the MSI Installer](http://{{HOST}}/sqlcmd-{{CLI_VERSION}}.msi) - -When the installer asks if it can make changes to your computer, click the `Yes` box. - -### Uninstall - -You can uninstall the `SqlCmd Tools` from the Windows _Apps and Features_ list. To uninstall: - -| Platform | Instructions | -| ------------- |--------------------------------------------------------| -| Windows 10 | Start > Settings > Apps | -| Windows 8 | Start > Control Panel > Programs > Uninstall a program | - -The program to uninstall is listed as **Sqlcmd Tools** . Select this application, then click the `Uninstall` button. +# Windows MSI + +This document provides instructions on creating the MSI. + +## Prerequisites + +1. WIX Toolset +2. Turn on the '.NET Framework 3.5' Windows Feature (required for WIX Toolset) +3. Install [WIX Toolset build tools](http://wixtoolset.org/releases/) if not already installed +4. Install [Microsoft Build Tools](https://www.microsoft.com/en-us/download/details.aspx?id=48159) + +## Building + +1. Set the `CLI_VERSION` environment variable +2. Run `release\windows\msi\scripts\pipeline.cmd` +3. The unsigned MSI will be in the `.\output\msi` folder + +> **Note:** For `building step 1.` above set both env-vars to the same version-tag for the immediate, this will consolidated in the future. + +## Release Install/Update/Uninstall Steps + +> **Note:** Replace `{{HOST}}` and `{{CLI_VERSION}}` with the appropriate values. + +### Install `Sqlcmd Tools` on Windows + +The MSI distributable is used for installing or updating the `Sqlcmd Tools` CLI on Windows. + +[Download the MSI Installer](http://{{HOST}}/sqlcmd-{{CLI_VERSION}}.msi) + +When the installer asks if it can make changes to your computer, click the `Yes` box. + +### Uninstall + +You can uninstall the `SqlCmd Tools` from the Windows _Apps and Features_ list. To uninstall: + +| Platform | Instructions | +| ------------- |--------------------------------------------------------| +| Windows 10 | Start > Settings > Apps | +| Windows 8 | Start > Control Panel > Programs > Uninstall a program | + +The program to uninstall is listed as **Sqlcmd Tools** . Select this application, then click the `Uninstall` button. diff --git a/release/windows/msi/eula/eulatext_1033.rtf b/release/windows/msi/eula/eulatext_1033.rtf index 72daf047..1218f61d 100644 --- a/release/windows/msi/eula/eulatext_1033.rtf +++ b/release/windows/msi/eula/eulatext_1033.rtf @@ -1,274 +1,274 @@ -{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff44\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} -{\f2\fbidi \fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New{\*\falt Arial};}{\f3\fbidi \froman\fcharset2\fprq2{\*\panose 05050102010706020507}Symbol{\*\falt Times};} -{\f10\fbidi \fnil\fcharset2\fprq2{\*\panose 05000000000000000000}Wingdings{\*\falt Symbol};}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math{\*\falt Calisto MT};} -{\f44\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604030504040204}Tahoma{\*\falt ?? ??};}{\f45\fbidi \fswiss\fcharset0\fprq2{\*\panose 00000000000000000000}Segoe UI{\*\falt Century Gothic};} -{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};}{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} -{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} -{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};}{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} -{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} -{\f1168\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\f1169\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};}{\f1171\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};} -{\f1172\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};}{\f1173\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\f1174\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\f1175\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\f1176\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\f1188\fbidi \fmodern\fcharset238\fprq1 Courier New CE{\*\falt Arial};} -{\f1189\fbidi \fmodern\fcharset204\fprq1 Courier New Cyr{\*\falt Arial};}{\f1191\fbidi \fmodern\fcharset161\fprq1 Courier New Greek{\*\falt Arial};}{\f1192\fbidi \fmodern\fcharset162\fprq1 Courier New Tur{\*\falt Arial};} -{\f1193\fbidi \fmodern\fcharset177\fprq1 Courier New (Hebrew){\*\falt Arial};}{\f1194\fbidi \fmodern\fcharset178\fprq1 Courier New (Arabic){\*\falt Arial};}{\f1195\fbidi \fmodern\fcharset186\fprq1 Courier New Baltic{\*\falt Arial};} -{\f1196\fbidi \fmodern\fcharset163\fprq1 Courier New (Vietnamese){\*\falt Arial};}{\f1508\fbidi \froman\fcharset238\fprq2 Cambria Math CE{\*\falt Calisto MT};}{\f1509\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr{\*\falt Calisto MT};} -{\f1511\fbidi \froman\fcharset161\fprq2 Cambria Math Greek{\*\falt Calisto MT};}{\f1512\fbidi \froman\fcharset162\fprq2 Cambria Math Tur{\*\falt Calisto MT};}{\f1515\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic{\*\falt Calisto MT};} -{\f1516\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese){\*\falt Calisto MT};}{\f1608\fbidi \fswiss\fcharset238\fprq2 Tahoma CE{\*\falt ?? ??};}{\f1609\fbidi \fswiss\fcharset204\fprq2 Tahoma Cyr{\*\falt ?? ??};} -{\f1611\fbidi \fswiss\fcharset161\fprq2 Tahoma Greek{\*\falt ?? ??};}{\f1612\fbidi \fswiss\fcharset162\fprq2 Tahoma Tur{\*\falt ?? ??};}{\f1613\fbidi \fswiss\fcharset177\fprq2 Tahoma (Hebrew){\*\falt ?? ??};} -{\f1614\fbidi \fswiss\fcharset178\fprq2 Tahoma (Arabic){\*\falt ?? ??};}{\f1615\fbidi \fswiss\fcharset186\fprq2 Tahoma Baltic{\*\falt ?? ??};}{\f1616\fbidi \fswiss\fcharset163\fprq2 Tahoma (Vietnamese){\*\falt ?? ??};} -{\f1617\fbidi \fswiss\fcharset222\fprq2 Tahoma (Thai){\*\falt ?? ??};}{\f1618\fbidi \fswiss\fcharset238\fprq2 Segoe UI CE{\*\falt Century Gothic};}{\f1619\fbidi \fswiss\fcharset204\fprq2 Segoe UI Cyr{\*\falt Century Gothic};} -{\f1621\fbidi \fswiss\fcharset161\fprq2 Segoe UI Greek{\*\falt Century Gothic};}{\f1622\fbidi \fswiss\fcharset162\fprq2 Segoe UI Tur{\*\falt Century Gothic};}{\f1623\fbidi \fswiss\fcharset177\fprq2 Segoe UI (Hebrew){\*\falt Century Gothic};} -{\f1624\fbidi \fswiss\fcharset178\fprq2 Segoe UI (Arabic){\*\falt Century Gothic};}{\f1625\fbidi \fswiss\fcharset186\fprq2 Segoe UI Baltic{\*\falt Century Gothic};}{\f1626\fbidi \fswiss\fcharset163\fprq2 Segoe UI (Vietnamese){\*\falt Century Gothic};} -{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} -{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;} -{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;} -{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;} -{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} -{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} -{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;} -{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} -{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} -{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} -{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} -{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} -{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255; -\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0; -\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;\caccentone\ctint255\cshade191\red46\green116\blue181;\caccentone\ctint255\cshade127\red31\green77\blue120;}{\*\defchp -\fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 -\rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 -\rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink15 \sqformat heading 1;}{ -\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink16 \sqformat -heading 2;}{\s3\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 -\sbasedon0 \snext0 \slink17 \sqformat heading 3;}{\s4\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel3\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 -\fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink18 \sqformat heading 4;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* -\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 -\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused -Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \af0\afs32 \ltrch\fcs0 \fs32\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink1 \slocked \spriority9 Heading 1 Char;}{\*\cs16 \additive \rtlch\fcs1 \af0\afs26 \ltrch\fcs0 -\fs26\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink2 \slocked \ssemihidden \spriority9 Heading 2 Char;}{\*\cs17 \additive \rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs24\cf20\loch\f31502\hich\af31502\dbch\af31501 -\sbasedon10 \slink3 \slocked \ssemihidden \spriority9 Heading 3 Char;}{\*\cs18 \additive \rtlch\fcs1 \ai\af0\afs24 \ltrch\fcs0 \i\fs24\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink4 \slocked \ssemihidden \spriority9 Heading 4 Char;}{\* -\cs19 \additive \rtlch\fcs1 \af44 \ltrch\fcs0 \f44\lang1033\langfe1033\langnp1033\langfenp1033 \sbasedon10 \styrsid6953517 Body 2 Char;}{\s20\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af45\afs18\alang1025 \ltrch\fcs0 -\fs18\lang1033\langfe1033\loch\f45\hich\af45\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext20 \slink21 \ssemihidden \sunhideused \styrsid6953517 Balloon Text;}{\*\cs21 \additive \rtlch\fcs1 \af45\afs18 \ltrch\fcs0 \f45\fs18 -\sbasedon10 \slink20 \slocked \ssemihidden \styrsid6953517 Balloon Text Char;}{\*\cs22 \additive \rtlch\fcs1 \af0\afs16 \ltrch\fcs0 \fs16 \sbasedon10 \ssemihidden \sunhideused \styrsid16546649 annotation reference;}{ -\s23\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs20\alang1025 \ltrch\fcs0 \fs20\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 -\sbasedon0 \snext23 \slink24 \ssemihidden \sunhideused \styrsid16546649 annotation text;}{\*\cs24 \additive \rtlch\fcs1 \af44\afs20 \ltrch\fcs0 \f44\fs20 \sbasedon10 \slink23 \slocked \ssemihidden \styrsid16546649 Comment Text Char;}{ -\s25\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \ab\af44\afs20\alang1025 \ltrch\fcs0 \b\fs20\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 -\sbasedon23 \snext23 \slink26 \ssemihidden \sunhideused \styrsid16546649 annotation subject;}{\*\cs26 \additive \rtlch\fcs1 \ab\af44\afs20 \ltrch\fcs0 \b\f44\fs20 \sbasedon24 \slink25 \slocked \ssemihidden \styrsid16546649 Comment Subject Char;}} -{\*\listtable{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1077\lin1077 } -{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1797\lin1797 }{\listlevel\levelnfc23\levelnfcn23 -\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2517\lin2517 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3237\lin3237 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative -\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3957\lin3957 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext -\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4677\lin4677 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 -\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5397\lin5397 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 -\fi-360\li6117\lin6117 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6837\lin6837 } -{\listname ;}\listid1331057573}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 -\fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1440\lin1440 }{\listlevel -\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0 -\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 -\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext -\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691 -\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;} -\f10\fbias0 \fi-360\li6480\lin6480 }{\listname ;}\listid1549293683}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 -\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1083\lin1083 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 -\fi-360\li1803\lin1803 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2523\lin2523 } -{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3243\lin3243 }{\listlevel\levelnfc23\levelnfcn23 -\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3963\lin3963 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4683\lin4683 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative -\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5403\lin5403 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0 -{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6123\lin6123 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693 -\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6843\lin6843 }{\listname ;}\listid1551651435}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext -\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1077\lin1077 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691 -\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1797\lin1797 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;} -\f10\fbias0 \fi-360\li2517\lin2517 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 -\fi-360\li3237\lin3237 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3957\lin3957 }{\listlevel -\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4677\lin4677 }{\listlevel\levelnfc23\levelnfcn23\leveljc0 -\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5397\lin5397 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6117\lin6117 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 -\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6837\lin6837 }{\listname ;}\listid1579903185}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1083\lin1083 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 -\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1803\lin1803 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext -\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2523\lin2523 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 -\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3243\lin3243 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 -\fi-360\li3963\lin3963 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4683\lin4683 } -{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5403\lin5403 }{\listlevel\levelnfc23\levelnfcn23 -\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6123\lin6123 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 -\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6843\lin6843 }{\listname ;}\listid1705671626}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23 -\leveljc0\leveljcn0\levelfollow0\levelstartat0\levelspace0\levelindent0{\leveltext\leveltemplateid1236590892\'01\u-3913 ?;}{\levelnumbers;}\loch\af45\hich\af45\dbch\af31505\fbias0 \fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0 -\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative -\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0 -{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691 -\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 -\fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5040\lin5040 }{\listlevel -\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0 -\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6480\lin6480 }{\listname ;}\listid1945310376}}{\*\listoverridetable{\listoverride\listid1549293683 -\listoverridecount0\ls1}{\listoverride\listid1945310376\listoverridecount0\ls2}{\listoverride\listid1551651435\listoverridecount0\ls3}{\listoverride\listid1705671626\listoverridecount0\ls4}{\listoverride\listid1579903185\listoverridecount0\ls5} -{\listoverride\listid1331057573\listoverridecount0\ls6}}{\*\pgptbl {\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid155283\rsid407813\rsid1509566\rsid2964304\rsid3172971\rsid3688533\rsid4327701\rsid5321394\rsid5972129\rsid6190351\rsid6953517 -\rsid6964208\rsid8141829\rsid8220119\rsid8328010\rsid8522844\rsid9448370\rsid10163802\rsid10312625\rsid11538022\rsid13516973\rsid13829328\rsid14769922\rsid15139785\rsid15549610\rsid16546649\rsid16605584}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0 -\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim0}{\info{\author Brent Lazarenko (Withincompliance)}{\operator Stuart Padley}{\creatim\yr2017\mo3\dy7\hr12\min41}{\revtim\yr2022\mo3\dy16\hr2\min2}{\version5}{\edmins3} -{\nofpages1}{\nofwords0}{\nofchars0}{\nofcharsws0}{\vern45}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect -\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701 -\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot11538022 \nouicompat \fet0{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1 -\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5 -\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang -{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\sb120\sa120\widctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 -\fs24\lang1033\langfe1033\loch\af44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af45\afs20 \ltrch\fcs0 \f45\fs20\lang1036\langfe1033\langnp1036\insrsid10312625\charrsid10163802 -\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a -9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad -5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 -b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 -0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 -a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f -c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 -0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 -a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 -6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b -4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b -4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100aa5225dfc60600008b1a0000160000007468656d652f7468656d652f -7468656d65312e786d6cec595d8bdb46147d2ff43f08bd3bfe92fcb1c41b6cd9ceb6d94d42eca4e4716c8fadc98e344633de8d0981923c160aa569e943037deb -43691b48a02fe9afd936a54d217fa17746b63c638fbb9b2585a5640d8b343af7ce997bafce1d4997afdc8fa87384134e58dc708b970aae83e3211b9178d2706f -f7bbb99aeb7081e211a22cc60d778eb97b65f7c30f2ea31d11e2083b601ff31dd4704321a63bf93c1fc230e297d814c7706dcc920809384d26f951828ec16f44 -f3a542a1928f10895d274611b8bd311e932176fad2a5bbbb74dea1701a0b2e078634e949d7d8b050d8d1615122f89c0734718e106db830cf881df7f17de13a14 -7101171a6e41fdb9f9ddcb79b4b330a2628bad66d7557f0bbb85c1e8b0a4e64c26836c52cff3bd4a33f3af00546ce23ad54ea553c9fc29001a0e61a52917dda7 -dfaab7dafe02ab81d2438bef76b55d2e1a78cd7f798373d3973f03af40a97f6f03dfed06104503af4029dedfc07b5eb51478065e81527c65035f2d34db5ed5c0 -2b5048497cb8812ef89572b05c6d061933ba6785d77daf5b2d2d9caf50500d5975c929c62c16db6a2d42f758d2058004522448ec88f9148fd110aa3840940c12 -e2ec93490885374531e3305c2815ba8532fc973f4f1da988a01d8c346bc90b98f08d21c9c7e1c3844c45c3fd18bcba1ae4cdcb1fdfbc7cee9c3c7a71f2e89793 -c78f4f1efd9c3a32acf6503cd1ad5e7fffc5df4f3f75fe7afeddeb275fd9f15cc7fffed367bffdfaa51d082b5d85e0d5d7cffe78f1ecd5379ffff9c3130bbc99 -a0810eef930873e73a3e766eb10816a6426032c783e4ed2cfa2122ba45339e701423398bc57f478406fafa1c5164c1b5b019c13b09488c0d787576cf20dc0b93 -9920168fd7c2c8001e30465b2cb146e19a9c4b0b737f164fec9327331d770ba123dbdc018a8dfc766653d05662731984d8a07993a258a0098eb170e4357688b1 -6575770931e27a408609e36c2c9cbbc46921620d499f0c8c6a5a19ed9108f232b711847c1bb139b8e3b418b5adba8d8f4c24dc15885ac8f73135c27815cd048a -6c2efb28a27ac0f791086d247bf364a8e33a5c40a6279832a733c29cdb6c6e24b05e2de9d7405eec693fa0f3c84426821cda7cee23c674649b1d06218aa6366c -8fc4a18efd881f428922e7261336f80133ef10790e7940f1d674df21d848f7e96a701b9455a7b42a107965965872791533a37e7b733a4658490d08bfa1e71189 -4f15f73559f7ff5b5907217df5ed53cbaa2eaaa0371362bda3f6d6647c1b6e5dbc03968cc8c5d7ee369ac53731dc2e9b0decbd74bf976ef77f2fdddbeee7772f -d82b8d06f9965bc574abae36eed1d67dfb9850da13738af7b9daba73e84ca32e0c4a3bf5cc8ab3e7b8690887f24e86090cdc2441cac64998f88488b017a229ec -ef8bae7432e10bd713ee4c19876dbf1ab6fa96783a8b0ed8287d5c2d16e5a3692a1e1c89d578c1cfc6e15143a4e84a75f50896b9576c27ea51794940dabe0d09 -6d329344d942a2ba1c9441520fe610340b09b5b277c2a26e615193ee97a9da6001d4b2acc0d6c9810d57c3f53d30012378a242148f649ed2542fb3ab92f92e33 -bd2d984605c03e625901ab4cd725d7adcb93ab4b4bed0c99364868e566925091513d8c87688417d52947cf42e36d735d5fa5d4a02743a1e683d25ad1a8d6fe8d -c579730d76ebda40635d2968ec1c37dc4ad9879219a269c31dc3633f1c4653a81d2eb7bc884ee0ddd95024e90d7f1e6599265cb4110fd3802bd149d520220227 -0e2551c395cbcfd24063a5218a5bb104827061c9d541562e1a3948ba99643c1ee3a1d0d3ae8dc848a7a7a0f0a95658af2af3f383a5259b41ba7be1e8d819d059 -720b4189f9d5a20ce0887078fb534ca33922f03a3313b255fdad35a685eceaef13550da5e3884e43b4e828ba98a77025e5191d7596c5403b5bac1902aa8564d1 -080713d960f5a01add34eb1a2987ad5df7742319394d34573dd35015d935ed2a66ccb06c036bb13c5f93d7582d430c9aa677f854bad725b7bed4bab57d42d625 -20e059fc2c5df70c0d41a3b69acca026196fcab0d4ecc5a8d93b960b3c85da599a84a6fa95a5dbb5b8653dc23a1d0c9eabf383dd7ad5c2d078b9af549156df3d -f44f136c700fc4a30d2f81675470954af8f09020d810f5d49e24950db845ee8bc5ad0147ce2c210df741c16f7a41c90f72859adfc97965af90abf9cd72aee9fb -e562c72f16daadd243682c228c8a7efacda50bafa2e87cf1e5458d6f7c7d89966fdb2e0d599467eaeb4a5e11575f5f8aa5ed5f5f1c02a2f3a052ead6cbf55625 -572f37bb39afddaae5ea41a5956b57826abbdb0efc5abdfbd0758e14d86b9603afd2a9e52ac520c8799582a45fabe7aa5ea9d4f4aacd5ac76b3e5c6c6360e5a9 -7c2c6201e155bc76ff010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f -7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be -9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980 -ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5b -babac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e -745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f -2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f74 -68656d654d616e616765722e786d6c504b01022d0014000600080000002100aa5225dfc60600008b1a00001600000000000000000000000000d6020000746865 -6d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b0100002700000000000000000000000000d00900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cb0a00000000} -{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d -617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 -6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 -656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} -{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdlocked0 heading 1;\lsdqformat1 \lsdlocked0 heading 2; -\lsdqformat1 \lsdlocked0 heading 3;\lsdqformat1 \lsdlocked0 heading 4;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; -\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; -\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; -\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid; -\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid; -\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2; -\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1; -\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1; -\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; -\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2; -\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3; -\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; -\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; -\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; -\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; -\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; -\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; -\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; -\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; -\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; -\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; -\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; -\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; -\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; -\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; -\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; -\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; -\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; -\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; -\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; -\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; -\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; -\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; -\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; -\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; -\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; -\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; -\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; -\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; -\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000 -02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 -d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000a0a0 -44801439d801feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 +{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff44\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe1033\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} +{\f2\fbidi \fmodern\fcharset0\fprq1{\*\panose 02070309020205020404}Courier New{\*\falt Arial};}{\f3\fbidi \froman\fcharset2\fprq2{\*\panose 05050102010706020507}Symbol{\*\falt Times};} +{\f10\fbidi \fnil\fcharset2\fprq2{\*\panose 05000000000000000000}Wingdings{\*\falt Symbol};}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math{\*\falt Calisto MT};} +{\f44\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604030504040204}Tahoma{\*\falt ?? ??};}{\f45\fbidi \fswiss\fcharset0\fprq2{\*\panose 00000000000000000000}Segoe UI{\*\falt Century Gothic};} +{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};}{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} +{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} +{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};}{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} +{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman{\*\falt Times};} +{\f1168\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\f1169\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};}{\f1171\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};} +{\f1172\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};}{\f1173\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\f1174\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\f1175\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\f1176\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\f1188\fbidi \fmodern\fcharset238\fprq1 Courier New CE{\*\falt Arial};} +{\f1189\fbidi \fmodern\fcharset204\fprq1 Courier New Cyr{\*\falt Arial};}{\f1191\fbidi \fmodern\fcharset161\fprq1 Courier New Greek{\*\falt Arial};}{\f1192\fbidi \fmodern\fcharset162\fprq1 Courier New Tur{\*\falt Arial};} +{\f1193\fbidi \fmodern\fcharset177\fprq1 Courier New (Hebrew){\*\falt Arial};}{\f1194\fbidi \fmodern\fcharset178\fprq1 Courier New (Arabic){\*\falt Arial};}{\f1195\fbidi \fmodern\fcharset186\fprq1 Courier New Baltic{\*\falt Arial};} +{\f1196\fbidi \fmodern\fcharset163\fprq1 Courier New (Vietnamese){\*\falt Arial};}{\f1508\fbidi \froman\fcharset238\fprq2 Cambria Math CE{\*\falt Calisto MT};}{\f1509\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr{\*\falt Calisto MT};} +{\f1511\fbidi \froman\fcharset161\fprq2 Cambria Math Greek{\*\falt Calisto MT};}{\f1512\fbidi \froman\fcharset162\fprq2 Cambria Math Tur{\*\falt Calisto MT};}{\f1515\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic{\*\falt Calisto MT};} +{\f1516\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese){\*\falt Calisto MT};}{\f1608\fbidi \fswiss\fcharset238\fprq2 Tahoma CE{\*\falt ?? ??};}{\f1609\fbidi \fswiss\fcharset204\fprq2 Tahoma Cyr{\*\falt ?? ??};} +{\f1611\fbidi \fswiss\fcharset161\fprq2 Tahoma Greek{\*\falt ?? ??};}{\f1612\fbidi \fswiss\fcharset162\fprq2 Tahoma Tur{\*\falt ?? ??};}{\f1613\fbidi \fswiss\fcharset177\fprq2 Tahoma (Hebrew){\*\falt ?? ??};} +{\f1614\fbidi \fswiss\fcharset178\fprq2 Tahoma (Arabic){\*\falt ?? ??};}{\f1615\fbidi \fswiss\fcharset186\fprq2 Tahoma Baltic{\*\falt ?? ??};}{\f1616\fbidi \fswiss\fcharset163\fprq2 Tahoma (Vietnamese){\*\falt ?? ??};} +{\f1617\fbidi \fswiss\fcharset222\fprq2 Tahoma (Thai){\*\falt ?? ??};}{\f1618\fbidi \fswiss\fcharset238\fprq2 Segoe UI CE{\*\falt Century Gothic};}{\f1619\fbidi \fswiss\fcharset204\fprq2 Segoe UI Cyr{\*\falt Century Gothic};} +{\f1621\fbidi \fswiss\fcharset161\fprq2 Segoe UI Greek{\*\falt Century Gothic};}{\f1622\fbidi \fswiss\fcharset162\fprq2 Segoe UI Tur{\*\falt Century Gothic};}{\f1623\fbidi \fswiss\fcharset177\fprq2 Segoe UI (Hebrew){\*\falt Century Gothic};} +{\f1624\fbidi \fswiss\fcharset178\fprq2 Segoe UI (Arabic){\*\falt Century Gothic};}{\f1625\fbidi \fswiss\fcharset186\fprq2 Segoe UI Baltic{\*\falt Century Gothic};}{\f1626\fbidi \fswiss\fcharset163\fprq2 Segoe UI (Vietnamese){\*\falt Century Gothic};} +{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} +{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;} +{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;} +{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;} +{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} +{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};} +{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;} +{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} +{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} +{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE{\*\falt Times};}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr{\*\falt Times};} +{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek{\*\falt Times};}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur{\*\falt Times};} +{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew){\*\falt Times};}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic){\*\falt Times};} +{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic{\*\falt Times};}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese){\*\falt Times};}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255; +\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0; +\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;\caccentone\ctint255\cshade191\red46\green116\blue181;\caccentone\ctint255\cshade127\red31\green77\blue120;}{\*\defchp +\fs22\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 +\rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 +\rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink15 \sqformat heading 1;}{ +\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink16 \sqformat +heading 2;}{\s3\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 +\sbasedon0 \snext0 \slink17 \sqformat heading 3;}{\s4\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel3\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext0 \slink18 \sqformat heading 4;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* +\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1 +\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp1033 \snext11 \ssemihidden \sunhideused +Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \af0\afs32 \ltrch\fcs0 \fs32\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink1 \slocked \spriority9 Heading 1 Char;}{\*\cs16 \additive \rtlch\fcs1 \af0\afs26 \ltrch\fcs0 +\fs26\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink2 \slocked \ssemihidden \spriority9 Heading 2 Char;}{\*\cs17 \additive \rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs24\cf20\loch\f31502\hich\af31502\dbch\af31501 +\sbasedon10 \slink3 \slocked \ssemihidden \spriority9 Heading 3 Char;}{\*\cs18 \additive \rtlch\fcs1 \ai\af0\afs24 \ltrch\fcs0 \i\fs24\cf19\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink4 \slocked \ssemihidden \spriority9 Heading 4 Char;}{\* +\cs19 \additive \rtlch\fcs1 \af44 \ltrch\fcs0 \f44\lang1033\langfe1033\langnp1033\langfenp1033 \sbasedon10 \styrsid6953517 Body 2 Char;}{\s20\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af45\afs18\alang1025 \ltrch\fcs0 +\fs18\lang1033\langfe1033\loch\f45\hich\af45\dbch\af31505\cgrid\langnp1033\langfenp1033 \sbasedon0 \snext20 \slink21 \ssemihidden \sunhideused \styrsid6953517 Balloon Text;}{\*\cs21 \additive \rtlch\fcs1 \af45\afs18 \ltrch\fcs0 \f45\fs18 +\sbasedon10 \slink20 \slocked \ssemihidden \styrsid6953517 Balloon Text Char;}{\*\cs22 \additive \rtlch\fcs1 \af0\afs16 \ltrch\fcs0 \fs16 \sbasedon10 \ssemihidden \sunhideused \styrsid16546649 annotation reference;}{ +\s23\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs20\alang1025 \ltrch\fcs0 \fs20\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 +\sbasedon0 \snext23 \slink24 \ssemihidden \sunhideused \styrsid16546649 annotation text;}{\*\cs24 \additive \rtlch\fcs1 \af44\afs20 \ltrch\fcs0 \f44\fs20 \sbasedon10 \slink23 \slocked \ssemihidden \styrsid16546649 Comment Text Char;}{ +\s25\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \ab\af44\afs20\alang1025 \ltrch\fcs0 \b\fs20\lang1033\langfe1033\loch\f44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 +\sbasedon23 \snext23 \slink26 \ssemihidden \sunhideused \styrsid16546649 annotation subject;}{\*\cs26 \additive \rtlch\fcs1 \ab\af44\afs20 \ltrch\fcs0 \b\f44\fs20 \sbasedon24 \slink25 \slocked \ssemihidden \styrsid16546649 Comment Subject Char;}} +{\*\listtable{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1077\lin1077 } +{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1797\lin1797 }{\listlevel\levelnfc23\levelnfcn23 +\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2517\lin2517 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3237\lin3237 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative +\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3957\lin3957 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext +\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4677\lin4677 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 +\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5397\lin5397 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 +\fi-360\li6117\lin6117 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6837\lin6837 } +{\listname ;}\listid1331057573}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 +\fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1440\lin1440 }{\listlevel +\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0 +\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 +\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext +\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691 +\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;} +\f10\fbias0 \fi-360\li6480\lin6480 }{\listname ;}\listid1549293683}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 +\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1083\lin1083 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 +\fi-360\li1803\lin1803 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2523\lin2523 } +{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3243\lin3243 }{\listlevel\levelnfc23\levelnfcn23 +\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3963\lin3963 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4683\lin4683 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative +\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5403\lin5403 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0 +{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6123\lin6123 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693 +\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6843\lin6843 }{\listname ;}\listid1551651435}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\leveltext +\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1077\lin1077 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691 +\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1797\lin1797 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;} +\f10\fbias0 \fi-360\li2517\lin2517 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 +\fi-360\li3237\lin3237 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3957\lin3957 }{\listlevel +\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4677\lin4677 }{\listlevel\levelnfc23\levelnfcn23\leveljc0 +\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5397\lin5397 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6117\lin6117 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 +\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6837\lin6837 }{\listname ;}\listid1579903185}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li1083\lin1083 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360 +\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1803\lin1803 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext +\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2523\lin2523 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689 +\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li3243\lin3243 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 +\fi-360\li3963\lin3963 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li4683\lin4683 } +{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5403\lin5403 }{\listlevel\levelnfc23\levelnfcn23 +\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li6123\lin6123 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 +\levelstartat1\lvltentative\levelspace360\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6843\lin6843 }{\listname ;}\listid1705671626}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23 +\leveljc0\leveljcn0\levelfollow0\levelstartat0\levelspace0\levelindent0{\leveltext\leveltemplateid1236590892\'01\u-3913 ?;}{\levelnumbers;}\loch\af45\hich\af45\dbch\af31505\fbias0 \fi-360\li720\lin720 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0 +\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative +\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li2160\lin2160 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0 +{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691 +\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 +\fi-360\li4320\lin4320 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01\u-3913 ?;}{\levelnumbers;}\f3\fbias0 \fi-360\li5040\lin5040 }{\listlevel +\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01o;}{\levelnumbers;}\f2\fbias0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0 +\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01\u-3929 ?;}{\levelnumbers;}\f10\fbias0 \fi-360\li6480\lin6480 }{\listname ;}\listid1945310376}}{\*\listoverridetable{\listoverride\listid1549293683 +\listoverridecount0\ls1}{\listoverride\listid1945310376\listoverridecount0\ls2}{\listoverride\listid1551651435\listoverridecount0\ls3}{\listoverride\listid1705671626\listoverridecount0\ls4}{\listoverride\listid1579903185\listoverridecount0\ls5} +{\listoverride\listid1331057573\listoverridecount0\ls6}}{\*\pgptbl {\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid155283\rsid407813\rsid1509566\rsid2964304\rsid3172971\rsid3688533\rsid4327701\rsid5321394\rsid5972129\rsid6190351\rsid6953517 +\rsid6964208\rsid8141829\rsid8220119\rsid8328010\rsid8522844\rsid9448370\rsid10163802\rsid10312625\rsid11538022\rsid13516973\rsid13829328\rsid14769922\rsid15139785\rsid15549610\rsid16546649\rsid16605584}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0 +\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim0}{\info{\author Brent Lazarenko (Withincompliance)}{\operator Stuart Padley}{\creatim\yr2017\mo3\dy7\hr12\min41}{\revtim\yr2022\mo3\dy16\hr2\min2}{\version5}{\edmins3} +{\nofpages1}{\nofwords0}{\nofchars0}{\nofcharsws0}{\vern45}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect +\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701 +\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale100\rsidroot11538022 \nouicompat \fet0{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1 +\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5 +\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang +{\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\sb120\sa120\widctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af44\afs24\alang1025 \ltrch\fcs0 +\fs24\lang1033\langfe1033\loch\af44\hich\af44\dbch\af31505\cgrid\langnp1033\langfenp1033 {\rtlch\fcs1 \af45\afs20 \ltrch\fcs0 \f45\fs20\lang1036\langfe1033\langnp1036\insrsid10312625\charrsid10163802 +\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a +9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad +5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 +b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 +0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 +a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f +c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 +0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 +a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 +6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b +4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b +4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100aa5225dfc60600008b1a0000160000007468656d652f7468656d652f +7468656d65312e786d6cec595d8bdb46147d2ff43f08bd3bfe92fcb1c41b6cd9ceb6d94d42eca4e4716c8fadc98e344633de8d0981923c160aa569e943037deb +43691b48a02fe9afd936a54d217fa17746b63c638fbb9b2585a5640d8b343af7ce997bafce1d4997afdc8fa87384134e58dc708b970aae83e3211b9178d2706f +f7bbb99aeb7081e211a22cc60d778eb97b65f7c30f2ea31d11e2083b601ff31dd4704321a63bf93c1fc230e297d814c7706dcc920809384d26f951828ec16f44 +f3a542a1928f10895d274611b8bd311e932176fad2a5bbbb74dea1701a0b2e078634e949d7d8b050d8d1615122f89c0734718e106db830cf881df7f17de13a14 +7101171a6e41fdb9f9ddcb79b4b330a2628bad66d7557f0bbb85c1e8b0a4e64c26836c52cff3bd4a33f3af00546ce23ad54ea553c9fc29001a0e61a52917dda7 +dfaab7dafe02ab81d2438bef76b55d2e1a78cd7f798373d3973f03af40a97f6f03dfed06104503af4029dedfc07b5eb51478065e81527c65035f2d34db5ed5c0 +2b5048497cb8812ef89572b05c6d061933ba6785d77daf5b2d2d9caf50500d5975c929c62c16db6a2d42f758d2058004522448ec88f9148fd110aa3840940c12 +e2ec93490885374531e3305c2815ba8532fc973f4f1da988a01d8c346bc90b98f08d21c9c7e1c3844c45c3fd18bcba1ae4cdcb1fdfbc7cee9c3c7a71f2e89793 +c78f4f1efd9c3a32acf6503cd1ad5e7fffc5df4f3f75fe7afeddeb275fd9f15cc7fffed367bffdfaa51d082b5d85e0d5d7cffe78f1ecd5379ffff9c3130bbc99 +a0810eef930873e73a3e766eb10816a6426032c783e4ed2cfa2122ba45339e701423398bc57f478406fafa1c5164c1b5b019c13b09488c0d787576cf20dc0b93 +9920168fd7c2c8001e30465b2cb146e19a9c4b0b737f164fec9327331d770ba123dbdc018a8dfc766653d05662731984d8a07993a258a0098eb170e4357688b1 +6575770931e27a408609e36c2c9cbbc46921620d499f0c8c6a5a19ed9108f232b711847c1bb139b8e3b418b5adba8d8f4c24dc15885ac8f73135c27815cd048a +6c2efb28a27ac0f791086d247bf364a8e33a5c40a6279832a733c29cdb6c6e24b05e2de9d7405eec693fa0f3c84426821cda7cee23c674649b1d06218aa6366c +8fc4a18efd881f428922e7261336f80133ef10790e7940f1d674df21d848f7e96a701b9455a7b42a107965965872791533a37e7b733a4658490d08bfa1e71189 +4f15f73559f7ff5b5907217df5ed53cbaa2eaaa0371362bda3f6d6647c1b6e5dbc03968cc8c5d7ee369ac53731dc2e9b0decbd74bf976ef77f2fdddbeee7772f +d82b8d06f9965bc574abae36eed1d67dfb9850da13738af7b9daba73e84ca32e0c4a3bf5cc8ab3e7b8690887f24e86090cdc2441cac64998f88488b017a229ec +ef8bae7432e10bd713ee4c19876dbf1ab6fa96783a8b0ed8287d5c2d16e5a3692a1e1c89d578c1cfc6e15143a4e84a75f50896b9576c27ea51794940dabe0d09 +6d329344d942a2ba1c9441520fe610340b09b5b277c2a26e615193ee97a9da6001d4b2acc0d6c9810d57c3f53d30012378a242148f649ed2542fb3ab92f92e33 +bd2d984605c03e625901ab4cd725d7adcb93ab4b4bed0c99364868e566925091513d8c87688417d52947cf42e36d735d5fa5d4a02743a1e683d25ad1a8d6fe8d +c579730d76ebda40635d2968ec1c37dc4ad9879219a269c31dc3633f1c4653a81d2eb7bc884ee0ddd95024e90d7f1e6599265cb4110fd3802bd149d520220227 +0e2551c395cbcfd24063a5218a5bb104827061c9d541562e1a3948ba99643c1ee3a1d0d3ae8dc848a7a7a0f0a95658af2af3f383a5259b41ba7be1e8d819d059 +720b4189f9d5a20ce0887078fb534ca33922f03a3313b255fdad35a685eceaef13550da5e3884e43b4e828ba98a77025e5191d7596c5403b5bac1902aa8564d1 +080713d960f5a01add34eb1a2987ad5df7742319394d34573dd35015d935ed2a66ccb06c036bb13c5f93d7582d430c9aa677f854bad725b7bed4bab57d42d625 +20e059fc2c5df70c0d41a3b69acca026196fcab0d4ecc5a8d93b960b3c85da599a84a6fa95a5dbb5b8653dc23a1d0c9eabf383dd7ad5c2d078b9af549156df3d +f44f136c700fc4a30d2f81675470954af8f09020d810f5d49e24950db845ee8bc5ad0147ce2c210df741c16f7a41c90f72859adfc97965af90abf9cd72aee9fb +e562c72f16daadd243682c228c8a7efacda50bafa2e87cf1e5458d6f7c7d89966fdb2e0d599467eaeb4a5e11575f5f8aa5ed5f5f1c02a2f3a052ead6cbf55625 +572f37bb39afddaae5ea41a5956b57826abbdb0efc5abdfbd0758e14d86b9603afd2a9e52ac520c8799582a45fabe7aa5ea9d4f4aacd5ac76b3e5c6c6360e5a9 +7c2c6201e155bc76ff010000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f +7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be +9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980 +ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5b +babac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e +745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f +2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f74 +68656d654d616e616765722e786d6c504b01022d0014000600080000002100aa5225dfc60600008b1a00001600000000000000000000000000d6020000746865 +6d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b0100002700000000000000000000000000d00900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cb0a00000000} +{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d +617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 +6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 +656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} +{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdlocked0 heading 1;\lsdqformat1 \lsdlocked0 heading 2; +\lsdqformat1 \lsdlocked0 heading 3;\lsdqformat1 \lsdlocked0 heading 4;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; +\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; +\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid; +\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid; +\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2; +\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1; +\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1; +\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; +\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2; +\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3; +\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; +\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; +\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; +\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; +\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; +\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; +\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; +\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; +\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; +\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; +\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; +\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; +\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; +\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; +\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; +\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; +\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; +\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; +\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; +\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; +\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; +\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; +\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; +\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; +\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000 +02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e5000000000000000000000000a0a0 +44801439d801feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/release/windows/msi/product.wxs b/release/windows/msi/product.wxs index 09bf3db2..c402b6a4 100644 --- a/release/windows/msi/product.wxs +++ b/release/windows/msi/product.wxs @@ -1,74 +1,74 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/release/windows/msi/scripts/pipeline-test.ps1 b/release/windows/msi/scripts/pipeline-test.ps1 index 5f5065ba..bf582ad8 100644 --- a/release/windows/msi/scripts/pipeline-test.ps1 +++ b/release/windows/msi/scripts/pipeline-test.ps1 @@ -1,45 +1,45 @@ -#------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All rights reserved. -#------------------------------------------------------------------------------ - -# Description: -# -# Instructions to be invoked under the build CI pipeline in AzureDevOps. -# -# Kickoff MSI package install test. -# -# Usage: -# -# set SYSTEM_ARTIFACTSDIRECTORY=\path\to\msi\sqlcmd-.msi -# -# $ pipeline-test.ps1 - -if (-not (Test-Path env:CLI_VERSION)) { $env:CLI_VERSION = '0.0.1' } -if (-not (Test-Path env:CLI_VERSION_REVISION)) { $env:CLI_VERSION_REVISION = '1' } - -tree /A /F $env:SYSTEM_ARTIFACTSDIRECTORY - -$msiPath = Join-Path $env:SYSTEM_ARTIFACTSDIRECTORY ("sqlcmd_" + $env:CLI_VERSION + "-" + $env:CLI_VERSION_REVISION + ".msi") - -$msiPath - -$InstallArgs = @( - "/I" - $msiPath - "/norestart" - "/L*v" - ".\install-logs.txt" - "/qn" -) - -Write-Output "Starting msi install $msiPath..." -Start-Process "msiexec.exe" -ArgumentList $InstallArgs -Wait -NoNewWindow -Get-Content .\install-logs.txt - -Write-Output "Done installing msi, checking PATH setup..." -Write-Output "$env:path" -$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") -Write-Output "$env:path" - -$sqlcmd = $env:ProgramFiles + "\SqlCmd\Tools\sqlcmd" -& $sqlcmd --help +#------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +#------------------------------------------------------------------------------ + +# Description: +# +# Instructions to be invoked under the build CI pipeline in AzureDevOps. +# +# Kickoff MSI package install test. +# +# Usage: +# +# set SYSTEM_ARTIFACTSDIRECTORY=\path\to\msi\sqlcmd-.msi +# +# $ pipeline-test.ps1 + +if (-not (Test-Path env:CLI_VERSION)) { $env:CLI_VERSION = '0.0.1' } +if (-not (Test-Path env:CLI_VERSION_REVISION)) { $env:CLI_VERSION_REVISION = '1' } + +tree /A /F $env:SYSTEM_ARTIFACTSDIRECTORY + +$msiPath = Join-Path $env:SYSTEM_ARTIFACTSDIRECTORY ("sqlcmd_" + $env:CLI_VERSION + "-" + $env:CLI_VERSION_REVISION + ".msi") + +$msiPath + +$InstallArgs = @( + "/I" + $msiPath + "/norestart" + "/L*v" + ".\install-logs.txt" + "/qn" +) + +Write-Output "Starting msi install $msiPath..." +Start-Process "msiexec.exe" -ArgumentList $InstallArgs -Wait -NoNewWindow +Get-Content .\install-logs.txt + +Write-Output "Done installing msi, checking PATH setup..." +Write-Output "$env:path" +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +Write-Output "$env:path" + +$sqlcmd = $env:ProgramFiles + "\SqlCmd\Tools\sqlcmd" +& $sqlcmd --help diff --git a/release/windows/msi/scripts/pipeline.cmd b/release/windows/msi/scripts/pipeline.cmd index 1e9acb37..39ece774 100644 --- a/release/windows/msi/scripts/pipeline.cmd +++ b/release/windows/msi/scripts/pipeline.cmd @@ -1,76 +1,76 @@ -@echo off -SetLocal EnableDelayedExpansion -echo build a msi installer. You need to have curl.exe, unzip.exe and msbuild.exe available under PATH -echo. - -set "PATH=%PATH%;%ProgramFiles%\Git\bin;%ProgramFiles%\Git\usr\bin;C:\Program Files (x86)\Git\bin;C:\Program Files (x86)\MSBuild\14.0\Bin;C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin;C:\Program Files (x86)\Windows Kits\10;" -echo %PATH% - -if "%CLI_VERSION%"=="" ( - set CLI_VERSION=0.0.1 -) - -if "%WIX_DOWNLOAD_URL%"=="" ( - echo Please set the WIX_DOWNLOAD_URL environment variable, e.g. https://host/wix314-binaries-mirror.zip - goto ERROR -) - -:: Set up the output directory and temp. directories -echo Cleaning previous build artifacts... -set OUTPUT_DIR=%~dp0..\..\..\..\output\msi -if exist %OUTPUT_DIR% rmdir /s /q %OUTPUT_DIR% -mkdir %OUTPUT_DIR% - -set ARTIFACTS_DIR=%~dp0..\..\..\..\output\msi\artifacts -mkdir %ARTIFACTS_DIR% - -set WIX_DIR=%ARTIFACTS_DIR%\wix -set REPO_ROOT=%~dp0..\..\..\.. - -set PIPELINE_WORKSPACE=%ARTIFACTS_DIR%\workspace - -mkdir %PIPELINE_WORKSPACE%\SqlcmdWindowsAmd64 - -copy /y %REPO_ROOT%\sqlcmd.exe %PIPELINE_WORKSPACE%\SqlcmdWindowsAmd64\sqlcmd.exe - -::ensure wix is available -if exist %WIX_DIR% ( - echo Using existing Wix at %WIX_DIR% -) -if not exist %WIX_DIR% ( - mkdir %WIX_DIR% - pushd %WIX_DIR% - echo Downloading Wix. - curl -o wix-archive.zip %WIX_DOWNLOAD_URL% -k - unzip -q wix-archive.zip - if %errorlevel% neq 0 goto ERROR - del wix-archive.zip - echo Wix downloaded and extracted successfully. - popd -) - -if %errorlevel% neq 0 goto ERROR - -set PATH=%PATH%;%WIX_DIR% - -@echo off - -:: During pipeline we want to skip msbuild here and use the AzureDevOps Task instead -if "%1"=="--skip-msbuild" ( - echo Skipping inline MSI Build... -) else ( - echo Building MSI... - cd %OUTPUT_DIR% - msbuild /t:rebuild /p:Configuration=Release %REPO_ROOT%\release\windows\msi\sqlcmd.wixproj -p:OutDir=%OUTPUT_DIR%\ - start %OUTPUT_DIR% -) - -goto END - -:ERROR -echo Error occurred, please check the output for details. -exit /b 1 - -:END -exit /b 0 -popd +@echo off +SetLocal EnableDelayedExpansion +echo build a msi installer. You need to have curl.exe, unzip.exe and msbuild.exe available under PATH +echo. + +set "PATH=%PATH%;%ProgramFiles%\Git\bin;%ProgramFiles%\Git\usr\bin;C:\Program Files (x86)\Git\bin;C:\Program Files (x86)\MSBuild\14.0\Bin;C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin;C:\Program Files (x86)\Windows Kits\10;" +echo %PATH% + +if "%CLI_VERSION%"=="" ( + set CLI_VERSION=0.0.1 +) + +if "%WIX_DOWNLOAD_URL%"=="" ( + echo Please set the WIX_DOWNLOAD_URL environment variable, e.g. https://host/wix314-binaries-mirror.zip + goto ERROR +) + +:: Set up the output directory and temp. directories +echo Cleaning previous build artifacts... +set OUTPUT_DIR=%~dp0..\..\..\..\output\msi +if exist %OUTPUT_DIR% rmdir /s /q %OUTPUT_DIR% +mkdir %OUTPUT_DIR% + +set ARTIFACTS_DIR=%~dp0..\..\..\..\output\msi\artifacts +mkdir %ARTIFACTS_DIR% + +set WIX_DIR=%ARTIFACTS_DIR%\wix +set REPO_ROOT=%~dp0..\..\..\.. + +set PIPELINE_WORKSPACE=%ARTIFACTS_DIR%\workspace + +mkdir %PIPELINE_WORKSPACE%\SqlcmdWindowsAmd64 + +copy /y %REPO_ROOT%\sqlcmd.exe %PIPELINE_WORKSPACE%\SqlcmdWindowsAmd64\sqlcmd.exe + +::ensure wix is available +if exist %WIX_DIR% ( + echo Using existing Wix at %WIX_DIR% +) +if not exist %WIX_DIR% ( + mkdir %WIX_DIR% + pushd %WIX_DIR% + echo Downloading Wix. + curl -o wix-archive.zip %WIX_DOWNLOAD_URL% -k + unzip -q wix-archive.zip + if %errorlevel% neq 0 goto ERROR + del wix-archive.zip + echo Wix downloaded and extracted successfully. + popd +) + +if %errorlevel% neq 0 goto ERROR + +set PATH=%PATH%;%WIX_DIR% + +@echo off + +:: During pipeline we want to skip msbuild here and use the AzureDevOps Task instead +if "%1"=="--skip-msbuild" ( + echo Skipping inline MSI Build... +) else ( + echo Building MSI... + cd %OUTPUT_DIR% + msbuild /t:rebuild /p:Configuration=Release %REPO_ROOT%\release\windows\msi\sqlcmd.wixproj -p:OutDir=%OUTPUT_DIR%\ + start %OUTPUT_DIR% +) + +goto END + +:ERROR +echo Error occurred, please check the output for details. +exit /b 1 + +:END +exit /b 0 +popd diff --git a/release/windows/msi/sqlcmd.sln b/release/windows/msi/sqlcmd.sln index ba7eafad..5969ab16 100644 --- a/release/windows/msi/sqlcmd.sln +++ b/release/windows/msi/sqlcmd.sln @@ -1,31 +1,31 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31729.503 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "sqlcmd", "sqlcmd.wixproj", "{10101119-735C-48F9-A3FD-B3362A78BA8A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x64.ActiveCfg = Debug|x64 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x64.Build.0 = Debug|x64 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x86.ActiveCfg = Debug|x86 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x86.Build.0 = Debug|x86 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x64.ActiveCfg = Release|x64 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x64.Build.0 = Release|x64 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x86.ActiveCfg = Release|x86 - {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x86.Build.0 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {D9594663-3E11-4507-AEEB-AB2714988330} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31729.503 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "sqlcmd", "sqlcmd.wixproj", "{10101119-735C-48F9-A3FD-B3362A78BA8A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x64.ActiveCfg = Debug|x64 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x64.Build.0 = Debug|x64 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x86.ActiveCfg = Debug|x86 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Debug|x86.Build.0 = Debug|x86 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x64.ActiveCfg = Release|x64 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x64.Build.0 = Release|x64 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x86.ActiveCfg = Release|x86 + {10101119-735C-48F9-A3FD-B3362A78BA8A}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D9594663-3E11-4507-AEEB-AB2714988330} + EndGlobalSection +EndGlobal diff --git a/release/windows/msi/sqlcmd.wixproj b/release/windows/msi/sqlcmd.wixproj index 5efe52a9..79393634 100644 --- a/release/windows/msi/sqlcmd.wixproj +++ b/release/windows/msi/sqlcmd.wixproj @@ -1,47 +1,47 @@ - - - - Debug - x64 - 3.10 - {10101119-735c-48f9-a3fd-b3362a78ba8a} - 2.0 - sqlcmd - Package - 0.0.1 - B8C0DBFD-DCED-44D4-9660-206CDE2D91BC - false - $(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets - $(WIX_DIR)\Wix.targets - $(WIX_DIR) - $(WixToolPath)\wixtasks.dll - $(WixToolPath) - $(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix.targets - - - bin\$(Configuration)\ - obj\$(Configuration)\ - Debug;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE) - - - bin\$(Configuration)\ - obj\$(Configuration)\ - Release;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE) - - - - - True - Debug;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE);CLI_VERSION=0.1.0.0 - - - - - - - $(WixExtDir)\WixUIExtension.dll - WixUIExtension - - - - + + + + Debug + x64 + 3.10 + {10101119-735c-48f9-a3fd-b3362a78ba8a} + 2.0 + sqlcmd + Package + 0.0.1 + B8C0DBFD-DCED-44D4-9660-206CDE2D91BC + false + $(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets + $(WIX_DIR)\Wix.targets + $(WIX_DIR) + $(WixToolPath)\wixtasks.dll + $(WixToolPath) + $(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix.targets + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE) + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Release;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE) + + + + + True + Debug;SQLCMD_PACKAGE_VERSION=$(SQLCMD_PACKAGE_VERSION);SQLCMD_UPGRADE_CODE=$(SQLCMD_UPGRADE_CODE);CLI_VERSION=0.1.0.0 + + + + + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + +