From c7da81c5eb5b98b69889774adb1d95b0ab02d42a Mon Sep 17 00:00:00 2001 From: Gefei Hou Date: Sun, 7 Jun 2026 21:38:14 -0700 Subject: [PATCH] Added current_database() to supply the correct value at deploy time --- pgpm/export/__tests__/dynamic-fields.test.ts | 21 +++++++++++++++ pgpm/export/__tests__/export-flow.test.ts | 28 ++++++++++++++++++++ pgpm/export/src/export-graphql-meta.ts | 21 +++++++++++++++ pgpm/export/src/export-meta.ts | 21 +++++++++++++++ pgpm/export/src/export-utils.ts | 14 +++++++++- 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/pgpm/export/__tests__/dynamic-fields.test.ts b/pgpm/export/__tests__/dynamic-fields.test.ts index f52af7f5e..37e1bf951 100644 --- a/pgpm/export/__tests__/dynamic-fields.test.ts +++ b/pgpm/export/__tests__/dynamic-fields.test.ts @@ -322,6 +322,27 @@ describe('typeOverrides should take precedence over introspected types', () => { const field = META_TABLE_CONFIG.field; expect(field.typeOverrides).toBeUndefined(); }); + + it('columnDefaults should be set for tables with environment-specific columns', () => { + // apis.dbname defaults to current_database() and must not be exported + // as a hardcoded literal — see constructive-db commit 348a5b402e. + const apis = META_TABLE_CONFIG.apis; + expect(apis.columnDefaults).toBeDefined(); + expect(apis.columnDefaults!.dbname).toBe('current_database()'); + + // sites.dbname has the same portability issue + const sites = META_TABLE_CONFIG.sites; + expect(sites.columnDefaults).toBeDefined(); + expect(sites.columnDefaults!.dbname).toBe('current_database()'); + }); + + it('tables without columnDefaults should have no columnDefaults key', () => { + const database = META_TABLE_CONFIG.database; + expect(database.columnDefaults).toBeUndefined(); + + const domains = META_TABLE_CONFIG.domains; + expect(domains.columnDefaults).toBeUndefined(); + }); }); // ============================================================================= diff --git a/pgpm/export/__tests__/export-flow.test.ts b/pgpm/export/__tests__/export-flow.test.ts index 42bb9f175..1bcc0d50a 100644 --- a/pgpm/export/__tests__/export-flow.test.ts +++ b/pgpm/export/__tests__/export-flow.test.ts @@ -730,5 +730,33 @@ relocatable = false const structure = getDirectoryStructure(svcDeployDir); expect(structure).toMatchSnapshot('pets-export-svc deploy folder'); }); + + // Behavioral test: columnDefaults columns must be absent from the generated SQL. + // The apis and sites tables have dbname DEFAULT current_database(), which + // captures an environment-specific literal during export. columnDefaults + // strips the column from the INSERT so the DDL default supplies the correct + // value at deploy time (constructive-db commit 348a5b402e). + it('should exclude dbname from apis and sites INSERTs (columnDefaults)', () => { + const apisSqlPath = join(exportWorkspaceDir, 'packages', META_EXTENSION_NAME, 'deploy', 'migrate', 'apis.sql'); + const sitesSqlPath = join(exportWorkspaceDir, 'packages', META_EXTENSION_NAME, 'deploy', 'migrate', 'sites.sql'); + + if (existsSync(apisSqlPath)) { + const apisContent = readFileSync(apisSqlPath, 'utf-8'); + // dbname must NOT appear — it's stripped by columnDefaults + expect(apisContent).not.toContain('dbname'); + // But other columns should still be present + expect(apisContent).toContain('name'); + expect(apisContent).toContain('is_public'); + } + + if (existsSync(sitesSqlPath)) { + const sitesContent = readFileSync(sitesSqlPath, 'utf-8'); + // dbname must NOT appear — it's stripped by columnDefaults + expect(sitesContent).not.toContain('dbname'); + // But other columns should still be present + expect(sitesContent).toContain('title'); + expect(sitesContent).toContain('description'); + } + }); }); }); diff --git a/pgpm/export/src/export-graphql-meta.ts b/pgpm/export/src/export-graphql-meta.ts index 6f589ec78..7f9bcf1d9 100644 --- a/pgpm/export/src/export-graphql-meta.ts +++ b/pgpm/export/src/export-graphql-meta.ts @@ -94,6 +94,16 @@ const buildDynamicFieldsFromGraphQL = async ( } } + // Omit columns that are marked as columnDefaults — their DDL DEFAULT (e.g. + // current_database()) will supply the correct value at deploy time, so the + // exported INSERT must not hardcode an environment-specific literal. + if (tableConfig.columnDefaults) { + for (const colName of Object.keys(tableConfig.columnDefaults)) { + delete dynamicFields[colName]; + enumFields.delete(colName); + } + } + return { fields: dynamicFields, enumFields }; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); @@ -192,6 +202,17 @@ export const exportGraphQLMeta = async ({ if (Object.keys(dynamicFields).length === 0) return; + // Omit columnDefaults columns from row data so the Parser never sees them. + // configFields already excludes them (via buildDynamicFieldsFromGraphQL), + // so dynamicFields won't contain them either — but the pgRow data still does. + if (tableConfig.columnDefaults) { + for (const colName of Object.keys(tableConfig.columnDefaults)) { + for (const row of pgRows) { + delete row[colName]; + } + } + } + const parser = new Parser({ schema: tableConfig.schema, table: tableConfig.table, diff --git a/pgpm/export/src/export-meta.ts b/pgpm/export/src/export-meta.ts index 951e77115..2239140cd 100644 --- a/pgpm/export/src/export-meta.ts +++ b/pgpm/export/src/export-meta.ts @@ -57,6 +57,15 @@ const buildDynamicFields = async ( } } + // Omit columns that are marked as columnDefaults — their DDL DEFAULT (e.g. + // current_database()) will supply the correct value at deploy time, so the + // exported INSERT must not hardcode an environment-specific literal. + if (tableConfig.columnDefaults) { + for (const colName of Object.keys(tableConfig.columnDefaults)) { + delete dynamicFields[colName]; + } + } + return dynamicFields; }; @@ -138,6 +147,18 @@ export const exportMeta = async ({ opts, dbname, database_id }: ExportMetaParams } } + // Omit columnDefaults columns from row data so the Parser never sees them. + // The Parser's field config already excludes them (via buildDynamicFields), + // so they would be ignored anyway, but removing them from the data is cleaner. + const tblCfg = META_TABLE_CONFIG[key]; + if (tblCfg?.columnDefaults) { + for (const colName of Object.keys(tblCfg.columnDefaults)) { + for (const row of result.rows) { + delete row[colName]; + } + } + } + const parsed = await parser.parse(result.rows); if (parsed) { sql[key] = parsed; diff --git a/pgpm/export/src/export-utils.ts b/pgpm/export/src/export-utils.ts index 8a8b39233..a21101f10 100644 --- a/pgpm/export/src/export-utils.ts +++ b/pgpm/export/src/export-utils.ts @@ -202,6 +202,12 @@ export interface TableConfig { conflictDoNothing?: boolean; typeOverrides?: Record; // only for special types (image, upload, url) that can't be inferred gqlTypeName?: string; // override for GraphQL type name when automatic derivation doesn't match PostGraphile's inflector + /** Columns whose values are environment-specific and should be excluded from the + * exported INSERT so that the column's DDL DEFAULT applies at deploy time. + * Key = column name, Value = the SQL expression the column defaults to (for documentation). + * E.g. { dbname: 'current_database()' } — the exporter omits `dbname` from the + * INSERT, and `DEFAULT current_database()` in the table definition supplies it. */ + columnDefaults?: Record; } /** @@ -307,11 +313,17 @@ export const META_TABLE_CONFIG: Record = { favicon: 'upload', apple_touch_icon: 'image', logo: 'image' + }, + columnDefaults: { + dbname: 'current_database()' } }, apis: { schema: 'services_public', - table: 'apis' + table: 'apis', + columnDefaults: { + dbname: 'current_database()' + } }, apps: { schema: 'services_public',