Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions pgpm/export/__tests__/dynamic-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

// =============================================================================
Expand Down
28 changes: 28 additions & 0 deletions pgpm/export/__tests__/export-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
});
21 changes: 21 additions & 0 deletions pgpm/export/src/export-graphql-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions pgpm/export/src/export-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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;
Expand Down
14 changes: 13 additions & 1 deletion pgpm/export/src/export-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,12 @@ export interface TableConfig {
conflictDoNothing?: boolean;
typeOverrides?: Record<string, FieldType>; // 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<string, string>;
}

/**
Expand Down Expand Up @@ -307,11 +313,17 @@ export const META_TABLE_CONFIG: Record<string, TableConfig> = {
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',
Expand Down
Loading