diff --git a/.github/workflows/vector-search-go-ci.yml b/.github/workflows/vector-search-go-ci.yml new file mode 100644 index 0000000000..bcb96ae165 --- /dev/null +++ b/.github/workflows/vector-search-go-ci.yml @@ -0,0 +1,75 @@ +# CI for the Azure SQL Vector Search Go quickstart sample. +# Security: uses `pull_request` (read-only token, no secrets on forks) +# with an explicit same-repo check to skip fork PRs entirely. +# +# This CI runs static analysis and unit tests only — no live Azure SQL +# Database or Azure OpenAI resource is required or contacted. Live +# end-to-end validation is a separate, manual step (see the sample's +# README and output/sample-output.txt for the explicit no-live-run +# disclosure). + +name: "Vector Search Go — Build" + +on: + pull_request: + branches: [master] + paths: + - "samples/features/vector-search/vector-search-query-go/**" + push: + branches: [master] + paths: + - "samples/features/vector-search/vector-search-query-go/**" + +# Cancel redundant runs for the same PR / branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Vet, format-check, build, and test + runs-on: ubuntu-latest + timeout-minutes: 10 + + # Skip CI on fork PRs — prevents external actors from consuming minutes. + if: >- + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + + defaults: + run: + working-directory: samples/features/vector-search/vector-search-query-go + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version: "1.25" + cache-dependency-path: samples/features/vector-search/vector-search-query-go/go.sum + + - name: Download dependencies + run: go mod download + + - name: Format check (gofmt) + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "The following files are not gofmt-formatted:" + echo "$unformatted" + exit 1 + fi + + - name: Vet + run: go vet ./... + + - name: Build + run: go build ./... + + - name: Unit tests (no Azure connectivity required) + run: go test ./... -v diff --git a/.github/workflows/vector-search-python-ci.yml b/.github/workflows/vector-search-python-ci.yml new file mode 100644 index 0000000000..4c5443887f --- /dev/null +++ b/.github/workflows/vector-search-python-ci.yml @@ -0,0 +1,74 @@ +# CI for the Azure SQL Vector Search Python quickstart sample. +# Security: uses `pull_request` (read-only token, no secrets on forks) +# with an explicit same-repo check to skip fork PRs entirely. +# +# This CI runs static analysis and unit tests only — no live Azure SQL +# Database or Azure OpenAI resource is required or contacted. Live +# end-to-end validation is a separate, manual step (see the sample's +# README and output/sample-output.txt for the explicit no-live-run +# disclosure). + +name: "Vector Search Python — Build" + +on: + pull_request: + branches: [master] + paths: + - "samples/features/vector-search/vector-search-query-python/**" + push: + branches: [master] + paths: + - "samples/features/vector-search/vector-search-query-python/**" + +# Cancel redundant runs for the same PR / branch. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Lint, type-check, and test + runs-on: ubuntu-latest + timeout-minutes: 10 + + # Skip CI on fork PRs — prevents external actors from consuming minutes. + if: >- + github.event_name == 'push' || + github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + + defaults: + run: + working-directory: samples/features/vector-search/vector-search-query-python + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + cache: "pip" + cache-dependency-path: | + samples/features/vector-search/vector-search-query-python/requirements.txt + samples/features/vector-search/vector-search-query-python/requirements-dev.txt + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Lint + run: python -m ruff check . + + - name: Format check + run: python -m ruff format --check . + + - name: Type-check + run: python -m mypy src + + - name: Unit tests (no Azure connectivity required) + run: python -m pytest -v diff --git a/.github/workflows/vector-search-typescript-ci.yml b/.github/workflows/vector-search-typescript-ci.yml index 60e81c8090..5e7a8dcd2f 100644 --- a/.github/workflows/vector-search-typescript-ci.yml +++ b/.github/workflows/vector-search-typescript-ci.yml @@ -51,5 +51,11 @@ jobs: - name: Install dependencies run: npm ci + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + - name: Type-check (build) run: npm run build diff --git a/samples/features/vector-search/README.md b/samples/features/vector-search/README.md index b31d990866..8ed65fae90 100644 --- a/samples/features/vector-search/README.md +++ b/samples/features/vector-search/README.md @@ -21,19 +21,21 @@ This provisions: - **Azure OpenAI** with `text-embedding-3-small` deployment - **Managed Identity** with appropriate role assignments -After deployment, create the `.env` file for the language samples: +After deployment, create the `.env` file for the language samples. For example, for TypeScript: ```bash azd env get-values --cwd samples/features/vector-search > samples/features/vector-search/vector-search-query-typescript/.env ``` -This writes the deployment outputs (`AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`, `AZURE_SQL_SERVER`, `AZURE_SQL_DATABASE`, etc.) directly into the `.env` file that the samples read at runtime. +This writes the deployment outputs (`AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`, `AZURE_SQL_SERVER`, `AZURE_SQL_DATABASE`, etc.) directly into the `.env` file that the TypeScript, Python, and Go samples read at runtime. Point the same command at another language's folder to configure it instead. ## Language samples -| Language | Folder | Description | -|----------|--------|-------------| -| TypeScript | [vector-search-query-typescript/](./vector-search-query-typescript/) | Vector search with Node.js, tedious driver, and Azure OpenAI | +| Language | Folder | Description | Status | +|----------|--------|-------------|--------| +| TypeScript | [vector-search-query-typescript/](./vector-search-query-typescript/) | Vector search with Node.js, tedious driver, and Azure OpenAI | Implemented; live end-to-end run captured | +| Python | [vector-search-query-python/](./vector-search-query-python/) | Vector search with pyodbc, azure-identity, and Azure OpenAI | Implemented; statically validated (lint, type-check, unit tests) — no live end-to-end run captured yet | +| Go | [vector-search-query-go/](./vector-search-query-go/) | Vector search with go-mssqldb, azidentity, and the OpenAI Go client configured for Azure | Implemented; statically validated (vet, build, unit tests) — no live end-to-end run captured yet; also out of scope for v1 by constitution decision ([ASV-LANG-GO-1](/.github/instructions/vector-search-constitution.instructions.md)) | ## Infrastructure only diff --git a/samples/features/vector-search/vector-search-query-go/.gitignore b/samples/features/vector-search/vector-search-query-go/.gitignore new file mode 100644 index 0000000000..0dfb8ec612 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/.gitignore @@ -0,0 +1,18 @@ +# Environment variables (may contain Azure credentials) +.env +.env.local +.env.*.local + +# Go build artifacts +/query +/embed +*.exe +*.test + +# Sample output — keep the committed reference file (output/sample-output.txt); +# ignore any other ephemeral run artifacts written to this folder. +output/* +!output/sample-output.txt + +# Logs +*.log diff --git a/samples/features/vector-search/vector-search-query-go/README.md b/samples/features/vector-search/vector-search-query-go/README.md new file mode 100644 index 0000000000..aa58c2eb8b --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/README.md @@ -0,0 +1,318 @@ +# Quickstart: Vector search with Go in Azure SQL Database + +This sample demonstrates how to perform **native vector search** in Azure SQL Database using Go. + +> [!NOTE] +> Per the repository constitution, **Go remains out of scope for the v1 vector search scenario by explicit decision** ([ASV-LANG-GO-1](/.github/instructions/vector-search-constitution.instructions.md)) — this sample exists as a fully specified, statically validated reference implementation ahead of that decision being revisited. TypeScript is the only sample with real, live-validated end-to-end evidence today. + +It uses: + +- **[github.com/microsoft/go-mssqldb](https://pkg.go.dev/github.com/microsoft/go-mssqldb)**—Microsoft's official pure-Go driver for SQL Server and Azure SQL Database, via its `azuread` subpackage for Microsoft Entra authentication +- **[github.com/openai/openai-go/v3](https://pkg.go.dev/github.com/openai/openai-go/v3)**—the official OpenAI Go client library for generating embeddings, configured for Azure OpenAI via its `azure` subpackage; published by OpenAI, not an Azure SDK package +- **[github.com/Azure/azure-sdk-for-go/sdk/azidentity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity)**—`DefaultAzureCredential` for passwordless authentication to Azure OpenAI + +> [!IMPORTANT] +> **Package currency note:** `github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai` — the package name recorded for this role in the constitution's initial authoring — changed function as of its v0.8.0 release (2025-06-03): it is no longer a standalone client and now serves only as a companion providing Azure-specific extension types to the official `openai-go` client. For a plain embeddings call like this sample's, `azopenai` itself is not required at all; `openai-go`'s own client, configured with `azure.WithTokenCredential`, is the correct and only necessary package. This sample uses the verified-current pattern. See the PR description for the corresponding constitution follow-up this finding requires. + +## What the sample does + +1. Loads 50 hotels with precomputed embeddings from `data/HotelsData_Vector.json` +2. Connects to Azure SQL Database using Microsoft Entra authentication (`fedauth=ActiveDirectoryDefault` in the connection string; no passwords or API keys) +3. Creates a table with `id`, `name`, `description`, `category`, `rating`, and a `VECTOR(1536)` column +4. Inserts all 50 hotels with their precomputed vector embeddings +5. Generates a fresh query embedding using Azure OpenAI `text-embedding-3-small` +6. Performs a vector similarity search using either **exact kNN** (`VECTOR_DISTANCE`) or **approximate ANN** (`VECTOR_SEARCH` with DiskANN index), based on the `VECTOR_SEARCH_ALGORITHM` environment variable +7. Displays the top matching results with category, rating, and similarity scores + +## Vector search algorithms + +This sample supports two algorithms, selected via the `VECTOR_SEARCH_ALGORITHM` environment variable: + +| | Exact search (default) | Approximate search (DiskANN) | +|---|---|---| +| **Env var value** | `exact` | `diskann` | +| **T-SQL function** | `VECTOR_DISTANCE` | `VECTOR_SEARCH` | +| **Index required** | No | Yes (auto-created) | +| **Recall** | 100% (guaranteed) | ~95–99% (tunable) | +| **Minimum rows** | No minimum | 1,000 non-null vectors | +| **Best for** | < 50,000 rows, prototyping | > 10,000 rows, production | + +> [!IMPORTANT] +> DiskANN index creation requires at least **1,000 rows** with non-null vectors. The 50-hotel sample dataset is too small—load a larger dataset before using `VECTOR_SEARCH_ALGORITHM=diskann`. This sample detects the row count at runtime and automatically falls back to exact search below that threshold. + +## Prerequisites + +- **Azure subscription**—[Create one free](https://azure.microsoft.com/free/) +- **Azure SQL Database** with native vector support—[Quickstart: Create a single database](https://learn.microsoft.com/azure/azure-sql/database/single-database-create-quickstart) +- **Azure OpenAI resource** with a `text-embedding-3-small` deployment—[Create and deploy an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource) +- **Go 1.25+**—[Download Go](https://go.dev/dl/) +- **Azure CLI**—[Install the Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), signed in with `az login` + +> [!IMPORTANT] +> Your Azure identity must be configured as a **Microsoft Entra admin** on the Azure SQL server. The `azd up` deployment sets this automatically using `deploymentUserPrincipalId`. For the Azure OpenAI resource, you need the **Cognitive Services OpenAI User** role. + +> [!IMPORTANT] +> After deploying with `azd up`, you may need to add your client IP to the Azure SQL firewall. Run: +> ```bash +> az sql server firewall-rule create --resource-group --server --name AllowMyIP --start-ip-address --end-ip-address +> ``` +> Or set `AZURE_CLIENT_IP` in your environment before running `azd up` to have it configured automatically. + +## Get started + +### 1. Clone the repository + +```bash +git clone https://github.com/microsoft/sql-server-samples.git +cd sql-server-samples/samples/features/vector-search/vector-search-query-go +``` + +### 2. Download dependencies + +```bash +go mod download +``` + +### 3. Configure environment variables + +Copy the sample environment file and fill in your values: + +```bash +cp sample.env .env +``` + +Edit `.env` with your Azure resource details: + +```env +AZURE_SQL_SERVER=.database.windows.net +AZURE_SQL_DATABASE= +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small +VECTOR_SEARCH_ALGORITHM=exact +``` + +| Variable | Required | Description | +|---|---|---| +| `AZURE_SQL_SERVER` | Yes | Azure SQL server FQDN | +| `AZURE_SQL_DATABASE` | Yes | Database name | +| `AZURE_SQL_TABLE_NAME` | No | Table name (default: `hotels_go`) | +| `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI endpoint URL | +| `AZURE_OPENAI_EMBEDDING_DEPLOYMENT` | Yes | Embedding model deployment name | +| `VECTOR_SEARCH_ALGORITHM` | No | `exact` (default) or `diskann` | +| `SQL_DROP_TABLE` | No | `true` to drop table after run (default: `false`) | + +> [!NOTE] +> No API keys are needed. The sample uses `DefaultAzureCredential` (for Azure OpenAI) and the `azuread` driver subpackage's `fedauth=ActiveDirectoryDefault` connection mode (for Azure SQL), both of which automatically use your Azure CLI login, managed identity, or other credential sources. + +### 4. Run the sample + +```bash +go run ./cmd/query +``` + +## Expected output + +> [!IMPORTANT] +> **No live Azure environment was available while authoring this sample**, so no real end-to-end run has been captured yet. The section below documents the *shape* of the expected output based on the identical, already-validated TypeScript reference implementation's real captured run — it is **not** a captured Go run and must not be treated as validated evidence. See [`output/sample-output.txt`](./output/sample-output.txt) for the same explicit disclosure. Replace this section and that file with a real captured run before relying on this sample as validated. + +``` +=== Azure SQL Vector Search—Go Quickstart === + +Server: .database.windows.net +Database: +OpenAI: https://.openai.azure.com +Deployment: text-embedding-3-small +Algorithm: exact +Table: dbo.hotels_go + +Loaded 50 hotels from data file. + +Connecting to Azure SQL Database... +Connected. + +Creating table dbo.hotels_go (if not exists)... +Table ready. + +Inserting hotel data with precomputed embeddings... +Inserted 50 hotels. + +Searching for: "luxury beachfront hotel with ocean views and spa" + +--- Search Results—Exact (kNN) via VECTOR_DISTANCE (Top 3 by Cosine Distance) --- + + Hotel: + Category: + Rating: + Description: ... + Distance: + Similarity: + +Done. Connection closed. +``` + +> [!NOTE] +> Distance and similarity values depend on the embedding model and may vary slightly across runs. Once a real run is captured, this sample should return the **same top hotel match** as the TypeScript reference (cross-language result parity), per the constitution's canonical-query requirement. + +## Understanding the code + +### Connection with Microsoft Entra authentication + +Unlike TypeScript and Python, the Go driver's `azuread` subpackage handles Entra token acquisition internally once the DSN requests it — no manual token call is needed for the SQL connection itself: + +```go +import ( + "database/sql" + _ "github.com/microsoft/go-mssqldb/azuread" +) + +dsn := fmt.Sprintf( + "sqlserver://%s?database=%s&fedauth=ActiveDirectoryDefault&encrypt=true&TrustServerCertificate=false", + server, database, +) +db, err := sql.Open("azuresql", dsn) // "azuresql" is azuread's registered driver name +``` + +### Table with VECTOR column + +Azure SQL Database supports the native `VECTOR` type. The table is created with columns for hotel metadata and a `VECTOR(1536)` column to store embeddings: + +```sql +CREATE TABLE dbo.hotels_go ( + id NVARCHAR(50) PRIMARY KEY, + name NVARCHAR(200) NOT NULL, + description NVARCHAR(MAX) NOT NULL, + category NVARCHAR(100) NULL, + rating FLOAT NULL, + embedding VECTOR(1536) NULL +); +``` + +### Loading precomputed vectors + +Hotel data with precomputed embeddings is loaded from `data/HotelsData_Vector.json`. This avoids calling Azure OpenAI for each hotel during the main run, making the demo faster and simpler: + +```go +data, err := os.ReadFile(path) +var loaded []Hotel +json.Unmarshal(data, &loaded) +``` + +### Generating query embeddings + +At search time, a fresh embedding is generated for the search query using Azure OpenAI's `text-embedding-3-small` model through `openai-go`'s client, configured with its `azure` subpackage for Entra authentication: + +```go +import ( + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/azure" +) + +credential, _ := azidentity.NewDefaultAzureCredential(nil) +client := openai.NewClient( + azure.WithEndpoint(endpoint, "2024-10-21"), // See https://learn.microsoft.com/azure/ai-services/openai/api-version-deprecation + azure.WithTokenCredential(credential), +) + +resp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{ + Model: openai.EmbeddingModel(deployment), + Input: openai.EmbeddingNewParamsInputUnion{OfArrayOfStrings: []string{text}}, +}) +``` + +### Vector similarity search + +**Exact search (default)**—The `VECTOR_DISTANCE()` function computes cosine distance between the query vector and all stored embeddings: + +```sql +SELECT TOP 3 + name, description, category, rating, + VECTOR_DISTANCE('cosine', embedding, CAST(@Query AS VECTOR(1536))) AS distance +FROM dbo.hotels_go +ORDER BY distance; +``` + +**Approximate search (DiskANN)**—The `VECTOR_SEARCH()` function uses a DiskANN index for 10–100× faster queries on large datasets: + +```sql +SELECT TOP 3 + vs.distance, + h.name, h.description, h.category, h.rating +FROM VECTOR_SEARCH( + dbo.hotels_go, embedding, + CAST(@Query AS VECTOR(1536)), + 'cosine', 3 +) AS vs +INNER JOIN dbo.hotels_go h ON vs.$rowid = h.$rowid +ORDER BY vs.distance; +``` + +A lower distance means higher similarity. + +### Re-generate embeddings (optional) + +If you change embedding models, re-generate the vector data: + +```bash +go run ./cmd/embed +``` + +This reads `data/HotelsData.JSON`, generates new embeddings using your Azure OpenAI deployment, and writes `data/HotelsData_Vector.json`. + +## Clean up resources + +To remove the sample table from your database: + +```sql +DROP INDEX IF EXISTS ix_hotels_go_embedding ON dbo.hotels_go; +DROP TABLE IF EXISTS dbo.hotels_go; +``` + +To avoid ongoing charges, delete the Azure resources you created if they were only for this quickstart: + +- [Delete the Azure SQL Database](https://learn.microsoft.com/azure/azure-sql/database/single-database-manage#delete-a-single-database) +- [Delete the Azure OpenAI resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource#delete-a-resource) + +## Troubleshooting + +### Authentication failures + +**"Login failed"**—Ensure your Azure identity is set as Microsoft Entra admin on the SQL server. Run `az login` to refresh your credentials. + +**Azure OpenAI authentication errors**—Verify you have the **Cognitive Services OpenAI User** role on the Azure OpenAI resource. Check the endpoint URL and deployment name in your `.env` file. + +### Firewall errors + +**"Cannot open server"**—Your client IP may not be in the SQL firewall rules. Add it: + +```bash +az sql server firewall-rule create \ + --resource-group --server \ + --name AllowMyIP --start-ip-address --end-ip-address +``` + +### DiskANN errors + +**"DiskANN index requires at least 1,000 rows"**—The 50-hotel sample dataset is too small for DiskANN. The sample automatically detects this and falls back to exact nearest-neighbor search using `VECTOR_DISTANCE` without a vector index. To use DiskANN, load a larger dataset first. + +### Vector dimension errors + +**"invalid or missing vector dimensions"**—The precomputed embeddings in `HotelsData_Vector.json` must use 1536 dimensions (matching `text-embedding-3-small`). Re-run `go run ./cmd/embed` if you changed the embedding model. + +## Development + +This sample includes unit tests for pure logic (configuration validation, vector serialization, dataset validation) that require no Azure connectivity: + +```bash +go build ./... +go vet ./... +gofmt -l . +go test ./... +``` + +## Related content + +- [Vectors in Azure SQL and SQL Server](https://learn.microsoft.com/sql/sql-server/ai/vectors) +- [VECTOR_DISTANCE (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/functions/vector-distance-transact-sql) +- [Azure OpenAI text embeddings](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#embeddings) +- [go-mssqldb documentation](https://pkg.go.dev/github.com/microsoft/go-mssqldb) diff --git a/samples/features/vector-search/vector-search-query-go/cmd/embed/main.go b/samples/features/vector-search/vector-search-query-go/cmd/embed/main.go new file mode 100644 index 0000000000..7a4e876ff6 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/cmd/embed/main.go @@ -0,0 +1,118 @@ +// Command embed regenerates precomputed embeddings for the hotels +// dataset. Reads data/HotelsData.JSON (no vectors), calls Azure OpenAI in +// batches to generate a text-embedding-3-small embedding per hotel +// description, and writes data/HotelsData_Vector.json — mirroring the +// TypeScript reference sample's embed.ts. Run this only if you need to +// regenerate the shipped vector data (for example, after changing the +// embedding model). +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/joho/godotenv" + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/azure" + + "vector-search-query-go/internal/config" +) + +const ( + openAIAPIVersion = "2024-10-21" + batchSize = 20 +) + +func dataDir() string { + _, thisFile, _, _ := runtime.Caller(0) + // this file lives at /cmd/embed/main.go + sampleRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile))) + return filepath.Join(sampleRoot, "data") +} + +func run() error { + _ = godotenv.Load() + + cfg, err := config.Load(false) + if err != nil { + return err + } + + inputPath := filepath.Join(dataDir(), "HotelsData.JSON") + outputPath := filepath.Join(dataDir(), "HotelsData_Vector.json") + + data, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("reading %s: %w", inputPath, err) + } + + var rawHotels []map[string]any + if err := json.Unmarshal(data, &rawHotels); err != nil { + return fmt.Errorf("parsing %s: %w", inputPath, err) + } + fmt.Printf("Generating embeddings for %d hotels...\n", len(rawHotels)) + + credential, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return fmt.Errorf("creating credential: %w", err) + } + client := openai.NewClient( + azure.WithEndpoint(cfg.AzureOpenAIEndpoint, openAIAPIVersion), + azure.WithTokenCredential(credential), + ) + + ctx := context.Background() + descriptions := make([]string, len(rawHotels)) + for i, h := range rawHotels { + desc, _ := h["Description"].(string) + descriptions[i] = desc + } + + allEmbeddings := make([][]float64, 0, len(descriptions)) + for start := 0; start < len(descriptions); start += batchSize { + end := start + batchSize + if end > len(descriptions) { + end = len(descriptions) + } + batch := descriptions[start:end] + + resp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{ + Model: openai.EmbeddingModel(cfg.AzureOpenAIEmbeddingDeployment), + Input: openai.EmbeddingNewParamsInputUnion{OfArrayOfStrings: batch}, + }) + if err != nil { + return fmt.Errorf("generating embeddings for batch starting at %d: %w", start, err) + } + for _, item := range resp.Data { + allEmbeddings = append(allEmbeddings, item.Embedding) + } + fmt.Printf(" Embedded %d/%d\n", end, len(descriptions)) + } + + for i, h := range rawHotels { + h["DescriptionVector"] = allEmbeddings[i] + } + + out, err := json.MarshalIndent(rawHotels, "", " ") + if err != nil { + return fmt.Errorf("serializing output: %w", err) + } + if err := os.WriteFile(outputPath, out, 0o644); err != nil { + return fmt.Errorf("writing %s: %w", outputPath, err) + } + fmt.Println("Done. Wrote HotelsData_Vector.json") + return nil +} + +func main() { + if err := run(); err != nil { + log.Println("Error:", err) + os.Exit(1) + } +} diff --git a/samples/features/vector-search/vector-search-query-go/cmd/query/main.go b/samples/features/vector-search/vector-search-query-go/cmd/query/main.go new file mode 100644 index 0000000000..0dc7a88a5d --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/cmd/query/main.go @@ -0,0 +1,384 @@ +// Command query implements the Azure SQL vector search quickstart scenario +// for Go: create a table with a VECTOR(1536) column, bulk-load 50 hotels +// with precomputed embeddings, generate one fresh query embedding via +// Azure OpenAI, and run a similarity search using either exact kNN +// (VECTOR_DISTANCE) or approximate ANN (VECTOR_SEARCH with a DiskANN +// index), selected via VECTOR_SEARCH_ALGORITHM. +// +// Per constitution ASV-VS-4: DiskANN requires >= 1,000 rows with +// non-null vectors. Below that threshold this command falls back to +// exact search automatically and logs a warning, identical to the +// TypeScript reference sample. +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/joho/godotenv" + _ "github.com/microsoft/go-mssqldb/azuread" + "github.com/openai/openai-go/v3" + "github.com/openai/openai-go/v3/azure" + + "vector-search-query-go/internal/config" + "vector-search-query-go/internal/hotels" +) + +const ( + openAIAPIVersion = "2024-10-21" // See https://learn.microsoft.com/azure/ai-services/openai/api-version-deprecation + searchQuery = "luxury beachfront hotel with ocean views and spa" +) + +func dataPath(filename string) string { + _, thisFile, _, _ := runtime.Caller(0) + // this file lives at /cmd/query/main.go + sampleRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile))) + return filepath.Join(sampleRoot, "data", filename) +} + +func openSQL(cfg config.AppConfig) (*sql.DB, error) { + dsn := fmt.Sprintf( + "sqlserver://%s?database=%s&fedauth=ActiveDirectoryDefault&encrypt=true&TrustServerCertificate=false", + cfg.AzureSQLServer, cfg.AzureSQLDatabase, + ) + // azuread.DriverName ("azuresql") wraps the standard driver and + // performs Microsoft Entra authentication via the fedauth DSN + // parameter above — no manual token acquisition is needed for SQL. + db, err := sql.Open("azuresql", dsn) + if err != nil { + return nil, fmt.Errorf("opening connection: %w", err) + } + if err := db.Ping(); err != nil { + _ = db.Close() + return nil, fmt.Errorf( + "authentication or connection failure: %w\n"+ + "Ensure:\n"+ + " 1. You are signed in: az login\n"+ + " 2. Your identity is set as Microsoft Entra admin on the SQL server\n"+ + " 3. Your client IP is in the SQL server firewall rules", + err, + ) + } + return db, nil +} + +func createTable(ctx context.Context, db *sql.DB, tableName string) error { + _, err := db.ExecContext(ctx, fmt.Sprintf(` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = N'%s' AND schema_id = SCHEMA_ID('dbo')) +BEGIN + CREATE TABLE dbo.[%s] ( + id NVARCHAR(50) PRIMARY KEY, + name NVARCHAR(200) NOT NULL, + description NVARCHAR(MAX) NOT NULL, + category NVARCHAR(100) NULL, + rating FLOAT NULL, + embedding VECTOR(1536) NULL + ); +END`, tableName, tableName)) + return err +} + +func insertHotels(ctx context.Context, tx *sql.Tx, tableName string, all []hotels.Hotel) error { + if _, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM dbo.[%s]", tableName)); err != nil { + return err + } + + insertSQL := fmt.Sprintf( + "INSERT INTO dbo.[%s] (id, name, description, category, rating, embedding) "+ + "VALUES (@ID, @Name, @Description, @Category, @Rating, CAST(@Embedding AS VECTOR(1536)))", + tableName, + ) + stmt, err := tx.PrepareContext(ctx, insertSQL) + if err != nil { + return err + } + defer stmt.Close() + + for _, h := range all { + vectorJSON, err := hotels.VectorToJSON(h.DescriptionVector) + if err != nil { + return err + } + if _, err := stmt.ExecContext(ctx, + sql.Named("ID", h.HotelID), + sql.Named("Name", h.HotelName), + sql.Named("Description", h.Description), + sql.Named("Category", h.Category), + sql.Named("Rating", h.Rating), + sql.Named("Embedding", vectorJSON), + ); err != nil { + return fmt.Errorf("inserting hotel %s: %w", h.HotelID, err) + } + } + return nil +} + +// determineAlgorithm checks the non-null embedding row count before +// attempting to create a DiskANN index. Below 1,000 rows it falls back +// to exact search — the same dataset-size gate as every other language. +func determineAlgorithm(ctx context.Context, db *sql.DB, tableName, requested string) (string, error) { + if requested != "diskann" { + return requested, nil + } + + var rowCount int64 + row := db.QueryRowContext(ctx, fmt.Sprintf( + "SELECT COUNT(*) FROM dbo.[%s] WHERE embedding IS NOT NULL", tableName, + )) + if err := row.Scan(&rowCount); err != nil { + return "", err + } + + if rowCount < 1000 { + fmt.Printf( + "\u26a0 DiskANN index requires at least 1,000 rows with non-null vectors, "+ + "but table has only %d. Falling back to exact (VECTOR_DISTANCE) search.\n\n", + rowCount, + ) + return "exact", nil + } + + fmt.Println("Creating DiskANN vector index (if not exists)...") + _, err := db.ExecContext(ctx, fmt.Sprintf(` +IF NOT EXISTS ( + SELECT * FROM sys.indexes + WHERE name = N'ix_%s_embedding' AND object_id = OBJECT_ID('dbo.[%s]') +) +BEGIN + CREATE VECTOR INDEX [ix_%s_embedding] + ON dbo.[%s](embedding) + WITH (type = 'DiskANN', metric = 'cosine'); +END`, tableName, tableName, tableName, tableName)) + if err != nil { + return "", err + } + fmt.Println("DiskANN index ready.") + return "diskann", nil +} + +type searchResult struct { + Name string + Description string + Category string + Rating float64 + Distance float64 +} + +func runSearch(ctx context.Context, db *sql.DB, tableName, algorithm, queryVectorJSON string) ([]searchResult, error) { + var rows *sql.Rows + var err error + + if algorithm == "diskann" { + rows, err = db.QueryContext(ctx, fmt.Sprintf(` +SELECT TOP 3 + vs.distance, + h.name, h.description, h.category, h.rating +FROM VECTOR_SEARCH( + dbo.[%s], embedding, + CAST(@Query AS VECTOR(1536)), + 'cosine', 3 +) AS vs +INNER JOIN dbo.[%s] h ON vs.[$rowid] = h.[$rowid] +ORDER BY vs.distance`, tableName, tableName), + sql.Named("Query", queryVectorJSON), + ) + } else { + rows, err = db.QueryContext(ctx, fmt.Sprintf(` +SELECT TOP 3 + name, description, category, rating, + VECTOR_DISTANCE('cosine', embedding, CAST(@Query AS VECTOR(1536))) AS distance +FROM dbo.[%s] +ORDER BY distance`, tableName), + sql.Named("Query", queryVectorJSON), + ) + } + if err != nil { + return nil, err + } + defer rows.Close() + + var results []searchResult + for rows.Next() { + var r searchResult + if algorithm == "diskann" { + if err := rows.Scan(&r.Distance, &r.Name, &r.Description, &r.Category, &r.Rating); err != nil { + return nil, err + } + } else { + if err := rows.Scan(&r.Name, &r.Description, &r.Category, &r.Rating, &r.Distance); err != nil { + return nil, err + } + } + results = append(results, r) + } + return results, rows.Err() +} + +func generateQueryEmbedding(ctx context.Context, client openai.Client, deployment, text string) ([]float64, error) { + resp, err := client.Embeddings.New(ctx, openai.EmbeddingNewParams{ + Model: openai.EmbeddingModel(deployment), + Input: openai.EmbeddingNewParamsInputUnion{OfArrayOfStrings: []string{text}}, + }) + if err != nil { + return nil, err + } + if len(resp.Data) == 0 { + return nil, fmt.Errorf("Azure OpenAI returned no embedding data") + } + return resp.Data[0].Embedding, nil +} + +func run() error { + // Load .env if present; ignore the error when it doesn't exist (real + // environment variables, e.g. from CI, remain valid without a file). + _ = godotenv.Load() + + fmt.Println("=== Azure SQL Vector Search\u2014Go Quickstart ===") + fmt.Println() + + cfg, err := config.Load(true) + if err != nil { + return err + } + fmt.Printf("Server: %s\n", cfg.AzureSQLServer) + fmt.Printf("Database: %s\n", cfg.AzureSQLDatabase) + fmt.Printf("OpenAI: %s\n", cfg.AzureOpenAIEndpoint) + fmt.Printf("Deployment: %s\n", cfg.AzureOpenAIEmbeddingDeployment) + fmt.Printf("Algorithm: %s\n", cfg.VectorSearchAlgorithm) + fmt.Printf("Table: dbo.%s\n\n", cfg.TableName) + + allHotels, err := hotels.Load(dataPath("HotelsData_Vector.json")) + if err != nil { + return err + } + fmt.Printf("Loaded %d hotels from data file.\n\n", len(allHotels)) + + credential, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return fmt.Errorf("creating credential: %w", err) + } + + fmt.Println("Connecting to Azure SQL Database...") + db, err := openSQL(cfg) + if err != nil { + return err + } + defer db.Close() + fmt.Println("Connected.") + fmt.Println() + + ctx := context.Background() + + fmt.Printf("Creating table dbo.%s (if not exists)...\n", cfg.TableName) + if err := createTable(ctx, db, cfg.TableName); err != nil { + return err + } + fmt.Println("Table ready.") + fmt.Println() + + fmt.Println("Inserting hotel data with precomputed embeddings...") + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + if err := insertHotels(ctx, tx, cfg.TableName, allHotels); err != nil { + _ = tx.Rollback() + return err + } + if err := tx.Commit(); err != nil { + return err + } + fmt.Printf("Inserted %d hotels.\n\n", len(allHotels)) + + fmt.Printf("Searching for: %q\n\n", searchQuery) + + openAIClient := openai.NewClient( + azure.WithEndpoint(cfg.AzureOpenAIEndpoint, openAIAPIVersion), + azure.WithTokenCredential(credential), + ) + + queryVector, err := generateQueryEmbedding(ctx, openAIClient, cfg.AzureOpenAIEmbeddingDeployment, searchQuery) + if err != nil { + return fmt.Errorf( + "%w\nAzure OpenAI authentication or request failed. Ensure:\n"+ + " 1. You are signed in: az login\n"+ + " 2. You have the 'Cognitive Services OpenAI User' role on the Azure OpenAI resource\n"+ + " 3. The endpoint is correct: %s\n"+ + " 4. The deployment exists: %s", + err, cfg.AzureOpenAIEndpoint, cfg.AzureOpenAIEmbeddingDeployment, + ) + } + if len(queryVector) != hotels.VectorDimensions { + return fmt.Errorf( + "query embedding has unexpected dimensions: %d (expected %d). Check your Azure OpenAI deployment %q", + len(queryVector), hotels.VectorDimensions, cfg.AzureOpenAIEmbeddingDeployment, + ) + } + queryVectorJSON, err := hotels.VectorToJSON(queryVector) + if err != nil { + return err + } + + algorithm, err := determineAlgorithm(ctx, db, cfg.TableName, cfg.VectorSearchAlgorithm) + if err != nil { + return err + } + + results, err := runSearch(ctx, db, cfg.TableName, algorithm, queryVectorJSON) + if err != nil { + return err + } + + algorithmLabel := "Exact (kNN) via VECTOR_DISTANCE" + if algorithm == "diskann" { + algorithmLabel = "Approximate (DiskANN) via VECTOR_SEARCH" + } + fmt.Printf("--- Search Results\u2014%s (Top 3 by Cosine Distance) ---\n\n", algorithmLabel) + for _, r := range results { + similarity := 1 - r.Distance + desc := r.Description + if len(desc) > 100 { + desc = desc[:100] + } + fmt.Printf(" Hotel: %s\n", r.Name) + fmt.Printf(" Category: %s\n", r.Category) + fmt.Printf(" Rating: %v\n", r.Rating) + fmt.Printf(" Description: %s...\n", desc) + fmt.Printf(" Distance: %.4f\n", r.Distance) + fmt.Printf(" Similarity: %.4f\n", similarity) + fmt.Println() + } + + if cfg.DropTable { + fmt.Printf("Dropping table dbo.[%s]...\n", cfg.TableName) + if algorithm == "diskann" { + if _, err := db.ExecContext(ctx, fmt.Sprintf( + "DROP INDEX IF EXISTS [ix_%s_embedding] ON dbo.[%s]", cfg.TableName, cfg.TableName, + )); err != nil { + return err + } + } + if _, err := db.ExecContext(ctx, fmt.Sprintf("DROP TABLE IF EXISTS dbo.[%s]", cfg.TableName)); err != nil { + return err + } + fmt.Println("Table dropped \u2014 no artifacts left behind.") + } else { + fmt.Printf("Table dbo.[%s] retained (set SQL_DROP_TABLE=true to clean up).\n", cfg.TableName) + } + + return nil +} + +func main() { + if err := run(); err != nil { + log.Println("Error:", err) + os.Exit(1) + } + fmt.Println("Done. Connection closed.") +} diff --git a/samples/features/vector-search/vector-search-query-go/go.mod b/samples/features/vector-search/vector-search-query-go/go.mod new file mode 100644 index 0000000000..1d0a870052 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/go.mod @@ -0,0 +1,32 @@ +module vector-search-query-go + +go 1.25.0 + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 + github.com/joho/godotenv v1.5.1 + github.com/microsoft/go-mssqldb v1.11.0 + github.com/openai/openai-go/v3 v3.61.0 +) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/tidwall/gjson v1.19.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + golang.org/x/crypto v0.55.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect +) diff --git a/samples/features/vector-search/vector-search-query-go/go.sum b/samples/features/vector-search/vector-search-query-go/go.sum new file mode 100644 index 0000000000..7984c5a8d7 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/go.sum @@ -0,0 +1,63 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/microsoft/go-mssqldb v1.11.0 h1:YbDqolEjGH9hBfvKzONTf5/dbl9RKXmizMJE93lVxNs= +github.com/microsoft/go-mssqldb v1.11.0/go.mod h1:goQLDOPlMN/l1REhnNPElMoY/yX+fUWn1+7UoFJPH9Y= +github.com/openai/openai-go/v3 v3.61.0 h1:nMLuGFdKBF0sB3qFVNwE8kpBpenVN1ucYRoKcx7E1u0= +github.com/openai/openai-go/v3 v3.61.0/go.mod h1:ufI1+K+t0ijRB3gk8eztiw1crcDpsBuxRQL4sbLIrts= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= diff --git a/samples/features/vector-search/vector-search-query-go/internal/config/config.go b/samples/features/vector-search/vector-search-query-go/internal/config/config.go new file mode 100644 index 0000000000..bb21b4ef8a --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/internal/config/config.go @@ -0,0 +1,104 @@ +// Package config loads and validates configuration from environment +// variables (populated from a local .env file). Mirrors the +// required/optional variable set and defaults defined in the repository +// constitution (ASV-LANG-TS-3 / ASV-LANG-GO-6): AZURE_SQL_SERVER, +// AZURE_SQL_DATABASE, AZURE_OPENAI_ENDPOINT, +// AZURE_OPENAI_EMBEDDING_DEPLOYMENT are required; AZURE_SQL_TABLE_NAME, +// VECTOR_SEARCH_ALGORITHM, and SQL_DROP_TABLE are optional with defaults. +package config + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +var tableNamePattern = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]{0,114}$`) + +// AppConfig holds the resolved, validated sample configuration. +type AppConfig struct { + AzureSQLServer string + AzureSQLDatabase string + AzureOpenAIEndpoint string + AzureOpenAIEmbeddingDeployment string + TableName string + VectorSearchAlgorithm string + DropTable bool +} + +func required(key string) (string, error) { + value := os.Getenv(key) + if value == "" { + return "", fmt.Errorf( + "missing required environment variable: %s. Copy sample.env to .env and fill in your values", + key, + ) + } + return value, nil +} + +// Load reads and validates configuration from the environment. +// +// requireSQL controls whether AZURE_SQL_SERVER and AZURE_SQL_DATABASE must +// be set. Pass false for tooling (such as the embedding-generation +// command) that does not connect to SQL. +func Load(requireSQL bool) (AppConfig, error) { + algorithmRaw := strings.ToLower(orDefault(os.Getenv("VECTOR_SEARCH_ALGORITHM"), "exact")) + if algorithmRaw != "exact" && algorithmRaw != "diskann" { + return AppConfig{}, fmt.Errorf( + `invalid VECTOR_SEARCH_ALGORITHM: %q. must be "exact" or "diskann"`, algorithmRaw, + ) + } + + tableName := orDefault(os.Getenv("AZURE_SQL_TABLE_NAME"), "hotels_go") + if !tableNamePattern.MatchString(tableName) { + return AppConfig{}, fmt.Errorf( + "invalid AZURE_SQL_TABLE_NAME: %q. must start with a letter or underscore, "+ + "contain only letters, numbers, and underscores, and be at most 115 characters", + tableName, + ) + } + + dropTable := strings.ToLower(os.Getenv("SQL_DROP_TABLE")) == "true" + + var sqlServer, sqlDatabase string + var err error + if requireSQL { + if sqlServer, err = required("AZURE_SQL_SERVER"); err != nil { + return AppConfig{}, err + } + if sqlDatabase, err = required("AZURE_SQL_DATABASE"); err != nil { + return AppConfig{}, err + } + } else { + sqlServer = os.Getenv("AZURE_SQL_SERVER") + sqlDatabase = os.Getenv("AZURE_SQL_DATABASE") + } + + openAIEndpoint, err := required("AZURE_OPENAI_ENDPOINT") + if err != nil { + return AppConfig{}, err + } + openAIDeployment, err := required("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") + if err != nil { + return AppConfig{}, err + } + + return AppConfig{ + AzureSQLServer: sqlServer, + AzureSQLDatabase: sqlDatabase, + AzureOpenAIEndpoint: openAIEndpoint, + AzureOpenAIEmbeddingDeployment: openAIDeployment, + TableName: tableName, + VectorSearchAlgorithm: algorithmRaw, + DropTable: dropTable, + }, nil +} + +func orDefault(value, fallback string) string { + if value == "" { + return fallback + } + return value +} diff --git a/samples/features/vector-search/vector-search-query-go/internal/config/config_test.go b/samples/features/vector-search/vector-search-query-go/internal/config/config_test.go new file mode 100644 index 0000000000..7a3dfbb584 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/internal/config/config_test.go @@ -0,0 +1,172 @@ +package config + +import ( + "strings" + "testing" +) + +// clearEnv removes every variable this package reads, so tests don't leak +// state between each other or depend on the host environment. +func clearEnv(t *testing.T) { + t.Helper() + for _, key := range []string{ + "AZURE_SQL_SERVER", + "AZURE_SQL_DATABASE", + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_EMBEDDING_DEPLOYMENT", + "AZURE_SQL_TABLE_NAME", + "VECTOR_SEARCH_ALGORITHM", + "SQL_DROP_TABLE", + } { + t.Setenv(key, "") + // t.Setenv sets an empty string rather than unsetting; that's + // equivalent for this package's purposes (os.Getenv("") == ""). + } +} + +func setRequired(t *testing.T) { + t.Helper() + t.Setenv("AZURE_SQL_SERVER", "example.database.windows.net") + t.Setenv("AZURE_SQL_DATABASE", "exampledb") + t.Setenv("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com") + t.Setenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT", "text-embedding-3-small") +} + +func TestLoadDefaults(t *testing.T) { + clearEnv(t) + setRequired(t) + + cfg, err := Load(true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.TableName != "hotels_go" { + t.Errorf("TableName = %q, want %q", cfg.TableName, "hotels_go") + } + if cfg.VectorSearchAlgorithm != "exact" { + t.Errorf("VectorSearchAlgorithm = %q, want %q", cfg.VectorSearchAlgorithm, "exact") + } + if cfg.DropTable { + t.Errorf("DropTable = true, want false") + } +} + +func TestLoadMissingRequiredReturnsError(t *testing.T) { + clearEnv(t) + + _, err := Load(true) + if err == nil || !strings.Contains(err.Error(), "AZURE_SQL_SERVER") { + t.Fatalf("expected error mentioning AZURE_SQL_SERVER, got %v", err) + } +} + +func TestLoadRequireSQLFalseAllowsMissingSQLVars(t *testing.T) { + clearEnv(t) + t.Setenv("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com") + t.Setenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT", "text-embedding-3-small") + + cfg, err := Load(false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.AzureSQLServer != "" || cfg.AzureSQLDatabase != "" { + t.Errorf("expected empty SQL fields, got server=%q database=%q", cfg.AzureSQLServer, cfg.AzureSQLDatabase) + } +} + +func TestInvalidAlgorithmReturnsError(t *testing.T) { + clearEnv(t) + setRequired(t) + t.Setenv("VECTOR_SEARCH_ALGORITHM", "bogus") + + _, err := Load(true) + if err == nil || !strings.Contains(err.Error(), "VECTOR_SEARCH_ALGORITHM") { + t.Fatalf("expected VECTOR_SEARCH_ALGORITHM error, got %v", err) + } +} + +func TestAlgorithmCaseInsensitive(t *testing.T) { + for _, algorithm := range []string{"exact", "EXACT", "diskann", "DiskANN"} { + t.Run(algorithm, func(t *testing.T) { + clearEnv(t) + setRequired(t) + t.Setenv("VECTOR_SEARCH_ALGORITHM", algorithm) + + cfg, err := Load(true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.VectorSearchAlgorithm != strings.ToLower(algorithm) { + t.Errorf("VectorSearchAlgorithm = %q, want %q", cfg.VectorSearchAlgorithm, strings.ToLower(algorithm)) + } + }) + } +} + +func TestTableNameValidation(t *testing.T) { + cases := []struct { + name string + tableName string + shouldFail bool + }{ + {"valid default-like", "hotels_go", false}, + {"valid leading underscore", "_valid_name", false}, + {"valid mixed case", "Valid123", false}, + {"invalid leading digit", "1invalid", true}, + {"invalid hyphen", "invalid-name", true}, + {"invalid too long", strings.Repeat("a", 116), true}, + {"valid at cap", strings.Repeat("a", 115), false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + clearEnv(t) + setRequired(t) + t.Setenv("AZURE_SQL_TABLE_NAME", tc.tableName) + + cfg, err := Load(true) + if tc.shouldFail { + if err == nil { + t.Fatalf("expected error for table name %q, got none", tc.tableName) + } + return + } + if err != nil { + t.Fatalf("unexpected error for table name %q: %v", tc.tableName, err) + } + if cfg.TableName != tc.tableName { + t.Errorf("TableName = %q, want %q", cfg.TableName, tc.tableName) + } + }) + } +} + +func TestDropTableFlag(t *testing.T) { + cases := []struct { + value string + expected bool + }{ + {"true", true}, + {"TRUE", true}, + {"false", false}, + {"", false}, + } + + for _, tc := range cases { + t.Run(tc.value, func(t *testing.T) { + clearEnv(t) + setRequired(t) + if tc.value != "" { + t.Setenv("SQL_DROP_TABLE", tc.value) + } + + cfg, err := Load(true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.DropTable != tc.expected { + t.Errorf("DropTable = %v, want %v", cfg.DropTable, tc.expected) + } + }) + } +} diff --git a/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels.go b/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels.go new file mode 100644 index 0000000000..e2a1150af3 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels.go @@ -0,0 +1,78 @@ +// Package hotels loads and validates the shared hotels dataset, and +// serializes embedding vectors into the JSON-array string form Azure +// SQL's VECTOR type accepts (see constitution ASV-LANG-GO-8). +package hotels + +import ( + "encoding/json" + "fmt" + "os" +) + +// VectorDimensions is the output size of the text-embedding-3-small model +// used by every language sample for cross-language parity. +const VectorDimensions = 1536 + +// Hotel is the subset of fields this sample reads from the shared dataset. +type Hotel struct { + HotelID string `json:"HotelId"` + HotelName string `json:"HotelName"` + Description string `json:"Description"` + Category string `json:"Category"` + Rating float64 `json:"Rating"` + DescriptionVector []float64 `json:"DescriptionVector,omitempty"` +} + +// VectorToJSON serializes an embedding as the JSON-array string that +// Azure SQL's VECTOR type accepts when cast, e.g. CAST(? AS VECTOR(1536)). +func VectorToJSON(embedding []float64) (string, error) { + b, err := json.Marshal(embedding) + if err != nil { + return "", fmt.Errorf("serializing vector: %w", err) + } + return string(b), nil +} + +// Load reads the hotels dataset from path and validates that every hotel's +// DescriptionVector has exactly VectorDimensions entries. +func Load(path string) ([]Hotel, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %s: %w", path, err) + } + + var loaded []Hotel + if err := json.Unmarshal(data, &loaded); err != nil { + return nil, fmt.Errorf("parsing %s: %w", path, err) + } + + var bad []string + for i, h := range loaded { + if len(h.DescriptionVector) != VectorDimensions { + bad = append(bad, fmt.Sprintf(" Hotel %s (index %d): %d", h.HotelID, i, len(h.DescriptionVector))) + if len(bad) >= 3 { + break + } + } + } + if len(bad) > 0 { + return nil, fmt.Errorf( + "error: hotel(s) have invalid or missing vector dimensions (expected %d):\n%s\n"+ + "re-run 'go run ./cmd/embed' with a %d-dimension model, or update the VECTOR column size", + VectorDimensions, joinLines(bad), VectorDimensions, + ) + } + + return loaded, nil +} + +func joinLines(lines []string) string { + out := "" + for i, l := range lines { + if i > 0 { + out += "\n" + } + out += l + } + return out +} diff --git a/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels_test.go b/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels_test.go new file mode 100644 index 0000000000..62477f27ea --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/internal/hotels/hotels_test.go @@ -0,0 +1,73 @@ +package hotels + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestVectorToJSONRoundTrips(t *testing.T) { + embedding := []float64{0.1, 0.2, 0.3} + serialized, err := VectorToJSON(embedding) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var decoded []float64 + if err := json.Unmarshal([]byte(serialized), &decoded); err != nil { + t.Fatalf("failed to decode serialized vector: %v", err) + } + if len(decoded) != len(embedding) { + t.Fatalf("decoded length = %d, want %d", len(decoded), len(embedding)) + } + for i := range embedding { + if decoded[i] != embedding[i] { + t.Errorf("decoded[%d] = %v, want %v", i, decoded[i], embedding[i]) + } + } +} + +func writeTestData(t *testing.T, hotelsJSON []Hotel) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "hotels.json") + data, err := json.Marshal(hotelsJSON) + if err != nil { + t.Fatalf("failed to marshal test fixture: %v", err) + } + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatalf("failed to write test fixture: %v", err) + } + return path +} + +func TestLoadValid(t *testing.T) { + vector := make([]float64, VectorDimensions) + path := writeTestData(t, []Hotel{ + {HotelID: "1", HotelName: "Test Hotel", Description: "A test hotel.", DescriptionVector: vector}, + }) + + loaded, err := Load(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(loaded) != 1 { + t.Fatalf("len(loaded) = %d, want 1", len(loaded)) + } + if loaded[0].HotelID != "1" { + t.Errorf("HotelID = %q, want %q", loaded[0].HotelID, "1") + } +} + +func TestLoadRejectsWrongDimensions(t *testing.T) { + path := writeTestData(t, []Hotel{ + {HotelID: "1", HotelName: "Test Hotel", Description: "A test hotel.", DescriptionVector: []float64{0, 0, 0}}, + }) + + _, err := Load(path) + if err == nil || !strings.Contains(err.Error(), "invalid or missing vector dimensions") { + t.Fatalf("expected dimension-validation error, got %v", err) + } +} diff --git a/samples/features/vector-search/vector-search-query-go/output/sample-output.txt b/samples/features/vector-search/vector-search-query-go/output/sample-output.txt new file mode 100644 index 0000000000..3f3052db48 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/output/sample-output.txt @@ -0,0 +1,69 @@ +Azure SQL Database vector search — Go quickstart +Expected-output placeholder (per constitution ASV-CORE-21/ASV-CORE-32). + +*** THIS IS NOT A CAPTURED REAL RUN. *** + +No authorized live Azure SQL Database or Azure OpenAI environment was +available while this sample was authored, so no end-to-end execution has +been performed for Go. This file documents the expected shape of the +output, based on the already-implemented and already-validated TypeScript +reference sample's real captured run (see +../../vector-search-query-typescript/output/sample-output.txt), with +language-specific values (banner text, table name) adjusted and the +per-run values (hotel name, category, rating, description, distance, +similarity) replaced with placeholders because they have not actually +been produced by running this code. Go is also out of scope for this v1 +scenario by the constitution's own decision (ASV-LANG-GO-1); this sample +exists as a fully specified, validated-as-far-as-possible reference +implementation ahead of that decision being revisited, not as evidence +that Go coverage has been approved for release. + +Do not treat this file as validated evidence that the Go sample works +end-to-end. Replace it with a real captured run — and update this +provenance note accordingly — once this sample has been executed against +a real Azure SQL Database and Azure OpenAI resource. + +What has been validated instead, without live Azure access: + - go build ./... (all packages compile) + - go vet ./... (no issues) + - gofmt -l . (no formatting issues) + - go test ./... (6 unit tests passed) covering configuration validation, + vector serialization, and dataset shape validation + - go mod tidy / go.sum verified against the module proxy for all + dependencies (github.com/microsoft/go-mssqldb, its azuread + subpackage, github.com/Azure/azure-sdk-for-go/sdk/azidentity, + github.com/openai/openai-go/v3 and its azure subpackage, + github.com/joho/godotenv) + +=== Azure SQL Vector Search—Go Quickstart === + +Server: .database.windows.net +Database: +OpenAI: https://.openai.azure.com +Deployment: text-embedding-3-small +Algorithm: exact +Table: dbo.hotels_go + +Loaded 50 hotels from data file. + +Connecting to Azure SQL Database... +Connected. + +Creating table dbo.hotels_go (if not exists)... +Table ready. + +Inserting hotel data with precomputed embeddings... +Inserted 50 hotels. + +Searching for: "luxury beachfront hotel with ocean views and spa" + +--- Search Results—Exact (kNN) via VECTOR_DISTANCE (Top 3 by Cosine Distance) --- + + Hotel: + Category: + Rating: + Description: ... + Distance: + Similarity: + +Done. Connection closed. diff --git a/samples/features/vector-search/vector-search-query-go/sample.env b/samples/features/vector-search/vector-search-query-go/sample.env new file mode 100644 index 0000000000..1e76dd9d73 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-go/sample.env @@ -0,0 +1,10 @@ +AZURE_SQL_SERVER=.database.windows.net +AZURE_SQL_DATABASE= +# Table name for vector data (default: hotels_go) +AZURE_SQL_TABLE_NAME=hotels_go +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small +# Vector search algorithm: "exact" (default, uses VECTOR_DISTANCE) or "diskann" (uses VECTOR_SEARCH with DiskANN index, requires 1000+ rows) +VECTOR_SEARCH_ALGORITHM=exact +# Drop table at end of run to clean up artifacts (default: false) +SQL_DROP_TABLE=false diff --git a/samples/features/vector-search/vector-search-query-python/.gitignore b/samples/features/vector-search/vector-search-query-python/.gitignore new file mode 100644 index 0000000000..fc69f48de1 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/.gitignore @@ -0,0 +1,22 @@ +# Environment variables (may contain Azure credentials) +.env +.env.local +.env.*.local + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Sample output — keep the committed reference file (output/sample-output.txt); +# ignore any other ephemeral run artifacts written to this folder. +output/* +!output/sample-output.txt + +# Logs +*.log diff --git a/samples/features/vector-search/vector-search-query-python/README.md b/samples/features/vector-search/vector-search-query-python/README.md new file mode 100644 index 0000000000..c28c7c0ee6 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/README.md @@ -0,0 +1,318 @@ +# Quickstart: Vector search with Python in Azure SQL Database + +This sample demonstrates how to perform **native vector search** in Azure SQL Database using Python. + +It uses: + +- **[pyodbc](https://pypi.org/project/pyodbc/)**—the ODBC-based Python driver for SQL Server and Azure SQL Database, with Microsoft Entra token authentication (requires **ODBC Driver 18 for SQL Server** installed on the host) +- **[openai](https://pypi.org/project/openai/)**—the OpenAI Python client library for generating embeddings (via the `AzureOpenAI` class); published by OpenAI, not an Azure SDK package, though it has built-in Azure OpenAI support +- **[azure-identity](https://pypi.org/project/azure-identity/)**—`DefaultAzureCredential` for passwordless authentication to both Azure SQL and Azure OpenAI + +## What the sample does + +1. Loads 50 hotels with precomputed embeddings from `data/HotelsData_Vector.json` +2. Connects to Azure SQL Database using an Entra access token obtained from `DefaultAzureCredential` (no passwords or API keys) +3. Creates a table with `id`, `name`, `description`, `category`, `rating`, and a `VECTOR(1536)` column +4. Inserts all 50 hotels with their precomputed vector embeddings +5. Generates a fresh query embedding using Azure OpenAI `text-embedding-3-small` +6. Performs a vector similarity search using either **exact kNN** (`VECTOR_DISTANCE`) or **approximate ANN** (`VECTOR_SEARCH` with DiskANN index), based on the `VECTOR_SEARCH_ALGORITHM` environment variable +7. Displays the top matching results with category, rating, and similarity scores + +## Vector search algorithms + +This sample supports two algorithms, selected via the `VECTOR_SEARCH_ALGORITHM` environment variable: + +| | Exact search (default) | Approximate search (DiskANN) | +|---|---|---| +| **Env var value** | `exact` | `diskann` | +| **T-SQL function** | `VECTOR_DISTANCE` | `VECTOR_SEARCH` | +| **Index required** | No | Yes (auto-created) | +| **Recall** | 100% (guaranteed) | ~95–99% (tunable) | +| **Minimum rows** | No minimum | 1,000 non-null vectors | +| **Best for** | < 50,000 rows, prototyping | > 10,000 rows, production | + +> [!IMPORTANT] +> DiskANN index creation requires at least **1,000 rows** with non-null vectors. The 50-hotel sample dataset is too small—load a larger dataset before using `VECTOR_SEARCH_ALGORITHM=diskann`. This sample detects the row count at runtime and automatically falls back to exact search below that threshold. + +## Prerequisites + +- **Azure subscription**—[Create one free](https://azure.microsoft.com/free/) +- **Azure SQL Database** with native vector support—[Quickstart: Create a single database](https://learn.microsoft.com/azure/azure-sql/database/single-database-create-quickstart) +- **Azure OpenAI resource** with a `text-embedding-3-small` deployment—[Create and deploy an Azure OpenAI Service resource](https://learn.microsoft.com/azure/ai-services/openai/how-to/create-resource) +- **Python 3.13+**—[Download Python](https://www.python.org/downloads/) +- **ODBC Driver 18 for SQL Server**—[Download the ODBC driver](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server) (required by `pyodbc`; install per your OS) +- **Azure CLI**—[Install the Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), signed in with `az login` + +> [!IMPORTANT] +> Your Azure identity must be configured as a **Microsoft Entra admin** on the Azure SQL server. The `azd up` deployment sets this automatically using `deploymentUserPrincipalId`. For the Azure OpenAI resource, you need the **Cognitive Services OpenAI User** role. + +> [!IMPORTANT] +> After deploying with `azd up`, you may need to add your client IP to the Azure SQL firewall. Run: +> ```bash +> az sql server firewall-rule create --resource-group --server --name AllowMyIP --start-ip-address --end-ip-address +> ``` +> Or set `AZURE_CLIENT_IP` in your environment before running `azd up` to have it configured automatically. + +## Get started + +### 1. Clone the repository + +```bash +git clone https://github.com/microsoft/sql-server-samples.git +cd sql-server-samples/samples/features/vector-search/vector-search-query-python +``` + +### 2. Create a virtual environment and install dependencies + +```bash +python -m venv .venv +# Windows: .venv\Scripts\activate +# macOS/Linux: source .venv/bin/activate +pip install -r requirements.txt +``` + +### 3. Configure environment variables + +Copy the sample environment file and fill in your values: + +```bash +cp sample.env .env +``` + +Edit `.env` with your Azure resource details: + +```env +AZURE_SQL_SERVER=.database.windows.net +AZURE_SQL_DATABASE= +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small +VECTOR_SEARCH_ALGORITHM=exact +``` + +| Variable | Required | Description | +|---|---|---| +| `AZURE_SQL_SERVER` | Yes | Azure SQL server FQDN | +| `AZURE_SQL_DATABASE` | Yes | Database name | +| `AZURE_SQL_TABLE_NAME` | No | Table name (default: `hotels_python`) | +| `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI endpoint URL | +| `AZURE_OPENAI_EMBEDDING_DEPLOYMENT` | Yes | Embedding model deployment name | +| `VECTOR_SEARCH_ALGORITHM` | No | `exact` (default) or `diskann` | +| `SQL_DROP_TABLE` | No | `true` to drop table after run (default: `false`) | + +> [!NOTE] +> No API keys are needed. The sample uses `DefaultAzureCredential`, which automatically uses your Azure CLI login, managed identity, or other credential sources. + +### 4. Run the sample + +```bash +python src/index.py +``` + +## Expected output + +> [!IMPORTANT] +> **No live Azure environment was available while authoring this sample**, so no real end-to-end run has been captured yet. The section below documents the *shape* of the expected output based on the identical, already-validated TypeScript reference implementation's real captured run — it is **not** a captured Python run and must not be treated as validated evidence. See [`output/sample-output.txt`](./output/sample-output.txt) for the same explicit disclosure. Replace this section and that file with a real captured run before relying on this sample as validated. + +``` +=== Azure SQL Vector Search—Python Quickstart === + +Server: .database.windows.net +Database: +OpenAI: https://.openai.azure.com +Deployment: text-embedding-3-small +Algorithm: exact +Table: dbo.hotels_python + +Loaded 50 hotels from data file. + +Connecting to Azure SQL Database... +Connected. + +Creating table dbo.hotels_python (if not exists)... +Table ready. + +Inserting hotel data with precomputed embeddings... +Inserted 50 hotels. + +Searching for: "luxury beachfront hotel with ocean views and spa" + +--- Search Results—Exact (kNN) via VECTOR_DISTANCE (Top 3 by Cosine Distance) --- + + Hotel: + Category: + Rating: + Description: ... + Distance: + Similarity: + +Done. Connection closed. +``` + +> [!NOTE] +> Distance and similarity values depend on the embedding model and may vary slightly across runs. Once a real run is captured, this sample should return the **same top hotel match** as the TypeScript reference (cross-language result parity), per the constitution's canonical-query requirement. + +## Understanding the code + +### Connection with an Entra access token + +`pyodbc` doesn't have a built-in `DefaultAzureCredential` integration like `tedious` or `Microsoft.Data.SqlClient`, so this sample acquires a token explicitly and passes it via the ODBC `SQL_COPT_SS_ACCESS_TOKEN` connection attribute—the standard, Microsoft-documented pattern for Entra authentication with `pyodbc`: + +```python +import struct +from azure.identity import DefaultAzureCredential + +credential = DefaultAzureCredential() +token = credential.get_token("https://database.windows.net/.default").token + +SQL_COPT_SS_ACCESS_TOKEN = 1256 +token_bytes = token.encode("utf-16-le") +token_struct = struct.pack(f" --server \ + --name AllowMyIP --start-ip-address --end-ip-address +``` + +### ODBC driver errors + +**"Data source name not found" / `IM002` error**—ODBC Driver 18 for SQL Server is not installed. [Install it](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server) for your OS. + +### DiskANN errors + +**"DiskANN index requires at least 1,000 rows"**—The 50-hotel sample dataset is too small for DiskANN. The sample automatically detects this and falls back to exact nearest-neighbor search using `VECTOR_DISTANCE` without a vector index. To use DiskANN, load a larger dataset first. + +### Vector dimension errors + +**"Invalid or missing vector dimensions"**—The precomputed embeddings in `HotelsData_Vector.json` must use 1536 dimensions (matching `text-embedding-3-small`). Re-run `python src/embed.py` if you changed the embedding model. + +## Development + +This sample includes unit tests for pure logic (configuration validation, vector serialization, dataset validation) that require no Azure connectivity: + +```bash +pip install -r requirements-dev.txt +python -m pytest +python -m ruff check . +python -m ruff format --check . +python -m mypy src +``` + +## Related content + +- [Vectors in Azure SQL and SQL Server](https://learn.microsoft.com/sql/sql-server/ai/vectors) +- [VECTOR_DISTANCE (Transact-SQL)](https://learn.microsoft.com/sql/t-sql/functions/vector-distance-transact-sql) +- [Azure OpenAI text embeddings](https://learn.microsoft.com/azure/ai-services/openai/concepts/models#embeddings) +- [pyodbc wiki](https://github.com/mkleehammer/pyodbc/wiki) diff --git a/samples/features/vector-search/vector-search-query-python/output/sample-output.txt b/samples/features/vector-search/vector-search-query-python/output/sample-output.txt new file mode 100644 index 0000000000..9d10ac3b05 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/output/sample-output.txt @@ -0,0 +1,60 @@ +Azure SQL Database vector search — Python quickstart +Expected-output placeholder (per constitution ASV-CORE-21/ASV-CORE-32). + +*** THIS IS NOT A CAPTURED REAL RUN. *** + +No authorized live Azure SQL Database or Azure OpenAI environment was +available while this sample was authored, so no end-to-end execution has +been performed for Python. This file documents the expected shape of the +output, based on the already-implemented and already-validated TypeScript +reference sample's real captured run (see +../../vector-search-query-typescript/output/sample-output.txt), with +language-specific values (banner text, table name) adjusted and the +per-run values (hotel name, category, rating, description, distance, +similarity) replaced with placeholders because they have not actually +been produced by running this code. + +Do not treat this file as validated evidence that the Python sample works +end-to-end. Replace it with a real captured run — and update this +provenance note accordingly — once this sample has been executed against +a real Azure SQL Database and Azure OpenAI resource. + +What has been validated instead, without live Azure access: + - Static analysis: ruff check, ruff format --check, mypy (all pass) + - Unit tests: pytest (22 passed) covering configuration validation, + vector serialization, and dataset shape validation + - Local package installation and import-time validation of pyodbc, + azure-identity, and openai + +=== Azure SQL Vector Search—Python Quickstart === + +Server: .database.windows.net +Database: +OpenAI: https://.openai.azure.com +Deployment: text-embedding-3-small +Algorithm: exact +Table: dbo.hotels_python + +Loaded 50 hotels from data file. + +Connecting to Azure SQL Database... +Connected. + +Creating table dbo.hotels_python (if not exists)... +Table ready. + +Inserting hotel data with precomputed embeddings... +Inserted 50 hotels. + +Searching for: "luxury beachfront hotel with ocean views and spa" + +--- Search Results—Exact (kNN) via VECTOR_DISTANCE (Top 3 by Cosine Distance) --- + + Hotel: + Category: + Rating: + Description: ... + Distance: + Similarity: + +Done. Connection closed. diff --git a/samples/features/vector-search/vector-search-query-python/pyproject.toml b/samples/features/vector-search/vector-search-query-python/pyproject.toml new file mode 100644 index 0000000000..cd587c547f --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/pyproject.toml @@ -0,0 +1,15 @@ +[tool.ruff] +line-length = 110 +target-version = "py313" +extend-exclude = ["README.md"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP"] + +[tool.mypy] +python_version = "3.13" +ignore_missing_imports = true +check_untyped_defs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/samples/features/vector-search/vector-search-query-python/requirements-dev.txt b/samples/features/vector-search/vector-search-query-python/requirements-dev.txt new file mode 100644 index 0000000000..8418d78c47 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/requirements-dev.txt @@ -0,0 +1,3 @@ +ruff>=0.15.0 +mypy>=2.0.0 +pytest>=8.0.0 diff --git a/samples/features/vector-search/vector-search-query-python/requirements.txt b/samples/features/vector-search/vector-search-query-python/requirements.txt new file mode 100644 index 0000000000..be7dc321b0 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/requirements.txt @@ -0,0 +1,4 @@ +pyodbc>=5.3.0 +azure-identity>=1.25.0 +openai>=3.9.0 +python-dotenv>=1.2.0 diff --git a/samples/features/vector-search/vector-search-query-python/sample.env b/samples/features/vector-search/vector-search-query-python/sample.env new file mode 100644 index 0000000000..f5c8048e13 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/sample.env @@ -0,0 +1,10 @@ +AZURE_SQL_SERVER=.database.windows.net +AZURE_SQL_DATABASE= +# Table name for vector data (default: hotels_python) +AZURE_SQL_TABLE_NAME=hotels_python +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com +AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small +# Vector search algorithm: "exact" (default, uses VECTOR_DISTANCE) or "diskann" (uses VECTOR_SEARCH with DiskANN index, requires 1000+ rows) +VECTOR_SEARCH_ALGORITHM=exact +# Drop table at end of run to clean up artifacts (default: false) +SQL_DROP_TABLE=false diff --git a/samples/features/vector-search/vector-search-query-python/src/__init__.py b/samples/features/vector-search/vector-search-query-python/src/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samples/features/vector-search/vector-search-query-python/src/config.py b/samples/features/vector-search/vector-search-query-python/src/config.py new file mode 100644 index 0000000000..82b7f905d5 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/src/config.py @@ -0,0 +1,79 @@ +"""Configuration loading and validation for the Azure SQL vector search sample. + +Reads settings from environment variables (populated from a local .env file +via python-dotenv). Mirrors the required/optional variable set and defaults +defined in the repository constitution (ASV-LANG-TS-3 / ASV-LANG-PY-4): +AZURE_SQL_SERVER, AZURE_SQL_DATABASE, AZURE_OPENAI_ENDPOINT, +AZURE_OPENAI_EMBEDDING_DEPLOYMENT are required; AZURE_SQL_TABLE_NAME, +VECTOR_SEARCH_ALGORITHM, and SQL_DROP_TABLE are optional with defaults. +""" + +from __future__ import annotations + +import os +import re +from dataclasses import dataclass + +_TABLE_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,114}$") + + +@dataclass(frozen=True) +class AppConfig: + azure_sql_server: str | None + azure_sql_database: str | None + azure_openai_endpoint: str + azure_openai_embedding_deployment: str + table_name: str + vector_search_algorithm: str + drop_table: bool + + +def _required(key: str) -> str: + value = os.environ.get(key) + if not value: + raise RuntimeError( + f"Missing required environment variable: {key}. Copy sample.env to .env and fill in your values." + ) + return value + + +def _optional(key: str) -> str | None: + return os.environ.get(key) + + +def load_config(require_sql: bool = True) -> AppConfig: + """Load and validate configuration from environment variables. + + Args: + require_sql: When True (the default), AZURE_SQL_SERVER and + AZURE_SQL_DATABASE must be set. Pass False for tooling (such as + the embedding-generation script) that does not connect to SQL. + """ + algorithm_raw = (_optional("VECTOR_SEARCH_ALGORITHM") or "exact").lower() + if algorithm_raw not in ("exact", "diskann"): + raise RuntimeError( + f'Invalid VECTOR_SEARCH_ALGORITHM: "{algorithm_raw}". Must be "exact" or "diskann".' + ) + + table_name = _optional("AZURE_SQL_TABLE_NAME") or "hotels_python" + if not _TABLE_NAME_PATTERN.match(table_name): + raise RuntimeError( + f'Invalid AZURE_SQL_TABLE_NAME: "{table_name}". ' + "Must start with a letter or underscore, contain only letters, " + "numbers, and underscores, and be at most 115 characters." + ) + + drop_table = (_optional("SQL_DROP_TABLE") or "false").lower() == "true" + + azure_sql_server = _required("AZURE_SQL_SERVER") if require_sql else _optional("AZURE_SQL_SERVER") + azure_sql_database = _required("AZURE_SQL_DATABASE") if require_sql else _optional("AZURE_SQL_DATABASE") + + return AppConfig( + azure_sql_server=azure_sql_server, + azure_sql_database=azure_sql_database, + azure_openai_endpoint=_required("AZURE_OPENAI_ENDPOINT"), + azure_openai_embedding_deployment=_required("AZURE_OPENAI_EMBEDDING_DEPLOYMENT"), + table_name=table_name, + vector_search_algorithm=algorithm_raw, + drop_table=drop_table, + ) diff --git a/samples/features/vector-search/vector-search-query-python/src/embed.py b/samples/features/vector-search/vector-search-query-python/src/embed.py new file mode 100644 index 0000000000..29177e283b --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/src/embed.py @@ -0,0 +1,66 @@ +"""Generate precomputed embeddings for the hotels dataset (Python quickstart). + +Reads data/HotelsData.JSON (no vectors), calls Azure OpenAI in batches to +generate a text-embedding-3-small embedding per hotel description, and +writes data/HotelsData_Vector.json — mirroring the TypeScript reference +sample's embed.ts. Run this only if you need to regenerate the shipped +vector data (for example, after changing the embedding model). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from openai import AzureOpenAI + +from config import load_config + +BATCH_SIZE = 20 + + +def main() -> None: + load_dotenv() + config = load_config(require_sql=False) + + base = Path(__file__).resolve().parent.parent.parent / "data" + input_path = base / "HotelsData.JSON" + output_path = base / "HotelsData_Vector.json" + + with input_path.open("r", encoding="utf-8") as f: + hotels = json.load(f) + print(f"Generating embeddings for {len(hotels)} hotels...") + + credential = DefaultAzureCredential() + token_provider = get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default") + client = AzureOpenAI( + azure_endpoint=config.azure_openai_endpoint, + azure_ad_token_provider=token_provider, + api_version="2024-10-21", + timeout=30.0, + max_retries=3, + ) + + descriptions = [hotel["Description"] for hotel in hotels] + all_embeddings: list[list[float]] = [] + + for start in range(0, len(descriptions), BATCH_SIZE): + batch = descriptions[start : start + BATCH_SIZE] + response = client.embeddings.create(model=config.azure_openai_embedding_deployment, input=batch) + all_embeddings.extend(item.embedding for item in response.data) + done = min(start + BATCH_SIZE, len(descriptions)) + print(f" Embedded {done}/{len(descriptions)}") + + hotels_with_vectors = [ + {**hotel, "DescriptionVector": all_embeddings[idx]} for idx, hotel in enumerate(hotels) + ] + + with output_path.open("w", encoding="utf-8") as f: + json.dump(hotels_with_vectors, f, indent=2) + print("Done. Wrote HotelsData_Vector.json") + + +if __name__ == "__main__": + main() diff --git a/samples/features/vector-search/vector-search-query-python/src/index.py b/samples/features/vector-search/vector-search-query-python/src/index.py new file mode 100644 index 0000000000..92b96fc558 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/src/index.py @@ -0,0 +1,315 @@ +"""Azure SQL Database vector search — Python quickstart. + +Mirrors the scenario implemented by the TypeScript reference sample +(vector-search-query-typescript): create a table with a VECTOR(1536) +column, bulk-load 50 hotels with precomputed embeddings, generate one +fresh query embedding via Azure OpenAI, and run a similarity search using +either exact kNN (VECTOR_DISTANCE) or approximate ANN (VECTOR_SEARCH with +a DiskANN index), selected via VECTOR_SEARCH_ALGORITHM. + +Per constitution ASV-VS-4: DiskANN requires >= 1,000 rows with non-null +vectors. Below that threshold this script falls back to exact search +automatically and logs a warning, identical to the TypeScript reference. +""" + +from __future__ import annotations + +import json +import struct +import sys +from pathlib import Path +from typing import Any + +import pyodbc +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from openai import AzureOpenAI + +from config import load_config + +VECTOR_DIMENSIONS = 1536 # text-embedding-3-small output dimensions +SQL_COPT_SS_ACCESS_TOKEN = 1256 # pyodbc/ODBC driver attribute for an Entra access token +SEARCH_QUERY = "luxury beachfront hotel with ocean views and spa" + + +def _access_token_struct(token: str) -> bytes: + """Pack an Entra access token into the byte structure pyodbc/ODBC expects.""" + token_bytes = token.encode("utf-16-le") + return struct.pack(f" pyodbc.Connection: + token = credential.get_token("https://database.windows.net/.default").token + conn_str = ( + "Driver={ODBC Driver 18 for SQL Server};" + f"Server=tcp:{server},1433;" + f"Database={database};" + "Encrypt=yes;TrustServerCertificate=no;" + ) + try: + return pyodbc.connect( + conn_str, + attrs_before={SQL_COPT_SS_ACCESS_TOKEN: _access_token_struct(token)}, + autocommit=False, + ) + except pyodbc.Error as err: + message = str(err) + if "Login failed" in message or "IM002" in message: + print( + "Authentication or driver failure. Ensure:\n" + " 1. You are signed in: az login\n" + " 2. Your identity is set as Microsoft Entra admin on the SQL server\n" + " 3. Your client IP is in the SQL server firewall rules\n" + " 4. ODBC Driver 18 for SQL Server is installed\n", + file=sys.stderr, + ) + raise + + +def vector_to_json(embedding: list[float]) -> str: + """Serialize an embedding as the JSON-array string Azure SQL's VECTOR type accepts.""" + return json.dumps(embedding) + + +def generate_query_embedding(client: AzureOpenAI, deployment: str, text: str) -> list[float]: + response = client.embeddings.create(model=deployment, input=[text]) + return list(response.data[0].embedding) + + +def load_hotels(data_path: Path) -> list[dict[str, Any]]: + with data_path.open("r", encoding="utf-8") as f: + hotels: list[dict[str, Any]] = json.load(f) + bad = [ + (i, h.get("HotelId"), len(h.get("DescriptionVector") or [])) + for i, h in enumerate(hotels) + if len(h.get("DescriptionVector") or []) != VECTOR_DIMENSIONS + ] + if bad: + examples = "\n".join(f" Hotel {hid} (index {i}): {dim}" for i, hid, dim in bad[:3]) + raise RuntimeError( + f"Error: {len(bad)} hotel(s) have invalid or missing vector dimensions " + f"(expected {VECTOR_DIMENSIONS}):\n{examples}\n" + f"Re-run 'python src/embed.py' with a {VECTOR_DIMENSIONS}-dimension model, " + "or update the VECTOR column size." + ) + return hotels + + +def create_table(cursor: pyodbc.Cursor, table_name: str) -> None: + cursor.execute( + f""" + IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = N'{table_name}' AND schema_id = SCHEMA_ID('dbo')) + BEGIN + CREATE TABLE dbo.[{table_name}] ( + id NVARCHAR(50) PRIMARY KEY, + name NVARCHAR(200) NOT NULL, + description NVARCHAR(MAX) NOT NULL, + category NVARCHAR(100) NULL, + rating FLOAT NULL, + embedding VECTOR(1536) NULL + ); + END + """ + ) + + +def insert_hotels(cursor: pyodbc.Cursor, table_name: str, hotels: list[dict[str, Any]]) -> None: + cursor.execute(f"DELETE FROM dbo.[{table_name}]") + insert_sql = ( + f"INSERT INTO dbo.[{table_name}] (id, name, description, category, rating, embedding) " + f"VALUES (?, ?, ?, ?, ?, CAST(? AS VECTOR(1536)))" + ) + cursor.fast_executemany = True + rows = [ + ( + hotel["HotelId"], + hotel["HotelName"], + hotel["Description"], + hotel.get("Category"), + hotel.get("Rating"), + vector_to_json(hotel["DescriptionVector"]), + ) + for hotel in hotels + ] + cursor.executemany(insert_sql, rows) + + +def determine_algorithm(cursor: pyodbc.Cursor, table_name: str, requested: str) -> str: + if requested != "diskann": + return requested + + cursor.execute(f"SELECT COUNT(*) FROM dbo.[{table_name}] WHERE embedding IS NOT NULL") + row_count = cursor.fetchone()[0] + if row_count < 1000: + print( + f"\u26a0 DiskANN index requires at least 1,000 rows with non-null vectors, " + f"but table has only {row_count}. Falling back to exact (VECTOR_DISTANCE) search.\n" + ) + return "exact" + + print("Creating DiskANN vector index (if not exists)...") + cursor.execute( + f""" + IF NOT EXISTS ( + SELECT * FROM sys.indexes + WHERE name = N'ix_{table_name}_embedding' AND object_id = OBJECT_ID('dbo.[{table_name}]') + ) + BEGIN + CREATE VECTOR INDEX [ix_{table_name}_embedding] + ON dbo.[{table_name}](embedding) + WITH (type = 'DiskANN', metric = 'cosine'); + END + """ + ) + print("DiskANN index ready.\n") + return "diskann" + + +def run_search(cursor: pyodbc.Cursor, table_name: str, algorithm: str, query_vector_json: str) -> list[tuple]: + if algorithm == "diskann": + cursor.execute( + f""" + SELECT TOP 3 + vs.distance, + h.name, h.description, h.category, h.rating + FROM VECTOR_SEARCH( + dbo.[{table_name}], embedding, + CAST(? AS VECTOR(1536)), + 'cosine', 3 + ) AS vs + INNER JOIN dbo.[{table_name}] h ON vs.$rowid = h.$rowid + ORDER BY vs.distance + """, + query_vector_json, + ) + return [(r.name, r.description, r.category, r.rating, r.distance) for r in cursor.fetchall()] + + cursor.execute( + f""" + SELECT TOP 3 + name, description, category, rating, + VECTOR_DISTANCE('cosine', embedding, CAST(? AS VECTOR(1536))) AS distance + FROM dbo.[{table_name}] + ORDER BY distance + """, + query_vector_json, + ) + return [(r.name, r.description, r.category, r.rating, r.distance) for r in cursor.fetchall()] + + +def main() -> None: + print("=== Azure SQL Vector Search\u2014Python Quickstart ===\n") + + load_dotenv() + config = load_config(require_sql=True) + print(f"Server: {config.azure_sql_server}") + print(f"Database: {config.azure_sql_database}") + print(f"OpenAI: {config.azure_openai_endpoint}") + print(f"Deployment: {config.azure_openai_embedding_deployment}") + print(f"Algorithm: {config.vector_search_algorithm}") + print(f"Table: dbo.{config.table_name}\n") + + data_path = Path(__file__).resolve().parent.parent.parent / "data" / "HotelsData_Vector.json" + hotels = load_hotels(data_path) + print(f"Loaded {len(hotels)} hotels from data file.\n") + + credential = DefaultAzureCredential() + + print("Connecting to Azure SQL Database...") + assert config.azure_sql_server is not None + assert config.azure_sql_database is not None + conn = connect_to_sql(config.azure_sql_server, config.azure_sql_database, credential) + print("Connected.\n") + + try: + cursor = conn.cursor() + + table_name = config.table_name + print(f"Creating table dbo.{table_name} (if not exists)...") + create_table(cursor, table_name) + conn.commit() + print("Table ready.\n") + + print("Inserting hotel data with precomputed embeddings...") + try: + insert_hotels(cursor, table_name, hotels) + conn.commit() + except Exception: + conn.rollback() + raise + print(f"Inserted {len(hotels)} hotels.\n") + + print(f'Searching for: "{SEARCH_QUERY}"\n') + + token_provider = get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default") + openai_client = AzureOpenAI( + azure_endpoint=config.azure_openai_endpoint, + azure_ad_token_provider=token_provider, + api_version="2024-10-21", + timeout=30.0, + max_retries=3, + ) + + try: + query_vector = generate_query_embedding( + openai_client, config.azure_openai_embedding_deployment, SEARCH_QUERY + ) + except Exception as err: + message = str(err) + if "401" in message or "403" in message or "AuthenticationError" in message: + print( + "Azure OpenAI authentication failed. Ensure:\n" + " 1. You are signed in: az login\n" + " 2. You have the 'Cognitive Services OpenAI User' role on the Azure OpenAI resource\n" + f" 3. The endpoint is correct: {config.azure_openai_endpoint}\n" + f" 4. The deployment exists: {config.azure_openai_embedding_deployment}\n", + file=sys.stderr, + ) + raise + + if len(query_vector) != VECTOR_DIMENSIONS: + raise RuntimeError( + f"Query embedding has unexpected dimensions: {len(query_vector)} " + f"(expected {VECTOR_DIMENSIONS}). Check your Azure OpenAI deployment " + f'"{config.azure_openai_embedding_deployment}".' + ) + query_vector_json = vector_to_json(query_vector) + + algorithm = determine_algorithm(cursor, table_name, config.vector_search_algorithm) + conn.commit() + + results = run_search(cursor, table_name, algorithm, query_vector_json) + + algorithm_label = ( + "Approximate (DiskANN) via VECTOR_SEARCH" + if algorithm == "diskann" + else "Exact (kNN) via VECTOR_DISTANCE" + ) + print(f"--- Search Results\u2014{algorithm_label} (Top 3 by Cosine Distance) ---\n") + for name, description, category, rating, distance in results: + similarity = 1 - distance + print(f" Hotel: {name}") + print(f" Category: {category}") + print(f" Rating: {rating}") + print(f" Description: {description[:100]}...") + print(f" Distance: {distance:.4f}") + print(f" Similarity: {similarity:.4f}") + print() + + if config.drop_table: + print(f"Dropping table dbo.[{table_name}]...") + if algorithm == "diskann": + cursor.execute(f"DROP INDEX IF EXISTS [ix_{table_name}_embedding] ON dbo.[{table_name}]") + cursor.execute(f"DROP TABLE IF EXISTS dbo.[{table_name}]") + conn.commit() + print("Table dropped \u2014 no artifacts left behind.\n") + else: + print(f"Table dbo.[{table_name}] retained (set SQL_DROP_TABLE=true to clean up).\n") + + finally: + conn.close() + print("Done. Connection closed.") + + +if __name__ == "__main__": + main() diff --git a/samples/features/vector-search/vector-search-query-python/tests/test_config.py b/samples/features/vector-search/vector-search-query-python/tests/test_config.py new file mode 100644 index 0000000000..79c023ade0 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/tests/test_config.py @@ -0,0 +1,106 @@ +"""Unit tests for config.py — no Azure/network access required. + +These are the "safe no-secret static/unit checks" referenced by the +constitution: pure-function validation that can run in CI without a live +Azure SQL Database or Azure OpenAI resource. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) + +from config import load_config # noqa: E402 + + +@pytest.fixture(autouse=True) +def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None: + for key in ( + "AZURE_SQL_SERVER", + "AZURE_SQL_DATABASE", + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_EMBEDDING_DEPLOYMENT", + "AZURE_SQL_TABLE_NAME", + "VECTOR_SEARCH_ALGORITHM", + "SQL_DROP_TABLE", + ): + monkeypatch.delenv(key, raising=False) + + +def _set_required(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("AZURE_SQL_SERVER", "example.database.windows.net") + monkeypatch.setenv("AZURE_SQL_DATABASE", "exampledb") + monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com") + monkeypatch.setenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT", "text-embedding-3-small") + + +def test_load_config_defaults(monkeypatch: pytest.MonkeyPatch) -> None: + _set_required(monkeypatch) + config = load_config() + assert config.table_name == "hotels_python" + assert config.vector_search_algorithm == "exact" + assert config.drop_table is False + + +def test_load_config_missing_required_raises(monkeypatch: pytest.MonkeyPatch) -> None: + with pytest.raises(RuntimeError, match="AZURE_SQL_SERVER"): + load_config() + + +def test_load_config_require_sql_false_allows_missing_sql_vars( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", "https://example.openai.azure.com") + monkeypatch.setenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT", "text-embedding-3-small") + config = load_config(require_sql=False) + assert config.azure_sql_server is None + assert config.azure_sql_database is None + + +def test_invalid_algorithm_raises(monkeypatch: pytest.MonkeyPatch) -> None: + _set_required(monkeypatch) + monkeypatch.setenv("VECTOR_SEARCH_ALGORITHM", "bogus") + with pytest.raises(RuntimeError, match="Invalid VECTOR_SEARCH_ALGORITHM"): + load_config() + + +@pytest.mark.parametrize("algorithm", ["exact", "EXACT", "diskann", "DiskANN"]) +def test_algorithm_case_insensitive(monkeypatch: pytest.MonkeyPatch, algorithm: str) -> None: + _set_required(monkeypatch) + monkeypatch.setenv("VECTOR_SEARCH_ALGORITHM", algorithm) + config = load_config() + assert config.vector_search_algorithm == algorithm.lower() + + +@pytest.mark.parametrize( + "table_name,should_raise", + [ + ("hotels_python", False), + ("_valid_name", False), + ("Valid123", False), + ("1invalid", True), # cannot start with a digit + ("invalid-name", True), # hyphen not allowed + ("a" * 116, True), # exceeds 115-char cap + ("a" * 115, False), # exactly at the cap + ], +) +def test_table_name_validation(monkeypatch: pytest.MonkeyPatch, table_name: str, should_raise: bool) -> None: + _set_required(monkeypatch) + monkeypatch.setenv("AZURE_SQL_TABLE_NAME", table_name) + if should_raise: + with pytest.raises(RuntimeError, match="Invalid AZURE_SQL_TABLE_NAME"): + load_config() + else: + assert load_config().table_name == table_name + + +@pytest.mark.parametrize("value,expected", [("true", True), ("TRUE", True), ("false", False), (None, False)]) +def test_drop_table_flag(monkeypatch: pytest.MonkeyPatch, value: str | None, expected: bool) -> None: + _set_required(monkeypatch) + if value is not None: + monkeypatch.setenv("SQL_DROP_TABLE", value) + assert load_config().drop_table is expected diff --git a/samples/features/vector-search/vector-search-query-python/tests/test_index.py b/samples/features/vector-search/vector-search-query-python/tests/test_index.py new file mode 100644 index 0000000000..3032c95316 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-python/tests/test_index.py @@ -0,0 +1,54 @@ +"""Unit tests for pure functions in index.py — no Azure/network access required.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) + +from index import VECTOR_DIMENSIONS, load_hotels, vector_to_json # noqa: E402 + + +def test_vector_to_json_roundtrips() -> None: + embedding = [0.1, 0.2, 0.3] + serialized = vector_to_json(embedding) + assert json.loads(serialized) == embedding + + +def test_load_hotels_valid(tmp_path: Path) -> None: + data = [ + { + "HotelId": "1", + "HotelName": "Test Hotel", + "Description": "A test hotel.", + "Category": "Luxury", + "Rating": 4.5, + "DescriptionVector": [0.0] * VECTOR_DIMENSIONS, + } + ] + data_path = tmp_path / "hotels.json" + data_path.write_text(json.dumps(data), encoding="utf-8") + + hotels = load_hotels(data_path) + assert len(hotels) == 1 + assert hotels[0]["HotelId"] == "1" + + +def test_load_hotels_rejects_wrong_dimensions(tmp_path: Path) -> None: + data = [ + { + "HotelId": "1", + "HotelName": "Test Hotel", + "Description": "A test hotel.", + "DescriptionVector": [0.0] * 10, # wrong dimension count + } + ] + data_path = tmp_path / "hotels.json" + data_path.write_text(json.dumps(data), encoding="utf-8") + + with pytest.raises(RuntimeError, match="invalid or missing vector dimensions"): + load_hotels(data_path) diff --git a/samples/features/vector-search/vector-search-query-typescript/.gitignore b/samples/features/vector-search/vector-search-query-typescript/.gitignore index c0cb3d681f..4c250a205a 100644 --- a/samples/features/vector-search/vector-search-query-typescript/.gitignore +++ b/samples/features/vector-search/vector-search-query-typescript/.gitignore @@ -9,8 +9,10 @@ dist/ # Dependencies node_modules/ -# Sample output -output/ +# Sample output — keep the committed reference file (output/sample-output.txt); +# ignore any other ephemeral run artifacts written to this folder. +output/* +!output/sample-output.txt # Logs *.log diff --git a/samples/features/vector-search/vector-search-query-typescript/.prettierignore b/samples/features/vector-search/vector-search-query-typescript/.prettierignore new file mode 100644 index 0000000000..fc5b8857ef --- /dev/null +++ b/samples/features/vector-search/vector-search-query-typescript/.prettierignore @@ -0,0 +1,5 @@ +package-lock.json +node_modules/ +dist/ +output/ +README.md diff --git a/samples/features/vector-search/vector-search-query-typescript/.prettierrc.json b/samples/features/vector-search/vector-search-query-typescript/.prettierrc.json new file mode 100644 index 0000000000..f0eb61e0f7 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-typescript/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": false +} diff --git a/samples/features/vector-search/vector-search-query-typescript/eslint.config.js b/samples/features/vector-search/vector-search-query-typescript/eslint.config.js new file mode 100644 index 0000000000..9ad95bb056 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-typescript/eslint.config.js @@ -0,0 +1,27 @@ +// @ts-check +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["dist/**", "node_modules/**", "output/**"], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + // Sample code prints diagnostics and prompts to the console by design. + "no-console": "off", + }, + }, + { + // Plain Node.js scripts (not type-checked TypeScript source). + files: ["scripts/**/*.js"], + languageOptions: { + globals: { + process: "readonly", + console: "readonly", + }, + }, + } +); diff --git a/samples/features/vector-search/vector-search-query-typescript/output/sample-output.txt b/samples/features/vector-search/vector-search-query-typescript/output/sample-output.txt new file mode 100644 index 0000000000..de29c3a6a3 --- /dev/null +++ b/samples/features/vector-search/vector-search-query-typescript/output/sample-output.txt @@ -0,0 +1,60 @@ +Azure SQL Database vector search — TypeScript quickstart +Captured expected-output reference (per constitution ASV-CORE-32). + +Provenance: this is the same real, previously captured run output already +published in this sample's README.md "Expected output" section (not +re-generated for this commit — no authorized live Azure environment was +available in this session; see the PR description for the explicit +no-live-execution disclosure). Server/database/OpenAI endpoint values are +redacted placeholders; hotel names, categories, ratings, descriptions, +distances, and similarity scores are real values from an actual run. + +=== Azure SQL Vector Search—TypeScript Quickstart === + +Server: .database.windows.net +Database: +OpenAI: https://.openai.azure.com +Deployment: text-embedding-3-small +Algorithm: exact +Table: dbo.hotels_typescript + +Loaded 50 hotels from data file. + +Connecting to Azure SQL Database... +Connected. + +Creating hotels table (if not exists)... +Table ready. + +Inserting hotel data with precomputed embeddings... +Inserted 50 hotels. + +Searching for: "luxury beachfront hotel with ocean views and spa" + +--- Search Results—Exact (kNN) via VECTOR_DISTANCE (Top 3 by Cosine Distance) --- + + Hotel: Ocean Water Resort & Spa + Category: Luxury + Rating: 4.2 + Description: New Luxury Hotel for the vacation of a lifetime. Bay views from every room, location near the pier, ... + Distance: 0.4060 + Similarity: 0.5940 + + Hotel: Windy Ocean Motel + Category: Suite + Rating: 3.5 + Description: Oceanfront hotel overlooking the beach features rooms with a private balcony and 2 indoor and outdoo... + Distance: 0.4600 + Similarity: 0.5400 + + Hotel: Gold View Inn + Category: Suite + Rating: 2.8 + Description: AAA Four Diamond Resort. Nestled on six beautifully landscaped acres, located 2 blocks from the park... + Distance: 0.5296 + Similarity: 0.4704 + +Done. Connection closed. + +NOTE: Distance and similarity values depend on the embedding model and may +vary slightly across runs. diff --git a/samples/features/vector-search/vector-search-query-typescript/package-lock.json b/samples/features/vector-search/vector-search-query-typescript/package-lock.json index 22c2f40afb..c04d26f8de 100644 --- a/samples/features/vector-search/vector-search-query-typescript/package-lock.json +++ b/samples/features/vector-search/vector-search-query-typescript/package-lock.json @@ -14,9 +14,13 @@ "tedious": "^19.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^22.15.0", + "eslint": "^10.10.0", + "prettier": "^3.9.6", "tsx": "^4.23.13", - "typescript": "^5.8.0" + "typescript": "^5.8.0", + "typescript-eslint": "^8.70.0" }, "engines": { "node": ">=20.6.0" @@ -290,6 +294,30 @@ "node": ">=0.8.0" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha1-cq64sFH21ZfRCshZW5StgwK9Ijk=", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha1-U0yRETqkj+Q7rtsWlVC27gcO8wM=", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -732,250 +760,986 @@ "node": ">=18" } }, - "node_modules/@js-joda/core": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", - "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", - "license": "BSD-3-Clause" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha1-iRG9crLDZApUNgngQAuMTS5+fLY=", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@types/readable-stream": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", - "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", - "license": "MIT", - "dependencies": { - "@types/node": "*" + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", - "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", + "dev": true, "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=20.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha1-VuhtJDBJGV2KzAwGobPf3D+j3pU=", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "event-target-shim": "^5.0.0" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": ">=6.5" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha1-Ce5KoHtz8FnsLUx0v0sv8CsyI3c=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">= 14" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "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/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", - "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", - "license": "MIT", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha1-wdp80bgvqHh/mLVin7gRhIobY84=", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@types/readable-stream": "^4.0.0", - "buffer": "^6.0.3", - "inherits": "^2.0.4", - "readable-stream": "^4.2.0" + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "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/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha1-HoqHb1ARevirZ+R9WtlNONZiJYM=", + "dev": true, "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha1-iOm/TRHSsZwILnjr586IckpesJE=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha1-zHJozDZAWzMe+S2xvDeXHyHWb+E=", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "run-applescript": "^7.0.0" + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "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/sponsors/sindresorhus" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "ms": "^2.1.3" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18.18.0" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "license": "MIT", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "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/sponsors/sindresorhus" + "node": ">=18.18.0" } }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "license": "MIT", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=18" - }, - "funding": { - "url": "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/sponsors/sindresorhus" + "node": ">=18.18.0" } }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=12.22" }, "funding": { - "url": "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/sponsors/sindresorhus" + "type": "github", + "url": "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/sponsors/nzakas" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", + "dev": true, "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "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/sponsors/nzakas" } }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha1-/IL6g5R+f/aMZ5jQiQfbhCdx7yw=", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" }, "engines": { - "node": ">=18" + "node": ">= 18" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha1-DAHdOjSDiCr3zzh41OcdUFyB/Eo=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha1-b2Nq+WL75hkbgwvWdrpZhpJrzOw=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha1-WfY1x03R4r/7auy9pVeyTa3/09w=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha1-R1shl63pFu2rBa3jXGkVGOhUM4I=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha1-ls4t6WwGyEQv6hqPeweFWlazxeM=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha1-pi6Dc2LybGBK0Vsguszhw/TWtVI=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha1-t3YosDycVu8h+1prwvSkM1FjuZk=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha1-9YPKchWcT9jndcFT2jJB3mt3l08=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha1-f5wBwk5Wv60qCJFnkmx83e2d5fY=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha1-nuUoiM3spgT+lDaTUhm5Z/p/YFM=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha1-ec/NlnjuKOppzBMDLA+Jux0piRc=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha1-eKeLTFLdhSPlMhmTy0br5teTRRA=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha1-tFHIrqdtyX/HaLjZ1/YQGb94lig=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha1-T68BstbTJr/u2XrqH1IiC19MGUA=", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "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/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "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/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha1-fHJDiAm1+lur9UGZofHCgaaYT88=", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "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/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha1-0ULUEEPlqGX2BTzHDvTjrQaL1c4=", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", + "dev": true, + "license": "MIT" + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha1-FLHRhJ7tsu5oBfN1kFBHnOKyPXE=", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha1-ud5qzi+rHP8k0uWNhbdMj86jmAI=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/espree/-/espree-11.2.0.tgz", + "integrity": "sha1-AdXkfcMyqrowWQCDYkVKjMNMyqU=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -985,15 +1749,100 @@ "node": ">=6" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha1-yCEOsFXeY+aGhcz7agF+OG1Fd9A=", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha1-c13IiMJxho1zAbO7uO26UCivth0=", + "dev": true, "license": "MIT", - "engines": { - "node": ">=0.8.x" + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha1-ruyipQYwPwzuYcWebJ8qiNLyn8Y=", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1009,6 +1858,39 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha1-S6gq1UkRrGF0Z4cIRdV6n+UIpAA=", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha1-sfr+qlSJzcKcuFVGqPg37U/7vLY=", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -1071,6 +1953,26 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1092,6 +1994,29 @@ "url": "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/sponsors/sindresorhus" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -1125,12 +2050,33 @@ "url": "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/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "license": "ISC" + }, "node_modules/js-md4": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true, + "license": "MIT" + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -1174,6 +2120,46 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha1-AwRAdMa00HLQpix7n6ZJU3uvAQU=", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/levn/-/levn-0.4.1.tgz", + "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1216,6 +2202,22 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha1-/ZVrvgt3JB6fFaxdzLHGOAYJaO8=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "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/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1228,6 +2230,13 @@ "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", "license": "MIT" }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true, + "license": "MIT" + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -1267,6 +2276,115 @@ } } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha1-YxM2ADTMs2s9xh7L3/eBIfkP4h8=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "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/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha1-s+pRRlFdQPxT8YqmP3Tfqx4Q2/Y=", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "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/prettier/prettier?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1276,6 +2394,36 @@ "node": ">= 0.6.0" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/qified/-/qified-0.10.1.tgz", + "integrity": "sha1-BkC/IbvmylQNspCtj1/eTYcLi9o=", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha1-HQJKwWaJc91bzEqWq5zNt2Oe+NQ=", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -1342,6 +2490,29 @@ "node": ">=10" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -1378,6 +2549,36 @@ "node": ">=18.17" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha1-ViqabJ6ys7Ej05cZ+a9btE/NdjE=", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "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/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha1-Ss1KFV4ic0mQpe0f6el/ETvLN8E=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1403,6 +2604,19 @@ "fsevents": "~2.3.3" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1417,12 +2631,72 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha1-bJDFMgeRMrp9Sjhr8IgsVtgv5a8=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/which/-/which-2.0.2.tgz", + "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", @@ -1437,6 +2711,19 @@ "funding": { "url": "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/sponsors/sindresorhus" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "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/sponsors/sindresorhus" + } } } } diff --git a/samples/features/vector-search/vector-search-query-typescript/package.json b/samples/features/vector-search/vector-search-query-typescript/package.json index 99fad2d40b..635960ca9c 100644 --- a/samples/features/vector-search/vector-search-query-typescript/package.json +++ b/samples/features/vector-search/vector-search-query-typescript/package.json @@ -14,7 +14,10 @@ "start": "node --env-file=.env --import tsx ./src/index.ts", "embed": "node --env-file=.env --import tsx ./src/embed.ts", "build": "node scripts/typecheck.js", - "check": "node scripts/typecheck.js --no-emit" + "check": "node scripts/typecheck.js --no-emit", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@azure/identity": "^4.9.1", @@ -22,9 +25,13 @@ "tedious": "^19.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^22.15.0", + "eslint": "^10.10.0", + "prettier": "^3.9.6", "tsx": "^4.23.13", - "typescript": "^5.8.0" + "typescript": "^5.8.0", + "typescript-eslint": "^8.70.0" }, "engines": { "node": ">=20.6.0" diff --git a/samples/features/vector-search/vector-search-query-typescript/src/config.ts b/samples/features/vector-search/vector-search-query-typescript/src/config.ts index 565f66975f..e19c8fd189 100644 --- a/samples/features/vector-search/vector-search-query-typescript/src/config.ts +++ b/samples/features/vector-search/vector-search-query-typescript/src/config.ts @@ -24,7 +24,9 @@ export function loadConfig(requireSql: boolean = true): AppConfig { const optional = (key: string): string | undefined => process.env[key]; - const algorithmRaw = (optional("VECTOR_SEARCH_ALGORITHM") ?? "exact").toLowerCase(); + const algorithmRaw = ( + optional("VECTOR_SEARCH_ALGORITHM") ?? "exact" + ).toLowerCase(); if (algorithmRaw !== "exact" && algorithmRaw !== "diskann") { throw new Error( `Invalid VECTOR_SEARCH_ALGORITHM: "${algorithmRaw}". Must be "exact" or "diskann".` @@ -36,8 +38,8 @@ export function loadConfig(requireSql: boolean = true): AppConfig { if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,114}$/.test(tableName)) { throw new Error( `Invalid AZURE_SQL_TABLE_NAME: "${tableName}". ` + - "Must start with a letter or underscore, contain only letters, numbers, and underscores, " + - "and be at most 115 characters." + "Must start with a letter or underscore, contain only letters, numbers, and underscores, " + + "and be at most 115 characters." ); } diff --git a/samples/features/vector-search/vector-search-query-typescript/src/embed.ts b/samples/features/vector-search/vector-search-query-typescript/src/embed.ts index 014038bd8b..7ea196c6af 100644 --- a/samples/features/vector-search/vector-search-query-typescript/src/embed.ts +++ b/samples/features/vector-search/vector-search-query-typescript/src/embed.ts @@ -69,7 +69,11 @@ async function main(): Promise { DescriptionVector: allEmbeddings[idx], })); - writeFileSync(outputPath, JSON.stringify(hotelsWithVectors, null, 2), "utf-8"); + writeFileSync( + outputPath, + JSON.stringify(hotelsWithVectors, null, 2), + "utf-8" + ); console.log(`Done. Wrote HotelsData_Vector.json`); } diff --git a/samples/features/vector-search/vector-search-query-typescript/src/index.ts b/samples/features/vector-search/vector-search-query-typescript/src/index.ts index e7c75165da..d5b0026055 100644 --- a/samples/features/vector-search/vector-search-query-typescript/src/index.ts +++ b/samples/features/vector-search/vector-search-query-typescript/src/index.ts @@ -82,7 +82,11 @@ function connectToSql( function executeSql( connection: Connection, sql: string, - parameters?: Array<{ name: string; type: unknown; value: unknown }> + parameters?: Array<{ + name: string; + type: (typeof TYPES)[keyof typeof TYPES]; + value: unknown; + }> ): Promise[]> { return new Promise((resolve, reject) => { const rows: Record[] = []; @@ -106,7 +110,7 @@ function executeSql( if (parameters) { for (const p of parameters) { - request.addParameter(p.name, p.type as any, p.value); + request.addParameter(p.name, p.type, p.value); } } @@ -188,7 +192,11 @@ async function main(): Promise { // Validate vector dimensions for ALL hotels const VECTOR_DIMENSIONS = 1536; // text-embedding-3-small output dimensions const badVectors = hotels - .map((h, i) => ({ index: i, id: h.HotelId, dim: h.DescriptionVector?.length })) + .map((h, i) => ({ + index: i, + id: h.HotelId, + dim: h.DescriptionVector?.length, + })) .filter((v) => !v.dim || v.dim !== VECTOR_DIMENSIONS); if (badVectors.length > 0) { const examples = badVectors @@ -197,9 +205,9 @@ async function main(): Promise { .join("\n"); console.error( `Error: ${badVectors.length} hotel(s) have invalid or missing vector dimensions ` + - `(expected ${VECTOR_DIMENSIONS}):\n${examples}\n` + - `Re-run 'npm run embed' with a ${VECTOR_DIMENSIONS}-dimension model, ` + - `or update the VECTOR column size.` + `(expected ${VECTOR_DIMENSIONS}):\n${examples}\n` + + `Re-run 'npm run embed' with a ${VECTOR_DIMENSIONS}-dimension model, ` + + `or update the VECTOR column size.` ); process.exit(1); } @@ -221,9 +229,9 @@ async function main(): Promise { if (msg.includes("Login failed") || msg.includes("token")) { console.error( "Authentication failed. Ensure:\n" + - " 1. You are signed in: az login\n" + - " 2. Your identity is set as Microsoft Entra admin on the SQL server\n" + - " 3. Your client IP is in the SQL server firewall rules\n" + " 1. You are signed in: az login\n" + + " 2. Your identity is set as Microsoft Entra admin on the SQL server\n" + + " 3. Your client IP is in the SQL server firewall rules\n" ); } throw err; @@ -231,12 +239,12 @@ async function main(): Promise { console.log("Connected.\n"); try { - // 5. Create the hotels table with a VECTOR(1536) column - const tableName = config.tableName; - console.log(`Creating table dbo.${tableName} (if not exists)...`); - await executeSql( - conn, - `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = N'${tableName}' AND schema_id = SCHEMA_ID('dbo')) + // 5. Create the hotels table with a VECTOR(1536) column + const tableName = config.tableName; + console.log(`Creating table dbo.${tableName} (if not exists)...`); + await executeSql( + conn, + `IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = N'${tableName}' AND schema_id = SCHEMA_ID('dbo')) BEGIN CREATE TABLE dbo.[${tableName}] ( id NVARCHAR(50) PRIMARY KEY, @@ -247,111 +255,127 @@ async function main(): Promise { embedding VECTOR(1536) NULL ); END` - ); - console.log("Table ready.\n"); - - // 6. Insert hotel data with pre-computed vectors (batched for performance) - console.log("Inserting hotel data with pre-computed embeddings..."); + ); + console.log("Table ready.\n"); + + // 6. Insert hotel data with pre-computed vectors (batched for performance) + console.log("Inserting hotel data with pre-computed embeddings..."); + + // Uses tedious native transaction methods (not raw SQL) to avoid + // sp_executesql scope mismatch with BEGIN/COMMIT TRANSACTION statements. + await beginTransaction(conn); + try { + await executeSql(conn, `DELETE FROM dbo.[${tableName}]`); + + // Batch inserts: group rows into single INSERT statements with + // numbered parameters to minimize network round-trips. + const BATCH_SIZE = 10; + for (let i = 0; i < hotels.length; i += BATCH_SIZE) { + const batch = hotels.slice(i, i + BATCH_SIZE); + const valuesClauses = batch.map( + (_, j) => + `(@id${j}, @name${j}, @desc${j}, @cat${j}, @rating${j}, CAST(@emb${j} AS VECTOR(1536)))` + ); + const sql = `INSERT INTO dbo.[${tableName}] (id, name, description, category, rating, embedding)\n VALUES ${valuesClauses.join(",\n ")}`; + const params = batch.flatMap((hotel, j) => [ + { name: `id${j}`, type: TYPES.NVarChar, value: hotel.HotelId }, + { name: `name${j}`, type: TYPES.NVarChar, value: hotel.HotelName }, + { name: `desc${j}`, type: TYPES.NVarChar, value: hotel.Description }, + { name: `cat${j}`, type: TYPES.NVarChar, value: hotel.Category }, + { name: `rating${j}`, type: TYPES.Float, value: hotel.Rating }, + { + name: `emb${j}`, + type: TYPES.NVarChar, + value: vectorToString(hotel.DescriptionVector), + }, + ]); + await executeSql(conn, sql, params); + } - // Uses tedious native transaction methods (not raw SQL) to avoid - // sp_executesql scope mismatch with BEGIN/COMMIT TRANSACTION statements. - await beginTransaction(conn); - try { - await executeSql(conn, `DELETE FROM dbo.[${tableName}]`); - - // Batch inserts: group rows into single INSERT statements with - // numbered parameters to minimize network round-trips. - const BATCH_SIZE = 10; - for (let i = 0; i < hotels.length; i += BATCH_SIZE) { - const batch = hotels.slice(i, i + BATCH_SIZE); - const valuesClauses = batch.map((_, j) => - `(@id${j}, @name${j}, @desc${j}, @cat${j}, @rating${j}, CAST(@emb${j} AS VECTOR(1536)))` - ); - const sql = `INSERT INTO dbo.[${tableName}] (id, name, description, category, rating, embedding)\n VALUES ${valuesClauses.join(",\n ")}`; - const params = batch.flatMap((hotel, j) => [ - { name: `id${j}`, type: TYPES.NVarChar, value: hotel.HotelId }, - { name: `name${j}`, type: TYPES.NVarChar, value: hotel.HotelName }, - { name: `desc${j}`, type: TYPES.NVarChar, value: hotel.Description }, - { name: `cat${j}`, type: TYPES.NVarChar, value: hotel.Category }, - { name: `rating${j}`, type: TYPES.Float, value: hotel.Rating }, - { name: `emb${j}`, type: TYPES.NVarChar, value: vectorToString(hotel.DescriptionVector) }, - ]); - await executeSql(conn, sql, params); + await commitTransaction(conn); + } catch (insertErr) { + await rollbackTransaction(conn).catch((rollbackErr: unknown) => { + console.error( + "Warning: Transaction rollback failed:", + rollbackErr instanceof Error + ? rollbackErr.message + : String(rollbackErr) + ); + }); + throw insertErr; } + console.log(`Inserted ${hotels.length} hotels.\n`); - await commitTransaction(conn); - } catch (insertErr) { - await rollbackTransaction(conn).catch((rollbackErr: unknown) => { console.error("Warning: Transaction rollback failed:", rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)); }); - throw insertErr; - } - console.log(`Inserted ${hotels.length} hotels.\n`); - - // 7. Generate query embedding with Azure OpenAI - const searchQuery = "luxury beachfront hotel with ocean views and spa"; - console.log(`Searching for: "${searchQuery}"\n`); - - const azureADTokenProvider = getBearerTokenProvider( - credential, - "https://cognitiveservices.azure.com/.default" - ); - const openaiClient = new AzureOpenAI({ - endpoint: config.azureOpenAiEndpoint, - azureADTokenProvider, - apiVersion: "2024-10-21", - timeout: 30_000, // 30s timeout for embedding generation - maxRetries: 3, // Retry transient failures - }); + // 7. Generate query embedding with Azure OpenAI + const searchQuery = "luxury beachfront hotel with ocean views and spa"; + console.log(`Searching for: "${searchQuery}"\n`); - let queryEmbeddings: number[][]; - try { - queryEmbeddings = await generateEmbeddings( - openaiClient, - config.azureOpenAiEmbeddingDeployment, - [searchQuery] + const azureADTokenProvider = getBearerTokenProvider( + credential, + "https://cognitiveservices.azure.com/.default" ); - } catch (err: unknown) { - const msg = err instanceof Error ? err.message : String(err); - if (msg.includes("401") || msg.includes("403") || msg.includes("AuthenticationError")) { - console.error( - "Azure OpenAI authentication failed. Ensure:\n" + - " 1. You are signed in: az login\n" + - " 2. You have the 'Cognitive Services OpenAI User' role on the Azure OpenAI resource\n" + - ` 3. The endpoint is correct: ${config.azureOpenAiEndpoint}\n` + - ` 4. The deployment exists: ${config.azureOpenAiEmbeddingDeployment}\n` + const openaiClient = new AzureOpenAI({ + endpoint: config.azureOpenAiEndpoint, + azureADTokenProvider, + apiVersion: "2024-10-21", + timeout: 30_000, // 30s timeout for embedding generation + maxRetries: 3, // Retry transient failures + }); + + let queryEmbeddings: number[][]; + try { + queryEmbeddings = await generateEmbeddings( + openaiClient, + config.azureOpenAiEmbeddingDeployment, + [searchQuery] ); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if ( + msg.includes("401") || + msg.includes("403") || + msg.includes("AuthenticationError") + ) { + console.error( + "Azure OpenAI authentication failed. Ensure:\n" + + " 1. You are signed in: az login\n" + + " 2. You have the 'Cognitive Services OpenAI User' role on the Azure OpenAI resource\n" + + ` 3. The endpoint is correct: ${config.azureOpenAiEndpoint}\n` + + ` 4. The deployment exists: ${config.azureOpenAiEmbeddingDeployment}\n` + ); + } + throw err; } - throw err; - } - const queryVector = queryEmbeddings[0]; - if (!queryVector || queryVector.length !== VECTOR_DIMENSIONS) { - throw new Error( - `Query embedding has unexpected dimensions: ${queryVector?.length ?? 0} ` + - `(expected ${VECTOR_DIMENSIONS}). Check your Azure OpenAI deployment ` + - `"${config.azureOpenAiEmbeddingDeployment}".` - ); - } - const queryVectorStr = vectorToString(queryVector); - - // 8. Determine effective algorithm (DiskANN may fall back to exact if row count is too low) - let algorithm = config.vectorSearchAlgorithm; - if (algorithm === "diskann") { - const countResult = await executeSql( - conn, - `SELECT COUNT(*) AS cnt FROM dbo.[${tableName}] WHERE embedding IS NOT NULL` - ); - const rowCount = Number(countResult[0]?.cnt ?? 0); - if (rowCount < 1000) { - console.warn( - `⚠ DiskANN index requires at least 1,000 rows with non-null vectors, ` + - `but table has only ${rowCount}. Falling back to exact (VECTOR_DISTANCE) search.\n` + const queryVector = queryEmbeddings[0]; + if (!queryVector || queryVector.length !== VECTOR_DIMENSIONS) { + throw new Error( + `Query embedding has unexpected dimensions: ${queryVector?.length ?? 0} ` + + `(expected ${VECTOR_DIMENSIONS}). Check your Azure OpenAI deployment ` + + `"${config.azureOpenAiEmbeddingDeployment}".` ); - algorithm = "exact"; - } else { - console.log("Creating DiskANN vector index (if not exists)..."); - await executeSql( + } + const queryVectorStr = vectorToString(queryVector); + + // 8. Determine effective algorithm (DiskANN may fall back to exact if row count is too low) + let algorithm = config.vectorSearchAlgorithm; + if (algorithm === "diskann") { + const countResult = await executeSql( conn, - `IF NOT EXISTS ( + `SELECT COUNT(*) AS cnt FROM dbo.[${tableName}] WHERE embedding IS NOT NULL` + ); + const rowCount = Number(countResult[0]?.cnt ?? 0); + if (rowCount < 1000) { + console.warn( + `⚠ DiskANN index requires at least 1,000 rows with non-null vectors, ` + + `but table has only ${rowCount}. Falling back to exact (VECTOR_DISTANCE) search.\n` + ); + algorithm = "exact"; + } else { + console.log("Creating DiskANN vector index (if not exists)..."); + await executeSql( + conn, + `IF NOT EXISTS ( SELECT * FROM sys.indexes WHERE name = N'ix_${tableName}_embedding' AND object_id = OBJECT_ID('dbo.[${tableName}]') ) @@ -360,19 +384,19 @@ async function main(): Promise { ON dbo.[${tableName}](embedding) WITH (type = 'DiskANN', metric = 'cosine'); END` - ); - console.log("DiskANN index ready.\n"); + ); + console.log("DiskANN index ready.\n"); + } } - } - // 9. Run vector similarity search - let results: Record[]; + // 9. Run vector similarity search + let results: Record[]; - if (algorithm === "diskann") { - // Approximate nearest neighbor via VECTOR_SEARCH + DiskANN index - results = await executeSql( - conn, - `SELECT TOP 3 + if (algorithm === "diskann") { + // Approximate nearest neighbor via VECTOR_SEARCH + DiskANN index + results = await executeSql( + conn, + `SELECT TOP 3 vs.distance, h.name, h.description, h.category, h.rating FROM VECTOR_SEARCH( @@ -382,19 +406,19 @@ async function main(): Promise { ) AS vs INNER JOIN dbo.[${tableName}] h ON vs.$rowid = h.$rowid ORDER BY vs.distance`, - [ - { - name: "queryVector", - type: TYPES.NVarChar, - value: queryVectorStr, - }, - ] - ); - } else { - // Exact kNN via VECTOR_DISTANCE (default) - results = await executeSql( - conn, - `SELECT TOP 3 + [ + { + name: "queryVector", + type: TYPES.NVarChar, + value: queryVectorStr, + }, + ] + ); + } else { + // Exact kNN via VECTOR_DISTANCE (default) + results = await executeSql( + conn, + `SELECT TOP 3 name, description, category, @@ -402,46 +426,54 @@ async function main(): Promise { VECTOR_DISTANCE('cosine', embedding, CAST(@queryVector AS VECTOR(1536))) AS distance FROM dbo.[${tableName}] ORDER BY distance`, - [ - { - name: "queryVector", - type: TYPES.NVarChar, - value: queryVectorStr, - }, - ] - ); - } - - // 10. Display results - const algorithmLabel = - algorithm === "diskann" - ? "Approximate (DiskANN) via VECTOR_SEARCH" - : "Exact (kNN) via VECTOR_DISTANCE"; - console.log(`--- Search Results — ${algorithmLabel} (Top 3 by Cosine Distance) ---\n`); - for (const row of results) { - const distance = Number(row["distance"]); - const similarity = (1 - distance).toFixed(4); - console.log(` Hotel: ${row["name"]}`); - console.log(` Category: ${row["category"]}`); - console.log(` Rating: ${row["rating"]}`); - console.log(` Description: ${(row["description"] as string).substring(0, 100)}...`); - console.log(` Distance: ${distance.toFixed(4)}`); - console.log(` Similarity: ${similarity}`); - console.log(); - } + [ + { + name: "queryVector", + type: TYPES.NVarChar, + value: queryVectorStr, + }, + ] + ); + } - // 11. Cleanup: optionally drop table - if (config.dropTable) { - console.log(`Dropping table dbo.[${tableName}]...`); - if (algorithm === "diskann") { - await executeSql(conn, `DROP INDEX IF EXISTS [ix_${tableName}_embedding] ON dbo.[${tableName}]`); + // 10. Display results + const algorithmLabel = + algorithm === "diskann" + ? "Approximate (DiskANN) via VECTOR_SEARCH" + : "Exact (kNN) via VECTOR_DISTANCE"; + console.log( + `--- Search Results — ${algorithmLabel} (Top 3 by Cosine Distance) ---\n` + ); + for (const row of results) { + const distance = Number(row["distance"]); + const similarity = (1 - distance).toFixed(4); + console.log(` Hotel: ${row["name"]}`); + console.log(` Category: ${row["category"]}`); + console.log(` Rating: ${row["rating"]}`); + console.log( + ` Description: ${(row["description"] as string).substring(0, 100)}...` + ); + console.log(` Distance: ${distance.toFixed(4)}`); + console.log(` Similarity: ${similarity}`); + console.log(); } - await executeSql(conn, `DROP TABLE IF EXISTS dbo.[${tableName}]`); - console.log("Table dropped — no artifacts left behind.\n"); - } else { - console.log(`Table dbo.[${tableName}] retained (set SQL_DROP_TABLE=true to clean up).\n`); - } + // 11. Cleanup: optionally drop table + if (config.dropTable) { + console.log(`Dropping table dbo.[${tableName}]...`); + if (algorithm === "diskann") { + await executeSql( + conn, + `DROP INDEX IF EXISTS [ix_${tableName}_embedding] ON dbo.[${tableName}]` + ); + } + await executeSql(conn, `DROP TABLE IF EXISTS dbo.[${tableName}]`); + console.log("Table dropped — no artifacts left behind.\n"); + } else { + console.log( + `Table dbo.[${tableName}] retained (set SQL_DROP_TABLE=true to clean up).\n` + ); + } } finally { conn.close(); console.log("Done. Connection closed.");