Skip to content

Commit

Permalink
Add db:truncate command with TestUtils helper (#889)
Browse files Browse the repository at this point in the history
* fix: truncate a table with reserved name was throwing with PG

Like "user" that is a reserved word in postgres

* feat: add `db:truncate` command + TestUtils helper

* test: fix failing tests

* chore: fix container ports

* fix: cleanup table created in another test
  • Loading branch information
Julien-R44 committed Oct 26, 2022
1 parent 6afea22 commit 215631e
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 8 deletions.
1 change: 1 addition & 0 deletions adonis-typings/test-utils.ts
Expand Up @@ -15,6 +15,7 @@ declare module '@ioc:Adonis/Core/TestUtils' {
db(connectionName?: string): {
seed: HookCallback
migrate: HookCallback
truncate: HookCallback
}
}
}
139 changes: 139 additions & 0 deletions commands/DbTruncate.ts
@@ -0,0 +1,139 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { BaseCommand, flags } from '@adonisjs/core/build/standalone'
import { QueryClientContract } from '@ioc:Adonis/Lucid/Database'

export default class DbTruncate extends BaseCommand {
public static commandName = 'db:truncate'
public static description = 'Truncate all tables in database'
public static settings = {
loadApp: true,
}

/**
* Choose a custom pre-defined connection. Otherwise, we use the
* default connection
*/
@flags.string({ description: 'Define a custom database connection', alias: 'c' })
public connection: string

/**
* Force command execution in production
*/
@flags.boolean({ description: 'Explicitly force command to run in production' })
public force: boolean

/**
* Not a valid connection
*/
private printNotAValidConnection(connection: string) {
this.logger.error(
`"${connection}" is not a valid connection name. Double check "config/database" file`
)
}

/**
* Prompts to take consent when truncating the database in production
*/
private async takeProductionConstent(): Promise<boolean> {
/**
* Do not prompt when CLI is not interactive
*/
if (!this.isInteractive) {
return false
}

const question = 'You are in production environment. Want to continue truncating the database?'
try {
return await this.prompt.confirm(question)
} catch (error) {
return false
}
}

/**
* Truncate all tables except adonis migrations table
*/
private async performTruncate(client: QueryClientContract) {
let tables = await client.getAllTables(['public'])
tables = tables.filter((table) => !['adonis_schema', 'adonis_schema_versions'].includes(table))

await Promise.all(tables.map((table) => client.truncate(table, true)))
this.logger.success('Truncated tables successfully')
}

/**
* Run as a subcommand. Never close database connections or exit
* process inside this method
*/
private async runAsSubCommand() {
const db = this.application.container.use('Adonis/Lucid/Database')
this.connection = this.connection || db.primaryConnectionName
const connection = db.connection(this.connection || db.primaryConnectionName)

/**
* Continue with clearing the database when not in production
* or force flag is passed
*/
let continueTruncate = !this.application.inProduction || this.force
if (!continueTruncate) {
continueTruncate = await this.takeProductionConstent()
}

/**
* Do not continue when in prod and the prompt was cancelled
*/
if (!continueTruncate) {
return
}

/**
* Invalid database connection
*/
if (!db.manager.has(this.connection)) {
this.printNotAValidConnection(this.connection)
this.exitCode = 1
return
}

await this.performTruncate(connection)
}

/**
* Branching out, so that if required we can implement
* "runAsMain" separately from "runAsSubCommand".
*
* For now, they both are the same
*/
private async runAsMain() {
await this.runAsSubCommand()
}

/**
* Handle command
*/
public async run(): Promise<void> {
if (this.isMain) {
await this.runAsMain()
} else {
await this.runAsSubCommand()
}
}

/**
* Lifecycle method invoked by ace after the "run"
* method.
*/
public async completed() {
if (this.isMain) {
await this.application.container.use('Adonis/Lucid/Database').manager.closeAll(true)
}
}
}
3 changes: 2 additions & 1 deletion commands/index.ts
Expand Up @@ -9,11 +9,12 @@

