This commit is contained in:
stuartpa 2022-11-10 07:11:08 -08:00
Родитель f3d651a3b2
Коммит 5a0c911abe
30 изменённых файлов: 3950 добавлений и 3948 удалений

2
.gitattributes поставляемый Normal file
Просмотреть файл

@ -0,0 +1,2 @@
* text=auto

40
.github/workflows/golangci-lint.yml поставляемый
Просмотреть файл

@ -1,20 +1,20 @@
name: golangci-lint
on:
push:
branches:
- main
pull_request:
jobs:
golangci-pr:
name: lint-pr-changes
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true
name: golangci-lint
on:
push:
branches:
- main
pull_request:
jobs:
golangci-pr:
name: lint-pr-changes
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
only-new-issues: true

Просмотреть файл

@ -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()

Просмотреть файл

@ -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'

2
cmd/sqlcmd/testdata/select100.sql поставляемый
Просмотреть файл

@ -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)
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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

Разница между файлами не показана из-за своего большого размера Загрузить разницу

4
pkg/sqlcmd/testdata/singlebatchnogo.sql поставляемый
Просмотреть файл

@ -1,2 +1,2 @@
select 100 as num
select 'string' as title
select 100 as num
select 'string' as title

6
pkg/sqlcmd/testdata/twobatchnoendinggo.sql поставляемый
Просмотреть файл

@ -1,3 +1,3 @@
select 100 as num
go
select 'string' as title
select 100 as num
go
select 'string' as title

8
pkg/sqlcmd/testdata/twobatchwithgo.sql поставляемый
Просмотреть файл

@ -1,4 +1,4 @@
select 100 as num
GO
select 'string' as title
GO
select 100 as num
GO
select 'string' as title
GO

Просмотреть файл

@ -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)
}
}
}

6
testdata/sql.txt поставляемый
Просмотреть файл

@ -1,3 +1,3 @@
select 1 as col1
go
select 1 as col1
go