diff --git a/pgpm/core/__tests__/export/export-meta.test.ts b/pgpm/core/__tests__/export/export-meta.test.ts new file mode 100644 index 000000000..2386479da --- /dev/null +++ b/pgpm/core/__tests__/export/export-meta.test.ts @@ -0,0 +1,154 @@ +/** + * Tests for export-meta.ts configuration validation + * + * These tests validate that the export-meta config uses correct table names + * and includes all required fields for exporting meta_public and collections_public data. + */ + +import { readFileSync } from 'fs'; +import { join } from 'path'; + +describe('Export Meta Config Validation', () => { + let exportMetaSource: string; + + beforeAll(() => { + // Read the source file to validate the config + const filePath = join(__dirname, '../../src/export/export-meta.ts'); + exportMetaSource = readFileSync(filePath, 'utf-8'); + }); + + describe('table name validation', () => { + it('should use singular table name "database_extension" (not plural "database_extensions")', () => { + // The actual table in collections_public is database_extension (singular) + // This test ensures the config uses the correct table name + + // Check that the config defines the table correctly + expect(exportMetaSource).toContain("table: 'database_extension'"); + + // Ensure we're not using the incorrect plural form in the table definition + // Note: We check specifically in the config section, not the query section + const configSection = exportMetaSource.split('const config:')[1]?.split('interface ExportMetaParams')[0] || ''; + expect(configSection).not.toContain("table: 'database_extensions'"); + }); + + it('should query the correct table name in collections_public.database_extension', () => { + // The query should use the correct singular table name + expect(exportMetaSource).toContain('FROM collections_public.database_extension'); + }); + + it('should include field table in queries', () => { + // The field table should be queried (it was missing before) + expect(exportMetaSource).toContain("queryAndParse('field'"); + expect(exportMetaSource).toContain('FROM collections_public.field'); + }); + }); + + describe('collections_public tables', () => { + it('should include all required collections_public tables in config', () => { + const requiredTables = [ + 'database', + 'database_extension', + 'schema', + 'table', + 'field' + ]; + + for (const table of requiredTables) { + expect(exportMetaSource).toContain(`queryAndParse('${table}'`); + } + }); + }); + + describe('meta_public tables', () => { + it('should include all required meta_public tables in config', () => { + const requiredTables = [ + 'domains', + 'sites', + 'apis', + 'apps', + 'site_modules', + 'site_themes', + 'api_modules', + 'api_extensions', + 'api_schemata', + 'rls_module', + 'user_auth_module' + ]; + + for (const table of requiredTables) { + expect(exportMetaSource).toContain(`queryAndParse('${table}'`); + } + }); + }); + + describe('query order validation', () => { + it('should query database before dependent tables', () => { + // database should be queried first as other tables depend on it + const databaseQueryIndex = exportMetaSource.indexOf("queryAndParse('database',"); + const schemaQueryIndex = exportMetaSource.indexOf("queryAndParse('schema',"); + const tableQueryIndex = exportMetaSource.indexOf("queryAndParse('table',"); + + expect(databaseQueryIndex).toBeLessThan(schemaQueryIndex); + expect(schemaQueryIndex).toBeLessThan(tableQueryIndex); + }); + }); +}); + +describe('Export Meta Config Drift Detection', () => { + it('should document the expected table names in collections_public', () => { + // This test documents the expected table names that export-meta.ts should use + // If these change, the export-meta.ts config needs to be updated + const expectedCollectionsPublicTables = [ + 'database', + 'database_extension', // NOT 'database_extensions' (plural) + 'schema', + 'table', + 'field' + ]; + + // Document the expected tables + expect(expectedCollectionsPublicTables).toContain('database_extension'); + expect(expectedCollectionsPublicTables).not.toContain('database_extensions'); + }); + + it('should document the expected table names in meta_public', () => { + // This test documents the expected table names that export-meta.ts should use + const expectedMetaPublicTables = [ + 'apis', + 'api_extensions', + 'api_modules', + 'api_schemata', + 'apps', + 'domains', + 'site_modules', + 'site_themes', + 'sites', + 'rls_module', + 'user_auth_module' + ]; + + // Document the expected tables + expect(expectedMetaPublicTables.length).toBe(11); + }); + + it('should document the bug: config uses database_extensions but table is database_extension', () => { + // BUG DOCUMENTATION: + // In export-meta.ts, line 26, the config defines: + // table: 'database_extensions' (plural) + // But the actual table in db-meta-schema is: + // collections_public.database_extension (singular) + // + // This causes the Parser to generate INSERT statements with the wrong table name, + // which will fail when the exported SQL is replayed. + // + // FIX: Change line 26 in export-meta.ts from: + // table: 'database_extensions' + // to: + // table: 'database_extension' + + const buggyConfigTableName = 'database_extensions'; + const correctTableName = 'database_extension'; + + expect(buggyConfigTableName).not.toBe(correctTableName); + }); +}); diff --git a/pgpm/core/src/export/export-meta.ts b/pgpm/core/src/export/export-meta.ts index 60af58835..3b10c83f4 100644 --- a/pgpm/core/src/export/export-meta.ts +++ b/pgpm/core/src/export/export-meta.ts @@ -23,7 +23,7 @@ const config: Record = { }, database_extension: { schema: 'collections_public', - table: 'database_extensions', + table: 'database_extension', fields: { name: 'text', database_id: 'uuid' @@ -243,6 +243,7 @@ export const exportMeta = async ({ opts, dbname, database_id }: ExportMetaParams await queryAndParse('database', `SELECT * FROM collections_public.database WHERE id = $1`); await queryAndParse('schema', `SELECT * FROM collections_public.schema WHERE database_id = $1`); await queryAndParse('table', `SELECT * FROM collections_public.table WHERE database_id = $1`); + await queryAndParse('field', `SELECT * FROM collections_public.field WHERE database_id = $1`); await queryAndParse('domains', `SELECT * FROM meta_public.domains WHERE database_id = $1`); await queryAndParse('apis', `SELECT * FROM meta_public.apis WHERE database_id = $1`); await queryAndParse('sites', `SELECT * FROM meta_public.sites WHERE database_id = $1`); diff --git a/plan.md b/plan.md new file mode 100644 index 000000000..27275e322 --- /dev/null +++ b/plan.md @@ -0,0 +1,114 @@ +# Export Meta Fix Plan + +## Problem Statement + +The `pgpm export` command's `meta_public` schema export is broken. The export functionality in `pgpm/core/src/export/export-meta.ts` has several issues that cause the generated SQL to fail when replayed. + +## Issues Identified + +### Critical Issues + +1. **Table name mismatch (`database_extension`)** + - Location: `pgpm/core/src/export/export-meta.ts` line 26-27 + - Config defines: `table: 'database_extensions'` (plural) + - Actual table: `collections_public.database_extension` (singular) + - Impact: Generated INSERT statements target non-existent table + +2. **Missing columns in `user_auth_module` config** + - Actual table has columns not in export config: + - `audits_table_id` + - `audits_table_name` + - `verify_password_function` + - `check_password_function` + - Impact: Exported data is incomplete + +3. **Missing `field` query** + - The `field` table is defined in config (lines 54-65) but never queried + - No `queryAndParse('field', ...)` call exists + - Impact: Field data is never exported + +4. **Missing `site_metadata` table** + - Table exists in `meta_public` schema but not in export config + - Impact: Site metadata is never exported + +### Secondary Issues + +5. **Insert order vs FK constraints** + - Export queries `domains` before `apis`/`sites` + - `domains` has FK references to both `apis` and `sites` + - Code relies on `SET session_replication_role TO replica;` (requires superuser) + - Impact: May fail in non-superuser environments + +6. **Type mismatches** (may or may not cause issues depending on csv-to-pg handling) + - `meta_public.sites.favicon`: actual `attachment`, config says `upload` + - `meta_public.domains.subdomain/domain`: actual `hostname`, config says `text` + - `meta_public.api_modules.data`: actual `pg_catalog.json`, config says `jsonb` + - `meta_public.site_modules.data`: actual `pg_catalog.json`, config says `jsonb` + +## Proposed Solution + +### Phase 1: Create Test Infrastructure + +1. **Create a new test module** in `extensions/` directory + - Name: `@pgpm/export-meta-test` or similar + - Purpose: Test the export functionality with seed data + +2. **Use `pgsql-test` for database testing** + - Leverage existing test infrastructure + - Transaction-isolated tests with automatic rollback + +3. **Test approach**: + - Install `@pgpm/db-meta-schema` and `@pgpm/db-meta-modules` + - Seed representative data into all relevant tables + - Call `exportMeta()` to generate SQL + - Execute generated SQL in a fresh transaction + - Verify data integrity + +### Phase 2: Fix Export Issues + +1. Fix `database_extensions` -> `database_extension` table name +2. Add missing columns to `user_auth_module` config +3. Add `queryAndParse('field', ...)` call +4. Add `site_metadata` table to config and query +5. Fix insert order to respect FK dependencies +6. Update type mappings if needed + +### Phase 3: Add Schema Drift Detection + +1. Create a test that compares `information_schema.columns` against the config +2. Fail if config doesn't cover all required columns +3. Prevent future regressions when schema changes + +## Test Module Structure + +``` +extensions/@pgpm/export-meta-test/ +├── package.json +├── jest.config.js +├── __tests__/ +│ ├── export-meta.test.ts # Main export tests +│ └── schema-drift.test.ts # Schema drift detection +└── seed/ + └── seed-data.ts # Seed data helpers +``` + +## Dependencies + +- `@pgpm/db-meta-schema` - Core schema definitions +- `@pgpm/db-meta-modules` - Module table definitions +- `pgsql-test` - Test infrastructure +- `@pgpmjs/core` - Export functionality to test + +## Success Criteria + +1. Export generates valid SQL that can be replayed +2. All tables in `meta_public` and `collections_public` are properly exported +3. FK constraints are respected (either via proper ordering or explicit handling) +4. Tests catch schema drift automatically +5. CI passes with new tests + +## Questions to Resolve + +1. Should this be a separate module or part of existing `db-meta-schema` tests? +2. Do we need to support non-superuser deployments (affects `session_replication_role` approach)? +3. Should we add the tests to `launchql-extensions` repo as well for parity?