// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package com.multiclouddb.api; import com.multiclouddb.api.changefeed.ChangeFeedPage; import com.multiclouddb.api.changefeed.ChangeFeedRequest; import java.util.List; import java.util.Map; /** * Portable client interface for CRUD + query operations across cloud database * providers. *
* All operations use a provider-neutral synchronous contract. * Provider selection is configuration-only — no code changes are required to * switch providers. Async APIs are out of scope for v1. *
* There are no code-level escape hatches. Diagnostics and provider-specific
* opt-ins are controlled via {@link MulticloudDbClientConfig} only.
*/
public interface MulticloudDbClient extends AutoCloseable {
/**
* Insert a new document. Fails if a document with the same key already exists.
*
* @param address target database + collection
* @param key document key
* @param document document payload
* @param options operation options (timeout, etc.)
* @throws MulticloudDbException with category CONFLICT if the key already exists
*/
void create(ResourceAddress address, MulticloudDbKey key, Map
* Idempotent: deleting a key that does not exist is a silent no-op on every
* provider. This is the LCD across Cosmos (404 swallowed), DynamoDB
* ({@code DeleteItem} naturally no-ops) and Spanner ({@code Mutation.delete}
* naturally no-ops).
*
* Callers that need to detect whether a key exists should use
* {@link #read(ResourceAddress, MulticloudDbKey, OperationOptions)} — it
* returns {@code null} on every provider when the key does not exist, and
* does not mutate state. {@code update()} also throws {@code NOT_FOUND} on
* a missing key, but it requires a document body and overwrites
* the existing document on hit, so it is not a safe pure existence probe.
*
* @param address target database + collection
* @param key document key
* @param options operation options
* @throws MulticloudDbException for any provider error; a missing key is
* silently ignored and does not throw
*/
void delete(ResourceAddress address, MulticloudDbKey key, OperationOptions options);
/**
* Delete a document by key, using default options.
*/
default void delete(ResourceAddress address, MulticloudDbKey key) {
delete(address, key, OperationOptions.defaults());
}
/**
* Execute a query and return a single page of results.
*
* @param address target database + collection
* @param query query request (expression, parameters, page size, continuation
* token)
* @param options operation options
* @return a page of results with optional continuation token
*/
QueryPage query(ResourceAddress address, QueryRequest query, OperationOptions options);
/**
* Execute a query using default options.
*/
default QueryPage query(ResourceAddress address, QueryRequest query) {
return query(address, query, OperationOptions.defaults());
}
/**
* Read a page of changes from the change feed of the addressed collection.
*
* Capability gates:
*
* Each returned string is an opaque, provider-scoped partition identifier.
* Pass these to
* {@link com.multiclouddb.api.changefeed.FeedScope#physicalPartition(String)}
* to consume a single partition's slice of the change feed in parallel.
* IDs are not portable across providers.
*
* @throws MulticloudDbException with category UNSUPPORTED_CAPABILITY when
* the provider does not support the change
* feed
*/
List
* This is an idempotent operation — if the database already exists the call
* succeeds silently. Use this at application startup to guarantee the required
* databases are in place before performing data operations.
*
* For providers without a native database concept (e.g., DynamoDB), this is a
* no-op.
*
* Permission note: this operation uses each provider's standard
* data-plane SDK and is subject to the caller's runtime permissions. When
* the caller lacks sufficient permissions (e.g., Cosmos DB data-plane RBAC
* without a control-plane role), the SDK throws a
* {@link MulticloudDbException} with category {@code PERMISSION_DENIED}.
* Provision the database out-of-band (portal, CLI, IaC) if needed.
*
* @param database the logical database name to create if absent
* @throws MulticloudDbException with category {@code PERMISSION_DENIED} when
* the caller lacks permissions, or
* {@code CONFLICT} / {@code INTERNAL_ERROR} for
* other failures
*/
void ensureDatabase(String database);
/**
* Ensure a container (table) exists within the given database, creating it if
* it does not already exist.
*
* This is an idempotent operation — if the container already exists the call
* succeeds silently. Use this at application startup to guarantee the required
* containers are in place before performing data operations.
*
* Containers are always created with the SDK's standard schema conventions
* (partition key path {@code /partitionKey}, sort key column {@code sortKey}).
*
* @param address the database + collection identifying the container to create
* if absent
* @throws MulticloudDbException if the creation fails for a reason other than
* the resource already existing
*/
void ensureContainer(ResourceAddress address);
/**
* Provision a full schema of databases and containers in a single call.
*
* Equivalent to calling {@link #ensureDatabase} for every database key and
* {@link #ensureContainer} for every collection, but executes both phases in
* parallel using a bounded thread pool (max 10 threads) for efficiency.
*
* All operations are idempotent — existing resources are left unchanged.
* Use this at application startup to guarantee the entire required schema is
* in place before performing data operations.
*
* @param schema map of database name → list of collection/table names to ensure
* @throws MulticloudDbException if any database or container creation fails
*/
void provisionSchema(java.util.Map
*
* Returns {@link ChangeFeedPage#events()} (possibly empty) with a
* resumption {@link ChangeFeedPage#continuationToken()}.
*
* @throws MulticloudDbException with category UNSUPPORTED_CAPABILITY when a
* required capability is missing,
* INVALID_REQUEST for malformed/cross-provider
* tokens, or CHECKPOINT_EXPIRED when the
* cursor has been trimmed
*/
ChangeFeedPage readChanges(ChangeFeedRequest request, OperationOptions options);
/** Read changes using default options. */
default ChangeFeedPage readChanges(ChangeFeedRequest request) {
return readChanges(request, OperationOptions.defaults());
}
/**
* List the provider-native physical partitions for the given collection.
*