Skip to content
This repository was archived by the owner on Jun 6, 2025. It is now read-only.

Commit 4af54f0

Browse files
authored
Match TimeStamp and Etag precision and value (Azure#1661)
* changed table etag and last mod time handling phase 1 * Updated table API to match timestamp and etag
1 parent c8b679c commit 4af54f0

File tree

10 files changed

+184
-93
lines changed

10 files changed

+184
-93
lines changed

ChangeLog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ General:
88

99
- Make emulator start commands async so that they can be awaited by clients.
1010

11+
Table:
12+
13+
- TimeStamp and Etag use the same high precision value as source.
14+
1115
## 2022.09 Version 3.19.0
1216

1317
General:

src/common/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function truncatedISO8061Date(
9898
if (hrtimePrecision) {
9999
return (
100100
dateString.substring(0, dateString.length - 1) +
101-
process.hrtime()[1].toString().padStart(4, "0").slice(0, 3) +
101+
process.hrtime()[1].toString().padStart(4, "0").slice(0, 4) +
102102
"Z"
103103
);
104104
}

src/table/entity/NormalizedEntity.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { truncatedISO8061Date } from "../../common/utils/utils";
12
import { Entity } from "../persistence/ITableMetadataStore";
23
import { ODATA_TYPE } from "../utils/constants";
3-
import { getTimestampString } from "../utils/utils";
44
import { EdmString } from "./EdmString";
55
import { EntityProperty, parseEntityProperty } from "./EntityProperty";
66
// import { EdmType } from "./IEdmType";
@@ -34,11 +34,11 @@ export class NormalizedEntity {
3434
this.propertiesMap.RowKey = rowKeyProperty;
3535

3636
// Sync Timestamp from entity last modified time
37-
entity.properties.Timestamp = getTimestampString(
38-
typeof entity.lastModifiedTime === "string"
39-
? new Date(entity.lastModifiedTime)
40-
: entity.lastModifiedTime
41-
);
37+
entity.properties.Timestamp =
38+
typeof entity.lastModifiedTime === "string" &&
39+
entity.lastModifiedTime === ""
40+
? truncatedISO8061Date(new Date(), true, true)
41+
: entity.lastModifiedTime;
4242
entity.properties["Timestamp@odata.type"] = "Edm.DateTime";
4343

4444
for (const key in entity.properties) {

src/table/handlers/TableHandler.ts

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import toReadableStream from "to-readable-stream";
22

33
import BufferStream from "../../common/utils/BufferStream";
44
import {
5-
checkEtagIsInvalidFormat,
5+
isEtagValid,
66
getUTF8ByteSize,
77
newTableEntityEtag
88
} from "../utils/utils";
@@ -40,6 +40,7 @@ import {
4040
} from "../utils/utils";
4141
import BaseHandler from "./BaseHandler";
4242
import { EdmType, getEdmType } from "../entity/IEdmType";
43+
import { truncatedISO8061Date } from "../../common/utils/utils";
4344

4445
interface IPartialResponsePreferProperties {
4546
statusCode: 200 | 201 | 204;
@@ -198,18 +199,17 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
198199
throw StorageErrorFactory.getPropertiesNeedValue(context);
199200
}
200201

201-
this.checkProperties(context, options.tableEntityProperties);
202-
203-
const entity: Entity = {
204-
PartitionKey: options.tableEntityProperties.PartitionKey,
205-
RowKey: options.tableEntityProperties.RowKey,
206-
properties: options.tableEntityProperties,
207-
lastModifiedTime: context.startTime!,
208-
eTag: newTableEntityEtag(context.startTime!)
209-
};
210202
// check that key properties are valid
211-
this.validateKey(context, entity.PartitionKey);
212-
this.validateKey(context, entity.RowKey);
203+
this.validateKey(context, options.tableEntityProperties.PartitionKey);
204+
this.validateKey(context, options.tableEntityProperties.RowKey);
205+
206+
this.checkProperties(context, options.tableEntityProperties);
207+
const entity: Entity = this.createPersistedEntity(
208+
context,
209+
options,
210+
options.tableEntityProperties.PartitionKey,
211+
options.tableEntityProperties.RowKey
212+
);
213213
let normalizedEntity;
214214
try {
215215
normalizedEntity = new NormalizedEntity(entity);
@@ -284,6 +284,31 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
284284
return response;
285285
}
286286

287+
private createPersistedEntity(
288+
context: Context,
289+
options:
290+
| Models.TableMergeEntityOptionalParams
291+
| Models.TableInsertEntityOptionalParams
292+
| Models.TableUpdateEntityOptionalParams,
293+
partitionKey: string,
294+
rowKey: string
295+
) {
296+
const modTime = truncatedISO8061Date(context.startTime!, true, true);
297+
const eTag = newTableEntityEtag(modTime);
298+
299+
const entity: Entity = {
300+
PartitionKey: partitionKey,
301+
RowKey: rowKey,
302+
properties:
303+
options.tableEntityProperties === undefined
304+
? {}
305+
: options.tableEntityProperties,
306+
lastModifiedTime: modTime,
307+
eTag
308+
};
309+
return entity;
310+
}
311+
287312
private static getAndCheck(
288313
key: string | undefined,
289314
getFromContext: () => string,
@@ -367,25 +392,21 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
367392
throw StorageErrorFactory.getPreconditionFailed(context);
368393
}
369394
if (options?.ifMatch && options.ifMatch !== "*") {
370-
if (checkEtagIsInvalidFormat(options.ifMatch)) {
395+
if (isEtagValid(options.ifMatch)) {
371396
throw StorageErrorFactory.getInvalidOperation(context);
372397
}
373398
}
399+
// check that key properties are valid
400+
this.validateKey(context, partitionKey);
401+
this.validateKey(context, rowKey);
374402

375-
const eTag = newTableEntityEtag(context.startTime!);
376-
377-
// Entity, which is used to update an existing entity
378-
const entity: Entity = {
379-
PartitionKey: partitionKey,
380-
RowKey: rowKey,
381-
properties: options.tableEntityProperties,
382-
lastModifiedTime: context.startTime!,
383-
eTag
384-
};
403+
const entity: Entity = this.createPersistedEntity(
404+
context,
405+
options,
406+
partitionKey,
407+
rowKey
408+
);
385409

386-
// check that key properties are valid
387-
this.validateKey(context, entity.PartitionKey);
388-
this.validateKey(context, entity.RowKey);
389410
let normalizedEntity;
390411
try {
391412
normalizedEntity = new NormalizedEntity(entity);
@@ -413,7 +434,7 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
413434
requestId: tableContext.contextID,
414435
version: TABLE_API_VERSION,
415436
date: context.startTime,
416-
eTag,
437+
eTag: entity.eTag,
417438
statusCode: 204
418439
};
419440

@@ -443,7 +464,7 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
443464
throw StorageErrorFactory.getPropertiesNeedValue(context);
444465
}
445466
if (options?.ifMatch && options.ifMatch !== "*" && options.ifMatch !== "") {
446-
if (checkEtagIsInvalidFormat(options.ifMatch)) {
467+
if (isEtagValid(options.ifMatch)) {
447468
throw StorageErrorFactory.getInvalidOperation(context);
448469
}
449470
}
@@ -456,20 +477,17 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
456477
`TableHandler:mergeEntity() Incoming PartitionKey:${partitionKey} RowKey:${rowKey} in URL parameters don't align with entity body PartitionKey:${options.tableEntityProperties.PartitionKey} RowKey:${options.tableEntityProperties.RowKey}.`
457478
);
458479
}
480+
// check that key properties are valid
481+
this.validateKey(context, partitionKey);
482+
this.validateKey(context, rowKey);
459483

460484
this.checkProperties(context, options.tableEntityProperties);
461-
const eTag = newTableEntityEtag(context.startTime!);
462-
463-
const entity: Entity = {
464-
PartitionKey: partitionKey,
465-
RowKey: rowKey,
466-
properties: options.tableEntityProperties,
467-
lastModifiedTime: context.startTime!,
468-
eTag
469-
};
470-
// check that key properties are valid
471-
this.validateKey(context, entity.PartitionKey);
472-
this.validateKey(context, entity.RowKey);
485+
const entity: Entity = this.createPersistedEntity(
486+
context,
487+
options,
488+
partitionKey,
489+
rowKey
490+
);
473491
let normalizedEntity;
474492
try {
475493
normalizedEntity = new NormalizedEntity(entity);
@@ -497,7 +515,7 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
497515
version: TABLE_API_VERSION,
498516
date: context.startTime,
499517
statusCode: 204,
500-
eTag
518+
eTag: entity.eTag
501519
};
502520

503521
return response;
@@ -524,7 +542,7 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
524542
if (ifMatch === "" || ifMatch === undefined) {
525543
throw StorageErrorFactory.getPreconditionFailed(context);
526544
}
527-
if (ifMatch !== "*" && checkEtagIsInvalidFormat(ifMatch)) {
545+
if (ifMatch !== "*" && isEtagValid(ifMatch)) {
528546
throw StorageErrorFactory.getInvalidOperation(context);
529547
}
530548
// currently the props are not coming through as args, so we take them from the table context
@@ -1009,7 +1027,7 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
10091027
// key is a string value that may be up to 1 KiB in size.
10101028
// although a little arbitrary, for performance and
10111029
// generally a better idea, choosing a shorter length
1012-
if (key.length > DEFAULT_KEY_MAX_LENGTH) {
1030+
if (key !== undefined && key.length > DEFAULT_KEY_MAX_LENGTH) {
10131031
throw StorageErrorFactory.getInvalidInput(context);
10141032
}
10151033
const match = key.match(/[\u0000-\u001f\u007f-\u009f\/\\\#\?]+/);
@@ -1037,14 +1055,17 @@ export default class TableHandler extends BaseHandler implements ITableHandler {
10371055
) {
10381056
for (const prop in properties) {
10391057
if (properties.hasOwnProperty(prop)) {
1040-
if (null !== properties[prop] && undefined !== properties[prop].length) {
1058+
if (
1059+
null !== properties[prop] &&
1060+
undefined !== properties[prop].length
1061+
) {
10411062
const typeKey = `${prop}${ODATA_TYPE}`;
10421063
let type;
10431064
if (properties[typeKey]) {
1044-
type = getEdmType(properties[typeKey])
1065+
type = getEdmType(properties[typeKey]);
10451066
}
10461067
if (type === EdmType.Binary) {
1047-
if (Buffer.from(properties[prop], 'base64').length > 64 * 1024) {
1068+
if (Buffer.from(properties[prop], "base64").length > 64 * 1024) {
10481069
throw StorageErrorFactory.getPropertyValueTooLargeError(context);
10491070
}
10501071
} else if (properties[prop].length > 32 * 1024) {

src/table/persistence/ITableMetadataStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface IEntity {
3737
PartitionKey: string;
3838
RowKey: string;
3939
eTag: string;
40-
lastModifiedTime: Date;
40+
lastModifiedTime: string;
4141
properties: {
4242
[propertyName: string]: string | number | boolean | null;
4343
};

src/table/persistence/LokiTableMetadataStore.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as Models from "../generated/artifacts/models";
77
import Context from "../generated/Context";
88
import { Entity, Table } from "../persistence/ITableMetadataStore";
99
import { ODATA_TYPE, QUERY_RESULT_MAX_NUM } from "../utils/constants";
10-
import { getTimestampString } from "../utils/utils";
1110
import ITableMetadataStore, { TableACL } from "./ITableMetadataStore";
1211

1312
/** MODELS FOR SERVICE */
@@ -303,7 +302,7 @@ export default class LokiTableMetadataStore implements ITableMetadataStore {
303302
throw StorageErrorFactory.getEntityAlreadyExist(context);
304303
}
305304

306-
entity.properties.Timestamp = getTimestampString(entity.lastModifiedTime);
305+
entity.properties.Timestamp = entity.lastModifiedTime;
307306
entity.properties["Timestamp@odata.type"] = "Edm.DateTime";
308307

309308
if (batchId !== "" && batchId !== undefined) {
@@ -650,7 +649,7 @@ export default class LokiTableMetadataStore implements ITableMetadataStore {
650649
) {
651650
tableEntityCollection.remove(doc);
652651

653-
entity.properties.Timestamp = getTimestampString(entity.lastModifiedTime);
652+
entity.properties.Timestamp = entity.lastModifiedTime;
654653
entity.properties["Timestamp@odata.type"] = "Edm.DateTime";
655654

656655
tableEntityCollection.insert(entity);

src/table/utils/utils.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,14 @@ export function validateTableName(context: Context, tableName: string) {
272272
}
273273
}
274274

275-
export function newTableEntityEtag(startTime: Date): string {
275+
export function newTableEntityEtag(highPresModTime: string): string {
276276
// Etag as returned by Table Storage should match W/"datetime'<ISO8601datetime>'"
277-
// we use the additional hrtime precsion option
278-
return (
279-
"W/\"datetime'" +
280-
encodeURIComponent(truncatedISO8061Date(startTime, true, true)) +
281-
"'\""
282-
);
277+
// as we need the same value for last Modification time, we now only accept a string here
278+
return "W/\"datetime'" + encodeURIComponent(highPresModTime) + "'\"";
279+
}
280+
281+
export function newHighPrecisionTimeStamp(startTime: Date): string {
282+
return truncatedISO8061Date(startTime, true, true);
283283
}
284284

285285
/**
@@ -289,7 +289,7 @@ export function newTableEntityEtag(startTime: Date): string {
289289
* @param {string} etag
290290
* @return {*} {boolean}
291291
*/
292-
export function checkEtagIsInvalidFormat(etag: string): boolean {
292+
export function isEtagValid(etag: string): boolean {
293293
// Weak etag is required. This is parity with Azure and legacy emulator.
294294
// Source for regex: https://stackoverflow.com/a/11572348
295295
const match = etag.match(/^[wW]\/"([^"]|\\")*"$/);

tests/table/apis/table.entity.azure.data-tables.test.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,7 +1123,7 @@ describe("table Entity APIs test - using Azure/data-tables", () => {
11231123
await tableClient.deleteTable();
11241124
});
11251125

1126-
[2, 1, 0].map(delta => {
1126+
[2, 1, 0].map((delta) => {
11271127
it(`Should insert entities containing binary properties less than or equal than 64K bytes (delta ${delta}), @loki`, async () => {
11281128
const tableClient = createAzureDataTablesClient(
11291129
testLocalAzuriteInstance,
@@ -1134,20 +1134,16 @@ describe("table Entity APIs test - using Azure/data-tables", () => {
11341134
const testEntity: AzureDataTablesTestEntity =
11351135
createBasicEntityForTest(partitionKey);
11361136

1137-
testEntity.binaryField = Buffer.alloc((64 * 1024) - delta);
1137+
testEntity.binaryField = Buffer.alloc(64 * 1024 - delta);
11381138

11391139
const result = await tableClient.createEntity(testEntity);
1140-
assert.notStrictEqual(
1141-
result.etag,
1142-
undefined,
1143-
"Did not create entity!"
1144-
);
1140+
assert.notStrictEqual(result.etag, undefined, "Did not create entity!");
11451141

11461142
await tableClient.deleteTable();
11471143
});
11481144
});
11491145

1150-
[1, 2, 3].map(delta => {
1146+
[1, 2, 3].map((delta) => {
11511147
it(`Should not insert entities containing binary properties greater than 64K bytes (delta ${delta}), @loki`, async () => {
11521148
const tableClient = createAzureDataTablesClient(
11531149
testLocalAzuriteInstance,
@@ -1158,7 +1154,7 @@ describe("table Entity APIs test - using Azure/data-tables", () => {
11581154
const testEntity: AzureDataTablesTestEntity =
11591155
createBasicEntityForTest(partitionKey);
11601156

1161-
testEntity.binaryField = Buffer.alloc((64 * 1024) + delta);
1157+
testEntity.binaryField = Buffer.alloc(64 * 1024 + delta);
11621158
try {
11631159
const result = await tableClient.createEntity(testEntity);
11641160
assert.strictEqual(
@@ -1413,8 +1409,8 @@ describe("table Entity APIs test - using Azure/data-tables", () => {
14131409
a: "a".repeat(32 * 1024),
14141410
b: "b".repeat(32 * 1024),
14151411
c: "c".repeat(32 * 1024),
1416-
d: "d".repeat(32 * 1024),
1417-
})
1412+
d: "d".repeat(32 * 1024)
1413+
});
14181414
}
14191415

14201416
try {
@@ -1658,7 +1654,6 @@ describe("table Entity APIs test - using Azure/data-tables", () => {
16581654
await tableClient.deleteTable();
16591655
});
16601656

1661-
16621657
it("Should insert entities with null properties, @loki", async () => {
16631658
const tableClient = createAzureDataTablesClient(
16641659
testLocalAzuriteInstance,

0 commit comments

Comments
 (0)