diff --git a/install/package.json b/install/package.json index 448ef25a0a..b715497852 100644 --- a/install/package.json +++ b/install/package.json @@ -100,8 +100,8 @@ "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.8", "nodebb-plugin-composer-default": "10.2.44", - "nodebb-plugin-dbsearch": "6.2.7", - "nodebb-plugin-emoji": "6.0.1", + "nodebb-plugin-dbsearch": "6.2.8", + "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", "nodebb-plugin-markdown": "13.0.0", "nodebb-plugin-mentions": "4.6.10", diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index 5e42a4f26d..958d5384f3 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -188,7 +188,7 @@ module.exports = function (module) { }; module.deleteObjectField = async function (key, field) { - await module.deleteObjectFields(key, [field]); + await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]); }; module.deleteObjectFields = async function (key, fields) { diff --git a/src/database/postgres/hash.js b/src/database/postgres/hash.js index ced3207822..5e3a842d22 100644 --- a/src/database/postgres/hash.js +++ b/src/database/postgres/hash.js @@ -295,7 +295,7 @@ SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b }; module.deleteObjectField = async function (key, field) { - await module.deleteObjectFields(key, [field]); + await module.deleteObjectFields(key, Array.isArray(field) ? field : [field]); }; module.deleteObjectFields = async function (key, fields) { @@ -377,12 +377,34 @@ RETURNING ("data"->>$2::TEXT)::NUMERIC v`, if (!Array.isArray(data) || !data.length) { return; } - // TODO: perf? - await Promise.all(data.map(async (item) => { - for (const [field, value] of Object.entries(item[1])) { - // eslint-disable-next-line no-await-in-loop - await module.incrObjectFieldBy(item[0], field, value); - } - })); + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'hash'); + + const keys = data.map(item => item[0]); + const dataStrings = data.map(item => JSON.stringify(item[1])); + + await client.query({ + name: 'incrObjectFieldByBulk', + text: ` +INSERT INTO "legacy_hash" ("_key", "data") +SELECT k, d +FROM UNNEST($1::TEXT[], $2::JSONB[]) vs(k, d) +ON CONFLICT ("_key") +DO UPDATE SET "data" = ( + SELECT jsonb_object_agg( + key, + CASE + WHEN jsonb_typeof(legacy_hash.data -> key) = 'number' + AND jsonb_typeof(EXCLUDED.data -> key) = 'number' + THEN to_jsonb((legacy_hash.data ->> key)::NUMERIC + (EXCLUDED.data ->> key)::NUMERIC) + ELSE COALESCE(EXCLUDED.data -> key, legacy_hash.data -> key) + END + ) + FROM jsonb_each(legacy_hash.data || EXCLUDED.data) AS merged(key, value) +);`, + values: [keys, dataStrings], + }); + }); }; }; diff --git a/src/database/postgres/list.js b/src/database/postgres/list.js index a950f4ce41..5e91b8c947 100644 --- a/src/database/postgres/list.js +++ b/src/database/postgres/list.js @@ -75,9 +75,26 @@ RETURNING A."array"[array_length(A."array", 1)] v`, if (!key) { return; } - // TODO: remove all values with one query + if (Array.isArray(value)) { - await Promise.all(value.map(v => module.listRemoveAll(key, v))); + await module.pool.query({ + name: 'listRemoveAllMultiple', + text: ` + UPDATE "legacy_list" l + SET "array" = ( + SELECT ARRAY( + SELECT elem + FROM unnest(l."array") WITH ORDINALITY AS u(elem, ord) + WHERE elem NOT IN (SELECT unnest($2::TEXT[])) + ORDER BY ord + ) + ) + FROM "legacy_object_live" o + WHERE o."_key" = l."_key" + AND o."type" = l."type" + AND o."_key" = $1::TEXT;`, + values: [key, value], + }); return; } await module.pool.query({ diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js index 27168493a7..916425e0c1 100644 --- a/src/database/postgres/sorted.js +++ b/src/database/postgres/sorted.js @@ -547,8 +547,38 @@ RETURNING "score" s`, }; module.sortedSetIncrByBulk = async function (data) { - // TODO: perf single query? - return await Promise.all(data.map(item => module.sortedSetIncrBy(item[0], item[1], item[2]))); + if (!data.length) { + return []; + } + + return await module.transaction(async (client) => { + await helpers.ensureLegacyObjectsType(client, data.map(item => item[0]), 'zset'); + + const values = []; + const queryParams = []; + let paramIndex = 1; + + data.forEach(([key, increment, value]) => { + value = helpers.valueToString(value); + increment = parseFloat(increment); + values.push(key, value, increment); + queryParams.push(`($${paramIndex}::TEXT, $${paramIndex + 1}::TEXT, $${paramIndex + 2}::NUMERIC)`); + paramIndex += 3; + }); + + const query = ` +INSERT INTO "legacy_zset" ("_key", "value", "score") +VALUES ${queryParams.join(', ')} +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = "legacy_zset"."score" + EXCLUDED."score" +RETURNING "value", "score"`; + + const res = await client.query({ + text: query, + values, + }); + return res.rows.map(row => parseFloat(row.score)); + }); }; module.getSortedSetRangeByLex = async function (key, min, max, start, count) {