diff --git a/backend/api/src/unresolve.ts b/backend/api/src/unresolve.ts index 0fc260f31b..7ea97e239c 100644 --- a/backend/api/src/unresolve.ts +++ b/backend/api/src/unresolve.ts @@ -258,15 +258,11 @@ const undoResolution = async ( } if (contract.mechanism === 'cpmm-multi-1' && !answerId) { // remove resolutionTime and resolverId from all answers in the contract - await pg.none( - `update answers - set data = data - 'resolutionTime' - 'resolverId' - where contract_id = $1`, - [contractId] - ) const newAnswers = await pg.map( `update answers - set resolution_time = null + set + resolution_time = null, + resolver_id = null where contract_id = $1 returning *`, [contractId], @@ -276,7 +272,11 @@ const undoResolution = async ( } else if (answerId) { const answer = await pg.one( `update answers - set data = data - '{resolution,resolutionTime,resolutionProbability,resolverId}'::text[] + set + resolution = null, + resolution_time = null, + resolution_probability = null, + resolver_id = null where id = $1 returning *`, [answerId], diff --git a/backend/shared/src/helpers/add-house-subsidy.ts b/backend/shared/src/helpers/add-house-subsidy.ts index 311db74d6f..87b1ac22ec 100644 --- a/backend/shared/src/helpers/add-house-subsidy.ts +++ b/backend/shared/src/helpers/add-house-subsidy.ts @@ -66,14 +66,14 @@ export const addHouseSubsidyToAnswer = async ( await tx.none( `update answers - set data = data || - jsonb_build_object('totalLiquidity', data->>'totalLiquidity'::numeric + $1) || - jsonb_build_object('subsidyPool', data->>'subsidyPool'::numeric + $1) + set + total_liquidity = total_liquidity + $1, + subsidy_pool = subsidy_pool + $1 where id = $2`, [amount, answerId] ) - await updateContract(pg, contractId, { + await updateContract(tx, contractId, { totalLiquidity: FieldVal.increment(amount), }) }) diff --git a/backend/shared/src/supabase/answers.ts b/backend/shared/src/supabase/answers.ts index 7b18c73936..ebf909a5ed 100644 --- a/backend/shared/src/supabase/answers.ts +++ b/backend/shared/src/supabase/answers.ts @@ -1,4 +1,4 @@ -import { SupabaseDirectClient } from 'shared/supabase/init' +import { type SupabaseDirectClient } from 'shared/supabase/init' import { convertAnswer } from 'common/supabase/contracts' import { groupBy } from 'lodash' import { Answer } from 'common/answer' @@ -6,16 +6,17 @@ import { bulkInsert, updateData, insert, - DataUpdate, bulkUpdateData, + bulkUpdate, + update, } from './utils' -import { randomString } from 'common/util/random' import { removeUndefinedProps } from 'common/util/object' -import { DataFor, millisToTs } from 'common/supabase/utils' +import { Row, millisToTs } from 'common/supabase/utils' import { broadcastNewAnswer, broadcastUpdatedAnswers, } from 'shared/websockets/helpers' +import { pick } from 'lodash' export const getAnswer = async (pg: SupabaseDirectClient, id: string) => { const row = await pg.oneOrNone(`select * from answers where id = $1`, [id]) @@ -72,9 +73,14 @@ export const bulkInsertAnswers = async ( export const updateAnswer = async ( pg: SupabaseDirectClient, answerId: string, - update: DataUpdate<'answers'> + data: Partial ) => { - const row = await updateData(pg, 'answers', 'id', { ...update, id: answerId }) + const row = await update( + pg, + 'answers', + 'id', + partialAnswerToRow({ ...data, id: answerId }) + ) const answer = convertAnswer(row) broadcastUpdatedAnswers(answer.contractId, [answer]) return answer @@ -83,21 +89,46 @@ export const updateAnswer = async ( export const updateAnswers = async ( pg: SupabaseDirectClient, contractId: string, - updates: (Partial> & { id: string })[] + updates: (Partial & { id: string })[] ) => { - await bulkUpdateData<'answers'>(pg, 'answers', updates) + await bulkUpdate(pg, 'answers', ['id'], updates.map(partialAnswerToRow)) + broadcastUpdatedAnswers(contractId, updates) } -export const answerToRow = (answer: Omit & { id?: string }) => ({ - id: 'id' in answer ? answer.id : randomString(), +const answerToRow = (answer: Omit & { id?: string }) => ({ + id: answer.id, index: answer.index, contract_id: answer.contractId, user_id: answer.userId, text: answer.text, + color: answer.color, pool_yes: answer.poolYes, pool_no: answer.poolNo, prob: answer.prob, - created_time: answer.createdTime ? millisToTs(answer.createdTime) : undefined, - data: JSON.stringify(removeUndefinedProps(answer)) + '::jsonb', + total_liquidity: answer.totalLiquidity, + subsidy_pool: answer.subsidyPool, + created_time: answer.createdTime + ? millisToTs(answer.createdTime) + '::timestamptz' + : undefined, + resolution: answer.resolution, + resolution_time: answer.resolutionTime + ? millisToTs(answer.resolutionTime) + '::timestamptz' + : undefined, + resolution_probability: answer.resolutionProbability, + resolver_id: answer.resolverId, + prob_change_day: answer.probChanges?.day, + prob_change_week: answer.probChanges?.week, + prob_change_month: answer.probChanges?.month, + data: + JSON.stringify( + removeUndefinedProps(pick(answer, ['isOther', 'loverUserId'])) + ) + '::jsonb', }) + +// does not convert isOther, loverUserId +const partialAnswerToRow = (answer: Partial) => { + const partial: any = removeUndefinedProps(answerToRow(answer as any)) + delete partial.data + return partial as Partial> +} diff --git a/backend/shared/src/supabase/utils.ts b/backend/shared/src/supabase/utils.ts index c3b6b846aa..f9ded4f310 100644 --- a/backend/shared/src/supabase/utils.ts +++ b/backend/shared/src/supabase/utils.ts @@ -1,5 +1,5 @@ import { sortBy } from 'lodash' -import { pgp, SupabaseDirectClient, SupabaseDirectClientTimeout } from './init' +import { pgp, SupabaseDirectClient } from './init' import { DataFor, Tables, TableName, Column, Row } from 'common/supabase/utils' export async function getIds( @@ -16,8 +16,8 @@ export async function insert< const columnNames = Object.keys(values) const cs = new pgp.helpers.ColumnSet(columnNames, { table }) const query = pgp.helpers.insert(values, cs) - // Hack to properly cast jsonb values. - const q = query.replace(/::jsonb'/g, "'::jsonb") + // Hack to properly cast values. + const q = query.replace(/::(\w*)'/g, "'::$1") return await db.one>(q + ` returning *`) } @@ -31,30 +31,58 @@ export async function bulkInsert< const columnNames = Object.keys(values[0]) const cs = new pgp.helpers.ColumnSet(columnNames, { table }) const query = pgp.helpers.insert(values, cs) - // Hack to properly cast jsonb values. - const q = query.replace(/::jsonb'/g, "'::jsonb") + // Hack to properly cast values. + const q = query.replace(/::(\w*)'/g, "'::$1") return await db.many>(q + ` returning *`) } +export async function update< + T extends TableName, + ColumnValues extends Tables[T]['Update'] +>( + db: SupabaseDirectClient, + table: T, + idField: Column, + values: ColumnValues +) { + const columnNames = Object.keys(values) + const cs = new pgp.helpers.ColumnSet(columnNames, { table }) + if (!(idField in values)) { + throw new Error(`missing ${idField} in values for ${columnNames}`) + } + const clause = pgp.as.format( + `${idField} = $1`, + values[idField as keyof ColumnValues] + ) + const query = pgp.helpers.update(values, cs) + ` WHERE ${clause}` + // Hack to properly cast values. + const q = query.replace(/::(\w*)'/g, "'::$1") + return await db.one>(q + ` returning *`) +} + export async function bulkUpdate< T extends TableName, - ColumnValues extends Tables[T]['Update'], - Row extends Tables[T]['Row'] + ColumnValues extends Tables[T]['Update'] >( - db: SupabaseDirectClientTimeout, + db: SupabaseDirectClient, table: T, - idFields: (string & keyof Row)[], + idFields: Column[], values: ColumnValues[], - timeoutMs?: number + timeoutMs?: number // only works with SupabaseDirectClientTimeout ) { if (values.length) { const columnNames = Object.keys(values[0]) const cs = new pgp.helpers.ColumnSet(columnNames, { table }) const clause = idFields.map((f) => `v.${f} = t.${f}`).join(' and ') const query = pgp.helpers.update(values, cs) + ` WHERE ${clause}` - // Hack to properly cast jsonb values. - const q = query.replace(/::jsonb'/g, "'::jsonb") + // Hack to properly cast values. + const q = query.replace(/::(\w*)'/g, "'::$1") if (timeoutMs) { + if (!('timeout' in db)) { + throw new Error( + 'bulkUpdate with timeoutMs is not supported in a transaction' + ) + } await db.timeout(timeoutMs, (t) => t.none(q)) } else { await db.none(q) @@ -78,8 +106,8 @@ export async function bulkUpsert< const columnNames = Object.keys(values[0]) const cs = new pgp.helpers.ColumnSet(columnNames, { table }) const baseQuery = pgp.helpers.insert(values, cs) - // Hack to properly cast jsonb values. - const baseQueryReplaced = baseQuery.replace(/::jsonb'/g, "'::jsonb") + // Hack to properly cast values. + const baseQueryReplaced = baseQuery.replace(/::(\w*)'/g, "'::$1") const primaryKey = Array.isArray(idField) ? idField.join(', ') : idField const upsertAssigns = cs.assignColumns({ from: 'excluded', skip: idField }) diff --git a/backend/supabase/answers.sql b/backend/supabase/answers.sql index a441433bbd..1c6879ffa3 100644 --- a/backend/supabase/answers.sql +++ b/backend/supabase/answers.sql @@ -24,42 +24,6 @@ create table if not exists resolver_id text ); --- Triggers -create trigger answers_populate before insert -or -update on public.answers for each row -execute function answers_populate_cols (); - --- Functions -create -or replace function public.answers_populate_cols () returns trigger language plpgsql as $function$ -begin - if new.data is not null then - new.index := ((new.data) -> 'index')::int; - new.contract_id := (new.data) ->> 'contractId'; - new.user_id := (new.data) ->> 'userId'; - new.text := (new.data) ->> 'text'; - new.created_time := - case when new.data ? 'createdTime' then millis_to_ts(((new.data) ->> 'createdTime')::bigint) else null end; - new.color := (new.data) ->> 'color'; - new.pool_yes := ((new.data) ->> 'poolYes')::numeric; - new.pool_no := ((new.data) ->> 'poolNo')::numeric; - new.prob := ((new.data) ->> 'prob')::numeric; - new.total_liquidity := ((new.data) ->> 'totalLiquidity')::numeric; - new.subsidy_pool := ((new.data) ->> 'subsidyPool')::numeric; - new.prob_change_day := ((new.data) -> 'probChanges'->>'day')::numeric; - new.prob_change_week := ((new.data) -> 'probChanges'->>'week')::numeric; - new.prob_change_month := ((new.data) -> 'probChanges'->>'month')::numeric; - new.resolution_time := - case when new.data ? 'resolutionTime' then millis_to_ts(((new.data) ->> 'resolutionTime')::bigint) end; - new.resolution_probability := ((new.data) -> 'resolutionProbability')::numeric; - new.resolution := (new.data) ->> 'resolution'; - new.resolver_id := (new.data) ->> 'resolverId'; - end if; - return new; -end -$function$; - -- Policies alter table answers enable row level security;