Skip to content

Commit 86d6202

Browse files
authored
Use --! Included blocks on commit so uncommit can undo includes (#211)
2 parents 6ed5254 + 639ee91 commit 86d6202

8 files changed

Lines changed: 133 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,10 +796,12 @@ and when the migration is committed, watched, run, or compiled, the contents of
796796
executed:
797797
798798
```sql
799+
--! Included functions/myfunction.sql
799800
create or replace function myfunction(a int, b int)
800801
returns int as $$
801802
select a + b;
802803
$$ language sql stable;
804+
--! EndIncluded functions/myfunction.sql
803805
drop policy if exists access_by_numbers on mytable;
804806
create policy access_by_numbers on mytable for update using (myfunction(4, 2) < 42);
805807
```

__tests__/compile.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ select 2;
7171
),
7272
).toEqual(`\
7373
select 1;
74+
--! Included foo.sql
7475
select * from foo;
76+
--! EndIncluded foo.sql
7577
select 2;
7678
`);
7779
});

__tests__/helpers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,17 @@ export const makeMigrations = (commitMessage?: string) => {
266266
commitMessage ? `\n--! Message: ${commitMessage}` : ``
267267
}\n\n${MIGRATION_NOTRX_TEXT.trim()}\n`;
268268

269+
const MIGRATION_INCLUDED_FIXTURE = "select 42;\n";
270+
271+
const MIGRATION_INCLUDE_TEXT = `--!include foo.sql`;
272+
const MIGRATION_INCLUDE_COMPILED = `--! Included foo.sql\n${MIGRATION_INCLUDED_FIXTURE.trim()}\n--! EndIncluded foo.sql\n`;
273+
const MIGRATION_INCLUDE_HASH = createHash("sha1")
274+
.update(`${MIGRATION_INCLUDE_COMPILED.trim()}` + "\n")
275+
.digest("hex");
276+
const MIGRATION_INCLUDE_COMMITTED = `--! Previous: -\n--! Hash: sha1:${MIGRATION_INCLUDE_HASH}${
277+
commitMessage ? `\n--! Message: ${commitMessage}` : ``
278+
}\n\n${MIGRATION_INCLUDE_COMPILED.trim()}\n`;
279+
269280
const MIGRATION_MULTIFILE_FILES = {
270281
"migrations/links/two.sql": "select 2;",
271282
"migrations/current": {
@@ -308,6 +319,10 @@ select 3;
308319
MIGRATION_NOTRX_TEXT,
309320
MIGRATION_NOTRX_HASH,
310321
MIGRATION_NOTRX_COMMITTED,
322+
MIGRATION_INCLUDE_TEXT,
323+
MIGRATION_INCLUDE_HASH,
324+
MIGRATION_INCLUDE_COMMITTED,
325+
MIGRATION_INCLUDED_FIXTURE,
311326
MIGRATION_MULTIFILE_TEXT,
312327
MIGRATION_MULTIFILE_HASH,
313328
MIGRATION_MULTIFILE_COMMITTED,

__tests__/include.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ it("compiles an included file", async () => {
4242
FAKE_VISITED,
4343
),
4444
).toEqual(`\
45+
--! Included foo.sql
4546
select * from foo;
47+
--! EndIncluded foo.sql
4648
`);
4749
});
4850

@@ -64,9 +66,17 @@ it("compiles multiple included files", async () => {
6466
FAKE_VISITED,
6567
),
6668
).toEqual(`\
69+
--! Included dir1/foo.sql
6770
select * from foo;
71+
--! EndIncluded dir1/foo.sql
72+
--! Included dir2/bar.sql
6873
select * from bar;
74+
--! EndIncluded dir2/bar.sql
75+
--! Included dir3/baz.sql
76+
--! Included dir4/qux.sql
6977
select * from qux;
78+
--! EndIncluded dir4/qux.sql
79+
--! EndIncluded dir3/baz.sql
7080
`);
7181
});
7282

@@ -129,6 +139,7 @@ commit;
129139
FAKE_VISITED,
130140
),
131141
).toEqual(`\
142+
--! Included foo.sql
132143
begin;
133144
134145
create or replace function current_user_id() returns uuid as $$
@@ -140,6 +151,6 @@ comment on function current_user_id is E'The ID of the current user.';
140151
grant all on function current_user_id to :DATABASE_USER;
141152
142153
commit;
143-
154+
--! EndIncluded foo.sql
144155
`);
145156
});

__tests__/readCurrentMigration.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,8 @@ it("reads from current.sql, and processes included files", async () => {
111111

112112
const currentLocation = await getCurrentMigrationLocation(parsedSettings);
113113
const content = await readCurrentMigration(parsedSettings, currentLocation);
114-
expect(content).toEqual("-- TEST from foo");
114+
expect(content).toEqual(`\
115+
--! Included foo_current.sql
116+
-- TEST from foo
117+
--! EndIncluded foo_current.sql`);
115118
});

__tests__/uncommit.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,11 @@ describe.each([[undefined], ["My Commit Message"]])(
5555
const {
5656
MIGRATION_1_TEXT,
5757
MIGRATION_1_COMMITTED,
58+
MIGRATION_INCLUDE_TEXT,
59+
MIGRATION_INCLUDE_COMMITTED,
5860
MIGRATION_MULTIFILE_COMMITTED,
5961
MIGRATION_MULTIFILE_FILES,
62+
MIGRATION_INCLUDED_FIXTURE,
6063
} = makeMigrations(commitMessage);
6164

6265
it("rolls back migration", async () => {
@@ -88,6 +91,36 @@ describe.each([[undefined], ["My Commit Message"]])(
8891
).toEqual(MIGRATION_1_COMMITTED);
8992
});
9093

94+
it("rolls back a migration that has included another file", async () => {
95+
mockFs({
96+
[`migrations/committed/000001${commitMessageSlug}.sql`]:
97+
MIGRATION_INCLUDE_COMMITTED,
98+
"migrations/current.sql": "-- JUST A COMMENT\n",
99+
"migrations/fixtures/foo.sql": MIGRATION_INCLUDED_FIXTURE,
100+
});
101+
await migrate(settings);
102+
await uncommit(settings);
103+
104+
await expect(
105+
fsp.stat("migrations/committed/000001.sql"),
106+
).rejects.toMatchObject({
107+
code: "ENOENT",
108+
});
109+
expect(await fsp.readFile("migrations/current.sql", "utf8")).toEqual(
110+
(commitMessage ? `--! Message: ${commitMessage}\n\n` : "") +
111+
MIGRATION_INCLUDE_TEXT.trim() +
112+
"\n",
113+
);
114+
115+
await commit(settings);
116+
expect(
117+
await fsp.readFile(
118+
`migrations/committed/000001${commitMessageSlug}.sql`,
119+
"utf8",
120+
),
121+
).toEqual(MIGRATION_INCLUDE_COMMITTED);
122+
});
123+
91124
it("rolls back multifile migration", async () => {
92125
mockFs({
93126
[`migrations/committed/000001${commitMessageSlug}.sql`]:
@@ -139,5 +172,54 @@ describe.each([[undefined], ["My Commit Message"]])(
139172
),
140173
).toEqual(MIGRATION_MULTIFILE_COMMITTED);
141174
});
175+
176+
it("supports the same fixture twice", async () => {
177+
const current = `\
178+
--!include fixture2.sql
179+
select 22;
180+
--!include fixture2.sql
181+
`;
182+
mockFs({
183+
"migrations/fixtures/fixture1.sql": "select 'fixture1';",
184+
"migrations/fixtures/fixture2.sql":
185+
"select 1;\n--!include fixture1.sql\nselect 2;",
186+
[`migrations/committed/000001${commitMessageSlug}.sql`]:
187+
MIGRATION_1_COMMITTED,
188+
[`migrations/committed/000002${commitMessageSlug}.sql`]:
189+
MIGRATION_MULTIFILE_COMMITTED,
190+
"migrations/current/1.sql": current,
191+
});
192+
await migrate(settings);
193+
await commit(settings, commitMessage);
194+
expect(
195+
await fsp.readFile(
196+
`migrations/committed/000003${commitMessageSlug}.sql`,
197+
"utf8",
198+
),
199+
).toContain(
200+
`\
201+
--! Included fixture2.sql
202+
select 1;
203+
--! Included fixture1.sql
204+
select 'fixture1';
205+
--! EndIncluded fixture1.sql
206+
select 2;
207+
--! EndIncluded fixture2.sql
208+
select 22;
209+
--! Included fixture2.sql
210+
select 1;
211+
--! Included fixture1.sql
212+
select 'fixture1';
213+
--! EndIncluded fixture1.sql
214+
select 2;
215+
--! EndIncluded fixture2.sql
216+
`,
217+
);
218+
await uncommit(settings);
219+
220+
expect(await fsp.readFile(`migrations/current/1.sql`, "utf8")).toEqual(
221+
(commitMessage ? `--! Message: ${commitMessage}\n\n` : "") + current,
222+
);
223+
});
142224
},
143225
);

src/commands/uncommit.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,16 @@ export async function _uncommit(parsedSettings: ParsedSettings): Promise<void> {
4040
// Restore current.sql from migration
4141
const lastMigrationFilepath = lastMigration.fullPath;
4242
const contents = await fsp.readFile(lastMigrationFilepath, "utf8");
43-
const { headers, body } = parseMigrationText(lastMigrationFilepath, contents);
43+
const { headers, body: committedBody } = parseMigrationText(
44+
lastMigrationFilepath,
45+
contents,
46+
);
47+
48+
// Replace included migrations with their `--!include` equivalent
49+
const body = committedBody.replace(
50+
/^--! Included (?<filename>\S+)$[\s\S]*?^--! EndIncluded \k<filename>$/gm,
51+
(_, filename) => `--!include ${filename}`,
52+
);
4453

4554
// Drop Hash, Previous and AllowInvalidHash from headers; then write out
4655
const { Hash, Previous, AllowInvalidHash, ...otherHeaders } = headers;

src/migration.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ export async function compileIncludes(
132132
content: string,
133133
processedFiles: ReadonlySet<string>,
134134
): Promise<string> {
135+
if (/--!\s*(End)?Included?/.test(content)) {
136+
throw new Error(
137+
"`--! Included` / `--! EndIncluded` comments not allowed in user migrations. Use `--!include` instead.",
138+
);
139+
}
135140
const regex = /^--![ \t]*include[ \t]+(.*\.sql)[ \t]*$/gm;
136141

137142
// Find all includes in this `content`
@@ -208,7 +213,7 @@ export async function compileIncludes(
208213
(_match, rawSqlPath: string) => {
209214
const sqlPath = sqlPathByRawSqlPath[rawSqlPath];
210215
const content = contentBySqlPath[sqlPath];
211-
return content;
216+
return `--! Included ${rawSqlPath}\n${content.trim()}\n--! EndIncluded ${rawSqlPath}`;
212217
},
213218
);
214219

0 commit comments

Comments
 (0)