diff --git a/src/backend/src/om/entitystorage/SQLES.js b/src/backend/src/om/entitystorage/SQLES.js index c0f3126ed..ba5ac89bf 100644 --- a/src/backend/src/om/entitystorage/SQLES.js +++ b/src/backend/src/om/entitystorage/SQLES.js @@ -24,6 +24,9 @@ const { Entity } = require('./Entity'); const { WeakConstructorFeature } = require('../../traits/WeakConstructorFeature'); const { And, Or, Eq, Like, Null, Predicate, PredicateUtil, IsNotNull, StartsWith } = require('../query/query'); const { DB_WRITE } = require('../../services/database/consts'); +const { safeHasOwnProperty } = require('../../util/safety'); +const { ParallelTasks } = require('../../util/otelutil'); +const opentelemetry = require('@opentelemetry/api'); class RawCondition extends AdvancedBase { // properties: sql:string, values:any[] @@ -113,12 +116,9 @@ class SQLES extends BaseES { const rows = await this.db.read(stmt, values); - const entities = []; - for ( const data of rows ) { - const entity = await this.sql_row_to_entity_(data); - entities.push(entity); - } - + const entities = await Promise.all(rows.map(async (data) => { + return await this.sql_row_to_entity_(data); + })); return entities; }, @@ -187,6 +187,7 @@ class SQLES extends BaseES { async sql_row_to_entity_ (data) { const entity_data = {}; + const tasks = new ParallelTasks({ tracer: opentelemetry.trace.getTracer('sqles') }); for ( const prop of Object.values(this.om.properties) ) { const options = prop.descriptor.sql ?? {}; @@ -196,38 +197,23 @@ class SQLES extends BaseES { const col_name = options.column_name ?? prop.name; - if ( ! data.hasOwnProperty(col_name) ) { + if ( ! safeHasOwnProperty(data, col_name) ) { continue; } let value = data[col_name]; - value = await prop.sql_dereference(value); - - // TODO: This is not an ideal implementation, - // but this is only 6 lines of code so doing this - // "properly" is not sensible at this time. - // - // This is here because: - // - SQLES has access to the "db" object - // - // Writing this in `json`'s `sql_reference` method - // is also not ideal because that places the concern - // of supporting different database backends to - // property types. - // - // Best solution: SQLES has a SQLRefinements by - // composition. This SQLRefinements is applied - // to property types for the duration of this - // function. - if ( prop.typ.name === 'json' ) { - value = this.db.case({ - mysql: () => value, - otherwise: () => JSON.parse(value ?? '{}'), - })(); - } - - entity_data[prop.name] = value; + tasks.add(`sql_row_to_entity_::${prop.name}`, async () => { + value = await prop.sql_dereference(value); + if ( prop.typ.name === 'json' ) { + value = this.db.case({ + mysql: () => value, + otherwise: () => JSON.parse(value ?? '{}'), + })(); + } + entity_data[prop.name] = value; + }); } + await tasks.awaitAll(); const entity = await Entity.create({ om: this.om }, entity_data); entity.private_meta.mysql_id = data.id; return entity; diff --git a/src/backend/src/util/safety.js b/src/backend/src/util/safety.js new file mode 100644 index 000000000..733ec35a3 --- /dev/null +++ b/src/backend/src/util/safety.js @@ -0,0 +1,16 @@ +/** + * Instead of `myObject.hasOwnProperty(k)`, always write: + * `safeHasOwnProperty(myObject, k)`. + * + * This is a less verbose way to call `Object.prototype.hasOwnProperty.call`. + * This prevents unexpected behavior when `hasOwnProperty` is overridden, + * which is especially possible for objects parsed from user-sent JSON. + * + * explanation: https://eslint.org/docs/latest/rules/no-prototype-builtins + * @param {*} o + * @param {...any} a + * @returns + */ +export const safeHasOwnProperty = (o, ...a) => { + return Object.prototype.hasOwnProperty.call(o, ...a); +}; \ No newline at end of file