Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a9ed276
Add typed query to get groups with balances
FriesischScott Jun 5, 2025
beb89da
Return groups without expenses
FriesischScott Jun 7, 2025
df6a7ad
Order groups descending by newest
FriesischScott Jun 8, 2025
ab4c7ee
Compute blances for group overview in db
FriesischScott Jun 8, 2025
56af112
Compute balance list in db
FriesischScott Jun 8, 2025
de25193
Merge branch 'main' into sql-balances
FriesischScott Jun 9, 2025
24d1b99
Include group id in all balances
FriesischScott Jun 9, 2025
70e58c6
Fix linting
FriesischScott Jun 9, 2025
a40ca8c
Move prisma client to src/prisma/client
FriesischScott Jun 9, 2025
cee753c
Fix prettier warning
FriesischScott Jun 9, 2025
23fece2
Use path alias to move prisma client
FriesischScott Jun 9, 2025
b3002be
gitignore prisma client files
FriesischScott Jun 9, 2025
20fb843
Fix sql imports
FriesischScott Jun 9, 2025
44a3bdd
Set prisma client output
FriesischScott Jun 9, 2025
a36344d
Run lint --fix
FriesischScott Jun 9, 2025
cd32056
Fix type issues
FriesischScott Jun 9, 2025
c619e26
Fix prettier warnings
FriesischScott Jun 9, 2025
646c2e0
Add postgresql service container
FriesischScott Jun 9, 2025
7de67eb
Run migrations before generating sql types
FriesischScott Jun 9, 2025
bb996e9
Merge branch 'main' into sql-balances
FriesischScott Jun 9, 2025
fa22c43
Remove sql types
FriesischScott Jun 9, 2025
1ff2229
Fix workflow file
FriesischScott Jun 9, 2025
6ff9a98
Fix bigint conversions
FriesischScott Jun 9, 2025
0e5b29c
Fix lint
FriesischScott Jun 9, 2025
bec3f4d
Seed db for stress testing
FriesischScott Jun 10, 2025
af4c06d
Fix prettier warnings
FriesischScott Jun 10, 2025
2684a71
Exclude seed.ts
FriesischScott Jun 10, 2025
4b02518
Don't set prisma output folder
FriesischScott Jun 10, 2025
c26dc4a
Exclude deleted expenses from group balance
FriesischScott Jun 10, 2025
06eea78
Update seed
FriesischScott Jun 18, 2025
bca6c9d
Add userid index to ExpenseParticipant
FriesischScott Jun 18, 2025
4dc3d38
Merge branch 'main' into sql-balances
FriesischScott Jun 18, 2025
1f330a5
Fix prettier warnings
FriesischScott Jun 18, 2025
3270d55
Merge remote-tracking branch 'upstream/main' into sql-balances
FriesischScott Oct 9, 2025
510df2e
Format SQL queries
FriesischScott Oct 9, 2025
391f161
Switch service container to ossapps/postgres
FriesischScott Oct 11, 2025
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
18 changes: 18 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ jobs:
runs-on: ubuntu-latest
env:
SKIP_ENV_VALIDATION: true
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres"
services:
postgres:
image: ossapps/postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- name: Checkout
Expand All @@ -27,6 +40,11 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: generate typedSQL types
run: |
pnpm db:dev
pnpm generate --sql

- name: Apply prettier formatting
run: pnpm prettier --check .

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal

/src/prisma/client
# next.js
/.next/
/out/
Expand Down
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@
}
}
],
"ignorePatterns": ["prisma/seed.ts", "src/components/ui/*"]
"ignorePatterns": ["prisma/seed.ts", "src/components/ui/*", "prisma/largeSeed.ts"]
}
127 changes: 127 additions & 0 deletions prisma/largeSeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { PrismaClient, SplitType, User, Group } from '@prisma/client';
import { randomInt } from 'crypto';

const prisma = new PrismaClient();

async function createUsers() {
const data = Array.from({ length: 1000 }, (value, index) => index).map((i) => {
return {
name: `user${i}`,
email: `user${i}@example.com`,
currency: 'USD',
};
});
const users = await prisma.user.createMany({
data: data,
});
return prisma.user.findMany({
orderBy: {
id: 'asc',
},
});
}

