зеркало из https://github.com/microsoft/go-sqlcmd.git
Normalize eol
This commit is contained in:
Родитель
f3d651a3b2
Коммит
5a0c911abe
|
@ -0,0 +1,2 @@
|
|||
* text=auto
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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’
|
||||
|
|
|
@ -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)'
|
||||
|
|
@ -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()
|
||||
|
|
50
SUPPORT.md
50
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.
|
||||
|
|
|
@ -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 }}'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1 +1 @@
|
|||
select 100
|
||||
select 100
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
1326
pkg/sqlcmd/format.go
1326
pkg/sqlcmd/format.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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,2 +1,2 @@
|
|||
select 100 as num
|
||||
select 'string' as title
|
||||
select 100 as num
|
||||
select 'string' as title
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
select 100 as num
|
||||
go
|
||||
select 'string' as title
|
||||
select 100 as num
|
||||
go
|
||||
select 'string' as title
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
select 100 as num
|
||||
GO
|
||||
select 'string' as title
|
||||
GO
|
||||
select 100 as num
|
||||
GO
|
||||
select 'string' as title
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
select 1 as col1
|
||||
go
|
||||
|
||||
select 1 as col1
|
||||
go
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче