migrationLoaderStrategies lets you control how migration files are loaded based on file extension.
This is useful when you need custom loading behavior, or when you want SQL files to use the new grouped .up.sql / .down.sql strategy.
If migrationLoaderStrategies is not provided, the loader uses built-in defaults:
.sqlfiles use the legacy SQL loader (legacySql).jsand.tsfiles use the default loader (default)- unsupported extensions fall back to
default
This keeps existing behavior intact.
type MigrationLoader = (filePaths: string[]) => Promise<MigrationUnit[]>;
interface MigrationLoaderStrategy {
extensions: string[];
loader: MigrationLoader | 'default' | 'legacySql' | 'sql';
}This enables grouping *.up.sql and *.down.sql into one migration unit:
import { runner } from 'node-pg-migrate';
await runner({
databaseUrl: process.env.DATABASE_URL!,
dir: 'migrations',
direction: 'up',
migrationsTable: 'pgmigrations',
migrationLoaderStrategies: [{ extensions: ['.sql'], loader: 'sql' }],
});With this configuration:
001_init.up.sql+001_init.down.sqlare treated as one migration (001_init)- The migration
idis normalized to the equivalent.sqlform (001_init.up.sql/001_init.down.sql->001_init.sql). This means you can switch from a single001_init.sqlmigration to split.up/.downfiles (or vice versa) without creating a second entry inmigrationsTable. 001_init.sqlstill works as a single-file SQL migration- mixing
001_init.sqlwith001_init.up.sql/001_init.down.sqlthrows an error
You can provide a loader function directly:
import type { MigrationLoader } from 'node-pg-migrate';
import { runner } from 'node-pg-migrate';
const customLoader: MigrationLoader = async (filePaths) => {
// map files to migration units
return [];
};
await runner({
databaseUrl: process.env.DATABASE_URL!,
dir: 'migrations',
direction: 'up',
migrationsTable: 'pgmigrations',
migrationLoaderStrategies: [
{ extensions: ['.sql'], loader: 'sql' },
{ extensions: ['.mjs'], loader: customLoader },
],
});- Extension matching is case-insensitive
- Each strategy handles one or more extensions
- If no strategy matches an extension, the
defaultloader is used
The legacy SQL loader has been supported for a long time, even when it was less visible in the docs.
Common use cases include:
- onboarding an existing project by importing an initial schema dump as the first migration
- keeping specific advanced migrations as pure SQL when that is cleaner than a builder-based migration
So if your team already relies on plain .sql files, that workflow is still supported.
The classic SQL template uses marker comments:
-- Up Migration
-- Down MigrationBehavior for a single .sql file:
- when both markers are present,
upanddownsections are extracted - when no markers are present, the full file is treated as an
upmigration - if there is no
downsection, there is no actionabledownmigration