export default [
'@adonisjs/lucid/build/commands/DbSeed',
'@adonisjs/lucid/build/commands/DbWipe',
'@adonisjs/lucid/build/commands/DbTruncate',
'@adonisjs/lucid/build/commands/MakeModel',
'@adonisjs/lucid/build/commands/MakeMigration',
'@adonisjs/lucid/build/commands/MakeSeeder',
'@adonisjs/lucid/build/commands/MakeFactory',
'@adonisjs/lucid/build/commands/DbWipe',
'@adonisjs/lucid/build/commands/Migration/Run',
'@adonisjs/lucid/build/commands/Migration/Rollback',
'@adonisjs/lucid/build/commands/Migration/Status',
Expand Down
4 changes: 2 additions & 2 deletions scripts/docker-compose.yml
Expand Up @@ -49,7 +49,7 @@ services:
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
ports:
- '3305:3306'
- '3306:3306'
expose:
- '3306'

Expand All @@ -67,7 +67,7 @@ services:
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
ports:
- 21433:1433
- 1433:1433
expose:
- '1433'
environment:
Expand Down
4 changes: 4 additions & 0 deletions src/Bindings/TestUtils.ts
Expand Up @@ -13,6 +13,7 @@ import type Ace from '@ioc:Adonis/Core/Ace'

import { TestsSeeder } from '../TestUtils/Seeder'
import { TestsMigrator } from '../TestUtils/Migration'
import { TestsTruncator } from '../TestUtils/Truncator'

/**
* Define database testing utilities
Expand All @@ -26,6 +27,9 @@ export function defineTestUtils(testUtils: TestUtilsContract, ace: typeof Ace) {
seed() {
return new TestsSeeder(ace, connectionName).run()
},
truncate() {
return new TestsTruncator(ace, connectionName).run()
},
}
})
}
4 changes: 2 additions & 2 deletions src/Dialects/Pg.ts
Expand Up @@ -79,8 +79,8 @@ export class PgDialect implements DialectContract {
*/
public async truncate(table: string, cascade: boolean = false) {
return cascade
? this.client.rawQuery(`TRUNCATE ${table} RESTART IDENTITY CASCADE;`)
: this.client.rawQuery(`TRUNCATE ${table};`)
? this.client.rawQuery(`TRUNCATE "${table}" RESTART IDENTITY CASCADE;`)
: this.client.rawQuery(`TRUNCATE "${table}";`)
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/Dialects/Redshift.ts
Expand Up @@ -87,8 +87,8 @@ export class RedshiftDialect implements DialectContract {
*/
public async truncate(table: string, cascade: boolean = false) {
return cascade
? this.client.rawQuery(`TRUNCATE ${table} RESTART IDENTITY CASCADE;`)
: this.client.rawQuery(`TRUNCATE ${table};`)
? this.client.rawQuery(`TRUNCATE "${table}" RESTART IDENTITY CASCADE;`)
: this.client.rawQuery(`TRUNCATE "${table}";`)
}

/**
Expand Down
37 changes: 37 additions & 0 deletions src/TestUtils/Truncator.ts
@@ -0,0 +1,37 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import type Ace from '@ioc:Adonis/Core/Ace'

/**
* Migrator class to be used for testing.
*/
export class TestsTruncator {
constructor(private ace: typeof Ace, private connectionName?: string) {}

private async runCommand(commandName: string, args: string[] = []) {
if (this.connectionName) {
args.push(`--connection=${this.connectionName}`)
}

const command = await this.ace.exec(commandName, args)
if (command.exitCode) {
if (command.error) {
throw command.error
} else {
throw new Error(`"${commandName}" failed`)
}
}
}

public async run() {
await this.runCommand('migration:run', ['--compact-output'])
return () => this.runCommand('db:truncate')
}
}
79 changes: 79 additions & 0 deletions test/commands/db-truncate.spec.ts
@@ -0,0 +1,79 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { test } from '@japa/runner'
import { Kernel } from '@adonisjs/core/build/standalone'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
import { fs, setup, cleanup, getDb, setupApplication } from '../../test-helpers'
import DbTruncate from '../../commands/DbTruncate'

let app: ApplicationContract
let db: ReturnType<typeof getDb>

test.group('db:truncate', (group) => {
group.each.setup(async () => {
app = await setupApplication()
return () => fs.cleanup()
})

group.each.setup(async () => {
db = getDb(app)
app.container.bind('Adonis/Lucid/Database', () => db)
await setup()

return async () => {
await cleanup()
await cleanup(['adonis_schema', 'adonis_schema_versions'])
await db.manager.closeAll()
}
})

test('should truncate all tables', async ({ assert }) => {
const kernel = new Kernel(app).mockConsoleOutput()
kernel.register([DbTruncate])

await db.table('users').insert({ username: 'bonjour' })
await db.table('users').insert({ username: 'bonjour2' })
await db.table('friends').insert({ username: 'bonjour' })

await kernel.exec('db:truncate', [])

const usersCount = await db.from('users').count('*', 'total')
const friendsCount = await db.from('friends').count('*', 'total')

assert.equal(usersCount[0]['total'], 0)
assert.equal(friendsCount[0]['total'], 0)
})

test('should not truncate adonis migrations tables', async ({ assert }) => {
const kernel = new Kernel(app).mockConsoleOutput()
kernel.register([DbTruncate])

await db.connection().schema.createTable('adonis_schema', (table) => {
table.increments('id')
table.string('name')
})

await db.connection().schema.createTable('adonis_schema_versions', (table) => {
table.increments('id')
table.string('name')
})

await db.table('adonis_schema').insert({ name: 'bonjour' })
await db.table('adonis_schema_versions').insert({ name: 'bonjour' })

await kernel.exec('db:truncate', [])

const adonisSchemaCount = await db.from('adonis_schema').count('*', 'total')
const adonisSchemaVersionsCount = await db.from('adonis_schema_versions').count('*', 'total')

assert.equal(adonisSchemaCount[0]['total'], 1)
assert.equal(adonisSchemaVersionsCount[0]['total'], 1)
})
})
2 changes: 1 addition & 1 deletion test/database/drop-tables.spec.ts
Expand Up @@ -26,7 +26,7 @@ test.group('Query client | drop tables', (group) => {
})

group.teardown(async () => {
await cleanup(['temp_posts', 'temp_users', 'table_that_should_not_be_dropped'])
await cleanup(['temp_posts', 'temp_users', 'table_that_should_not_be_dropped', 'ignore_me'])
await cleanup()
await fs.cleanup()
})
Expand Down
16 changes: 16 additions & 0 deletions test/database/query-client.spec.ts
Expand Up @@ -143,6 +143,22 @@ test.group('Query client', (group) => {
await connection.disconnect()
})
}

test('truncate a table with reserved keywork', async () => {
const connection = new Connection('primary', getConfig(), app.logger)
connection.connect()

await connection.client?.schema.createTableIfNotExists('user', (table) => {
table.increments('id').primary()
table.string('username')
})

const client = new QueryClient('write', connection, app.container.use('Adonis/Core/Event'))
await client.truncate('user', true)

await connection.client?.schema.dropTable('user')
await connection.disconnect()
})
})

test.group('Query client | dual mode', (group) => {
Expand Down

0 comments on commit 215631e

Please sign in to comment.