diff --git a/README.md b/README.md index 8c5155a..85b12a0 100644 --- a/README.md +++ b/README.md @@ -768,8 +768,9 @@ drop policy if exists access_by_numbers on mytable; create policy access_by_numbers on mytable for update using (myfunction(4, 2) < 42); ``` -and when the migration is committed or watched, the contents of `myfunction.sql` -will be included in the result, such that the following SQL is executed: +and when the migration is committed, watched, run, or compiled, the contents of +`myfunction.sql` will be included in the result, such that the following SQL is +executed: ```sql create or replace function myfunction(a int, b int) diff --git a/__tests__/compile.test.ts b/__tests__/compile.test.ts index 4747483..51d0d97 100644 --- a/__tests__/compile.test.ts +++ b/__tests__/compile.test.ts @@ -1,12 +1,19 @@ import "./helpers"; -import * as mockFs from "mock-fs"; +import mockFs from "mock-fs"; -import { compile } from "../src"; +import { compile, Settings } from "../src"; let old: string | undefined; -beforeAll(() => { +let settings: Settings; +beforeAll(async () => { old = process.env.DATABASE_AUTHENTICATOR; process.env.DATABASE_AUTHENTICATOR = "dbauth"; + settings = { + connectionString: "postgres://dbowner:dbpassword@dbhost:1221/dbname", + placeholders: { + ":DATABASE_AUTHENTICATOR": "!ENV", + }, + }; }); afterAll(() => { process.env.DATABASE_AUTHENTICATOR = old; @@ -19,12 +26,7 @@ afterEach(() => { it("compiles SQL with settings", async () => { expect( await compile( - { - connectionString: "postgres://dbowner:dbpassword@dbhost:1221/dbname", - placeholders: { - ":DATABASE_AUTHENTICATOR": "!ENV", - }, - }, + settings, `\ BEGIN; GRANT CONNECT ON DATABASE :DATABASE_NAME TO :DATABASE_OWNER; @@ -38,6 +40,7 @@ CREATE EXTENSION IF NOT EXISTS citext WITH SCHEMA public; CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; COMMIT; `, + "stdin", ), ).toEqual(`\ BEGIN; @@ -53,3 +56,20 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public; COMMIT; `); }); + +it("will compile included files", async () => { + mockFs({ + "migrations/fixtures/foo.sql": "select * from foo;", + }); + expect( + await compile( + settings, + `\ +--!include foo.sql +`, + `${process.cwd()}/migrations/current.sql`, + ), + ).toEqual(`\ +select * from foo; +`); +}); diff --git a/src/commands/compile.ts b/src/commands/compile.ts index 2516cf5..5ccbc00 100644 --- a/src/commands/compile.ts +++ b/src/commands/compile.ts @@ -1,7 +1,8 @@ import * as fsp from "fs/promises"; +import { resolve } from "path"; import { CommandModule } from "yargs"; -import { compilePlaceholders } from "../migration"; +import { compileIncludes, compilePlaceholders } from "../migration"; import { parseSettings, Settings } from "../settings"; import { CommonArgv, getSettings, readStdin } from "./_common"; @@ -12,10 +13,16 @@ interface CompileArgv extends CommonArgv { export async function compile( settings: Settings, content: string, + filename: string, shadow = false, ): Promise { const parsedSettings = await parseSettings(settings, shadow); - return compilePlaceholders(parsedSettings, content, shadow); + const parsedContent = await compileIncludes( + parsedSettings, + content, + new Set([filename]), + ); + return compilePlaceholders(parsedSettings, parsedContent, shadow); } export const compileCommand: CommandModule< @@ -25,7 +32,7 @@ export const compileCommand: CommandModule< command: "compile [file]", aliases: [], describe: `\ -Compiles a SQL file, inserting all the placeholders and returning the result to STDOUT`, +Compiles a SQL file, resolving includes, inserting all the placeholders and returning the result to STDOUT`, builder: { shadow: { type: "boolean", @@ -35,12 +42,15 @@ Compiles a SQL file, inserting all the placeholders and returning the result to }, handler: async (argv) => { const settings = await getSettings({ configFile: argv.config }); - const content = + const { content, filename } = typeof argv.file === "string" - ? await fsp.readFile(argv.file, "utf8") - : await readStdin(); + ? { + filename: resolve(argv.file), + content: await fsp.readFile(argv.file, "utf8"), + } + : { filename: "stdin", content: await readStdin() }; - const compiled = await compile(settings, content, argv.shadow); + const compiled = await compile(settings, content, filename, argv.shadow); // eslint-disable-next-line no-console console.log(compiled); diff --git a/src/commands/run.ts b/src/commands/run.ts index 7d963c2..61c56e3 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -1,10 +1,11 @@ import * as fsp from "fs/promises"; +import { resolve } from "path"; import { QueryResultRow } from "pg"; import { CommandModule } from "yargs"; import { DO_NOT_USE_DATABASE_URL } from "../actions"; import { runQueryWithErrorInstrumentation } from "../instrumentation"; -import { compilePlaceholders } from "../migration"; +import { compileIncludes, compilePlaceholders } from "../migration"; import { withClient } from "../pgReal"; import { makeRootDatabaseConnectionString, @@ -35,7 +36,12 @@ export async function run( } = {}, ): Promise { const parsedSettings = await parseSettings(settings, shadow); - const sql = compilePlaceholders(parsedSettings, content, shadow); + const parsedContent = await compileIncludes( + parsedSettings, + content, + new Set([filename]), + ); + const sql = compilePlaceholders(parsedSettings, parsedContent, shadow); const baseConnectionString = rootDatabase ? parsedSettings.rootConnectionString : shadow @@ -62,7 +68,7 @@ export const runCommand: CommandModule, RunArgv> = { command: "run [file]", aliases: [], describe: `\ -Compiles a SQL file, inserting all the placeholders, and then runs it against the database. Useful for seeding. If called from an action will automatically run against the same database (via GM_DBURL envvar) unless --shadow or --rootDatabase are supplied.`, +Compiles a SQL file, resolving includes, inserting all the placeholders, and then runs it against the database. Useful for seeding. If called from an action will automatically run against the same database (via GM_DBURL envvar) unless --shadow or --rootDatabase are supplied.`, builder: { shadow: { type: "boolean", @@ -105,7 +111,7 @@ Compiles a SQL file, inserting all the placeholders, and then runs it against th const { content, filename } = typeof argv.file === "string" ? { - filename: argv.file, + filename: resolve(argv.file), content: await fsp.readFile(argv.file, "utf8"), } : { filename: "stdin", content: await readStdin() };