Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(rethinkdb): RetroReflection: Phase 2 #9834

Merged
merged 25 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bfa5c21
chore: read ReflectionGroups from PG
mattkrick May 29, 2024
7ac086e
Merge branch 'master' into chore/retrogroups3
mattkrick May 29, 2024
9844960
Merge branch 'master' into chore/retrogroups3
mattkrick May 30, 2024
62e123c
chore: add PG RetroReflection table
mattkrick May 30, 2024
c38f86c
chore: write to PG RetroReflection table
mattkrick Jun 3, 2024
d318560
fixup: inserts and updates
mattkrick Jun 4, 2024
16843dc
Merge branch 'master' into retroReflection-phase1e
mattkrick Jun 4, 2024
abb3bdf
fix: move to record literal types
mattkrick Jun 4, 2024
9911448
fix types
mattkrick Jun 4, 2024
af03b31
chore: migrate reflections to PG
mattkrick Jun 6, 2024
2a3c0ca
Merge branch 'master' into retroReflection-phase1e
mattkrick Jun 6, 2024
8dfa870
fix: remove hard delete of inactive groups
mattkrick Jun 6, 2024
3b48d22
Merge branch 'retroReflection-phase1e' into chore/retroReflection-phase2
mattkrick Jun 6, 2024
24da04b
fixup: remove unused var
mattkrick Jun 6, 2024
c60e502
Merge branch 'retroReflection-phase1e' into chore/retroReflection-phase2
mattkrick Jun 6, 2024
0d2c7d7
handle escape chars and commas
mattkrick Jun 6, 2024
9ef9346
chore: begin building equality checker
mattkrick Jun 6, 2024
76aeff9
fixup: account for lots of formatting in content
mattkrick Jun 7, 2024
0830c8e
Merge branch 'retroReflection-phase1e' into chore/retroReflection-phase2
mattkrick Jun 24, 2024
a12b08a
fix: rename extra spaces for conversion from plaintext to content
mattkrick Jun 24, 2024
a732657
fix: rename Reactji composite type attributes
mattkrick Jun 24, 2024
954503d
fix: constructor prop in lookup table
mattkrick Jun 25, 2024
c7775ad
handle reactji migration
mattkrick Jun 25, 2024
a6fd0d3
Merge branch 'master' into chore/retroReflection-phase2
mattkrick Jun 25, 2024
50ca56b
fix: migration order rename
mattkrick Jun 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/client/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
- Does the variable come from the GraphQL schema? If so, import it from a file in the __generated__ folder
- Is the variable a string? Create a string union & pass in a plain string to get type safety
*/
import {TaskStatusEnum} from '~/__generated__/UpdateTaskMutation.graphql'
import {ReadableReasonToDowngradeEnum} from '../../server/graphql/types/ReasonToDowngrade'
import {ReasonToDowngradeEnum} from '../__generated__/DowngradeToStarterMutation.graphql'
import {TimelineEventEnum} from '../__generated__/MyDashboardTimelineQuery.graphql'
import {TaskStatusEnum} from '../__generated__/UpdateTaskMutation.graphql'
import {Threshold} from '../types/constEnums'