async function createGroups(users: User[]) {
if (users.length) {
for (let i = 0; i < 100; i++) {
const s = i * 10;
const e = (i + 1) * 10 - 1;

// group of 10
const group = await prisma.group.create({
data: {
name: `Group_10_${i}`,
publicId: `Group-10-${i}`,
defaultCurrency: 'EUR',
userId: users[s].id,
},
});

await prisma.groupUser.createMany({
data: users.slice(s, e).map((u) => {
return {
groupId: group.id,
userId: u.id,
};
}),
});
}
}

const group = await prisma.group.create({
data: {
name: `Group_30`,
publicId: `Group-30`,
defaultCurrency: 'EUR',
userId: users[0].id,
},
});

await prisma.groupUser.createMany({
data: users.slice(0, 29).map((u) => {
return {
groupId: group.id,
userId: u.id,
};
}),
});

return prisma.group.findMany({
include: {
groupUsers: true,
},
});
}

async function createExpenses(groups: Group[]) {
const currencies = ['EUR', 'USD'];
for (const gid in groups) {
const group = groups[gid];
for (let i = 0; i < 10000; i++) {
const c = randomInt(0, 2);
const amount = BigInt(randomInt(1000, 10000));

const expense = await prisma.expense.create({
data: {
name: `Expense Group ${group.id} ${i}`,
paidBy: group.groupUsers[0].userId,
addedBy: group.groupUsers[0].userId,
category: 'general',
currency: currencies[c],
amount: amount,
groupId: group.id,
splitType: SplitType.EQUAL,
},
});

await prisma.expenseParticipant.createMany({
data: [
{ expenseId: expense.id, userId: group.groupUsers[0].userId, amount: amount },
...group.groupUsers.slice(1).map((u) => {
return {
expenseId: expense.id,
userId: u.userId,
amount: -amount / BigInt(group.groupUsers.length),
};
}),
],
});
}
}
}

async function main() {
const users = await createUsers();
const groups = await createGroups(users);
await createExpenses(groups);
console.log('Seeded db with users, groups and expenses');
}

