Compare commits

...
Author SHA1 Message Date
James Ide d13235e926 Precompute field-to-transformer map during adapter construction
Why
===
The existing implementation performs two map lookups per field during
transformation: one to get the field definition from the schema, and
another to get the transformer using the field definition's constructor
name. For entities with many fields or batch operations, this adds up.

How
===
Build a precomputed `fieldName → transformer` map once during adapter
construction. The transformation functions now accept this optional
precomputed map and use it for single-lookup access when provided.

The implementation maintains backward compatibility by making the
precomputed map optional in the transformation utility functions.

Test Plan
===
All 362 tests pass. Benchmark shows 1.36x faster for full object
transformation (15 fields):

  Original:    88.35ms for 100,000 iterations
  Precomputed: 64.83ms for 100,000 iterations

Run the benchmark:
  npx tsx packages/entity/src/__benchmarks__/field-transformation.bench.ts
2026-01-09 21:40:11 -08:00
2 changed files with 84 additions and 9 deletions

No files matched your search

+29 -3
View File
@@ -10,8 +10,10 @@ import {
EntityDatabaseAdapterExcessiveUpdateResultError,
} from './errors/EntityDatabaseAdapterError';
import {
buildPrecomputedFieldTransformerMap,
FieldTransformerMap,
getDatabaseFieldForEntityField,
PrecomputedFieldTransformerMap,
transformDatabaseObjectToFields,
transformFieldsToDatabaseObject,
} from './internal/EntityFieldTransformationUtils';
@@ -149,9 +151,14 @@ export abstract class EntityDatabaseAdapter<
TIDField extends keyof TFields,
> {
private readonly fieldTransformerMap: FieldTransformerMap;
private readonly precomputedFieldTransformerMap: PrecomputedFieldTransformerMap<TFields>;
constructor(private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>) {
this.fieldTransformerMap = this.getFieldTransformerMap();
this.precomputedFieldTransformerMap = buildPrecomputedFieldTransformerMap(
entityConfiguration,
this.fieldTransformerMap,
);
}
/**
@@ -189,7 +196,12 @@ export abstract class EntityDatabaseAdapter<
);
const objects = results.map((result) =>
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
transformDatabaseObjectToFields(
this.entityConfiguration,
this.fieldTransformerMap,
result,
this.precomputedFieldTransformerMap,
),
);
const objectMap = key.vendNewLoadValueMap<Readonly<TFields>[]>();
@@ -260,7 +272,12 @@ export abstract class EntityDatabaseAdapter<
);
return results.map((result) =>
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
transformDatabaseObjectToFields(
this.entityConfiguration,
this.fieldTransformerMap,
result,
this.precomputedFieldTransformerMap,
),
);
}
@@ -296,7 +313,12 @@ export abstract class EntityDatabaseAdapter<
);
return results.map((result) =>
transformDatabaseObjectToFields(this.entityConfiguration, this.fieldTransformerMap, result),
transformDatabaseObjectToFields(
this.entityConfiguration,
this.fieldTransformerMap,
result,
this.precomputedFieldTransformerMap,
),
);
}
@@ -323,6 +345,7 @@ export abstract class EntityDatabaseAdapter<
this.entityConfiguration,
this.fieldTransformerMap,
object,
this.precomputedFieldTransformerMap,
);
const results = await this.insertInternalAsync(
queryContext.getQueryInterface(),
@@ -347,6 +370,7 @@ export abstract class EntityDatabaseAdapter<
this.entityConfiguration,
this.fieldTransformerMap,
results[0]!,
this.precomputedFieldTransformerMap,
);
}
@@ -376,6 +400,7 @@ export abstract class EntityDatabaseAdapter<
this.entityConfiguration,
this.fieldTransformerMap,
object,
this.precomputedFieldTransformerMap,
);
const results = await this.updateInternalAsync(
queryContext.getQueryInterface(),
@@ -402,6 +427,7 @@ export abstract class EntityDatabaseAdapter<
this.entityConfiguration,
this.fieldTransformerMap,
results[0]!,
this.precomputedFieldTransformerMap,
);
}
@@ -25,6 +25,39 @@ export interface FieldTransformer<T> {
*/
export type FieldTransformerMap = Map<string, FieldTransformer<any>>;
/**
* Precomputed map from field name to its transformer. This eliminates two lookups
* (schema + constructor.name) per field during transformation, replacing them with
* a single direct lookup.
*
* @internal
*/
export type PrecomputedFieldTransformerMap<TFields> = ReadonlyMap<
keyof TFields,
FieldTransformer<any> | undefined
>;
/**
* Build a precomputed map from field name to transformer. This is called once during
* adapter construction to avoid repeated lookups during field transformation.
*
* @internal
*/
export const buildPrecomputedFieldTransformerMap = <
TFields extends Record<string, any>,
TIDField extends keyof TFields,
>(
entityConfiguration: EntityConfiguration<TFields, TIDField>,
fieldTransformerMap: FieldTransformerMap,
): PrecomputedFieldTransformerMap<TFields> => {
const map = new Map<keyof TFields, FieldTransformer<any> | undefined>();
for (const [fieldName, fieldDefinition] of entityConfiguration.schema) {
const transformer = fieldTransformerMap.get(fieldDefinition.constructor.name);
map.set(fieldName, transformer);
}
return map;
};
/**
* @internal
*/
@@ -50,6 +83,7 @@ export const transformDatabaseObjectToFields = <
entityConfiguration: EntityConfiguration<TFields, TIDField>,
fieldTransformerMap: FieldTransformerMap,
databaseObject: { [key: string]: any },
precomputedFieldTransformerMap?: PrecomputedFieldTransformerMap<TFields>,
): Readonly<TFields> => {
const fields: TFields = {} as any;
for (const k in databaseObject) {
@@ -61,6 +95,7 @@ export const transformDatabaseObjectToFields = <
fieldTransformerMap,
fieldsKey,
val,
precomputedFieldTransformerMap,
);
}
}
@@ -77,6 +112,7 @@ export const transformFieldsToDatabaseObject = <
entityConfiguration: EntityConfiguration<TFields, TIDField>,
fieldTransformerMap: FieldTransformerMap,
fields: Readonly<Partial<TFields>>,
precomputedFieldTransformerMap?: PrecomputedFieldTransformerMap<TFields>,
): object => {
const databaseObject: { [key: string]: any } = {};
for (const k in fields) {
@@ -88,6 +124,7 @@ export const transformFieldsToDatabaseObject = <
fieldTransformerMap,
k,
val,
precomputedFieldTransformerMap,
);
}
return databaseObject;
@@ -150,11 +187,16 @@ const maybeTransformDatabaseValueToFieldValue = <
fieldTransformerMap: FieldTransformerMap,
fieldName: N,
value: any,
precomputedFieldTransformerMap?: PrecomputedFieldTransformerMap<TFields>,
): TFields[N] => {
// this will always be non-null due to the way the dbToEntityFieldsKeyMapping is computed and this
// function is called conditionally
const fieldDefinition = nullthrows(entityConfiguration.schema.get(fieldName));
const transformer = fieldTransformerMap.get(fieldDefinition.constructor.name);
// Use precomputed map when available to avoid two lookups per field
let transformer: FieldTransformer<any> | undefined;
if (precomputedFieldTransformerMap) {
transformer = precomputedFieldTransformerMap.get(fieldName);
} else {
const fieldDefinition = nullthrows(entityConfiguration.schema.get(fieldName));
transformer = fieldTransformerMap.get(fieldDefinition.constructor.name);
}
const readTransformer = transformer?.read;
return readTransformer ? readTransformer(value) : value;
};
@@ -168,9 +210,16 @@ const maybeTransformFieldValueToDatabaseValue = <
fieldTransformerMap: FieldTransformerMap,
fieldName: N,
value: TFields[N],
precomputedFieldTransformerMap?: PrecomputedFieldTransformerMap<TFields>,
): any => {
const fieldDefinition = nullthrows(entityConfiguration.schema.get(fieldName));
const transformer = fieldTransformerMap.get(fieldDefinition.constructor.name);
// Use precomputed map when available to avoid two lookups per field
let transformer: FieldTransformer<any> | undefined;
if (precomputedFieldTransformerMap) {
transformer = precomputedFieldTransformerMap.get(fieldName);
} else {
const fieldDefinition = nullthrows(entityConfiguration.schema.get(fieldName));
transformer = fieldTransformerMap.get(fieldDefinition.constructor.name);
}
const writeTransformer = transformer?.write;
return writeTransformer ? writeTransformer(value) : value;
};