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
45 changes: 13 additions & 32 deletions pgpm/core/src/core/class/pgpm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { loadConfigSyncFromDir, resolvePgpmPath,walkUp } from '@pgpmjs/env';
import { Logger } from '@pgpmjs/logger';
import { errors, PgpmOptions, PgpmWorkspaceConfig } from '@pgpmjs/types';
import yanse from 'yanse';
import { execSync } from 'child_process';
import fs from 'fs';
import * as glob from 'glob';
Expand All @@ -10,24 +9,24 @@ import { parse } from 'parse-package-name';
import path, { dirname, resolve } from 'path';
import { getPgPool } from 'pg-cache';
import { PgConfig } from 'pg-env';
import yanse from 'yanse';

import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TTL_MS, DEFAULT_TEMPLATE_TOOL_NAME, scaffoldTemplate } from '../template-scaffold';
import { getAvailableExtensions } from '../../extensions/extensions';
import { generatePlan, writePlan, writePlanFile } from '../../files';
import { Tag, ExtendedPlanFile, Change } from '../../files/types';
import { parsePlanFile } from '../../files/plan/parser';
import { isValidTagName, isValidChangeName, parseReference } from '../../files/plan/validators';
import { getNow as getPlanTimestamp } from '../../files/plan/generator';
import { resolveTagToChangeName } from '../../resolution/resolve';
import {
ExtensionInfo,
getExtensionInfo,
getExtensionName,
getInstalledExtensions,
parseControlFile,
writeExtensions,
writeExtensions
} from '../../files';
import { generateControlFileContent, writeExtensionMakefile } from '../../files/extension/writer';
import { getNow as getPlanTimestamp } from '../../files/plan/generator';
import { parsePlanFile } from '../../files/plan/parser';
import { isValidChangeName, isValidTagName, parseReference } from '../../files/plan/validators';
import { Change, Tag } from '../../files/types';
import { PackageAnalysisIssue, PackageAnalysisResult, RenameOptions } from '../../files/types';
import { PgpmMigrate } from '../../migrate/client';
import {
getExtensionsAndModules,
Expand All @@ -37,10 +36,9 @@ import {
ModuleMap
} from '../../modules/modules';
import { packageModule } from '../../packaging/package';
import { resolveExtensionDependencies, resolveDependencies } from '../../resolution/deps';
import { PackageAnalysisIssue, PackageAnalysisResult, RenameOptions } from '../../files/types';

import { resolveDependencies,resolveExtensionDependencies } from '../../resolution/deps';
import { parseTarget } from '../../utils/target-utils';
import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, DEFAULT_TEMPLATE_TTL_MS, scaffoldTemplate } from '../template-scaffold';


const logger = new Logger('pgpm');
Expand All @@ -51,27 +49,10 @@ const logger = new Logger('pgpm');
*/
const EXTENSIONS_DIR = 'extensions';

function getUTCTimestamp(d: Date = new Date()): string {
return (
d.getUTCFullYear() +
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
'-' + String(d.getUTCDate()).padStart(2, '0') +
'T' + String(d.getUTCHours()).padStart(2, '0') +
':' + String(d.getUTCMinutes()).padStart(2, '0') +
':' + String(d.getUTCSeconds()).padStart(2, '0') +
'Z'
);
}

function sortObjectByKey<T extends Record<string, any>>(obj: T): T {
return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b))) as T;
}

const getNow = () =>
process.env.NODE_ENV === 'test'
? getUTCTimestamp(new Date('2017-08-11T08:11:51Z'))
: getUTCTimestamp(new Date());


