Skip to content

Commit

Permalink
Added Hierarchical Partition Key Support
Browse files Browse the repository at this point in the history
  • Loading branch information
vikask00 committed Nov 18, 2022
1 parent 81d5c30 commit 79df02f
Show file tree
Hide file tree
Showing 20 changed files with 301 additions and 208 deletions.
4 changes: 2 additions & 2 deletions sdk/cosmosdb/cosmos/src/ClientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Constants, HTTPMethod, OperationType, ResourceType } from "./common/con
import { getIdFromLink, getPathFromLink, parseLink } from "./common/helper";
import { StatusCodes, SubStatusCodes } from "./common/statusCodes";
import { Agent, CosmosClientOptions } from "./CosmosClientOptions";
import { ConnectionPolicy, ConsistencyLevel, DatabaseAccount, PartitionKey } from "./documents";
import { ConnectionPolicy, ConsistencyLevel, DatabaseAccount, PartitionKey, mapPartitionToInternal } from "./documents";
import { GlobalEndpointManager } from "./globalEndpointManager";
import { PluginConfig, PluginOn, executePlugins } from "./plugins/Plugin";
import { FetchFunctionCallback, SqlQuerySpec } from "./queryExecutionContext";
Expand Down Expand Up @@ -757,7 +757,7 @@ export class ClientContext {
options: requestContext.options,
partitionKeyRangeId: requestContext.partitionKeyRangeId,
useMultipleWriteLocations: this.connectionPolicy.useMultipleWriteLocations,
partitionKey: requestContext.partitionKey,
partitionKey: requestContext.partitionKey !== undefined ? mapPartitionToInternal(requestContext.partitionKey) : undefined,
});
}