/* Meeting Misc. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const sanitizeAnalyzedEntitiesResponse = (response: GoogleAnalyzedEntities | nul
if (!response) return null
const {entities} = response
if (!Array.isArray(entities)) return null
const validEntities = {} as {[lowerCaseName: string]: number}
// very important to Object.create(null) since validEntities['constructor'] would return a function!
const validEntities = Object.create(null) as {[lowerCaseName: string]: number}
entities.forEach((entity) => {
const {name, salience} = entity
if (!name || !salience) return
Expand Down
58 changes: 39 additions & 19 deletions packages/server/graphql/private/mutations/checkRethinkPgEquality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import getKysely from '../../../postgres/getKysely'
import {checkRowCount, checkTableEq} from '../../../postgres/utils/checkEqBase'
import {
compareDateAlmostEqual,
compareOptionalPlaintextContent,
compareRValOptionalPluckedArray,
compareRValUndefinedAsEmptyArray,
compareRValUndefinedAsNull,
compareRValUndefinedAsNullAndTruncateRVal,
compareRealNumber,
defaultEqFn
} from '../../../postgres/utils/rethinkEqualityFns'
import {MutationResolvers} from '../resolverTypes'
Expand All @@ -27,43 +32,58 @@ const handleResult = async (

const checkRethinkPgEquality: MutationResolvers['checkRethinkPgEquality'] = async (
_source,
{tableName, writeToFile}
{tableName, writeToFile, maxErrors}
) => {
const r = await getRethink()

if (tableName === 'RetroReflectionGroup') {
if (tableName === 'RetroReflection') {
const rowCountResult = await checkRowCount(tableName)
const rethinkQuery = (updatedAt: Date, id: string | number) => {
return r
.table('RetroReflectionGroup' as any)
.table('RetroReflection')
.between([updatedAt, id], [r.maxval, r.maxval], {
index: 'updatedAtId',
leftBound: 'open',
rightBound: 'closed'
})
.orderBy({index: 'updatedAtId'}) as any
}
const pgQuery = (ids: string[]) => {
const pgQuery = async (ids: string[]) => {
return getKysely()
.selectFrom('RetroReflectionGroup')
.selectFrom('RetroReflection')
.selectAll()
.select(({fn}) => [
fn('to_json', ['entities']).as('entities'),
fn('to_json', ['reactjis']).as('reactjis')
])
.where('id', 'in', ids)
.execute()
}
const errors = await checkTableEq(rethinkQuery, pgQuery, {
id: defaultEqFn,
createdAt: defaultEqFn,
updatedAt: compareDateAlmostEqual,
isActive: defaultEqFn,
meetingId: defaultEqFn,
promptId: defaultEqFn,
sortOrder: defaultEqFn,
voterIds: defaultEqFn,
smartTitle: compareRValUndefinedAsNullAndTruncateRVal(255),
title: compareRValUndefinedAsNullAndTruncateRVal(255),
summary: compareRValUndefinedAsNullAndTruncateRVal(2000),
discussionPromptQuestion: compareRValUndefinedAsNullAndTruncateRVal(2000)
})
const errors = await checkTableEq(
rethinkQuery,
pgQuery,
{
id: defaultEqFn,
createdAt: defaultEqFn,
updatedAt: compareDateAlmostEqual,
isActive: defaultEqFn,
meetingId: defaultEqFn,
promptId: defaultEqFn,
creatorId: compareRValUndefinedAsNull,
sortOrder: defaultEqFn,
reflectionGroupId: defaultEqFn,
content: compareRValUndefinedAsNullAndTruncateRVal(2000, 0.19),
plaintextContent: compareOptionalPlaintextContent,
entities: compareRValOptionalPluckedArray({
name: defaultEqFn,
salience: compareRealNumber,
lemma: compareRValUndefinedAsNull
}),
reactjis: compareRValUndefinedAsEmptyArray,
sentimentScore: compareRValUndefinedAsNull
},
maxErrors
)
return handleResult(tableName, rowCountResult, errors, writeToFile)
}
return 'Table not found'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function up() {

await sql`
ALTER TABLE "User"
ADD COLUMN "favoriteTemplateIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
ADD COLUMN IF NOT EXISTS "favoriteTemplateIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
`.execute(pg)
}

Expand Down
16 changes: 10 additions & 6 deletions packages/server/postgres/migrations/1716914102795_removeKudos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ export async function up() {
})
})

await pg.schema.dropTable('Kudos').execute()
await pg.schema
.alterTable('Team')
.dropColumn('giveKudosWithEmoji')
.dropColumn('kudosEmoji')
.execute()
await pg.schema.dropTable('Kudos').ifExists().execute()
try {
await pg.schema
.alterTable('Team')
.dropColumn('giveKudosWithEmoji')
.dropColumn('kudosEmoji')
.execute()
} catch {
// noop
}
}

export async function down() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function up() {
DO $$
BEGIN
ALTER TABLE "IntegrationProvider"
DROP CONSTRAINT global_provider_must_be_oauth2;
DROP CONSTRAINT IF EXISTS global_provider_must_be_oauth2;
END $$;
`)
await client.end()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import {ContentState, convertToRaw} from 'draft-js'
import {Kysely, PostgresDialect, sql} from 'kysely'
import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString'
import {r} from 'rethinkdb-ts'
import connectRethinkDB from '../../database/connectRethinkDB'
import getPg from '../getPg'

const convertTextToRaw = (text: string) => {
// plaintextContent can have a bunch of linebreaks like \n which get converted into new blocks.
// New blocks take up a BUNCH of space, so we'd rather preserve as much plaintextContent as possible.
const spaceFreeText = text
.split(/\s/)
.filter((s) => s.length)
.join(' ')
const contentState = ContentState.createFromText(spaceFreeText)
const raw = convertToRaw(contentState)
return JSON.stringify(raw)
}

export async function up() {
await connectRethinkDB()
const pg = new Kysely<any>({
dialect: new PostgresDialect({
pool: getPg()
})
})
try {
await r
.table('RetroReflection')
.indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')])
.run()
await r.table('RetroReflection').indexWait().run()
} catch {
// index already exists
}

const MAX_PG_PARAMS = 65545

const PG_COLS = [
'id',
'createdAt',
'updatedAt',
'isActive',
'meetingId',
'promptId',
'sortOrder',
'creatorId',
'content',
'plaintextContent',
'entities',
'sentimentScore',
'reactjis',
'reflectionGroupId'
] as const
type RetroReflection = {
[K in (typeof PG_COLS)[number]]: any
}
const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length)

const capContent = (content: string, plaintextContent: string) => {
let nextPlaintextContent = plaintextContent || extractTextFromDraftString(content)
// if they got out of hand with formatting, extract the text & convert it back
let nextContent = content.length <= 2000 ? content : convertTextToRaw(nextPlaintextContent)
while (nextContent.length > 2000 || nextPlaintextContent.length > 2000) {
const maxLen = Math.max(nextContent.length, nextPlaintextContent.length)
const overage = maxLen - 2000
const stopIdx = nextPlaintextContent.length - overage - 1
nextPlaintextContent = nextPlaintextContent.slice(0, stopIdx)
nextContent = convertTextToRaw(nextPlaintextContent)
}
return {content: nextContent, plaintextContent: nextPlaintextContent}
}

let curUpdatedAt = r.minval
let curId = r.minval

for (let i = 0; i < 1e6; i++) {
console.log('inserting row', i * BATCH_SIZE, curUpdatedAt, curId)
const rawRowsToInsert = (await r
.table('RetroReflection')
.between([curUpdatedAt, curId], [r.maxval, r.maxval], {
index: 'updatedAtId',
leftBound: 'open',
rightBound: 'closed'
})
.orderBy({index: 'updatedAtId'})
.limit(BATCH_SIZE)
.pluck(...PG_COLS)
.run()) as RetroReflection[]

const rowsToInsert = rawRowsToInsert.map((row) => {
const nonzeroEntities = row.entities?.length > 0 ? row.entities : undefined
const normalizedEntities = nonzeroEntities?.map((e: any) => ({
...e,
salience: typeof e.salience === 'number' ? e.salience : 0
}))
return {
...row,
...capContent(row.content, row.plaintextContent),
reactjis: row.reactjis?.map((r: any) => `(${r.id},${r.userId})`),
entities: normalizedEntities
? sql`(select array_agg((name, salience, lemma)::"GoogleAnalyzedEntity") from json_populate_recordset(null::"GoogleAnalyzedEntity", ${JSON.stringify(normalizedEntities)}))`
: undefined
}
})
if (rowsToInsert.length === 0) break
const lastRow = rowsToInsert[rowsToInsert.length - 1]
curUpdatedAt = lastRow.updatedAt
curId = lastRow.id
let isFailure = false
// NOTE: This migration inserts row-by-row because there are so many referential integrity errors
// Do not copy this migration logic for future migrations, it is slow!
const insertSingleRow = async (row: RetroReflection) => {
if (isFailure) return
try {
await pg
.insertInto('RetroReflection')
.values(row)
.onConflict((oc) => oc.doNothing())
.execute()
} catch (e) {
if (e.constraint === 'fk_reflectionGroupId') {
await pg
.insertInto('RetroReflectionGroup')
.values({
id: row.reflectionGroupId,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
isActive: row.isActive,
meetingId: row.meetingId,
promptId: row.promptId
})
// multiple reflections may be trying to create the same group
.onConflict((oc) => oc.doNothing())
.execute()
await insertSingleRow(row)
} else if (e.constraint === 'fk_creatorId') {
await r.table('RetroReflection').get(row.id).update({creatorId: null}).run()
await insertSingleRow({...row, creatorId: null})
} else {
isFailure = true
console.log(e, row)
}
}
}
await Promise.all(rowsToInsert.map(insertSingleRow))
if (isFailure) {
throw 'Failed batch'
}
}
}

export async function down() {
await connectRethinkDB()
try {
await r.table('RetroReflection').indexDrop('updatedAtId').run()
} catch {
// index already dropped
}
const pg = new Kysely<any>({
dialect: new PostgresDialect({
pool: getPg()
})
})
await pg.deleteFrom('RetroReflection').execute()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Client} from 'pg'
import getPgConfig from '../getPgConfig'

export async function up() {
const client = new Client(getPgConfig())
await client.connect()

await client.query(`
ALTER TYPE "Reactji" RENAME ATTRIBUTE "userid" to "userId";
ALTER TYPE "Reactji" RENAME ATTRIBUTE "shortname" to "id";
`)
await client.end()
}

export async function down() {
const client = new Client(getPgConfig())
await client.connect()
await client.query(`` /* Do undo magic */)
await client.end()
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ export const mapToTeamPromptResponse = (
return results.map((teamPromptResponse: any) => {
return {
...teamPromptResponse,
id: TeamPromptResponseId.join(teamPromptResponse.id),
reactjis: teamPromptResponse.reactjis.map(
(reactji: {shortname: string; userid: string}) =>
new Reactji({id: reactji.shortname, userId: reactji.userid})
)
id: TeamPromptResponseId.join(teamPromptResponse.id)
} as TeamPromptResponse
})
}
Expand Down
3 changes: 2 additions & 1 deletion packages/server/postgres/utils/checkEqBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ export async function checkTableEq(
rethinkQuery: (updatedAt: Date, id: string | number) => RSelection,
pgQuery: (ids: string[]) => Promise<PGDoc[] | null>,
equalityMap: Record<string, (a: unknown, b: unknown) => boolean>,
maxErrors = 10
maxErrors: number | null | undefined
) {
maxErrors = maxErrors || 10
const batchSize = 3000
const errors = [] as Diff[]
const propsToCheck = Object.keys(equalityMap)
Expand Down
Loading
Loading