/**
* Truncates workspace extensions to include only modules from the target onwards.
Expand All @@ -95,7 +76,7 @@ const truncateExtensionsToTarget = (
resolved: workspaceExtensions.resolved.slice(targetIndex),
external: workspaceExtensions.external
};
}
};

export enum PackageContext {
Outside = 'outside',
Expand Down Expand Up @@ -484,7 +465,7 @@ export class PgpmPackage {
cacheTtlMs: options.cacheTtlMs ?? DEFAULT_TEMPLATE_TTL_MS,
toolName: options.toolName ?? DEFAULT_TEMPLATE_TOOL_NAME,
cwd: this.cwd,
prompter: options.prompter,
prompter: options.prompter
});

// Only create pgpm files (pgpm.plan, .control, deploy/revert/verify dirs) for pgpm-managed modules
Expand Down Expand Up @@ -808,7 +789,7 @@ export class PgpmPackage {
}

if (!isValidChangeName(changeName)) {
throw errors.INVALID_NAME({ name: changeName, type: 'change', rules: "Change names must follow Sqitch naming rules" });
throw errors.INVALID_NAME({ name: changeName, type: 'change', rules: 'Change names must follow Sqitch naming rules' });
}

if (!this.isInWorkspace() && !this.isInModule()) {
Expand Down Expand Up @@ -1125,7 +1106,7 @@ ${dependencies.length > 0 ? dependencies.map(dep => `-- requires: ${dep}`).join(
getInstalledModules(): {
installed: string[];
installedVersions: Record<string, string>;
} {
} {
this.ensureModule();
this.ensureWorkspace();

Expand Down
49 changes: 41 additions & 8 deletions pgpm/core/src/files/plan/generator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import fs from 'fs';

/**
* Get a UTC timestamp string
* Fixed epoch timestamp used for deterministic plan generation.
* Can be overridden with the PGPM_FROZEN_TIME environment variable.
*/
function getUTCTimestamp(d: Date = new Date()): string {
const DEFAULT_FROZEN_TIME = '2017-08-11T08:11:51Z';

/**
* Get a UTC timestamp string from a Date object.
*/
function getUTCTimestamp(d: Date): string {
return (
d.getUTCFullYear() +
'-' + String(d.getUTCMonth() + 1).padStart(2, '0') +
Expand All @@ -16,12 +22,15 @@ function getUTCTimestamp(d: Date = new Date()): string {
}

/**
* Get a timestamp for the plan file
* Get a timestamp for the plan file.
* Uses the PGPM_FROZEN_TIME env var if present, otherwise the fixed epoch.
*/
export function getNow(): string {
return process.env.NODE_ENV === 'test'
? getUTCTimestamp(new Date('2017-08-11T08:11:51Z'))
: getUTCTimestamp(new Date());
const frozen = process.env.PGPM_FROZEN_TIME;
if (frozen) {
return getUTCTimestamp(new Date(frozen));
}
return DEFAULT_FROZEN_TIME;
}

export interface PlanEntry {
Expand All @@ -36,6 +45,26 @@ export interface GeneratePlanOptions {
entries: PlanEntry[];
}

/**
* Sort dependencies by their order in the supplied plan, so prerequisites
* (parent migrations) appear before their dependents in the bracket.
* Internal dependencies are sorted by their plan index, and external
* dependencies (not in the plan) preserve their original order.
*/
const sortDependencies = (deps: string[], order: Map<string, number>): string[] => {
const originalOrder = new Map(deps.map((dep, index) => [dep, index]));
return [...deps].sort((a, b) => {
const aIndex = order.get(a);
const bIndex = order.get(b);
const aOriginal = originalOrder.get(a)!;
const bOriginal = originalOrder.get(b)!;
if (aIndex !== undefined && bIndex !== undefined) return aIndex - bIndex || aOriginal - bOriginal;
if (aIndex !== undefined) return -1;
if (bIndex !== undefined) return 1;
return aOriginal - bOriginal;
});
};

/**
* Generate a plan file content from structured data
*/
Expand All @@ -49,11 +78,15 @@ export function generatePlan(options: GeneratePlanOptions): string {
`%uri=${uri || moduleName}`
];

// Build an order map from the entry list so brackets can be sorted by plan order
const entryOrder = new Map(entries.map((entry, index) => [entry.change, index]));

// Generate the plan entries
entries.forEach(entry => {
if (entry.dependencies && entry.dependencies.length > 0) {
const deps = sortDependencies(entry.dependencies || [], entryOrder);
if (deps.length > 0) {
planfile.push(
`${entry.change} [${entry.dependencies.join(' ')}] ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`
`${entry.change} [${deps.join(' ')}] ${now} constructive <constructive@5b0c196eeb62>${entry.comment ? ` # ${entry.comment}` : ''}`
);
} else {
planfile.push(
Expand Down
54 changes: 40 additions & 14 deletions pgpm/core/src/files/plan/writer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
import fs from 'fs';
import path from 'path';

import { Change, PlanFile, PgpmRow, Tag, ExtendedPlanFile } from '../types';
import { parseAuthor } from '../../utils/author';
import { Change, ExtendedPlanFile,PgpmRow, Tag } from '../types';

/**
* Sort dependencies by their order in the supplied plan, so prerequisites
* (parent migrations) appear before their dependents in the bracket.
* Internal dependencies are sorted by their plan index, and external
* dependencies (not in the plan) preserve their original order.
*/
const sortDependencies = (deps: string[], order: Map<string, number>): string[] => {
const originalOrder = new Map(deps.map((dep, index) => [dep, index]));
return [...deps].sort((a, b) => {
const aIndex = order.get(a);
const bIndex = order.get(b);
const aOriginal = originalOrder.get(a)!;
const bOriginal = originalOrder.get(b)!;
if (aIndex !== undefined && bIndex !== undefined) return aIndex - bIndex || aOriginal - bOriginal;
if (aIndex !== undefined) return -1;
if (bIndex !== undefined) return 1;
return aOriginal - bOriginal;
});
};

export interface PlanWriteOptions {
outdir: string;
Expand All @@ -26,6 +46,9 @@ export function writePgpmPlan(rows: PgpmRow[], opts: PlanWriteOptions): void {

const duplicates: Record<string, boolean> = {};

// Build an order map from the row list so brackets can be sorted by plan order
const rowOrder = new Map(rows.map((row, index) => [row.deploy, index]));

const plan = opts.replacer(`%syntax-version=1.0.0
%project=constructive-extension-name
%uri=constructive-extension-name
Expand All @@ -39,7 +62,7 @@ ${rows
duplicates[row.deploy] = true;

if (row.deps?.length) {
return `${row.deploy} [${row.deps.join(' ')}] ${date()} ${authorName} <${authorEmail}> # add ${row.name}`;
return `${row.deploy} [${sortDependencies(row.deps, rowOrder).join(' ')}] ${date()} ${authorName} <${authorEmail}> # add ${row.name}`;
}
return `${row.deploy} ${date()} ${authorName} <${authorEmail}> # add ${row.name}`;
})
Expand All @@ -62,42 +85,45 @@ export function writePlanFile(planPath: string, plan: ExtendedPlanFile): void {
*/
export function generatePlanFileContent(plan: ExtendedPlanFile): string {
const { package: packageName, uri, changes, tags } = plan;

let content = `%syntax-version=1.0.0\n`;
content += `%project=${packageName}\n`;

if (uri) {
content += `%uri=${uri}\n`;
}

content += `\n`;


// Build an order map from the change list so brackets can be sorted by plan order
const changeOrder = new Map(changes.map((change, index) => [change.name, index]));

// Add changes and their associated tags
for (const change of changes) {
content += generateChangeLineContent(change);
content += generateChangeLineContent(change, changeOrder);
content += `\n`;

const associatedTags = tags.filter(tag => tag.change === change.name);
for (const tag of associatedTags) {
content += generateTagLineContent(tag);
content += `\n`;
}
}

return content;
}

/**
* Generate a line for a change in a plan file
*/
export function generateChangeLineContent(change: Change): string {
export function generateChangeLineContent(change: Change, changeOrder?: Map<string, number>): string {
const { name, dependencies, timestamp, planner, email, comment } = change;

let line = name;
// Add dependencies if present

// Add dependencies if present, sorted by plan order
if (dependencies && dependencies.length > 0) {
line += ` [${dependencies.join(' ')}]`;
line += ` [${sortDependencies(dependencies, changeOrder || new Map()).join(' ')}]`;
}

// Add timestamp if present
Expand Down
20 changes: 17 additions & 3 deletions pgpm/export/src/export-graphql-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@
*/
import { Parser } from 'csv-to-pg';
import { toSnakeCase } from 'inflekt';

import { FieldType, META_TABLE_CONFIG, META_TABLE_ORDER, TableConfig } from './export-utils';
import { FieldType, getTimestampDefaultColumnsForTable, META_TABLE_CONFIG, META_TABLE_ORDER, TableConfig } from './export-utils';
import { GraphQLClient } from './graphql-client';
import {
buildFieldsFragment,
Expand Down Expand Up @@ -94,7 +93,22 @@ const buildDynamicFieldsFromGraphQL = async (
}
}

// Omit columns that are marked as columnDefaults — their DDL DEFAULT (e.g.
// Omit timestamptz columns whose default supplies the current time. Their
// DDL DEFAULT will apply at deploy time, keeping generated INSERT rows
// free of literal environment-specific timestamps.
const timestampDefaultColumns = getTimestampDefaultColumnsForTable(tableConfig.schema, tableConfig.table);
if (timestampDefaultColumns.length > 0) {
tableConfig.columnDefaults = tableConfig.columnDefaults || {};
for (const colName of timestampDefaultColumns) {
if (dynamicFields[colName] === 'timestamptz') {
delete dynamicFields[colName];
enumFields.delete(colName);
tableConfig.columnDefaults[colName] = '';
}
}
}

// Omit columns that are explicitly 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) {
Expand Down
Loading
Loading