-
Notifications
You must be signed in to change notification settings - Fork 331
feat: change public schema to trueforge schema for Postgres #488
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
Open
thesujai
wants to merge
2
commits into
main
Choose a base branch
from
feat/update-schema-trueforge
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@truefoundry/trueforge": minor | ||
| --- | ||
|
|
||
| Store Postgres app tables and Kysely migration bookkeeping in a dedicated `trueforge` schema, with an automatic one-time move from `public` so existing installs keep their data and migration history. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { type Kysely, sql } from 'kysely'; | ||
|
|
||
| import type { Database } from './types'; | ||
|
|
||
| export const TRUEFORGE_SCHEMA = 'trueforge'; | ||
|
|
||
| const TABLES_TO_MOVE = [ | ||
| 'kysely_migration', | ||
| 'kysely_migration_lock', | ||
| 'session', | ||
| 'turn', | ||
| 'turn_thread', | ||
| 'session_event', | ||
| 'thread_context_log', | ||
| 'thread_capability_state', | ||
| 'model_provider', | ||
| 'skill', | ||
| 'sandbox_provider', | ||
| 'agent', | ||
| 'schedule', | ||
| 'schedule_run', | ||
| 'mcp_server', | ||
| 'oauth_token', | ||
| 'oauth_pending_authorization', | ||
| ] as const; | ||
|
|
||
| export async function ensureTrueforgeSchema(db: Kysely<Database>): Promise<void> { | ||
| await db.connection().execute(async conn => { | ||
| await sql`SELECT pg_advisory_lock(hashtext('trueforge_schema_bootstrap'))`.execute(conn); | ||
| try { | ||
| await conn.transaction().execute(async trx => { | ||
| await sql`SET LOCAL lock_timeout = '5s'`.execute(trx); | ||
| await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(TRUEFORGE_SCHEMA)}`.execute(trx); | ||
| for (const tableName of TABLES_TO_MOVE) { | ||
| await sql` | ||
| ALTER TABLE IF EXISTS ${sql.id('public', tableName)} SET SCHEMA ${sql.id(TRUEFORGE_SCHEMA)} | ||
| `.execute(trx); | ||
| } | ||
| }); | ||
| } finally { | ||
| await sql`SELECT pg_advisory_unlock(hashtext('trueforge_schema_bootstrap'))`.execute(conn); | ||
| } | ||
| }); | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
packages/trueforge/tests/db/postgres/schemaBootstrap.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import { sql } from 'kysely'; | ||
| import { Pool } from 'pg'; | ||
| import { ulid } from 'ulid'; | ||
|
|
||
| import { migrateToLatest } from '../../../src/db/migratePostgres'; | ||
| import { createDb } from '../../../src/db/postgres/client'; | ||
| import { TRUEFORGE_SCHEMA } from '../../../src/db/postgres/schema'; | ||
| import { createPostgresTestDatabase, type PostgresTestDatabase } from './testDatabase'; | ||
|
|
||
| const describePg = process.env['PG_STORE_TESTS_ENABLED'] === '1' ? describe : describe.skip; | ||
|
|
||
| const TABLES_TO_CHECK = ['session', 'kysely_migration', 'kysely_migration_lock'] as const; | ||
|
|
||
| const APP_AND_KYSELY_TABLES = [ | ||
| 'kysely_migration_lock', | ||
| 'kysely_migration', | ||
| 'oauth_pending_authorization', | ||
| 'oauth_token', | ||
| 'mcp_server', | ||
| 'schedule_run', | ||
| 'schedule', | ||
| 'agent', | ||
| 'sandbox_provider', | ||
| 'skill', | ||
| 'model_provider', | ||
| 'thread_capability_state', | ||
| 'thread_context_log', | ||
| 'session_event', | ||
| 'turn_thread', | ||
| 'turn', | ||
| 'session', | ||
| ] as const; | ||
|
|
||
| async function tableSchema(db: PostgresTestDatabase['db'], tableName: string): Promise<string | undefined> { | ||
| const { rows } = await sql<{ table_schema: string }>` | ||
| SELECT table_schema | ||
| FROM information_schema.tables | ||
| WHERE table_name = ${tableName} | ||
| AND table_schema IN (${TRUEFORGE_SCHEMA}, 'public') | ||
| `.execute(db); | ||
| return rows[0]?.table_schema; | ||
| } | ||
|
|
||
| async function migrationNames(db: PostgresTestDatabase['db']): Promise<string[]> { | ||
| const { rows } = await sql<{ name: string }>` | ||
| SELECT name FROM kysely_migration ORDER BY name | ||
| `.execute(db); | ||
| return rows.map(row => row.name); | ||
| } | ||
|
|
||
| function withDatabase(connectionString: string, database: string): string { | ||
| const parsed = new URL(connectionString.replace(/^postgres:/, 'http:')); | ||
| parsed.pathname = `/${database}`; | ||
| return parsed.toString().replace(/^http:/, 'postgres:'); | ||
| } | ||
|
|
||
| describePg('trueforge Postgres schema bootstrap', () => { | ||
| it('places app and Kysely tables in the trueforge schema on a greenfield database', async () => { | ||
| const env = await createPostgresTestDatabase(); | ||
| if (env === undefined) { | ||
| throw new Error('Postgres test environment unavailable despite globalSetup probe'); | ||
| } | ||
| try { | ||
| for (const tableName of TABLES_TO_CHECK) { | ||
| expect(await tableSchema(env.db, tableName)).toBe(TRUEFORGE_SCHEMA); | ||
| } | ||
|
|
||
| const { rows: searchPathRows } = await sql<{ search_path: string }>` | ||
| SHOW search_path | ||
| `.execute(env.db); | ||
| expect(searchPathRows[0]?.search_path).toBe(TRUEFORGE_SCHEMA); | ||
|
|
||
| const names = await migrationNames(env.db); | ||
| expect(names.length).toBeGreaterThan(0); | ||
|
|
||
| await migrateToLatest(env.db); | ||
| expect(await migrationNames(env.db)).toEqual(names); | ||
| } finally { | ||
| await env.teardown(); | ||
| } | ||
| }, 120_000); | ||
|
|
||
| it('moves legacy public tables into trueforge and preserves migration history', async () => { | ||
| const adminUrl = process.env['PG_STORE_TESTS_ADMIN_URL']; | ||
| if (adminUrl === undefined || adminUrl === '') { | ||
| throw new Error('PG_STORE_TESTS_ADMIN_URL unset despite globalSetup probe'); | ||
| } | ||
|
|
||
| const databaseName = `test_${ulid().toLowerCase()}`; | ||
| if (!/^[a-z0-9_]+$/.test(databaseName)) { | ||
| throw new Error(`invalid database name: ${databaseName}`); | ||
| } | ||
|
|
||
| const adminPool = new Pool({ connectionString: adminUrl }); | ||
| try { | ||
| await adminPool.query(`CREATE DATABASE "${databaseName}"`); | ||
| } finally { | ||
| await adminPool.end(); | ||
| } | ||
|
|
||
| const databaseUrl = withDatabase(adminUrl, databaseName); | ||
| const db = createDb({ | ||
| connectionString: databaseUrl, | ||
| poolMax: 5, | ||
| statementTimeoutMs: 60_000, | ||
| idleInTransactionSessionTimeoutMs: 60_000, | ||
| }); | ||
|
|
||
| try { | ||
| await migrateToLatest(db); | ||
| const beforeNames = await migrationNames(db); | ||
|
|
||
| // Simulate a pre-upgrade install: objects in public, trueforge schema gone. | ||
| // Use a search_path-free pool so we can address trueforge.* after createDb pinned search_path. | ||
| const publicPool = new Pool({ connectionString: databaseUrl }); | ||
| try { | ||
| for (const tableName of APP_AND_KYSELY_TABLES) { | ||
| await publicPool.query(`ALTER TABLE IF EXISTS trueforge.${tableName} SET SCHEMA public`); | ||
| } | ||
| await publicPool.query('DROP SCHEMA IF EXISTS trueforge CASCADE'); | ||
| } finally { | ||
| await publicPool.end(); | ||
| } | ||
|
|
||
| await migrateToLatest(db); | ||
|
|
||
| for (const tableName of TABLES_TO_CHECK) { | ||
| expect(await tableSchema(db, tableName)).toBe(TRUEFORGE_SCHEMA); | ||
| } | ||
| expect(await migrationNames(db)).toEqual(beforeNames); | ||
|
|
||
| await migrateToLatest(db); | ||
| expect(await migrationNames(db)).toEqual(beforeNames); | ||
| } finally { | ||
| await db.destroy(); | ||
| const dropPool = new Pool({ connectionString: adminUrl }); | ||
| try { | ||
| await dropPool.query( | ||
| `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, | ||
| [databaseName], | ||
| ); | ||
| await dropPool.query(`DROP DATABASE IF EXISTS "${databaseName}"`); | ||
| } finally { | ||
| await dropPool.end(); | ||
| } | ||
| } | ||
| }, 120_000); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.