Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add db:truncate command with TestUtils helper #889

Merged
merged 5 commits into from Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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