Skip to content

Commit

Permalink
fix: Ensura hasura metadata is cleaned up after schema drop
Browse files Browse the repository at this point in the history
  • Loading branch information
morgsmccauley committed Jun 20, 2024
1 parent 91135bb commit ca53f70
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 18 deletions.
15 changes: 15 additions & 0 deletions runner/src/hasura-client/__snapshots__/hasura-client.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,21 @@ exports[`HasuraClient drops a datasource 1`] = `
}
`;

exports[`HasuraClient drops a schema 1`] = `
[
[
"mock-hasura-endpoint/v2/query",
{
"body": "{"type":"run_sql","args":{"sql":"DROP schema IF EXISTS schemaName CASCADE","read_only":false,"source":"dbName"}}",
"headers": {
"X-Hasura-Admin-Secret": "mock-hasura-admin-secret",
},
"method": "POST",
},
],
]
`;

exports[`HasuraClient gets table names within a schema 1`] = `
{
"args": {
Expand Down
14 changes: 14 additions & 0 deletions runner/src/hasura-client/hasura-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ describe('HasuraClient', () => {
expect(mockFetch.mock.calls).toMatchSnapshot();
});

it('drops a schema', async () => {
const mockFetch = jest
.fn()
.mockResolvedValue({
status: 200,
text: () => JSON.stringify({})
});
const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }, config);

await client.dropSchema('dbName', 'schemaName');

expect(mockFetch.mock.calls).toMatchSnapshot();
});

it('checks if a schema exists within source', async () => {
const mockFetch = jest
.fn()
Expand Down
7 changes: 7 additions & 0 deletions runner/src/hasura-client/hasura-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ export default class HasuraClient {
return result.length > 1;
}

async dropSchema (source: string, schemaName: string): Promise<any> {
return await this.executeSql(
`DROP schema IF EXISTS ${schemaName} CASCADE`,
{ source, readOnly: false }
);
}

async createSchema (source: string, schemaName: string): Promise<any> {
return await this.executeSql(`CREATE schema ${schemaName}`, {
source,
Expand Down
13 changes: 4 additions & 9 deletions runner/src/provisioner/provisioner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('Provisioner', () => {
dropDatasource: jest.fn().mockReturnValueOnce(null),
executeSqlOnSchema: jest.fn().mockReturnValueOnce(null),
createSchema: jest.fn().mockReturnValueOnce(null),
dropSchema: jest.fn().mockReturnValueOnce(null),
doesSourceExist: jest.fn().mockReturnValueOnce(false),
doesSchemaExist: jest.fn().mockReturnValueOnce(false),
untrackTables: jest.fn().mockReturnValueOnce(null),
Expand Down Expand Up @@ -75,15 +76,14 @@ describe('Provisioner', () => {
describe('deprovision', () => {
it('removes schema level resources', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [{ schema_name: 'another_one' }] }); // list schemas

await provisioner.deprovision(indexerConfig);

expect(hasuraClient.dropSchema).toBeCalledWith(indexerConfig.databaseName(), indexerConfig.schemaName());
expect(userPgClientQuery.mock.calls).toEqual([
['DROP SCHEMA IF EXISTS morgs_near_test_function CASCADE'],
["SELECT cron.unschedule('morgs_near_test_function_sys_logs_create_partition');"],
["SELECT cron.unschedule('morgs_near_test_function_sys_logs_delete_partition');"],
["SELECT schema_name FROM information_schema.schemata WHERE schema_owner = 'morgs_near'"],
Expand All @@ -92,15 +92,14 @@ describe('Provisioner', () => {

it('removes database level resources', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [] }); // list schemas

await provisioner.deprovision(indexerConfig);

expect(hasuraClient.dropSchema).toBeCalledWith(indexerConfig.databaseName(), indexerConfig.schemaName());
expect(userPgClientQuery.mock.calls).toEqual([
['DROP SCHEMA IF EXISTS morgs_near_test_function CASCADE'],
["SELECT cron.unschedule('morgs_near_test_function_sys_logs_create_partition');"],
["SELECT cron.unschedule('morgs_near_test_function_sys_logs_delete_partition');"],
["SELECT schema_name FROM information_schema.schemata WHERE schema_owner = 'morgs_near'"],
Expand All @@ -114,7 +113,6 @@ describe('Provisioner', () => {

it('handles revoke cron failures', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [] }); // list schemas
Expand All @@ -127,7 +125,6 @@ describe('Provisioner', () => {

it('handles drop role failures', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [] }); // list schemas
Expand All @@ -141,7 +138,6 @@ describe('Provisioner', () => {

it('handles drop database failures', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [] }); // list schemas
Expand All @@ -153,7 +149,6 @@ describe('Provisioner', () => {

it('handles drop datasource failures', async () => {
userPgClientQuery = jest.fn()
.mockResolvedValueOnce(null) // drop schema
.mockResolvedValueOnce(null) // unschedule create partition job
.mockResolvedValueOnce(null) // unschedule delete partition job
.mockResolvedValueOnce({ rows: [] }); // list schemas
Expand All @@ -164,7 +159,7 @@ describe('Provisioner', () => {
});

it('handles drop schema failures', async () => {
userPgClientQuery = jest.fn().mockRejectedValue(new Error('failed to drop'));
hasuraClient.dropSchema = jest.fn().mockRejectedValue(new Error('failed to drop schema'));
await expect(provisioner.deprovision(indexerConfig)).rejects.toThrow('Failed to deprovision: Failed to drop schema: failed to drop');
});

Expand Down
13 changes: 4 additions & 9 deletions runner/src/provisioner/provisioner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,10 @@ export default class Provisioner {
return await wrapError(async () => await this.hasuraClient.addDatasource(userName, password, databaseName), 'Failed to add datasource');
}

async dropSchema (userName: string, schemaName: string): Promise<void> {
async dropSchemaAndMetadata (databaseName: string, schemaName: string): Promise<void> {
await wrapError(async () => {
const userDbConnectionParameters = await this.getPostgresConnectionParameters(userName);

const userPgClient = new this.PgClient(userDbConnectionParameters);

await userPgClient.query(this.pgFormat('DROP SCHEMA IF EXISTS %I CASCADE', schemaName));

await userPgClient.end();
// Need to drop via Hasura to ensure metadata is cleaned up
await this.hasuraClient.dropSchema(databaseName, schemaName);
}, 'Failed to drop schema');
}

Expand Down Expand Up @@ -319,7 +314,7 @@ export default class Provisioner {

public async deprovision (config: ProvisioningConfig): Promise<void> {
await wrapError(async () => {
await this.dropSchema(config.userName(), config.schemaName());
await this.dropSchemaAndMetadata(config.userName(), config.schemaName());
await this.removeLogPartitionJobs(config.userName(), config.schemaName());

const schemas = await this.listUserOwnedSchemas(config.userName());
Expand Down

0 comments on commit ca53f70

Please sign in to comment.