UBER-734 Rework makeRank and migrate ranks (#7104)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-11-05 16:25:26 +07:00 committed by GitHub
parent 585b3f40e7
commit e3353df572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 175 additions and 7 deletions

View File

@ -21,7 +21,8 @@ import {
type Status,
type StatusCategory,
type Doc,
type Class
type Class,
type Rank
} from '@hcengineering/core'
import { Model, Prop, TypeRef, TypeString, UX } from '@hcengineering/model'
import { type Asset, type IntlString } from '@hcengineering/platform'
@ -54,7 +55,7 @@ export class TStatus extends TDoc implements Status {
@Prop(TypeString(), core.string.Description)
description!: string
rank!: string
rank!: Rank
}
@Model(core.class.StatusCategory, core.class.Doc, DOMAIN_MODEL)

View File

@ -17,6 +17,7 @@ import { type CollaborativeDoc, DOMAIN_TX, MeasureMetricsContext, SortingOrder }
import { type DocumentSnapshot, type Document, type Teamspace } from '@hcengineering/document'
import {
tryMigrate,
migrateSpaceRanks,
type MigrateOperation,
type MigrateUpdate,
type MigrationClient,
@ -270,6 +271,16 @@ async function restoreContentField (client: MigrationClient): Promise<void> {
}
}
async function migrateRanks (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(document.class.Teamspace)
for (const _class of classes) {
const spaces = await client.find<Teamspace>(DOMAIN_SPACE, { _class })
for (const space of spaces) {
await migrateSpaceRanks(client, DOMAIN_DOCUMENT, space)
}
}
}
export const documentOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, documentId, [
@ -306,6 +317,10 @@ export const documentOperation: MigrateOperation = {
{
state: 'restoreContentField',
func: restoreContentField
},
{
state: 'migrateRanks',
func: migrateRanks
}
])
},

View File

@ -32,6 +32,7 @@ import {
import {
createOrUpdate,
migrateSpace,
migrateSpaceRanks,
tryMigrate,
tryUpgrade,
type MigrateOperation,
@ -44,6 +45,7 @@ import core, { DOMAIN_SPACE } from '@hcengineering/model-core'
import tags from '@hcengineering/model-tags'
import {
taskId,
type Project,
type ProjectStatus,
type ProjectType,
type ProjectTypeDescriptor,
@ -548,6 +550,16 @@ export async function migrateDefaultStatusesBase<T extends Task> (
logger.log('Statuses updated: ', statusIdsBeingMigrated.length)
}
async function migrateRanks (client: MigrationClient): Promise<void> {
const classes = client.hierarchy.getDescendants(task.class.Project)
for (const _class of classes) {
const spaces = await client.find<Project>(DOMAIN_SPACE, { _class })
for (const space of spaces) {
await migrateSpaceRanks(client, DOMAIN_TASK, space)
}
}
}
function areSameArrays (arr1: any[] | undefined, arr2: any[] | undefined): boolean {
if (arr1 === arr2) {
return true
@ -590,6 +602,10 @@ export const taskOperation: MigrateOperation = {
func: async (client: MigrationClient): Promise<void> => {
await client.update(DOMAIN_TASK, { '%hash%': { $exists: true } }, { $set: { '%hash%': null } })
}
},
{
state: 'migrateRanks',
func: migrateRanks
}
])
},

View File

@ -42,6 +42,7 @@
"@hcengineering/platform": "^0.6.11",
"@hcengineering/storage": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/rank": "^0.6.4",
"toposort": "^2.0.2",
"fast-equals": "^5.0.1"
},

View File

@ -15,13 +15,16 @@ import core, {
ModelDb,
ObjQueryType,
PushOptions,
Rank,
Ref,
SortingOrder,
Space,
TxOperations,
UnsetOptions,
WorkspaceId,
generateId
} from '@hcengineering/core'
import { makeRank } from '@hcengineering/rank'
import { StorageAdapter } from '@hcengineering/storage'
import { ModelLogger } from './utils'
@ -252,3 +255,34 @@ export async function migrateSpace (
}
await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to })
}
export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise<void> {
type WithRank = Doc & { rank: Rank }
const iterator = await client.traverse<WithRank>(
domain,
{ space: space._id, rank: { $exists: true } },
{ sort: { rank: SortingOrder.Ascending } }
)
try {
let rank = '0|100000:'
while (true) {
const docs = await iterator.next(1000)
if (docs === null || docs.length === 0) {
break
}
const updates: { filter: MigrationDocumentQuery<Doc<Space>>, update: MigrateUpdate<Doc<Space>> }[] = []
for (const doc of docs) {
rank = makeRank(rank, undefined)
updates.push({ filter: { _id: doc._id }, update: { rank } })
}
await client.bulk(domain, updates)
}
} finally {
await iterator.close()
}
}

