Skip to content

Commit

Permalink
Merge changes from pre-release branch.
Browse files Browse the repository at this point in the history
  • Loading branch information
sbosio committed Apr 8, 2024
2 parents 7a66cbc + f455e4f commit c0956e8
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 186 deletions.
40 changes: 40 additions & 0 deletions packages/cli/src/commands/pg/settings/auto-explain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {flags} from '@heroku-cli/command'
import {Args} from '@oclif/core'
import heredoc from 'tsheredoc'
import {booleanConverter, PGSettingsCommand, type Setting, type SettingKey} from '../../../lib/pg/setter'

// ref: https://www.postgresql.org/docs/current/auto-explain.html
export default class AutoExplain extends PGSettingsCommand {
static topic = 'pg';
static description = heredoc(`
Automatically log execution plans of queries without running EXPLAIN by hand.
The auto_explain module is loaded at session-time so existing connections will not be logged.
Restart your Heroku app and/or restart existing connections for logging to start taking place.'
`)

static flags = {
app: flags.app({required: true}),
remote: flags.remote(),
}

static args = {
database: Args.string(),
value: Args.string(),
}

static strict = false

protected settingKey: SettingKey = 'auto_explain'

protected convertValue(val: boolean): boolean {
return booleanConverter(val)
}

protected explain(setting: Setting): string {
if (setting.value) {
return 'Execution plans of queries will be logged for future connections.'
}

return 'Execution plans of queries will not be logged for future connections.'
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Args} from '@oclif/core'
import heredoc from 'tsheredoc'
import {PGSettingsCommand, boolean} from '../../../../lib/pg/setter'
import {PGSettingsCommand, booleanConverter, BooleanAsString} from '../../../../lib/pg/setter'
import {FormationSetting, Setting} from '../../../../lib/pg/types'

export default class LogAnalyze extends PGSettingsCommand {
Expand All @@ -17,10 +17,10 @@ export default class LogAnalyze extends PGSettingsCommand {
value: Args.string(),
}

protected settingsName = 'auto_explain.log_analyze' as FormationSetting
protected settingKey = 'auto_explain.log_analyze' as FormationSetting

protected convertValue(val: string): boolean {
return boolean(val)
protected convertValue(val: BooleanAsString): boolean {
return booleanConverter(val)
}

protected explain(setting: Setting<boolean>) {
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/commands/pg/settings/auto-explain/log-buffers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Args} from '@oclif/core'
import heredoc from 'tsheredoc'
import {PGSettingsCommand, type Setting, type SettingKey, booleanConverter} from '../../../../lib/pg/setter'

export default class LogBuffersWaits extends PGSettingsCommand {
static topic = 'pg'
static description = heredoc(`
Includes buffer usage statistics when execution plans are logged.
This is equivalent to calling EXPLAIN BUFFERS and can only be used in conjunction with pg:settings:auto-explain:log-analyze turned on.
`)

static args = {
database: Args.string(),
value: Args.string(),
}

protected settingKey: SettingKey = 'auto_explain.log_buffers'

protected convertValue(val: boolean): boolean {
return booleanConverter(val)
}

protected explain(setting: Setting) {
if (setting.value) {
return 'Buffer statistics have been enabled for auto_explain.'
}

return 'Buffer statistics have been disabled for auto_explain.'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {Args} from '@oclif/core'
import heredoc from 'tsheredoc'
import {PGSettingsCommand, type Setting, type SettingKey} from '../../../../lib/pg/setter'

export default class LogMinDuration extends PGSettingsCommand {
static topic = 'pg'
static description = heredoc(`
Sets the minimum execution time in milliseconds for a statement's plan to be logged.
Setting this value to 0 will log all queries. Setting this value to -1 will disable logging entirely.
`)

static args = {
database: Args.string(),
value: Args.integer(),
}

protected settingKey:SettingKey = 'auto_explain.log_min_duration'

protected convertValue(val: number): number {
return val
}

protected explain(setting: Setting) {
if (setting.value === -1) {
return 'Execution plan logging has been disabled.'
}

if (setting.value === 0) {
return 'All queries will have their execution plans logged.'
}

return `All execution plans will be logged for queries taking up to ${setting.value} milliseconds or more.`
}
}
54 changes: 35 additions & 19 deletions packages/cli/src/lib/pg/setter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,8 @@ import host from './host'
import {essentialPlan} from './util'
import {FormationSetting, Setting, SettingsResponse} from './types'

export function boolean(value: string): boolean {
switch (value) {
case 'true': case 'TRUE': case 'ON': case 'on':
return true
case 'false': case 'FALSE': case 'OFF': case 'off':
return false
default:
throw new TypeError('Invalid value. Valid options are: a boolean value')
}
}

export abstract class PGSettingsCommand extends Command {
protected abstract settingsName: FormationSetting
protected abstract settingKey: FormationSetting
protected abstract convertValue(val: string): unknown
protected abstract explain(setting: Setting<unknown>): string

Expand All @@ -27,7 +16,7 @@ export abstract class PGSettingsCommand extends Command {
}

public async run(): Promise<void> {
const {flags, args} = await this.parse()
const {flags, args} = await this.parse()
const {app} = flags
const {value, database} = args as {value: string | undefined, database: string | undefined}

Expand All @@ -38,16 +27,43 @@ export abstract class PGSettingsCommand extends Command {
if (value) {
const {body: settings} = await this.heroku.patch<SettingsResponse>(`/postgres/v0/databases/${db.id}/config`, {
hostname: host(),
body: {[this.settingsName]: this.convertValue(value)},
body: {[this.settingKey]: this.convertValue(value)},
})
const setting = settings[this.settingsName]
ux.log(`${this.settingsName.replace(/_/g, '-')} has been set to ${setting.value} for ${db.name}.`)
const setting = settings[this.settingKey]
ux.log(`${this.settingKey.replace(/_/g, '-')} has been set to ${setting.value} for ${db.name}.`)
ux.log(this.explain(setting))
} else {
const {body: settings} = await this.heroku.get<SettingsResponse>(`/postgres/v0/databases/${db.id}/config`, {hostname: host()})
const setting = settings[this.settingsName]
ux.log(`${this.settingsName.replace(/_/g, '-')} is set to ${setting.value} for ${db.name}.`)
ux.log(this.explain(setting))
const setting = settings[this.settingKey]
ux.log(`${this.settingKey.replace(/_/g, '-')} is set to ${setting.value} for ${db.name}.`)
}
}
}

export type BooleanAsString = 'on' | 'ON' | 'true' | 'TRUE' | 'off' | 'OFF' | 'false' | 'FALSE'
export const booleanConverter = (value: BooleanAsString) => {
switch (value) {
case 'true':
case 'TRUE':
case 'ON':
case 'on':
return true
case 'false':
case 'FALSE':
case 'OFF':
case 'off':
case null:
return false
default:
throw new TypeError('Invalid value. Valid options are: a boolean value')
}
}

export const numericConverter = (value: string | number) => {
const n = Number(value)
if (!Number.isFinite(n)) {
throw new TypeError('Invalid value. Valid options are: a numeric value')
}

return n
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {expect} from '@oclif/test'
import * as nock from 'nock'
import {stdout} from 'stdout-stderr'
import heredoc from 'tsheredoc'
import runCommand from '../../../../helpers/runCommand'
import Cmd from '../../../../../src/commands/pg/settings/auto-explain'

describe('pg:settings:auto-explain', () => {
let api: nock.Scope
let pg: nock.Scope

beforeEach(() => {
const addon = {
id: 1,
name: 'postgres-1',
app: {name: 'myapp'},
config_vars: ['READONLY_URL', 'DATABASE_URL', 'HEROKU_POSTGRESQL_RED_URL'],
plan: {name: 'heroku-postgresql:standard-0'},
}

api = nock('https://api.heroku.com')
api.post('/actions/addons/resolve', {
app: 'myapp',
addon: 'test-database',
}).reply(200, [addon])

pg = nock('https://api.data.heroku.com')
})

afterEach(() => {
api.done()
pg.done()
})

it('shows settings for auto_explain with value', async () => {
pg.get('/postgres/v0/databases/1/config').reply(200, {auto_explain: {value: 'test_value'}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain is set to test_value for postgres-1.
Execution plans of queries will be logged for future connections.
`))
})

it('shows settings for auto_explain with no value', async () => {
pg.get('/postgres/v0/databases/1/config').reply(200, {auto_explain: {value: ''}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain is set to for postgres-1.
Execution plans of queries will not be logged for future connections.
`))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {expect} from '@oclif/test'
import * as nock from 'nock'
import {stdout} from 'stdout-stderr'
import heredoc from 'tsheredoc'
import runCommand from '../../../../../helpers/runCommand'
import Cmd from '../../../../../../src/commands/pg/settings/auto-explain/log-buffers'
import * as fixtures from '../../../../../fixtures/addons/fixtures'

describe('pg:settings:auto-explain:log-buffers', () => {
const addon = fixtures.addons['dwh-db']

beforeEach(() => {
nock('https://api.heroku.com')
.post('/actions/addons/resolve', {
app: 'myapp',
addon: 'test-database',
}).reply(200, [addon])
})

afterEach(() => {
nock.cleanAll()
})

it('shows settings for auto_explain with value', async () => {
nock('https://api.data.heroku.com')
.get(`/postgres/v0/databases/${addon.id}/config`).reply(200, {'auto_explain.log_buffers': {value: 'test_value'}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain.log-buffers is set to test_value for ${addon.name}.
Buffer statistics have been enabled for auto_explain.
`))
})

it('shows settings for auto_explain with no value', async () => {
nock('https://api.data.heroku.com')
.get(`/postgres/v0/databases/${addon.id}/config`).reply(200, {'auto_explain.log_buffers': {value: ''}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain.log-buffers is set to for ${addon.name}.
Buffer statistics have been disabled for auto_explain.
`))
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {expect} from '@oclif/test'
import * as nock from 'nock'
import {stdout} from 'stdout-stderr'
import heredoc from 'tsheredoc'
import runCommand from '../../../../../helpers/runCommand'
import Cmd from '../../../../../../src/commands/pg/settings/auto-explain/log-min-duration'
import * as fixtures from '../../../../../fixtures/addons/fixtures'

describe('pg:settings:auto-explain:log-min-duration', () => {
const addon = fixtures.addons['dwh-db']

beforeEach(() => {
nock('https://api.heroku.com')
.post('/actions/addons/resolve', {
app: 'myapp',
addon: 'test-database',
}).reply(200, [addon])
})

afterEach(() => {
nock.cleanAll()
})

it('shows settings for auto_explain with value', async () => {
nock('https://api.data.heroku.com')
.get(`/postgres/v0/databases/${addon.id}/config`).reply(200, {'auto_explain.log_min_duration': {value: 'test_value'}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain.log-min-duration is set to test_value for ${addon.name}.
All execution plans will be logged for queries taking up to test_value milliseconds or more.
`))
})

it('shows settings for auto_explain with no value', async () => {
nock('https://api.data.heroku.com')
.get(`/postgres/v0/databases/${addon.id}/config`).reply(200, {'auto_explain.log_min_duration': {value: -1}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain.log-min-duration is set to -1 for ${addon.name}.
Execution plan logging has been disabled.
`))
})

it('shows settings for auto_explain with no value', async () => {
nock('https://api.data.heroku.com')
.get(`/postgres/v0/databases/${addon.id}/config`).reply(200, {'auto_explain.log_min_duration': {value: 0}})
await runCommand(Cmd, ['--app', 'myapp', 'test-database'])
expect(stdout.output).to.equal(heredoc(`
auto-explain.log-min-duration is set to 0 for ${addon.name}.
All queries will have their execution plans logged.
`))
})
})
24 changes: 0 additions & 24 deletions packages/pg-v5/commands/settings/auto_explain.js

This file was deleted.

Loading

0 comments on commit c0956e8

Please sign in to comment.