diff --git a/.github/workflows/apidocs.yml b/.github/workflows/apidocs.yml new file mode 100644 index 0000000..9c3b285 --- /dev/null +++ b/.github/workflows/apidocs.yml @@ -0,0 +1,115 @@ +name: API Docs + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version_tag: + description: 'Git tag to build docs for (e.g., 3.8.0 or v3.8.0-rc1)' + required: true + smoke_test: + description: 'Run as smoke test (build & artifact only, without pushing to S3)' + type: boolean + required: false + default: false + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 + +jobs: + validate-inputs: + name: Validate Inputs + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.compute.outputs.tag }} + version: ${{ steps.compute.outputs.version }} + publish: ${{ steps.compute.outputs.publish }} + smoke: ${{ steps.compute.outputs.smoke }} + steps: + - id: compute + name: Compute tag, version, smoke flag + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_INPUT="${{ github.event.inputs.version_tag }}" + SMOKE_INPUT="${{ github.event.inputs.smoke_test }}" + else + TAG_INPUT="${{ github.ref_name }}" + SMOKE_INPUT="false" + fi + if [ -z "$TAG_INPUT" ]; then + echo "Tag input is required." >&2 + exit 1 + fi + if ! echo "$TAG_INPUT" | grep -Eq '^(v)?[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then + echo "Tag must be a semantic version (e.g., 3.8.0 or v3.8.0-rc1)." >&2 + exit 1 + fi + VER="${TAG_INPUT#v}" + echo "tag=$TAG_INPUT" >> $GITHUB_OUTPUT + echo "version=$VER" >> $GITHUB_OUTPUT + echo "smoke=$SMOKE_INPUT" >> $GITHUB_OUTPUT + if [ "$SMOKE_INPUT" = "true" ]; then echo "publish=false" >> $GITHUB_OUTPUT; else echo "publish=true" >> $GITHUB_OUTPUT; fi + + build-docs: + name: Build API Docs + runs-on: ubuntu-latest + needs: validate-inputs + env: + PACKAGE_VERSION: ${{ needs.validate-inputs.outputs.version }} + API_NAME: analytics-dotnet-client + AWS_REGION: us-west-1 + S3_BUCKET: docs.couchbase.com + steps: + - name: Checkout repo at tag + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate-inputs.outputs.tag }} + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + cache: false + + - name: Install DocFX + run: dotnet tool install -g docfx + + - name: Build docs + run: | + cd docs + ~/.dotnet/tools/docfx metadata + ~/.dotnet/tools/docfx build + + - name: Validate site generated + run: | + test -d docs/_site/api || (echo "No API output found" && exit 1) + test $(ls -1 docs/_site/api/*.html | wc -l) -ge 2 || (echo "Insufficient API HTML files" && exit 1) + + - name: Archive docs + run: | + zip -rq "${{ env.API_NAME }}-${{ env.PACKAGE_VERSION }}.zip" docs/_site/* + + - name: Upload API Docs Artifact + uses: actions/upload-artifact@v4 + with: + name: apidocs-zip + path: ${{ env.API_NAME }}-${{ env.PACKAGE_VERSION }}.zip + + - name: Configure AWS Credentials + if: needs.validate-inputs.outputs.publish == 'true' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Publish to S3 + if: needs.validate-inputs.outputs.publish == 'true' + run: | + aws s3 sync "./docs/_site/" "s3://$S3_BUCKET/sdk-api/$API_NAME-$PACKAGE_VERSION/" --acl public-read --delete + + + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..27f181a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,227 @@ +name: Release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version_tag: + description: 'Git tag to release (e.g., 3.8.0 or 3.8.0-rc1; optional leading v)' + required: true + smoke_test: + description: 'Run as smoke test (build, test, pack only; no NuGet publish)' + type: boolean + required: false + default: false + +env: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 + +jobs: + validate-inputs: + name: Validate Inputs + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.compute.outputs.tag }} + version: ${{ steps.compute.outputs.version }} + smoke: ${{ steps.compute.outputs.smoke }} + steps: + - id: compute + name: Compute tag, version, and smoke flag + shell: bash + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_INPUT="${{ github.event.inputs.version_tag }}" + SMOKE_INPUT="${{ github.event.inputs.smoke_test }}" + else + TAG_INPUT="${{ github.ref_name }}" + SMOKE_INPUT="false" + fi + if [ -z "$TAG_INPUT" ]; then + echo "Tag input is required." >&2 + exit 1 + fi + if ! echo "$TAG_INPUT" | grep -Eq '^(v)?[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$'; then + echo "Tag must be a semantic version (e.g., 3.8.0 or v3.8.0-rc1)." >&2 + exit 1 + fi + VER="${TAG_INPUT#v}" + echo "tag=$TAG_INPUT" >> $GITHUB_OUTPUT + echo "version=$VER" >> $GITHUB_OUTPUT + if [ "$SMOKE_INPUT" = "true" ]; then echo "smoke=true" >> $GITHUB_OUTPUT; else echo "smoke=false" >> $GITHUB_OUTPUT; fi + + build-and-test: + name: Build & Test (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + needs: validate-inputs + strategy: + fail-fast: false + matrix: + include: + - name: ubuntu + os: ubuntu-latest + - name: windows + os: windows-latest + - name: macos + os: macos-latest + - name: centos + os: ubuntu-latest + container: quay.io/centos/centos:stream9 + container: ${{ matrix.container }} + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate-inputs.outputs.tag }} + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + cache: false + + - name: Install prerequisites (CentOS) + if: matrix.name == 'centos' + run: | + yum -y update + yum -y install tar gzip curl ca-certificates krb5-libs libicu zlib openssl + + - name: Restore + run: dotnet restore src/Couchbase.Analytics/Couchbase.Analytics.csproj + + - name: Build (Release) + run: dotnet build src/Couchbase.Analytics/Couchbase.Analytics.csproj --configuration Release --no-restore + + - name: Test - Couchbase.Analytics.UnitTests (Release) + run: dotnet test tests/Couchbase.Analytics.UnitTests/Couchbase.Analytics.UnitTests.csproj --configuration Release --no-build --logger "trx;LogFileName=unit-tests.trx" --results-directory "TestResults" + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-results-${{ matrix.name }} + path: TestResults + + pack: + name: Pack (Windows) + runs-on: windows-latest + needs: [validate-inputs, build-and-test] + env: + PACKAGE_VERSION: ${{ needs.validate-inputs.outputs.version }} + NETSDK_SIGNKEY: ${{ secrets.NETSDK_SIGNKEY }} + steps: + - name: Checkout tag + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate-inputs.outputs.tag }} + fetch-depth: 0 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + cache: false + + - name: Restore + run: dotnet restore src/Couchbase.Analytics/Couchbase.Analytics.csproj + + - name: Prepare strong-name key (SNK) (optional) + if: env.NETSDK_SIGNKEY != '' + shell: pwsh + run: | + $bytes = [System.Convert]::FromBase64String("${{ env.NETSDK_SIGNKEY }}") + [IO.File]::WriteAllBytes("signkey.snk", $bytes) + + - name: Build (Release, signed) + shell: pwsh + run: | + $signProps = '' + if ("${{ env.NETSDK_SIGNKEY }}" -ne '') { $signProps = "/p:SignAssembly=true /p:AssemblyOriginatorKeyFile=signkey.snk" } + dotnet build src/Couchbase.Analytics/Couchbase.Analytics.csproj -c Release --no-restore /p:ContinuousIntegrationBuild=true /p:Version="${{ env.PACKAGE_VERSION }}" /p:IncludeSymbols=true /p:IncludeSource=true /p:SourceLinkCreate=true $signProps + + - name: Pack (Release, signed) + shell: pwsh + run: | + $signProps = '' + if ("${{ env.NETSDK_SIGNKEY }}" -ne '') { $signProps = "/p:SignAssembly=true /p:AssemblyOriginatorKeyFile=signkey.snk" } + dotnet pack src/Couchbase.Analytics/Couchbase.Analytics.csproj -c Release --no-build /p:ContinuousIntegrationBuild=true /p:Version="${{ env.PACKAGE_VERSION }}" /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg /p:IncludeSource=true /p:SourceLinkCreate=true $signProps + + - name: Upload Packages Artifact + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + **/*.nupkg + **/*.snupkg + + publish: + name: Publish to NuGet + runs-on: windows-latest + needs: [validate-inputs, pack] + environment: nuget-publish + # Only publish when not a smoke test + if: needs.validate-inputs.outputs.smoke != 'true' + env: + PACKAGE_VERSION: ${{ needs.validate-inputs.outputs.version }} + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + steps: + - name: Download Packages Artifact + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: ./artifacts + + - name: Publish to NuGet (nupkg) + if: env.NUGET_API_KEY != '' + shell: pwsh + run: | + $ver = "$Env:PACKAGE_VERSION" + Get-ChildItem -Path ./artifacts -Recurse -Filter *.nupkg | Where-Object { $_.Name -like "*.$ver.nupkg" } | ForEach-Object { + dotnet nuget push $_.FullName --source "https://api.nuget.org/v3/index.json" --api-key "${{ env.NUGET_API_KEY }}" --skip-duplicate + } + + - name: Publish symbols to NuGet (snupkg) + if: env.NUGET_API_KEY != '' + shell: pwsh + run: | + $ver = "$Env:PACKAGE_VERSION" + Get-ChildItem -Path ./artifacts -Recurse -Filter *.snupkg | Where-Object { $_.Name -like "*.$ver.snupkg" } | ForEach-Object { + dotnet nuget push $_.FullName --source "https://api.nuget.org/v3/index.json" --api-key "${{ env.NUGET_API_KEY }}" --skip-duplicate + } + + smoke-summary: + name: Smoke Summary + runs-on: ubuntu-latest + needs: [validate-inputs, build-and-test, pack] + if: github.event_name == 'workflow_dispatch' && github.event.inputs.smoke_test == 'true' + env: + PACKAGE_VERSION: ${{ needs.validate-inputs.outputs.version }} + steps: + - name: Download Packages Artifact + uses: actions/download-artifact@v4 + with: + name: nuget-packages + path: ./artifacts + + - name: Download Test Results Artifacts + uses: actions/download-artifact@v4 + with: + pattern: unit-test-results-* + merge-multiple: true + path: ./test-results + + - name: Print tag, package names, test result files + shell: bash + run: | + echo "Tag: ${{ needs.validate-inputs.outputs.tag }}" + echo "Package version: $PACKAGE_VERSION" + echo "Packages:" + find ./artifacts -type f -name "*.$PACKAGE_VERSION.nupkg" -print | sed 's/^/ /' || true + find ./artifacts -type f -name "*.$PACKAGE_VERSION.snupkg" -print | sed 's/^/ /' || true + echo "Test result files:" + find ./test-results -type f -name "*.trx" -print | sed 's/^/ /' || true + + + diff --git a/.gitignore b/.gitignore index 4f23cbd..8d4a80e 100644 --- a/.gitignore +++ b/.gitignore @@ -406,4 +406,8 @@ FodyWeavers.xsd *.sln.iml # test config -tests/Couchbase.Analytics.FunctionalTests/settings.json \ No newline at end of file +tests/Couchbase.Analytics.FunctionalTests/settings.json + +#generated api docs +docs/_site/ +docs/api/ diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..84430c9 --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "**/*.csproj" + ] + } + ], + "dest": "api" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "analytics-dotnet-client", + "_appTitle": "analytics-dotnet-client", + "_enableSearch": true, + "pdf": true + } + } +} \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..dbfda0d --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,104 @@ +# Getting Started + +This guide shows how to install the package, connect to a Couchbase cluster, and run Analytics queries. + +## Install + +Add the package to your project: + +```bash +dotnet add package Couchbase.Analytics +``` + +Requires .NET 8.0. + +## Connect + +Create a `Cluster` with a connection string and `Credential`. The connection string supports `http` or `https`, multiple hosts, and query/timeout/TLS parameters. + +```csharp +using Couchbase.AnalyticsClient; +using Couchbase.AnalyticsClient.HTTP; +using Couchbase.AnalyticsClient.Options; + +var credential = Credential.Create("username", "password"); + +var cluster = Cluster.Create( + connectionString: "https://analytics.my-couchbase.example.com:18095?max_retries=5", + credential: credential, + configureOptions: options => options + .WithTimeoutOptions(timeoutOpts => timeoutOpts + .WithQueryTimeout(TimeSpan.FromSeconds(15))) + .WithSecurityOptions(securityOpts => securityOpts + .WithTrustOnlyCapella()) +); +``` + +Notes: +- Use `http://host:8095` for non-TLS clusters, `https://host:18095` for TLS (or your own custom ports for a load balancer or proxy) +- If multiple IP addresses are resolved for a host, a connection will be attempted for a random IP address. If a connection attempt fails, another IP will be picked to attempt a connection, until all are exhausted. +- Connection string parameters include: + - `timeout.connect_timeout`, `timeout.dispatch_timeout`, `timeout.query_timeout` (in milliseconds) + - `security.trust_only_pem_file`, `security.disable_server_certificate_verification`, `security.cipher_suites` + - `max_retries` + +## Query + +Run an Analytics statement and stream rows: + +Note: Results are streamed by default. Use `QueryOptions.WithAsStreaming(false)` to get a blocking result. + +```csharp +using Couchbase.AnalyticsClient.Options; + +var result = await cluster.ExecuteQueryAsync( + "SELECT 1 AS one;", + new QueryOptions() + .WithReadOnly(true) + .WithScanConsistency(QueryScanConsistency.RequestPlus) +); + +await foreach (var row in result.Rows) +{ + Console.WriteLine(row.ContentAs()); +} +``` + +### Query with parameters + +```csharp +var statement = "SELECT * FROM `travel-sample`.inventory.airline WHERE country = $country LIMIT $limit"; + +var paramResult = await cluster.ExecuteQueryAsync( + statement, + new QueryOptions() + .WithNamedParameter("country", "United States") + .WithNamedParameter("limit", 10) +); +``` + +### Database and scope context + +Target a specific database and scope using `Database(...).Scope(...).ExecuteQueryAsync(...)`: + +```csharp +var db = cluster.Database("travel-sample"); +var scope = db.Scope("inventory"); + +var scoped = await scope.ExecuteQueryAsync( + "SELECT META().id FROM airline LIMIT 5" +); + +await foreach (var row in scoped.Rows) +{ + Console.WriteLine(row.Json.ToString()); +} +``` + +## Cleanup + +`Cluster` implements `IDisposable`. Dispose when done to release resources: + +```csharp +cluster.Dispose(); +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3ae36c1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +--- +redirect_url: introduction.html +--- \ No newline at end of file diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..497f672 --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,27 @@ +--- +_layout: landing +--- + +# Couchbase Analytics .NET Client + +This library provides a lightweight, focused .NET client for interacting with the Couchbase Analytics service. + +It is designed for high-throughput analytics workloads and exposes a simple API to: + +- Create a cluster connection with credentials and options +- Configure timeouts, retries, and TLS settings +- Execute Analytics statements and stream results +- Target queries to a specific database and scope + +## Supported platforms + +- .NET 8.0 + +## Packages + +This repo contains two projects: + +- `Couchbase.Analytics` — the main client library +- `Couchbase.Text.Json` — internal JSON utilities used by the client + +Installation instructions are in Getting Started](getting-started.md). \ No newline at end of file diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..d52fc0a --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,6 @@ +- name: Introduction + href: introduction.md +- name: Getting Started + href: getting-started.md +- name: API + href: api/ \ No newline at end of file diff --git a/src/Couchbase.Analytics/Query/QueryScanConsistency.cs b/src/Couchbase.Analytics/Query/QueryScanConsistency.cs index 5ac2fa9..c6ef7e0 100644 --- a/src/Couchbase.Analytics/Query/QueryScanConsistency.cs +++ b/src/Couchbase.Analytics/Query/QueryScanConsistency.cs @@ -19,6 +19,8 @@ * ************************************************************/ #endregion +using System.ComponentModel; + namespace Couchbase.AnalyticsClient.Query; /// @@ -27,12 +29,20 @@ namespace Couchbase.AnalyticsClient.Query; public enum QueryScanConsistency { /// - /// The index scan does not use a timestamp vector. This is the fastest mode, because it avoids the costs of obtaining the vector and waiting for the index to catch up to the vector. + /// The default which means that the query can return data that is currently indexed + /// and accessible by the index or the view. The query output can be arbitrarily + /// out-of-date if there are many pending mutations that have not been indexed by + /// the index or the view. This consistency level is useful for queries that favor + /// low latency and do not need precise and most up-to-date information. /// + [Description("not_bounded")] NotBounded, /// - /// This option implements bounded consistency. You can use this setting to implement read-your-own-writes (RYOW). + /// This level provides the strictest consistency level and thus executes with higher + /// latencies than the other levels. This consistency level requires all mutations, up + /// to the moment of the query request, to be processed before the query execution can start. /// + [Description("request_plus")] RequestPlus } \ No newline at end of file diff --git a/src/Couchbase.Analytics/Query/ScanConsistency.cs b/src/Couchbase.Analytics/Query/ScanConsistency.cs deleted file mode 100644 index 0aa3058..0000000 --- a/src/Couchbase.Analytics/Query/ScanConsistency.cs +++ /dev/null @@ -1,45 +0,0 @@ -#region License -/* ************************************************************ - * - * @author Couchbase - * @copyright 2025 Couchbase, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * ************************************************************/ -#endregion - -using System.ComponentModel; - -namespace Couchbase.AnalyticsClient.Query; - -public enum ScanConsistency -{ - /// - /// The default which means that the query can return data that is currently indexed - /// and accessible by the index or the view. The query output can be arbitrarily - /// out-of-date if there are many pending mutations that have not been indexed by - /// the index or the view. This consistency level is useful for queries that favor - /// low latency and do not need precise and most up-to-date information. - /// - [Description("not_bounded")] - NotBounded, - - /// - /// This level provides the strictest consistency level and thus executes with higher - /// latencies than the other levels. This consistency level requires all mutations, up - /// to the moment of the query request, to be processed before the query execution can start. - /// - [Description("request_plus")] - RequestPlus -} \ No newline at end of file