Expand Down
8 changes: 4 additions & 4 deletions sdk/cosmosdb/cosmos/src/client/Item/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ResourceType,
StatusCodes,
} from "../../common";
import { PartitionKey } from "../../documents";
import { PartitionKey, PartitionKeyInternal, mapPartitionToInternal } from "../../documents";
import { extractPartitionKey, undefinedPartitionKey } from "../../extractPartitionKey";
import { RequestOptions, Response } from "../../request";
import { PatchRequestBody } from "../../utils/patch";
Expand All @@ -24,7 +24,7 @@ import { ItemResponse } from "./ItemResponse";
* @see {@link Items} for operations on all items; see `container.items`.
*/
export class Item {
private partitionKey: PartitionKey;
private partitionKey: PartitionKeyInternal;
/**
* Returns a reference URL to the resource. Used for linking in Permissions.
*/
Expand All @@ -41,10 +41,10 @@ export class Item {
constructor(
public readonly container: Container,
public readonly id: string,
partitionKey: PartitionKey,
partitionKey: PartitionKey | undefined,
private readonly clientContext: ClientContext
) {
this.partitionKey = partitionKey;
this.partitionKey = partitionKey === undefined ? undefined : mapPartitionToInternal(partitionKey);
}

/**
Expand Down
51 changes: 31 additions & 20 deletions sdk/cosmosdb/cosmos/src/client/Item/Items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ import { ItemResponse } from "./ItemResponse";
import {
Batch,
isKeyInRange,
Operation,
getPartitionKeyToHash,
decorateOperation,
prepareOperations,
OperationResponse,
OperationInput,
BulkOptions,
decorateBatchOperation,
hasResource,
} from "../../utils/batch";
import { hashV1PartitionKey } from "../../utils/hashing/v1";
import { hashV2PartitionKey } from "../../utils/hashing/v2";
import { stripUndefined } from "../../utils/typeChecks";
import { hashPartitionKey } from "../../utils/hashing/hash";
import { PartitionKeyDefinition } from "../../documents";

/**
* @hidden
Expand Down Expand Up @@ -408,7 +408,8 @@ export class Items {
const { resources: partitionKeyRanges } = await this.container
.readPartitionKeyRanges()
.fetchAll();
const { resource: definition } = await this.container.getPartitionKeyDefinition();
let { resource } = await this.container.readPartitionKeyDefinition();
const partitionDefinition = stripUndefined(resource, "PartitionKeyDefinition.");
const batches: Batch[] = partitionKeyRanges.map((keyRange: PartitionKeyRange) => {
return {
min: keyRange.minInclusive,
Expand All @@ -418,19 +419,8 @@ export class Items {
operations: [],
};
});
operations
.map((operation) => decorateOperation(operation, definition, options))
.forEach((operation: Operation, index: number) => {
const partitionProp = definition.paths[0].replace("/", "");
const isV2 = definition.version && definition.version === 2;
const toHashKey = getPartitionKeyToHash(operation, partitionProp);
const hashed = isV2 ? hashV2PartitionKey(toHashKey) : hashV1PartitionKey(toHashKey);
const batchForKey = batches.find((batch: Batch) => {
return isKeyInRange(batch.min, batch.max, hashed);
});
batchForKey.operations.push(operation);
batchForKey.indexes.push(index);
});

this.groupOperationsBasedOnPartitionKey(operations, partitionDefinition, options, batches);

const path = getPathFromLink(this.container.url, ResourceType.item);

Expand Down Expand Up @@ -460,7 +450,7 @@ export class Items {
// partition key types as well since we don't support them, so for now we throw
if (err.code === 410) {
throw new Error(
"Partition key error. Either the partitions have split or an operation has an unsupported partitionKey type"
"Partition key error. Either the partitions have split or an operation has an unsupported partitionKey type" + err.message
);
}
throw new Error(`Bulk request errored with: ${err.message}`);
Expand All @@ -470,6 +460,27 @@ export class Items {
return orderedResponses;
}

/**
* Function to create batches based of partition key Ranges.
* 1. if {@link operation} has {@link hasResource} then {@link PartitionKey} is extracted from there.
* @param operations
* @param partitionDefinition
* @param options
* @param batches
*/
private groupOperationsBasedOnPartitionKey(operations: OperationInput[], partitionDefinition: PartitionKeyDefinition, options: RequestOptions | undefined, batches: Batch[]) {
operations
.forEach((operationInput, index: number) => {
const { operation, partitionKey } = prepareOperations(operationInput, partitionDefinition, options);
const hashed = hashPartitionKey(stripUndefined(partitionKey, "PartitionKey"), partitionDefinition);
const batchForKey = stripUndefined(batches.find((batch: Batch) => {
return isKeyInRange(batch.min, batch.max, hashed);
}), "No suitable Batch found.");
batchForKey.operations.push(operation);
batchForKey.indexes.push(index);
});
}

/**
* Execute transactional batch operations on items.
*
Expand Down
44 changes: 42 additions & 2 deletions sdk/cosmosdb/cosmos/src/documents/PartitionKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { PartitionKeyDefinition } from "./PartitionKeyDefinition";

export type PartitionKey = PartitionKeyDefinition | string | number | unknown;
export type PartitionKey = PrimitivePartitionKeyValue | PrimitivePartitionKeyValue[];

/**
* Internal Representation Of Partition Key.
*/
export type PartitionKeyInternal = PrimitivePartitionKeyValue[];

/**
* A primitive Partition Key value.
*/
export type PrimitivePartitionKeyValue =
| string
| number
| boolean
| NullPartitionType
| NonePartitionKey;

/**
* The returned object represents a partition key value that allows creating and accessing items
* with a null value for the partition key.
*/
export type NullPartitionType = null;
export const NullPartitionKeyLiteral: NullPartitionType = null;

/**
* The returned object represents a partition key value that allows creating and accessing items
* without a value for partition key
*/
export type NonePartitionKey = {
[K in any]: never;
};
export const NonePartitionKeyLiteral: NonePartitionKey = {};

/**
* Maps PartitionKey to InternalPartitionKey.
* @param partitionKey
* @returns
*/
export function mapPartitionToInternal(partitionKey: PartitionKey): PartitionKeyInternal {
if (Array.isArray(partitionKey)) return partitionKey;
else return [partitionKey];
}
9 changes: 8 additions & 1 deletion sdk/cosmosdb/cosmos/src/documents/PartitionKeyDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { PartitionKeyDefinitionVersion } from "./PartitionKeyDefinitionVersion";
import { PartitionKeyKind } from "./PartitionKind";

export interface PartitionKeyDefinition {
/**
* An array of paths for which data within the collection can be partitioned. Paths must not contain a wildcard or
Expand All @@ -11,6 +14,10 @@ export interface PartitionKeyDefinition {
* An optional field, if not specified the default value is 1. To use the large partition key set the version to 2.
* To learn about large partition keys, see [how to create containers with large partition key](https://docs.microsoft.com/en-us/azure/cosmos-db/large-partition-keys) article.
*/
version?: number;
version?: PartitionKeyDefinitionVersion;
systemKey?: boolean;
/**
* What kind of partition key is being defined (default: "Hash")
*/
kind?: PartitionKeyKind;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
export enum PartitionKeyDefinitionVersion {
V1 = 1,
V2 = 2
}
6 changes: 6 additions & 0 deletions sdk/cosmosdb/cosmos/src/documents/PartitionKind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
export enum PartitionKeyKind {
Hash = "Hash",
MultiHash = "MultiHash"
}
2 changes: 2 additions & 0 deletions sdk/cosmosdb/cosmos/src/documents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from "./IndexingMode";
export * from "./IndexingPolicy";
export * from "./IndexKind";
export * from "./PartitionKey";
export * from "./PartitionKeyDefinitionVersion"
export * from "./PartitionKind"
export * from "./PartitionKeyDefinition";
export * from "./PermissionMode";
export * from "./TriggerOperation";
Expand Down
46 changes: 33 additions & 13 deletions sdk/cosmosdb/cosmos/src/extractPartitionKey.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,67 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { AzureLogger, createClientLogger } from "@azure/logger";
import { parsePath } from "./common";
import { PartitionKey, PartitionKeyDefinition } from "./documents";
import { NonePartitionKeyLiteral, NullPartitionKeyLiteral, PartitionKeyDefinition, PartitionKeyInternal, PrimitivePartitionKeyValue } from "./documents";

const logger: AzureLogger = createClientLogger("extractPartitionKey");

/**
* Function to extract PartitionKey based on {@link PartitionKeyDefinition}
* from an object.
* Retuns
* 1. {@link PartitionKeyInternal[]} - if extraction is successful.
* 2. {@link undefined} - if either {@link partitionKeyDefinition} is not well formed
* or an unsupported partitionkey type is encountered.
* @hidden
*/
export function extractPartitionKey(
document: unknown,
partitionKeyDefinition: PartitionKeyDefinition
): PartitionKey[] {
partitionKeyDefinition?: PartitionKeyDefinition
): PartitionKeyInternal | undefined {
if (
partitionKeyDefinition &&
partitionKeyDefinition.paths &&
partitionKeyDefinition.paths.length > 0
) {
const partitionKey: PartitionKey[] = [];
const partitionKeys: PrimitivePartitionKeyValue[] = [];
partitionKeyDefinition.paths.forEach((path: string) => {
const pathParts = parsePath(path);
const pathParts: string[] = parsePath(path);
let obj = document;
for (const part of pathParts) {
if (typeof obj === "object" && part in obj) {
if (typeof obj === "object" && obj !==null && part in obj) {
obj = (obj as Record<string, unknown>)[part];
} else {
obj = undefined;
break;
}
}
partitionKey.push(obj);
if(typeof obj === "string" || typeof obj === "number" || typeof obj === "boolean" ) {
partitionKeys.push(obj);
} else if(obj === NullPartitionKeyLiteral) {
partitionKeys.push(NullPartitionKeyLiteral);
} else if(obj === undefined || JSON.stringify(obj) === JSON.stringify(NonePartitionKeyLiteral)) {
if(partitionKeyDefinition.systemKey === true) {
return []
}
partitionKeys.push(NonePartitionKeyLiteral);
} else {
logger.warning('Unsupported PartitionKey found.');
return undefined;
}
});
if (partitionKey.length === 1 && partitionKey[0] === undefined) {
return undefinedPartitionKey(partitionKeyDefinition);
}
return partitionKey;
return partitionKeys;
}
logger.warning("Unexpected Partition Key Definition Found.");
return undefined;
}
/**
* @hidden
*/
export function undefinedPartitionKey(partitionKeyDefinition: PartitionKeyDefinition): unknown[] {
export function undefinedPartitionKey(partitionKeyDefinition: PartitionKeyDefinition): PartitionKeyInternal {
if (partitionKeyDefinition.systemKey === true) {
return [];
} else {
return [{}];
return partitionKeyDefinition.paths.map(() => NonePartitionKeyLiteral);
}
}
7 changes: 2 additions & 5 deletions sdk/cosmosdb/cosmos/src/request/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { setAuthorizationHeader } from "../auth";
import { Constants, HTTPMethod, jsonStringifyAndEscapeNonASCII, ResourceType } from "../common";
import { CosmosClientOptions } from "../CosmosClientOptions";
import { PartitionKey } from "../documents";
import { PartitionKeyInternal } from "../documents";
import { CosmosHeaders } from "../queryExecutionContext";
import { FeedOptions, RequestOptions } from "./index";
import { defaultLogger } from "../common/logger";
Expand Down Expand Up @@ -41,7 +41,7 @@ interface GetHeadersOptions {
options: RequestOptions & FeedOptions;
partitionKeyRangeId?: string;
useMultipleWriteLocations?: boolean;
partitionKey?: PartitionKey;
partitionKey?: PartitionKeyInternal;
}

const JsonContentType = "application/json";
Expand Down Expand Up @@ -168,9 +168,6 @@ export async function getHeaders({
}

if (partitionKey !== undefined && !headers[Constants.HttpHeaders.PartitionKey]) {
if (partitionKey === null || !Array.isArray(partitionKey)) {
partitionKey = [partitionKey as string];
}
headers[Constants.HttpHeaders.PartitionKey] = jsonStringifyAndEscapeNonASCII(partitionKey);
}

Expand Down
Loading

0 comments on commit 79df02f

Please sign in to comment.