main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(() => {
prisma.$disconnect().catch(console.log);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX "ExpenseParticipant_userId_idx" ON "ExpenseParticipant" USING HASH ("userId");
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
previewFeatures = ["relationJoins", "typedSQL"]
}

datasource db {
Expand Down Expand Up @@ -208,6 +208,7 @@ model ExpenseParticipant {
expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade)

@@id([expenseId, userId])
@@index([userId], type: Hash)
@@schema("public")
}

Expand Down
21 changes: 21 additions & 0 deletions prisma/sql/getAllBalancesForGroup.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- @param {Int} $1:id of the group
SELECT
"groupId",
"userId" AS "borrowedBy",
"paidBy",
"currency",
CAST(Coalesce(-1 * sum("ExpenseParticipant".amount), 0) AS BIGINT) AS amount
FROM
"Expense"
JOIN "ExpenseParticipant" ON "ExpenseParticipant"."expenseId" = "Expense".id
WHERE
"groupId" = $1
AND "userId" != "paidBy"
AND "Expense"."deletedAt" IS NULL
GROUP BY
"userId",
"paidBy",
"currency",
"groupId"
ORDER BY
"currency"
24 changes: 24 additions & 0 deletions prisma/sql/getGroupsWithBalances.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- @param {Int} $1:id of the user
SELECT
"Group"."id",
"Group".name,
CAST(Coalesce(sum("ExpenseParticipant".amount), 0) AS BIGINT) AS balance,
Coalesce("Expense".currency, "Group"."defaultCurrency") AS currency,
"Group"."archivedAt"
FROM
"GroupUser"
JOIN "Group" ON "GroupUser"."groupId" = "Group".id
LEFT JOIN "Expense" ON "Expense"."groupId" = "Group".id
LEFT JOIN "ExpenseParticipant" ON "Expense".id = "ExpenseParticipant"."expenseId"
WHERE
"GroupUser"."userId" = $1
AND "deletedAt" IS NULL
AND ("ExpenseParticipant"."userId" = $1
OR "Expense".id IS NULL)
GROUP BY
"Group".id,
"Group".name,
"Expense".currency
ORDER BY
"Group"."createdAt" DESC,
balance DESC
29 changes: 17 additions & 12 deletions src/components/Expense/BalanceList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { GroupBalance, User } from '@prisma/client';
import type { User } from '@prisma/client';
import { type getAllBalancesForGroup } from '@prisma/client/sql';
import { Info } from 'lucide-react';

import { clsx } from 'clsx';
import { type ComponentProps, Fragment, useCallback, useMemo } from 'react';
import { EntityAvatar } from '~/components/ui/avatar';
Expand All @@ -19,9 +22,10 @@ interface UserWithBalance {
}

export const BalanceList: React.FC<{
groupBalances?: GroupBalance[];
users?: User[];
}> = ({ groupBalances = [], users = [] }) => {
groupId: number;
groupBalances: getAllBalancesForGroup.Result[];
users: User[];
}> = ({ groupId, groupBalances, users }) => {
const { displayName, t } = useTranslationWithUtils();
const userQuery = api.user.me.useQuery();

Expand All @@ -34,16 +38,17 @@ export const BalanceList: React.FC<{
return acc;
}, {});
groupBalances
.filter(({ amount }) => 0 < BigMath.abs(amount))
.filter(({ amount }) => amount != null && BigMath.abs(amount) > 0)
.forEach((balance) => {
if (!res[balance.userId]!.balances[balance.firendId]) {
res[balance.userId]!.balances[balance.firendId] = {};
if (!res[balance.paidBy]!.balances[balance.borrowedBy]) {
res[balance.paidBy]!.balances[balance.borrowedBy] = {};
}
const friendBalance = res[balance.userId]!.balances[balance.firendId]!;
friendBalance[balance.currency] = (friendBalance[balance.currency] ?? 0n) + balance.amount;
const friendBalance = res[balance.paidBy]!.balances[balance.borrowedBy]!;
friendBalance[balance.currency] =
(friendBalance[balance.currency] ?? 0n) + (balance.amount ?? 0n);

res[balance.userId]!.total[balance.currency] =
(res[balance.userId]!.total[balance.currency] ?? 0n) + balance.amount;
res[balance.paidBy]!.total[balance.currency] =
(res[balance.paidBy]!.total[balance.currency] ?? 0n) + (balance.amount ?? 0n);
});

return res;
Expand Down Expand Up @@ -164,7 +169,7 @@ export const BalanceList: React.FC<{
user={user}
amount={amount}
currency={currency}
groupId={groupBalances[0]!.groupId}
groupId={groupId}
>
<Button size="icon" variant="secondary" className="size-8">
<SETTLEUP_ICON className="size-4" />
Expand Down
18 changes: 11 additions & 7 deletions src/components/group/GroupMyBalance.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type GroupBalance, type User } from '@prisma/client';
import { type User } from '@prisma/client';
import { type getAllBalancesForGroup } from '@prisma/client/sql';
import React from 'react';
import { useTranslation } from 'next-i18next';

import { BigMath, toUIString } from '~/utils/numbers';

interface GroupMyBalanceProps {
userId: number;
groupBalances?: GroupBalance[];
users?: User[];
groupBalances: getAllBalancesForGroup.Result[] | undefined;
users: User[] | undefined;
}

const GroupMyBalance: React.FC<GroupMyBalanceProps> = ({
Expand All @@ -26,10 +27,13 @@ const GroupMyBalance: React.FC<GroupMyBalanceProps> = ({

const friendBalances = groupBalances.reduce(
(acc, balance) => {
if (balance.userId === userId && 0 < BigMath.abs(balance.amount)) {
acc[balance.firendId] ??= {};
const friendBalance = acc[balance.firendId]!;
friendBalance[balance.currency] = (friendBalance[balance.currency] ?? 0n) + balance.amount;
if (balance.paidBy === userId && balance.amount != null && BigMath.abs(balance.amount) > 0) {
if (!acc[balance.borrowedBy]) {
acc[balance.borrowedBy] = {};
}
const friendBalance = acc[balance.borrowedBy]!;
friendBalance[balance.currency] =
(friendBalance[balance.currency] ?? 0n) + (balance.amount ?? 0n);
}
return acc;
},
Expand Down
Loading
Loading