# Portable API Surface The Multicloud DB SDK's portable API surface covers capabilities that work identically across all three providers. The features listed below require no runtime capability checks - they are guaranteed to work on Azure Cosmos DB, Amazon DynamoDB, and Google Cloud Spanner. Some providers offer additional capabilities (e.g., `CROSS_PARTITION_QUERY`, `ORDER_BY`, `LIKE`); use `client.capabilities()` to discover what the current provider supports. --- ## What Works Everywhere Every capability listed below is fully supported on **all** providers. There are no asterisks, no provider-specific caveats, and no runtime checks required. ### CRUD Operations | Operation | Description | |-----------|-------------| | **Create** | Insert a new document (fails if the key already exists) | | **Read** | Point-read by partition key + sort key | | **Update** | Replace an existing document (fails if not found) | | **Upsert** | Create or replace - always succeeds | | **Delete** | Remove by key (idempotent — silent on missing; use `read()` to detect a missing key, since `read()` returns `null` on every provider) | ### Query - Portable Expression DSL Write a WHERE-clause filter once. The SDK translates it to the native query language of whichever provider is configured - Cosmos SQL, DynamoDB PartiQL, or Spanner GoogleSQL. | Feature | Operators / Functions | Example | |---------|----------------------|---------| | Comparison | `=`, `!=`, `<`, `>`, `<=`, `>=` | `status = 'active'` | | Logical | `AND`, `OR`, `NOT` | `age > 18 AND active = true` | | String functions | `STARTS_WITH`, `CONTAINS` | `STARTS_WITH(name, 'A')` | | Field introspection | `FIELD_EXISTS` | `FIELD_EXISTS(metadata)` | | Length functions | `STRING_LENGTH`, `COLLECTION_SIZE` | `STRING_LENGTH(name) > 3` | | Named parameters | `@paramName` | `price > @minPrice` | ```java QueryRequest query = QueryRequest.builder() .expression("STARTS_WITH(name, @prefix) AND age >= @minAge") .parameter("prefix", "J") .parameter("minAge", 21) .maxPageSize(50) .build(); QueryPage page = client.query(address, query); ``` ### Pagination | Feature | Description | |---------|-------------| | **Cursor-based paging** | Continuation-token pagination across all providers | | **Page size control** | `maxPageSize` to limit results per page | ### Data Management | Feature | Description | |---------|-------------| | **Schema provisioning** | `provisionSchema()` creates databases, containers, and tables portably | | **Transactions** | Multi-document transactional operations | | **Batch operations** | Batch read/write for throughput efficiency | | **Strong consistency** | Strongly-consistent reads | | **Change feed** | Change-data-capture (`MulticloudDbClient.readChanges`) — see matrix below | #### Change feed sub-capabilities | Capability | Cosmos DB | DynamoDB | Spanner | |------------|:---------:|:--------:|:-------:| | `change_feed` (basic CDC) | ✓ | ✓ | ✓ | | `change_feed_point_in_time` (`StartPosition.atTime`) | ✓ | ✗ | ✓ | | `change_feed_logical_partition_scope` (`FeedScope.logicalPartition`) | ✓ | ✗ | ✗ | DynamoDB Streams expose only `TRIM_HORIZON` / `LATEST` / sequence-number iterators (no timestamp start), and shards are physical (no logical-key scoping). Spanner Change Streams support timestamp start within the retention window but partition tokens are physical. `FeedScope.physicalPartition` is supported by all three providers and is the portable choice for parallel readers. ### Diagnostics & Error Handling | Feature | Description | |---------|-------------| | **Structured diagnostics** | Latency, request charge, and provider correlation IDs per operation | | **Portable error categories** | All provider exceptions mapped to `MulticloudDbErrorCategory` | | **Capability introspection** | `client.capabilities()` reports what the current provider supports | --- ## Portable Error Mapping All provider exceptions are mapped to portable `MulticloudDbErrorCategory` values. The raw HTTP or gRPC status code is also available via `error.statusCode()`. | Category | Cosmos DB | DynamoDB | Spanner | |-----------|------------|-----------|----------| | `INVALID_REQUEST` | HTTP 400 | ValidationException, HTTP 400 | INVALID_ARGUMENT, FAILED_PRECONDITION | | `AUTHENTICATION_FAILED` | HTTP 401 | UnrecognizedClientException, HTTP 401/403 | UNAUTHENTICATED | | `AUTHORIZATION_FAILED` | HTTP 403 | AccessDeniedException | PERMISSION_DENIED | | `NOT_FOUND` | HTTP 404 | ResourceNotFoundException, HTTP 404 | NOT_FOUND | | `CONFLICT` (409 - duplicate key) | HTTP 409 | `ConditionalCheckFailedException` from `create()` - `attribute_not_exists` guard fails when the item already exists | ALREADY_EXISTS | | `CONFLICT` (412 - precondition) | HTTP 412 | `ConditionalCheckFailedException` from `update()`/`upsert()` with a condition expression¹ | ABORTED | | `THROTTLED` | HTTP 429 | ProvisionedThroughputExceededException, ThrottlingException | RESOURCE_EXHAUSTED | | `TRANSIENT_FAILURE` | HTTP 449, 500, 502, 503 | HTTP 500–5xx | UNAVAILABLE | | `PERMANENT_FAILURE` | - | ItemCollectionSizeLimitExceededException | - | | `UNSUPPORTED_CAPABILITY` | - | - | UNIMPLEMENTED | | `PROVIDER_ERROR` | Other | Other | INTERNAL, Other | > ¹ DynamoDB uses `ConditionalCheckFailedException` for both the 409 (duplicate-key on `create`) and 412 > (precondition failure on conditional `update`/`upsert`) cases - both currently map to `CONFLICT`. > The portable API does not yet expose ETag-based conditional updates; when it does, the 412-equivalent > path will be split into a dedicated `PRECONDITION_FAILED` category (tracked in issue #29). ## Default Sort-Key Ordering All Cosmos DB and DynamoDB query paths return results sorted by the document's sort key ascending. > **Design note:** The default `ORDER BY` is applied to **all** Cosmos queries > (both partition-scoped and cross-partition), not just partition-scoped ones. > This gives the strongest consistency guarantee: every query, on every provider, > returns items sorted by sort key. The early PR description mentioned > partition-scoped only as a starting point; the final implementation was > intentionally broadened to cover all queries. ### Cosmos DB Cosmos DB appends `ORDER BY c.id ASC` to every query that does not already carry an explicit `ORDER BY` clause (and is not an aggregate / `GROUP BY` query). This is applied server-side, so the order is globally consistent across all pages. > **⚠️ Custom indexing policy - composite index required** > If your Cosmos container uses a **custom indexing policy** that does not include > a composite index on `(filterField ASC, id ASC)`, Cosmos DB will throw a > `400 Bad Request` at runtime for cross-partition queries that combine `WHERE` and > the default `ORDER BY c.id ASC`. The default indexing policy includes all paths > and supports this automatically. If you have tuned your indexing policy, add the > composite index for every field you filter on: > ```json > { "compositeIndexes": [ [{ "path": "/filterField", "order": "ascending" }, > { "path": "/id", "order": "ascending" }] ] } > ``` > > **⚠️ RU cost** > Appending `ORDER BY c.id ASC` to all Cosmos queries incurs an additional RU > charge versus unordered queries, proportional to result-set size. This cost is > the price of cross-provider consistency and is expected behavior. > > **⚠️ Aggregates and GROUP BY** > Cosmos DB rejects `ORDER BY` on aggregate expressions (`COUNT`, `SUM`, `MIN`, > `MAX`, `AVG`) and `GROUP BY` queries. The SDK automatically detects these patterns > and omits the default `ORDER BY` for them. ### DynamoDB DynamoDB results are sorted in memory per page after fetching (client-side). Within a single page, items are returned sorted by sort key ascending. For multi-page scans the overall order across pages is determined by DynamoDB's internal token-based traversal, not sort key - this is a known limitation. ### Spanner The Spanner provider does not yet implement default sort-key ordering. Consumers relying on consistent cross-provider sort behavior should not use the Spanner provider until this gap is addressed. > **Tracking**: A follow-up issue will be filed to implement default sort-key > ordering for the Spanner provider. Until resolved, do not mix Spanner with > Cosmos or DynamoDB in conformance-sensitive workloads. ## Escape Hatch Policy The SDK does not expose a `nativeClient()` method. Direct access to the underlying provider client is intentionally omitted to enforce portability guarantees - code written against the SDK must remain switchable between providers by configuration alone.