Skip to content

Commit

Permalink
feat: Beautify generated SQL for migrations (#6685)
Browse files Browse the repository at this point in the history
* feat: Beautify generated SQL for migrations

Allows user to pass an optional flag to beautify generated SQL for migrations

Closes: #4415

* fixed formatter version

Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
  • Loading branch information
artysidorenko and pleerock committed Sep 12, 2020
1 parent 330262d commit 370442c
Show file tree
Hide file tree
Showing 8 changed files with 326 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/migrations.md
Expand Up @@ -195,7 +195,7 @@ export class PostRefactoringTIMESTAMP implements MigrationInterface {
```

See, you don't need to write the queries on your own.
The rule of thumb for generating migrations is that you generate them after "each" change you made to your models.
The rule of thumb for generating migrations is that you generate them after "each" change you made to your models. To apply multi-line formatting to your generated migration queries, use the `p` (alias for `--pretty`) flag.

## Connection option
If you need to run/revert your migrations for another connection rather than the default, use the `-c` (alias for `--connection`) and pass the config name as an argument
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -104,6 +104,7 @@
"typescript": "^3.3.3333"
},
"dependencies": {
"@sqltools/formatter": "1.2.2",
"app-root-path": "^3.0.0",
"buffer": "^5.5.0",
"chalk": "^4.1.0",
Expand Down
24 changes: 24 additions & 0 deletions src/commands/MigrationGenerateCommand.ts
Expand Up @@ -7,6 +7,7 @@ import {camelCase} from "../util/StringUtils";
import * as yargs from "yargs";
import {AuroraDataApiDriver} from "../driver/aurora-data-api/AuroraDataApiDriver";
import chalk from "chalk";
import { format } from "@sqltools/formatter/lib/sqlFormatter";

/**
* Generates a new migration file with sql needs to be executed to update schema.
Expand All @@ -33,6 +34,12 @@ export class MigrationGenerateCommand implements yargs.CommandModule {
alias: "dir",
describe: "Directory where migration should be created."
})
.option("p", {
alias: "pretty",
type: "boolean",
default: false,
describe: "Pretty-print generated SQL",
})
.option("f", {
alias: "config",
default: "ormconfig",
Expand Down Expand Up @@ -76,6 +83,16 @@ export class MigrationGenerateCommand implements yargs.CommandModule {
});
connection = await createConnection(connectionOptions);
const sqlInMemory = await connection.driver.createSchemaBuilder().log();

if (args.pretty) {
sqlInMemory.upQueries.forEach(upQuery => {
upQuery.query = MigrationGenerateCommand.prettifyQuery(upQuery.query);
});
sqlInMemory.downQueries.forEach(downQuery => {
downQuery.query = MigrationGenerateCommand.prettifyQuery(downQuery.query);
});
}

const upSqls: string[] = [], downSqls: string[] = [];

// mysql is exceptional here because it uses ` character in to escape names in queries, that's why for mysql
Expand Down Expand Up @@ -160,4 +177,11 @@ ${downSqls.join(`
`;
}

/**
*
*/
protected static prettifyQuery(query: string) {
const formattedQuery = format(query, { indent: " " });
return "\n" + formattedQuery.replace(/^/gm, " ") + "\n ";
}
}
14 changes: 14 additions & 0 deletions test/github-issues/4415/entity/Post.ts
@@ -0,0 +1,14 @@
import { PrimaryGeneratedColumn, Entity, Column, CreateDateColumn } from "../../../../src";

@Entity()
export class Post {

@PrimaryGeneratedColumn()
id?: number;

@Column()
title: string;

@CreateDateColumn()
readonly createdAt?: Date;
}
15 changes: 15 additions & 0 deletions test/github-issues/4415/entity/Username.ts
@@ -0,0 +1,15 @@
import { Column } from "../../../../src/decorator/columns/Column";
import { PrimaryColumn } from "../../../../src/decorator/columns/PrimaryColumn";
import { Entity } from "../../../../src/decorator/entity/Entity";

@Entity()
export class Username {
@PrimaryColumn()
username: string;

@Column()
email: string;

@Column()
something: string;
}
108 changes: 108 additions & 0 deletions test/github-issues/4415/issue-4415.ts
@@ -0,0 +1,108 @@
import sinon from "sinon";
import { ConnectionOptions, ConnectionOptionsReader, DatabaseType } from "../../../src";
import { setupTestingConnections, createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
import { Username } from "./entity/Username";
import { CommandUtils } from "../../../src/commands/CommandUtils";
import { MigrationGenerateCommand } from "../../../src/commands/MigrationGenerateCommand";
import { Post } from "./entity/Post";
import { resultsTemplates } from "./results-templates";

describe("github issues > #4415 allow beautify generated migrations", () => {
let connectionOptions: ConnectionOptions[];
let createFileStub: sinon.SinonStub;
let getConnectionOptionsStub: sinon.SinonStub;
let migrationGenerateCommand: MigrationGenerateCommand;
let connectionOptionsReader: ConnectionOptionsReader;
let baseConnectionOptions: ConnectionOptions;

const enabledDrivers = [
"postgres",
"mssql",
"mysql",
"mariadb",
"sqlite",
"better-sqlite3",
"oracle",
"cockroachdb"
] as DatabaseType[];

// simulate args: `npm run typeorm migration:run -- -n test-migration -d test-directory`
const testHandlerArgs = (options: Record<string, any>) => ({
"$0": "test",
"_": ["test"],
"name": "test-migration",
"dir": "test-directory",
...options
});

before(async () => {
// clean out db from any prior tests in case previous state impacts the generated migrations
const connections = await createTestingConnections({
entities: [],
enabledDrivers
});
await reloadTestingDatabases(connections);
await closeTestingConnections(connections);

connectionOptions = await setupTestingConnections({
entities: [Username, Post],
enabledDrivers
});
connectionOptionsReader = new ConnectionOptionsReader();
migrationGenerateCommand = new MigrationGenerateCommand();
createFileStub = sinon.stub(CommandUtils, "createFile");
});
after(() => createFileStub.restore());

it("writes regular migration file when no option is passed", async () => {
for (const connectionOption of connectionOptions) {
createFileStub.resetHistory();
baseConnectionOptions = await connectionOptionsReader.get(connectionOption.name as string);
getConnectionOptionsStub = sinon.stub(ConnectionOptionsReader.prototype, "get").resolves({
...baseConnectionOptions,
entities: [Username, Post]
});

await migrationGenerateCommand.handler(testHandlerArgs({
"connection": connectionOption.name
}));

// compare against control test strings in results-templates.ts
for (const control of resultsTemplates[connectionOption.type as string].control) {
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.ts/),
sinon.match(control)
);
}

getConnectionOptionsStub.restore();
}
});

it("writes pretty printed file when pretty option is passed", async () => {
for (const connectionOption of connectionOptions) {
createFileStub.resetHistory();
baseConnectionOptions = await connectionOptionsReader.get(connectionOption.name as string);
getConnectionOptionsStub = sinon.stub(ConnectionOptionsReader.prototype, "get").resolves({
...baseConnectionOptions,
entities: [Username, Post]
});

await migrationGenerateCommand.handler(testHandlerArgs({
"connection": connectionOption.name,
"pretty": true
}));

// compare against "pretty" test strings in results-templates.ts
for (const pretty of resultsTemplates[connectionOption.type as string].pretty) {
sinon.assert.calledWith(
createFileStub,
sinon.match(/test-directory.*test-migration.ts/),
sinon.match(pretty)
);
}
getConnectionOptionsStub.restore();
}
});
});
157 changes: 157 additions & 0 deletions test/github-issues/4415/results-templates.ts
@@ -0,0 +1,157 @@
export const resultsTemplates: Record<string, any> = {

postgres: {
control: [
`CREATE TABLE "post" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`,
`CREATE TABLE "username" ("username" character varying NOT NULL, "email" character varying NOT NULL, "something" character varying NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))`
],
pretty: [
`
CREATE TABLE "post" (
"id" SERIAL NOT NULL,
"title" character varying NOT NULL,
"createdAt" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id")
)
`,
`
CREATE TABLE "username" (
"username" character varying NOT NULL,
"email" character varying NOT NULL,
"something" character varying NOT NULL,
CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username")
)
`
]
},

mssql: {
control: [
`CREATE TABLE "post" ("id" int NOT NULL IDENTITY(1,1), "title" nvarchar(255) NOT NULL, "createdAt" datetime2 NOT NULL CONSTRAINT "DF_fb91bea2d37140a877b775e6b2a" DEFAULT getdate(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`,
`CREATE TABLE "username" ("username" nvarchar(255) NOT NULL, "email" nvarchar(255) NOT NULL, "something" nvarchar(255) NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))`
],
pretty: [
`
CREATE TABLE "post" (
"id" int NOT NULL IDENTITY(1, 1),
"title" nvarchar(255) NOT NULL,
"createdAt" datetime2 NOT NULL CONSTRAINT "DF_fb91bea2d37140a877b775e6b2a" DEFAULT getdate(),
CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id")
)
`,
`
CREATE TABLE "username" (
"username" nvarchar(255) NOT NULL,
"email" nvarchar(255) NOT NULL,
"something" nvarchar(255) NOT NULL,
CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username")
)
`
]
},

sqlite: {
control: [
`CREATE TABLE "post" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')))`,
`CREATE TABLE "username" ("username" varchar PRIMARY KEY NOT NULL, "email" varchar NOT NULL, "something" varchar NOT NULL)`,
],
pretty: [
`
CREATE TABLE "post" (
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"title" varchar NOT NULL,
"createdAt" datetime NOT NULL DEFAULT (datetime('now'))
)
`,
`
CREATE TABLE "username" (
"username" varchar PRIMARY KEY NOT NULL,
"email" varchar NOT NULL,
"something" varchar NOT NULL
)
`
]
},

mysql: {
control: [
`CREATE TABLE \`post\` (\`id\` int NOT NULL AUTO_INCREMENT, \`title\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`,
`CREATE TABLE \`username\` (\`username\` varchar(255) NOT NULL, \`email\` varchar(255) NOT NULL, \`something\` varchar(255) NOT NULL, PRIMARY KEY (\`username\`)) ENGINE=InnoDB`
],
pretty: [
`
CREATE TABLE \`post\` (
\`id\` int NOT NULL AUTO_INCREMENT,
\`title\` varchar(255) NOT NULL,
\`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE = InnoDB
`,
`
CREATE TABLE \`username\` (
\`username\` varchar(255) NOT NULL,
\`email\` varchar(255) NOT NULL,
\`something\` varchar(255) NOT NULL,
PRIMARY KEY (\`username\`)
) ENGINE = InnoDB
`
]
},

oracle: {
control: [
`CREATE TABLE "post" ("id" number GENERATED BY DEFAULT AS IDENTITY, "title" varchar2(255) NOT NULL, "createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`,
`CREATE TABLE "username" ("username" varchar2(255) NOT NULL, "email" varchar2(255) NOT NULL, "something" varchar2(255) NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))`
],
pretty: [
`
CREATE TABLE "post" (
"id" number GENERATED BY DEFAULT AS IDENTITY,
"title" varchar2(255) NOT NULL,
"createdAt" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id")
)
`,
`
CREATE TABLE "username" (
"username" varchar2(255) NOT NULL,
"email" varchar2(255) NOT NULL,
"something" varchar2(255) NOT NULL,
CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username")
)
`
]
},

cockroachdb: {
control: [
`CREATE SEQUENCE "post_id_seq"`,
`CREATE TABLE "post" ("id" INT DEFAULT nextval('"post_id_seq"') NOT NULL, "title" varchar NOT NULL, "createdAt" timestamptz NOT NULL DEFAULT now(), CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id"))`,
`CREATE TABLE "username" ("username" varchar NOT NULL, "email" varchar NOT NULL, "something" varchar NOT NULL, CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username"))`
],
pretty: [
`
CREATE SEQUENCE "post_id_seq"
`,
`
CREATE TABLE "post" (
"id" INT DEFAULT nextval('"post_id_seq"') NOT NULL,
"title" varchar NOT NULL,
"createdAt" timestamptz NOT NULL DEFAULT now(),
CONSTRAINT "PK_be5fda3aac270b134ff9c21cdee" PRIMARY KEY ("id")
)
`,
`
CREATE TABLE "username" (
"username" varchar NOT NULL,
"email" varchar NOT NULL,
"something" varchar NOT NULL,
CONSTRAINT "PK_b39ad32e514b17e90c93988888a" PRIMARY KEY ("username")
)
`
]
},

get mariadb() { return this.mysql; },
get "better-sqlite3"() { return this.sqlite; },
};

0 comments on commit 370442c

Please sign in to comment.