Skip to content

Commit c662a31

Browse files
committed
db updates
1 parent 51d1b95 commit c662a31

File tree

4 files changed

+361
-4
lines changed

4 files changed

+361
-4
lines changed

packages/db/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import postgres from 'postgres'
33
import * as schema from './schema'
44

55
export * from './schema'
6+
export * from './triggers'
67

78
const connectionString = process.env.DATABASE_URL!
89
if (!connectionString) {

packages/db/migrations/0140_awesome_killer_shrike.sql

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ CREATE TABLE IF NOT EXISTS "user_table_definitions" (
88
"row_count" integer DEFAULT 0 NOT NULL,
99
"created_by" text NOT NULL,
1010
"created_at" timestamp DEFAULT now() NOT NULL,
11-
"updated_at" timestamp DEFAULT now() NOT NULL,
12-
"deleted_at" timestamp
11+
"updated_at" timestamp DEFAULT now() NOT NULL
1312
);
1413
--> statement-breakpoint
1514
CREATE TABLE IF NOT EXISTS "user_table_rows" (
@@ -62,12 +61,46 @@ CREATE INDEX IF NOT EXISTS "user_table_def_workspace_id_idx" ON "user_table_defi
6261
--> statement-breakpoint
6362
CREATE UNIQUE INDEX IF NOT EXISTS "user_table_def_workspace_name_unique" ON "user_table_definitions" USING btree ("workspace_id","name");
6463
--> statement-breakpoint
65-
CREATE INDEX IF NOT EXISTS "user_table_def_deleted_at_idx" ON "user_table_definitions" USING btree ("deleted_at");
66-
--> statement-breakpoint
6764
CREATE INDEX IF NOT EXISTS "user_table_rows_table_id_idx" ON "user_table_rows" USING btree ("table_id");
6865
--> statement-breakpoint
6966
CREATE INDEX IF NOT EXISTS "user_table_rows_workspace_id_idx" ON "user_table_rows" USING btree ("workspace_id");
7067
--> statement-breakpoint
7168
CREATE INDEX IF NOT EXISTS "user_table_rows_data_gin_idx" ON "user_table_rows" USING gin ("data");
7269
--> statement-breakpoint
7370
CREATE INDEX IF NOT EXISTS "user_table_rows_workspace_table_idx" ON "user_table_rows" USING btree ("workspace_id","table_id");
71+
--> statement-breakpoint
72+
-- Create function to increment row count on insert
73+
CREATE OR REPLACE FUNCTION increment_table_row_count()
74+
RETURNS TRIGGER AS $$
75+
BEGIN
76+
UPDATE user_table_definitions
77+
SET row_count = row_count + 1
78+
WHERE id = NEW.table_id;
79+
RETURN NEW;
80+
END;
81+
$$ LANGUAGE plpgsql;
82+
--> statement-breakpoint
83+
-- Create trigger for insert (drop first if exists to ensure clean state)
84+
DROP TRIGGER IF EXISTS trg_increment_row_count ON user_table_rows;
85+
CREATE TRIGGER trg_increment_row_count
86+
AFTER INSERT ON user_table_rows
87+
FOR EACH ROW
88+
EXECUTE FUNCTION increment_table_row_count();
89+
--> statement-breakpoint
90+
-- Create function to decrement row count on delete
91+
CREATE OR REPLACE FUNCTION decrement_table_row_count()
92+
RETURNS TRIGGER AS $$
93+
BEGIN
94+
UPDATE user_table_definitions
95+
SET row_count = GREATEST(0, row_count - 1)
96+
WHERE id = OLD.table_id;
97+
RETURN OLD;
98+
END;
99+
$$ LANGUAGE plpgsql;
100+
--> statement-breakpoint
101+
-- Create trigger for delete (drop first if exists to ensure clean state)
102+
DROP TRIGGER IF EXISTS trg_decrement_row_count ON user_table_rows;
103+
CREATE TRIGGER trg_decrement_row_count
104+
AFTER DELETE ON user_table_rows
105+
FOR EACH ROW
106+
EXECUTE FUNCTION decrement_table_row_count();
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* Seed script to populate the stress_test_users table.
3+
*
4+
* Usage:
5+
* cd packages/db && bun run scripts/seed-stress-test-users.ts
6+
*/
7+
8+
import { eq } from 'drizzle-orm'
9+
import { db, userTableDefinitions, userTableRows } from '../index'
10+
11+
const WORKSPACE_ID = '098d71e1-6a36-47e3-874d-818faee0bfe8'
12+
const TABLE_NAME = 'stress_test_users'
13+
const NUM_ROWS = 100000
14+
15+
interface UserRow {
16+
name: string
17+
email: string
18+
age: number
19+
department: string
20+
salary: number
21+
active: boolean
22+
hire_date: string
23+
country: string
24+
}
25+
26+
const departments = [
27+
'Engineering',
28+
'Sales',
29+
'Marketing',
30+
'HR',
31+
'Finance',
32+
'Operations',
33+
'Legal',
34+
'Product',
35+
]
36+
const countries = [
37+
'USA',
38+
'UK',
39+
'Germany',
40+
'France',
41+
'Canada',
42+
'Australia',
43+
'Japan',
44+
'India',
45+
'Brazil',
46+
'Singapore',
47+
]
48+
const firstNames = [
49+
'James',
50+
'Mary',
51+
'John',
52+
'Patricia',
53+
'Robert',
54+
'Jennifer',
55+
'Michael',
56+
'Linda',
57+
'William',
58+
'Elizabeth',
59+
'David',
60+
'Barbara',
61+
'Richard',
62+
'Susan',
63+
'Joseph',
64+
'Jessica',
65+
'Thomas',
66+
'Sarah',
67+
'Charles',
68+
'Karen',
69+
]
70+
const lastNames = [
71+
'Smith',
72+
'Johnson',
73+
'Williams',
74+
'Brown',
75+
'Jones',
76+
'Garcia',
77+
'Miller',
78+
'Davis',
79+
'Rodriguez',
80+
'Martinez',
81+
'Hernandez',
82+
'Lopez',
83+
'Gonzalez',
84+
'Wilson',
85+
'Anderson',
86+
'Thomas',
87+
'Taylor',
88+
'Moore',
89+
'Jackson',
90+
'Martin',
91+
]
92+
93+
function randomItem<T>(arr: T[]): T {
94+
return arr[Math.floor(Math.random() * arr.length)]
95+
}
96+
97+
function randomInt(min: number, max: number): number {
98+
return Math.floor(Math.random() * (max - min + 1)) + min
99+
}
100+
101+
function randomDate(start: Date, end: Date): string {
102+
const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()))
103+
return date.toISOString().split('T')[0]
104+
}
105+
106+
function generateUserRow(index: number): UserRow {
107+
const firstName = randomItem(firstNames)
108+
const lastName = randomItem(lastNames)
109+
const domain = randomItem(['gmail.com', 'yahoo.com', 'outlook.com', 'company.com', 'work.org'])
110+
111+
return {
112+
name: `${firstName} ${lastName}`,
113+
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${index}@${domain}`,
114+
age: randomInt(22, 65),
115+
department: randomItem(departments),
116+
salary: randomInt(40000, 200000),
117+
active: Math.random() > 0.1, // 90% active
118+
hire_date: randomDate(new Date('2015-01-01'), new Date('2024-12-31')),
119+
country: randomItem(countries),
120+
}
121+
}
122+
123+
async function main() {
124+
console.log(`Seeding ${TABLE_NAME} table for workspace ${WORKSPACE_ID}...`)
125+
126+
// Get user ID for created_by
127+
const userResult = await db.execute<{ id: string }[]>(`SELECT id FROM "user" LIMIT 1`)
128+
const userId = Array.isArray(userResult) && userResult[0] ? userResult[0].id : 'system'
129+
console.log(`Using user ID: ${userId}`)
130+
131+
// Check if table already exists
132+
const existingTable = await db
133+
.select()
134+
.from(userTableDefinitions)
135+
.where(eq(userTableDefinitions.workspaceId, WORKSPACE_ID))
136+
.limit(1)
137+
138+
let tableId: string
139+
140+
if (existingTable.length > 0 && existingTable[0].name === TABLE_NAME) {
141+
tableId = existingTable[0].id
142+
console.log(`Table ${TABLE_NAME} already exists (${tableId}), clearing existing rows...`)
143+
144+
// Delete existing rows
145+
await db.delete(userTableRows).where(eq(userTableRows.tableId, tableId))
146+
147+
// Reset row count (trigger will update it as we insert)
148+
await db
149+
.update(userTableDefinitions)
150+
.set({ rowCount: 0, updatedAt: new Date() })
151+
.where(eq(userTableDefinitions.id, tableId))
152+
} else {
153+
// Create table
154+
tableId = `tbl_${crypto.randomUUID().replace(/-/g, '')}`
155+
const now = new Date()
156+
157+
const tableSchema = {
158+
columns: [
159+
{ name: 'name', type: 'string', required: true },
160+
{ name: 'email', type: 'string', required: true, unique: true },
161+
{ name: 'age', type: 'number', required: true },
162+
{ name: 'department', type: 'string', required: true },
163+
{ name: 'salary', type: 'number', required: true },
164+
{ name: 'active', type: 'boolean', required: true },
165+
{ name: 'hire_date', type: 'string', required: true },
166+
{ name: 'country', type: 'string', required: true },
167+
],
168+
}
169+
170+
await db.insert(userTableDefinitions).values({
171+
id: tableId,
172+
workspaceId: WORKSPACE_ID,
173+
name: TABLE_NAME,
174+
description: 'Stress test table with sample user data',
175+
schema: tableSchema,
176+
maxRows: 10000,
177+
createdBy: userId,
178+
createdAt: now,
179+
updatedAt: now,
180+
})
181+
182+
console.log(`Created table ${TABLE_NAME} (${tableId})`)
183+
}
184+
185+
// Generate and insert rows in batches
186+
const batchSize = 1000
187+
const now = new Date()
188+
189+
console.log(`Inserting ${NUM_ROWS} rows in batches of ${batchSize}...`)
190+
191+
for (let i = 0; i < NUM_ROWS; i += batchSize) {
192+
const batch = []
193+
const endIdx = Math.min(i + batchSize, NUM_ROWS)
194+
195+
for (let j = i; j < endIdx; j++) {
196+
batch.push({
197+
id: `row_${crypto.randomUUID().replace(/-/g, '')}`,
198+
tableId,
199+
workspaceId: WORKSPACE_ID,
200+
data: generateUserRow(j),
201+
createdBy: userId,
202+
createdAt: now,
203+
updatedAt: now,
204+
})
205+
}
206+
207+
await db.insert(userTableRows).values(batch)
208+
console.log(` Inserted rows ${i + 1} to ${endIdx}`)
209+
}
210+
211+
// Verify final row count
212+
const finalTable = await db
213+
.select({ rowCount: userTableDefinitions.rowCount })
214+
.from(userTableDefinitions)
215+
.where(eq(userTableDefinitions.id, tableId))
216+
.limit(1)
217+
218+
console.log(`\nDone! Table ${TABLE_NAME} now has ${finalTable[0]?.rowCount ?? 0} rows.`)
219+
220+
process.exit(0)
221+
}
222+
223+
main().catch((err) => {
224+
console.error('Error seeding data:', err)
225+
process.exit(1)
226+
})

packages/db/triggers.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* PostgreSQL trigger definitions for user tables.
3+
*
4+
* These triggers automatically maintain row counts in user_table_definitions.
5+
* They are created as part of the migration but this file provides TypeScript
6+
* definitions for reference and programmatic trigger management if needed.
7+
*/
8+
9+
import { db } from './index'
10+
11+
/**
12+
* SQL for creating the increment row count function and trigger.
13+
* This function runs AFTER INSERT on user_table_rows.
14+
*/
15+
export const INCREMENT_ROW_COUNT_SQL = `
16+
CREATE OR REPLACE FUNCTION increment_table_row_count()
17+
RETURNS TRIGGER AS $$
18+
BEGIN
19+
UPDATE user_table_definitions
20+
SET row_count = row_count + 1
21+
WHERE id = NEW.table_id;
22+
RETURN NEW;
23+
END;
24+
$$ LANGUAGE plpgsql;
25+
26+
DROP TRIGGER IF EXISTS trg_increment_row_count ON user_table_rows;
27+
CREATE TRIGGER trg_increment_row_count
28+
AFTER INSERT ON user_table_rows
29+
FOR EACH ROW
30+
EXECUTE FUNCTION increment_table_row_count();
31+
`
32+
33+
/**
34+
* SQL for creating the decrement row count function and trigger.
35+
* This function runs AFTER DELETE on user_table_rows.
36+
*/
37+
export const DECREMENT_ROW_COUNT_SQL = `
38+
CREATE OR REPLACE FUNCTION decrement_table_row_count()
39+
RETURNS TRIGGER AS $$
40+
BEGIN
41+
UPDATE user_table_definitions
42+
SET row_count = GREATEST(0, row_count - 1)
43+
WHERE id = OLD.table_id;
44+
RETURN OLD;
45+
END;
46+
$$ LANGUAGE plpgsql;
47+
48+
DROP TRIGGER IF EXISTS trg_decrement_row_count ON user_table_rows;
49+
CREATE TRIGGER trg_decrement_row_count
50+
AFTER DELETE ON user_table_rows
51+
FOR EACH ROW
52+
EXECUTE FUNCTION decrement_table_row_count();
53+
`
54+
55+
/**
56+
* Creates or replaces the row count triggers on user_table_rows.
57+
* This is idempotent and can be safely called multiple times.
58+
*
59+
* @remarks
60+
* These triggers are typically created via migrations. This function
61+
* is provided for programmatic trigger management if needed.
62+
*
63+
* @example
64+
* ```ts
65+
* import { ensureRowCountTriggers } from '@sim/db/triggers'
66+
*
67+
* await ensureRowCountTriggers()
68+
* ```
69+
*/
70+
export async function ensureRowCountTriggers(): Promise<void> {
71+
await db.execute(INCREMENT_ROW_COUNT_SQL)
72+
await db.execute(DECREMENT_ROW_COUNT_SQL)
73+
}
74+
75+
/**
76+
* Verifies that row count triggers exist on user_table_rows.
77+
*
78+
* @returns Object with status of each trigger
79+
*/
80+
export async function verifyRowCountTriggers(): Promise<{
81+
incrementTrigger: boolean
82+
decrementTrigger: boolean
83+
}> {
84+
const result = await db.execute<{ tgname: string }[]>(`
85+
SELECT tgname
86+
FROM pg_trigger
87+
WHERE tgname IN ('trg_increment_row_count', 'trg_decrement_row_count')
88+
AND NOT tgisinternal
89+
`)
90+
91+
const triggers = Array.isArray(result) ? result.map((r) => r.tgname) : []
92+
93+
return {
94+
incrementTrigger: triggers.includes('trg_increment_row_count'),
95+
decrementTrigger: triggers.includes('trg_decrement_row_count'),
96+
}
97+
}

0 commit comments

Comments
 (0)