diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a944f693 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,46 @@ +# go-sqlcmd Development Container +FROM mcr.microsoft.com/devcontainers/go:1.24-bookworm + +# Install additional OS packages +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get install -y curl libkrb5-dev gnupg2 \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Install Microsoft ODBC driver and mssql-tools18 (legacy ODBC sqlcmd/bcp for compatibility testing) +RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg \ + && curl -fsSL https://packages.microsoft.com/config/debian/12/prod.list | tee /etc/apt/sources.list.d/mssql-release.list \ + && apt-get update \ + && ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools18 unixodbc-dev \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* +ENV PATH="/opt/mssql-tools18/bin:${PATH}" + +# Install golangci-lint for code quality +# Download pre-built binary with SHA256 checksum verification (supply chain security) +# Supports both amd64 and arm64 architectures +ARG GOLANGCI_LINT_VERSION=1.64.8 +ARG GOLANGCI_LINT_SHA256_AMD64=b6270687afb143d019f387c791cd2a6f1cb383be9b3124d241ca11bd3ce2e54e +ARG GOLANGCI_LINT_SHA256_ARM64=a6ab58ebcb1c48572622146cdaec2956f56871038a54ed1149f1386e287789a5 +RUN ARCH=$(dpkg --print-architecture) \ + && if [ "$ARCH" = "amd64" ]; then \ + CHECKSUM="${GOLANGCI_LINT_SHA256_AMD64}"; \ + elif [ "$ARCH" = "arm64" ]; then \ + CHECKSUM="${GOLANGCI_LINT_SHA256_ARM64}"; \ + else \ + echo "Unsupported architecture: $ARCH" && exit 1; \ + fi \ + && curl -fsSLO "https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}.tar.gz" \ + && echo "${CHECKSUM} golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}.tar.gz" | sha256sum -c - \ + && tar -xzf "golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}.tar.gz" \ + && mv "golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}/golangci-lint" /usr/local/bin/ \ + && rm -rf "golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}" "golangci-lint-${GOLANGCI_LINT_VERSION}-linux-${ARCH}.tar.gz" \ + && golangci-lint --version + +# Install additional Go tools (pinned versions for reproducibility) +RUN go install golang.org/x/tools/gopls@v0.18.1 \ + && go install github.com/go-delve/delve/cmd/dlv@v1.24.1 \ + && go install honnef.co/go/tools/cmd/staticcheck@v0.6.1 \ + && go install golang.org/x/text/cmd/gotext@v0.22.0 + +# Create bin directory for local sqlcmd builds +RUN mkdir -p /home/vscode/bin && chown vscode:vscode /home/vscode/bin +ENV PATH="/home/vscode/bin:${PATH}" diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000..13a5422c --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,51 @@ +# go-sqlcmd Development Container + +Dev Container / Codespaces environment with Go 1.24 and SQL Server 2025. + +## Quick Start + +**VS Code**: Open repo β†’ click "Reopen in Container" when prompted. + +**Codespaces**: Click "Code" β†’ "Codespaces" β†’ "Create codespace". + +First build takes ~5 minutes. + +## Commands + +| Alias | What it does | +|-------|--------------| +| `gtest` | Run tests | +| `ginstall` | Build and install sqlcmd to ~/bin | +| `glint` | Run golangci-lint | +| `sql` | Connect to SQL Server (go-sqlcmd) | +| `sql-legacy` | Connect with legacy ODBC sqlcmd | +| `test-db` | Test database connection | + +## SQL Server Connection + +- **Server**: `localhost,1433` +- **User**: `sa` +- **Password**: `$SQLCMDPASSWORD` env var (`SqlCmd@2025!` for local dev) +- **Database**: `master` or `SqlCmdTest` + +Port 1433 is forwarded β€” connect from host tools (ADS, SSMS) using same credentials. + +## Two sqlcmd Versions + +- **go-sqlcmd**: `~/bin/sqlcmd` (default in PATH, use `sql` alias) +- **Legacy ODBC**: `/opt/mssql-tools18/bin/sqlcmd` (use `sql-legacy` alias) + +## Customization + +**Change SQL version**: Edit `docker-compose.yml` image tag. + +**Add setup scripts**: Edit `.devcontainer/mssql/setup.sql`. + +**Change password**: Update `docker-compose.yml` and `devcontainer.json`. + +## Troubleshooting + +- **ARM64 (Apple Silicon)**: Use GitHub Codespaces instead - SQL Server has no native ARM64 images +- **SQL Server not starting**: Check `docker logs $(docker ps -qf "name=db")`. Needs 2GB+ RAM +- **Connection refused**: Wait ~30s for SQL Server to start +- **sqlcmd not found**: Run `ginstall` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..41021397 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,91 @@ +{ + "name": "go-sqlcmd Development", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspaces/go-sqlcmd", + "shutdownAction": "stopCompose", + + // Configure tool-specific properties + "customizations": { + "vscode": { + "extensions": [ + "golang.go", + "ms-mssql.mssql", + "ms-azuretools.vscode-docker", + "GitHub.copilot", + "GitHub.copilot-chat", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "streetsidesoftware.code-spell-checker" + ], + "settings": { + "go.toolsManagement.autoUpdate": false, + "go.useLanguageServer": true, + "go.lintTool": "golangci-lint", + "go.lintFlags": ["--fast"], + "go.testEnvVars": { + "SQLCMDSERVER": "localhost", + "SQLCMDUSER": "sa", + "SQLCMDPASSWORD": "${env:SQLCMDPASSWORD}", + "SQLCMDDATABASE": "master" + }, + "mssql.connections": [ + { + "server": "localhost,1433", + "database": "master", + "authenticationType": "SqlLogin", + "user": "sa", + "password": "", + "savePassword": false, + "profileName": "sqlcmd-container (use SQLCMDPASSWORD env var)", + "encrypt": "Optional", + "trustServerCertificate": true + } + ], + "editor.formatOnSave": true, + "editor.defaultFormatter": "golang.go", + "[go]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "terminal.integrated.defaultProfile.linux": "bash" + } + } + }, + + // Forward the SQL Server port + "forwardPorts": [1433], + "portsAttributes": { + "1433": { + "label": "SQL Server", + "onAutoForward": "silent" + } + }, + + // Use 'postCreateCommand' to run commands after the container is created + "postCreateCommand": "bash .devcontainer/post-create.sh", + + // Environment variables for tests - password must match docker-compose.yml + // This is a development-only container credential, not a production secret. + // For GitHub Codespaces, you can override SQLCMDPASSWORD via Codespaces Secrets. + "remoteEnv": { + "SQLCMDSERVER": "localhost", + "SQLCMDUSER": "sa", + "SQLCMDPASSWORD": "${localEnv:SQLCMDPASSWORD:SqlCmd@2025!}", + "SQLCMDDATABASE": "master", + "SQLCMDDBNAME": "master" + }, + + // Features to add to the dev container + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "latest", + "moby": true + }, + "ghcr.io/devcontainers/features/github-cli:1": { + "version": "latest" + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 00000000..010fd74f --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,37 @@ +version: '3.8' + +services: + devcontainer: + build: + context: . + dockerfile: Dockerfile + volumes: + - ..:/workspaces/go-sqlcmd:cached + # Overrides default command so things don't shut down after the process ends. + command: sleep infinity + # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. + network_mode: service:db + depends_on: + db: + condition: service_healthy + + db: + image: mcr.microsoft.com/mssql/server:2025-latest + restart: unless-stopped + environment: + ACCEPT_EULA: "Y" + # Password can be overridden via SQLCMDPASSWORD environment variable + SA_PASSWORD: "${SQLCMDPASSWORD:-SqlCmd@2025!}" + MSSQL_SA_PASSWORD: "${SQLCMDPASSWORD:-SqlCmd@2025!}" + MSSQL_PID: "Developer" + volumes: + - mssql-data:/var/opt/mssql + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \"$$MSSQL_SA_PASSWORD\" -C -Q \"SELECT 1\" || exit 1"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 45s + +volumes: + mssql-data: diff --git a/.devcontainer/mssql/setup.sql b/.devcontainer/mssql/setup.sql new file mode 100644 index 00000000..79aac446 --- /dev/null +++ b/.devcontainer/mssql/setup.sql @@ -0,0 +1,69 @@ +-- go-sqlcmd Development Database Setup +-- This script runs automatically when the devcontainer starts + +USE master; +GO + +-- Create a test database for development +IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'SqlCmdTest') +BEGIN + CREATE DATABASE SqlCmdTest; + PRINT 'Created database: SqlCmdTest'; +END +GO + +-- Enable contained database authentication for testing +-- Use TRY/CATCH in case this fails on certain SQL Server configurations +BEGIN TRY + EXEC sp_configure 'contained database authentication', 1; + RECONFIGURE; +END TRY +BEGIN CATCH + PRINT 'Note: Could not enable contained database authentication (may already be enabled or not supported)'; +END CATCH; +GO + +-- Make SqlCmdTest a contained database for testing +BEGIN TRY + ALTER DATABASE SqlCmdTest SET CONTAINMENT = PARTIAL; +END TRY +BEGIN CATCH + PRINT 'Note: Could not set database containment (may not be supported)'; +END CATCH; +GO + +USE SqlCmdTest; +GO + +-- Create a sample table for quick testing +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'TestTable') +BEGIN + CREATE TABLE TestTable ( + ID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(100) NOT NULL, + Value NVARCHAR(MAX), + CreatedAt DATETIME2 DEFAULT GETUTCDATE() + ); + + INSERT INTO TestTable (Name, Value) VALUES + ('Test1', 'Sample value 1'), + ('Test2', 'Sample value 2'), + ('Test3', 'Sample value 3'); + + PRINT 'Created table: TestTable with sample data'; +END +GO + +-- Create a view for testing +IF NOT EXISTS (SELECT * FROM sys.views WHERE name = 'TestView') +BEGIN + EXEC('CREATE VIEW TestView AS SELECT ID, Name, CreatedAt FROM TestTable'); + PRINT 'Created view: TestView'; +END +GO + +PRINT 'go-sqlcmd development database setup complete!'; +PRINT 'Test database: SqlCmdTest'; +PRINT 'Sample table: TestTable (3 rows)'; +PRINT 'Sample view: TestView'; +GO diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100644 index 00000000..364281ef --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,141 @@ +#!/bin/bash +set -e + +echo "=== go-sqlcmd Development Container Setup ===" + +# Dynamic workspace detection with fallback +if [ -n "${WORKSPACE_FOLDER}" ] && [ -d "${WORKSPACE_FOLDER}" ]; then + cd "${WORKSPACE_FOLDER}" +elif [ -d "/workspaces/go-sqlcmd" ]; then + cd /workspaces/go-sqlcmd +else + # Find workspace by looking for go.mod + workspace_go_mod="$(find /workspaces -maxdepth 2 -name 'go.mod' -type f -print -quit 2>/dev/null)" + if [ -n "$workspace_go_mod" ]; then + cd "$(dirname "$workspace_go_mod")" + else + echo "Error: Could not determine workspace directory" >&2 + exit 1 + fi +fi +echo "πŸ“ Working in: $(pwd)" + +# Download Go dependencies +echo "πŸ“¦ Downloading Go dependencies..." +go mod download + +# Build sqlcmd and add to PATH +echo "πŸ”¨ Building sqlcmd..." +go build -o ~/bin/sqlcmd ./cmd/modern +echo "βœ… sqlcmd built and added to PATH at ~/bin/sqlcmd" + +# Verify build works +echo "πŸ”¨ Verifying full build..." +go build ./... + +# Wait for SQL Server to be ready (health check should have done this, but let's verify) +echo "πŸ”„ Verifying SQL Server connection..." +max_attempts=30 +attempt=1 +sql_ready=false +while [ $attempt -le $max_attempts ]; do + if ~/bin/sqlcmd -S localhost -U sa -P "${SQLCMDPASSWORD}" -C -Q "SELECT 1" > /dev/null 2>&1; then + echo "βœ… SQL Server is ready!" + sql_ready=true + break + fi + echo " Waiting for SQL Server... (attempt $attempt/$max_attempts)" + sleep 2 + attempt=$((attempt + 1)) +done + +if [ "$sql_ready" = false ]; then + echo "⚠️ Warning: Could not verify SQL Server connection. Tests may fail." +fi + +# Run initial setup SQL if it exists and SQL Server is ready +if [ -f ".devcontainer/mssql/setup.sql" ]; then + if [ "$sql_ready" = true ]; then + echo "πŸ“‹ Running setup.sql..." + ~/bin/sqlcmd -S localhost -U sa -P "${SQLCMDPASSWORD}" -C -i .devcontainer/mssql/setup.sql + else + echo "⚠️ Skipping setup.sql because SQL Server connection could not be verified." + fi +fi + +# Create useful aliases in a dedicated directory (safe and idempotent) +echo "πŸ”§ Setting up helpful aliases..." +mkdir -p ~/.bash_aliases.d +cat > ~/.bash_aliases.d/go-sqlcmd << 'EOF' +# go-sqlcmd development aliases +alias gtest='go test ./...' +alias gtest-short='go test -short ./...' +alias gtest-v='go test -v ./...' +alias gbuild='go build ./cmd/modern && echo "Built: ./modern"' +alias ginstall='go build -o ~/bin/sqlcmd ./cmd/modern && echo "Installed to ~/bin/sqlcmd"' +alias gfmt='go fmt ./...' +alias gvet='go vet ./...' +alias glint='golangci-lint run' +alias ggen='go generate ./...' + +# sqlcmd shortcut - uses the locally built version +alias sql='~/bin/sqlcmd -S localhost -U sa -P "$SQLCMDPASSWORD" -C' + +# Legacy ODBC sqlcmd for compatibility testing +alias sql-legacy='/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SQLCMDPASSWORD" -C' + +# Quick test connection +alias test-db='~/bin/sqlcmd -S localhost -U sa -P "$SQLCMDPASSWORD" -C -Q "SELECT @@VERSION"' + +# Rebuild and test +alias rebuild='go build -o ~/bin/sqlcmd ./cmd/modern && echo "Rebuilt sqlcmd"' +EOF + +# Ensure aliases are sourced from .bashrc +if ! grep -q 'go-sqlcmd aliases' ~/.bashrc 2>/dev/null; then + { + echo '' + echo '# go-sqlcmd aliases' + echo 'if [ -f ~/.bash_aliases ]; then' + echo ' # Source traditional aliases file if present' + echo ' . ~/.bash_aliases' + echo 'fi' + echo '' + echo 'if [ -d ~/.bash_aliases.d ]; then' + echo ' # Source all alias snippets from ~/.bash_aliases.d' + echo ' for f in ~/.bash_aliases.d/*; do' + echo ' [ -r "$f" ] && . "$f"' + echo ' done' + echo 'fi' + } >> ~/.bashrc +fi + +echo "" +echo "=== Setup Complete! ===" +echo "" +echo "πŸ“– Quick Reference:" +echo " gtest - Run all tests" +echo " gtest-short - Run short tests" +echo " gtest-v - Run tests with verbose output" +echo " gbuild - Build sqlcmd locally" +echo " ginstall - Build and install sqlcmd to ~/bin" +echo " gfmt - Format code" +echo " gvet - Run go vet" +echo " glint - Run golangci-lint" +echo " ggen - Run go generate (for translations)" +echo " test-db - Test database connection" +echo " sql - Connect to SQL Server (go-sqlcmd)" +echo " sql-legacy - Connect using legacy ODBC sqlcmd" +echo " rebuild - Rebuild sqlcmd" +echo "" +echo "πŸ”§ Your locally built sqlcmd is at ~/bin/sqlcmd and in PATH" +echo "" +echo "πŸ”— SQL Server Connection:" +echo " Server: localhost,1433" +echo " User: sa" +echo " Password: (from SQLCMDPASSWORD environment variable)" +echo " Database: master (or SqlCmdTest)" +echo "" +echo "πŸ§ͺ Environment variables are pre-configured for tests." +echo " Run 'go test ./...' to execute the full test suite." +echo "" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..329bd7a0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,72 @@ +# go-sqlcmd Docker Ignore File +# Exclude files not needed in the container build context + +# Git +.git/ +.gitignore + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Go build artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Pre-built binaries (we build from source in the container) +darwin-amd64/ +darwin-arm64/ +linux-amd64/ +linux-arm64/ +linux-s390x/ +windows-amd64/ +windows-arm/ +windows-arm64/ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Test coverage +*.cover +*.coverprofile +coverage.out +coverage.html + +# Dependency directories +vendor/ + +# Documentation and non-essential files (keep devcontainer docs) +LICENSE* +SECURITY* +CONTRIBUTING* +CHANGELOG* + +# CI/CD files (not needed in container) +.github/ +.pipelines/ +appveyor.yml + +# Dev container configuration (not needed in image build) +.devcontainer/ + +# Test output and temporary files +*.log +*.tmp +tmp/ + +# Release files +release/ diff --git a/README.md b/README.md index e4a1e35d..576439da 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SQLCMD CLI +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](.devcontainer/README.md) + This repo contains the `sqlcmd` command line tool and Go packages for working with Microsoft SQL Server, Azure SQL Database, and Azure Synapse. Learn more about how `sqlcmd` is used from a articles/posts written by the community: [Community Buzz][]. @@ -307,6 +309,52 @@ e.g. sqlcmd.exe: error: sqlcmd.exe: '-w 4': Der Wert muss grâßer als 8 und kleiner als 65536 sein. ``` +## Development + +### Quick Start with Dev Containers + +The easiest way to develop and test sqlcmd is to use the included [Dev Container](.devcontainer/README.md), which works with: + +- **VS Code**: Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers), open this repo, and click "Reopen in Container" +- **GitHub Codespaces**: Click the "Code" button on GitHub and select "Create codespace" + +The dev container includes: +- Go 1.24 with all development tools (golangci-lint, gopls, delve) +- SQL Server 2025 ready for integration tests +- Your locally built `sqlcmd` added to PATH automatically +- Pre-configured environment variables for tests + +Once inside the container: +```bash +# Build sqlcmd from source +ginstall + +# Run the test suite +gtest + +# Connect to SQL Server +sql -Q "SELECT @@VERSION" +``` + +### Manual Setup + +If you prefer to set up your environment manually: + +1. Install Go 1.24 or higher +2. Clone this repository +3. Set up a SQL Server instance (2017 or later) +4. Configure environment variables: + - `SQLCMDSERVER` - Server hostname (e.g., `localhost`) + - `SQLCMDUSER` - Username (e.g., `sa`) + - `SQLCMDPASSWORD` - Password + - `SQLCMDDATABASE` - Database name (optional) + +5. Build and run: + ```bash + go build -o sqlcmd ./cmd/modern + ./sqlcmd --version + ``` + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a