View File

@ -0,0 +1,91 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { makeRank } from '..'
describe('makeRank', () => {
it('calculates rank when no prev and next', () => {
expect(makeRank(undefined, undefined)).toBe('0|hzzzzz:')
})
it.each([
['0|hzzzzz:', '0|i00007:'],
['0|i00007:', '0|i0000f:'],
['0|i0000f:', '0|i0000n:'],
['0|zzzzzz:', '0|zzzzzz:']
])('calculates rank value when prev is %p', (prev, expected) => {
expect(makeRank(prev, undefined)).toBe(expected)
})
it.each([
['0|hzzzzz:', '0|hzzzzr:'],
['0|hzzzzr:', '0|hzzzzj:'],
['0|hzzzzj:', '0|hzzzzb:'],
['0|000000:', '0|000000:']
])('calculates rank value when next is %p', (next, expected) => {
expect(makeRank(undefined, next)).toBe(expected)
})
it.each([
['0|000000:', '0|000001:', '0|000000:i'],
['0|hzzzzz:', '0|i0000f:', '0|i00007:'],
['0|hzzzzz:', '0|hzzzzz:', '0|i00007:'],
['0|i00007:', '0|i00007:', '0|i0000f:'],
['0|i00007:', '0|i00008:', '0|i00007:i']
])('calculates rank value when prev is %p and next is %p', (prev, next, expected) => {
expect(makeRank(prev, next)).toBe(expected)
})
it.each([
[10, '0|hzzzxr:'],
[100, '0|hzzzdr:'],
[1000, '0|hzzttr:'],
[10000, '0|hzya9r:']
])('produces prev rank of reasonable length for %p generations', (count, expected) => {
let rank = '0|hzzzzz:'
for (let i = 0; i < count; i++) {
rank = makeRank(undefined, rank)
}
expect(rank).toBe(expected)
})
it.each([
[5, '0|zfqzzz:'],
[10, '0|zzd7vh:'],
[50, '0|zzzzzy:zzzi'],
[100, '0|zzzzzy:zzzzzzzzzzzv']
])('produces middle rank of reasonable length for %p generations', (count, expected) => {
let rank = '0|hzzzzz:'
for (let i = 0; i < count; i++) {
rank = makeRank(rank, '0|zzzzzz')
}
expect(rank).toBe(expected)
})
it.each([
[10, '0|i00027:'],
[100, '0|i000m7:'],
[1000, '0|i00667:'],
[10000, '0|i01pq7:'],
[100000, '0|i0h5a7:'],
[1000000, '0|i4rgu7:']
])('produces next rank of reasonable length for %p generations', (count, expected) => {
let rank = '0|hzzzzz:'
for (let i = 0; i < count; i++) {
rank = makeRank(rank, undefined)
}
expect(rank).toBe(expected)
})
})

View File

@ -34,9 +34,19 @@ export function genRanks (count: number): Rank[] {
/** @public */
export function makeRank (prev: Rank | undefined, next: Rank | undefined): Rank {
const prevLexoRank = prev === undefined ? LexoRank.min() : LexoRank.parse(prev)
const nextLexoRank = next === undefined ? LexoRank.max() : LexoRank.parse(next)
return prevLexoRank.equals(nextLexoRank)
? prevLexoRank.genNext().toString()
: prevLexoRank.between(nextLexoRank).toString()
if (prev !== undefined && next !== undefined) {
const prevLexoRank = LexoRank.parse(prev)
const nextLexoRank = LexoRank.parse(next)
return prevLexoRank.equals(nextLexoRank)
? prevLexoRank.genNext().toString()
: prevLexoRank.between(nextLexoRank).toString()
} else if (prev !== undefined) {
const prevLexoRank = LexoRank.parse(prev)
return prevLexoRank.genNext().toString()
} else if (next !== undefined) {
const nextLexoRank = LexoRank.parse(next)
return nextLexoRank.genPrev().toString()
} else {
return LexoRank.middle().toString()
}
}