Create table like support (#1373) (#4728)

This commit is contained in:
Olivier Cavadenti 2021-10-15 15:57:46 +02:00 committed by GitHub
parent 2b1e37ca44
commit 7c52de9f23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 241 additions and 42 deletions

View File

@ -98,8 +98,8 @@ class Client extends EventEmitter {
return new SchemaCompiler(this, builder);
}
tableBuilder(type, tableName, fn) {
return new TableBuilder(this, type, tableName, fn);
tableBuilder(type, tableName, tableNameLike, fn) {
return new TableBuilder(this, type, tableName, tableNameLike, fn);
}
tableCompiler(tableBuilder) {

View File

@ -14,18 +14,24 @@ class TableCompiler_MSSQL extends TableCompiler {
super(client, tableBuilder);
}
createQuery(columns, ifNot) {
const createStatement = ifNot
? `if object_id('${this.tableName()}', 'U') is null CREATE TABLE `
: 'CREATE TABLE ';
const sql =
createStatement +
this.tableName() +
(this._formatting ? ' (\n ' : ' (') +
columns.sql.join(this._formatting ? ',\n ' : ', ') +
')';
createQuery(columns, ifNot, like) {
let createStatement = ifNot
? `if object_id('${this.tableName()}', 'U') is null `
: '';
this.pushQuery(sql);
if (like) {
// This query copy only columns and not all indexes and keys like other databases.
createStatement += `SELECT * INTO ${this.tableName()} FROM ${this.tableNameLike()} WHERE 0=1`;
} else {
createStatement +=
'CREATE TABLE ' +
this.tableName() +
(this._formatting ? ' (\n ' : ' (') +
columns.sql.join(this._formatting ? ',\n ' : ', ') +
')';
}
this.pushQuery(createStatement);
if (this.single.comment) {
this.comment(this.single.comment);

View File

@ -13,14 +13,19 @@ class TableCompiler_MySQL extends TableCompiler {
super(client, tableBuilder);
}
createQuery(columns, ifNot) {
createQuery(columns, ifNot, like) {
const createStatement = ifNot
? 'create table if not exists '
: 'create table ';
const { client } = this;
let conn = {};
const columnsSql = ' (' + columns.sql.join(', ') + ')';
let sql =
createStatement + this.tableName() + ' (' + columns.sql.join(', ') + ')';
createStatement +
this.tableName() +
(like && this.tableNameLike()
? ' like ' + this.tableNameLike()
: columnsSql);
// Check if the connection settings are set.
if (client.connectionSettings) {
@ -31,8 +36,7 @@ class TableCompiler_MySQL extends TableCompiler {
const collation = this.single.collate || conn.collate || '';
const engine = this.single.engine || '';
// var conn = builder.client.connectionSettings;
if (charset) sql += ` default character set ${charset}`;
if (charset && !like) sql += ` default character set ${charset}`;
if (collation) sql += ` collate ${collation}`;
if (engine) sql += ` engine = ${engine}`;

View File

@ -53,8 +53,13 @@ class TableCompiler_Oracle extends TableCompiler {
}
// Adds the "create" query to the query sequence.
createQuery(columns, ifNot) {
const sql = `create table ${this.tableName()} (${columns.sql.join(', ')})`;
createQuery(columns, ifNot, like) {
const columnsSql =
like && this.tableNameLike()
? ' as (select * from ' + this.tableNameLike() + ' where 0=1)'
: ' (' + columns.sql.join(', ') + ')';
const sql = `create table ${this.tableName()}${columnsSql}`;
this.pushQuery({
// catch "name is already used by an existing object" for workaround for "if not exists"
sql: ifNot ? utils.wrapSqlWithCatch(sql, -955) : sql,

View File

@ -40,12 +40,17 @@ class TableCompiler_PG extends TableCompiler {
}
// Adds the "create" query to the query sequence.
createQuery(columns, ifNot) {
createQuery(columns, ifNot, like) {
const createStatement = ifNot
? 'create table if not exists '
: 'create table ';
const columnsSql = ' (' + columns.sql.join(', ') + ')';
let sql =
createStatement + this.tableName() + ' (' + columns.sql.join(', ') + ')';
createStatement +
this.tableName() +
(like && this.tableNameLike()
? ' (like ' + this.tableNameLike() + ' including all)'
: columnsSql);
if (this.single.inherits)
sql += ` inherits (${this.formatter.wrap(this.single.inherits)})`;
this.pushQuery({

View File

@ -26,12 +26,17 @@ class TableCompiler_Redshift extends TableCompiler_PG {
// TODO: have to disable setting not null on columns that already exist...
// Adds the "create" query to the query sequence.
createQuery(columns, ifNot) {
createQuery(columns, ifNot, like) {
const createStatement = ifNot
? 'create table if not exists '
: 'create table ';
const columnsSql = ' (' + columns.sql.join(', ') + ')';
let sql =
createStatement + this.tableName() + ' (' + columns.sql.join(', ') + ')';
createStatement +
this.tableName() +
(like && this.tableNameLike()
? ' (like ' + this.tableNameLike() + ')'
: columnsSql);
if (this.single.inherits)
sql += ` like (${this.formatter.wrap(this.single.inherits)})`;
this.pushQuery({

View File

@ -12,20 +12,23 @@ class TableCompiler_SQLite3 extends TableCompiler {
}
// Create a new table.
createQuery(columns, ifNot) {
createQuery(columns, ifNot, like) {
const createStatement = ifNot
? 'create table if not exists '
: 'create table ';
let sql =
createStatement + this.tableName() + ' (' + columns.sql.join(', ');
// SQLite forces primary keys to be added when the table is initially created
// so we will need to check for a primary key commands and add the columns
// to the table's declaration here so they can be created on the tables.
sql += this.foreignKeys() || '';
sql += this.primaryKeys() || '';
sql += ')';
let sql = createStatement + this.tableName();
if (like && this.tableNameLike()) {
sql += ' as select * from ' + this.tableNameLike() + ' where 0=1';
} else {
// so we will need to check for a primary key commands and add the columns
// to the table's declaration here so they can be created on the tables.
sql += ' (' + columns.sql.join(', ');
sql += this.foreignKeys() || '';
sql += this.primaryKeys() || '';
sql += ')';
}
this.pushQuery(sql);
}

View File

@ -45,6 +45,7 @@ class SchemaBuilder extends EventEmitter {
[
'createTable',
'createTableIfNotExists',
'createTableLike',
'createSchema',
'createSchemaIfNotExists',
'dropSchema',

View File

@ -82,15 +82,14 @@ SchemaCompiler.prototype.dropTablePrefix = 'drop table ';
SchemaCompiler.prototype.alterTable = buildTable('alter');
SchemaCompiler.prototype.createTable = buildTable('create');
SchemaCompiler.prototype.createTableIfNotExists = buildTable('createIfNot');
SchemaCompiler.prototype.createTableLike = buildTable('createLike');
SchemaCompiler.prototype.pushQuery = pushQuery;
SchemaCompiler.prototype.pushAdditional = pushAdditional;
SchemaCompiler.prototype.unshiftQuery = unshiftQuery;
function buildTable(type) {
return function (tableName, fn) {
const builder = this.client.tableBuilder(type, tableName, fn);
function build(builder) {
// pass queryContext down to tableBuilder but do not overwrite it if already set
const queryContext = this.builder.queryContext();
if (queryContext !== undefined && builder.queryContext() === undefined) {
@ -103,7 +102,24 @@ function buildTable(type) {
for (let i = 0, l = sql.length; i < l; i++) {
this.sequence.push(sql[i]);
}
};
}
if (type === 'createLike') {
return function (tableName, tableNameLike, fn) {
const builder = this.client.tableBuilder(
type,
tableName,
tableNameLike,
fn
);
build.call(this, builder);
};
} else {
return function (tableName, fn) {
const builder = this.client.tableBuilder(type, tableName, null, fn);
build.call(this, builder);
};
}
}
function prefixedTableName(prefix, table) {

View File

@ -13,16 +13,17 @@ const helpers = require('../util/helpers');
const { isString, isFunction } = require('../util/is');
class TableBuilder {
constructor(client, method, tableName, fn) {
constructor(client, method, tableName, tableNameLike, fn) {
this.client = client;
this._fn = fn;
this._method = method;
this._schemaName = undefined;
this._tableName = tableName;
this._tableNameLike = tableNameLike;
this._statements = [];
this._single = {};
if (!isFunction(this._fn)) {
if (!tableNameLike && !isFunction(this._fn)) {
throw new TypeError(
'A callback function must be supplied to calls against `.createTable` ' +
'and `.table`'
@ -41,7 +42,10 @@ class TableBuilder {
if (this._method === 'alter') {
extend(this, AlterMethods);
}
this._fn.call(this, this);
// With 'create table ... like' callback function is useless.
if (this._fn) {
this._fn.call(this, this);
}
return this.client.tableCompiler(this).toSQL();
}

View File

@ -21,6 +21,7 @@ class TableCompiler {
this.method = tableBuilder._method;
this.schemaNameRaw = tableBuilder._schemaName;
this.tableNameRaw = tableBuilder._tableName;
this.tableNameLikeRaw = tableBuilder._tableNameLike;
this.single = tableBuilder._single;
this.grouped = groupBy(tableBuilder._statements, 'grouping');
@ -45,14 +46,14 @@ class TableCompiler {
// If this is a table "creation", we need to first run through all
// of the columns to build them into a single string,
// and then run through anything else and push it to the query sequence.
create(ifNot) {
create(ifNot, like) {
const columnBuilders = this.getColumns();
const columns = columnBuilders.map((col) => col.toSQL());
const columnTypes = this.getColumnTypes(columns);
if (this.createAlterTableMethods) {
this.alterTableForCreate(columnTypes);
}
this.createQuery(columnTypes, ifNot);
this.createQuery(columnTypes, ifNot, like);
this.columnQueries(columns);
delete this.single.comment;
this.alterTable();
@ -63,6 +64,14 @@ class TableCompiler {
this.create(true);
}
createLike() {
this.create(false, true);
}
createLikeIfNot() {
this.create(true, true);
}
// If we're altering the table, we need to one-by-one
// go through and handle each of the queries associated
// with altering the table's schema.
@ -219,6 +228,14 @@ class TableCompiler {
return this.formatter.wrap(name);
}
tableNameLike() {
const name = this.schemaNameRaw
? `${this.schemaNameRaw}.${this.tableNameLikeRaw}`
: this.tableNameLikeRaw;
return this.formatter.wrap(name);
}
// Generate all of the alter column statements necessary for the query.
alterTable() {
const alterTable = this.grouped.alterTable || [];

View File

@ -271,6 +271,57 @@ describe('Schema (misc)', () => {
});
describe('createTable', () => {
describe('like another table', () => {
before(async () => {
await knex.schema.createTable('table_to_copy', (table) => {
table.increments('id');
table.string('data');
table.index('data', 'data_index');
});
});
after(async () => {
await knex.schema.dropTableIfExists('table_copied');
await knex.schema.dropTableIfExists('table_to_copy');
});
it('copy table', async () => {
await knex.schema
.createTableLike('table_copied', 'table_to_copy')
.testSql((tester) => {
tester('mysql', [
'create table `table_copied` like `table_to_copy`',
]);
tester(
['pg', 'cockroachdb'],
[
'create table "table_copied" (like "table_to_copy" including all)',
]
);
tester('pg-redshift', [
'create table "table_copied" (like "table_to_copy")',
]);
tester('sqlite3', [
'create table `table_copied` as select * from `table_to_copy` where 0=1',
]);
tester('oracledb', [
'create table "table_copied" as (select * from "table_to_copy" where 0=1)',
]);
tester('mssql', [
'SELECT * INTO [table_copied] FROM [table_to_copy] WHERE 0=1',
]);
});
expect(await knex.schema.hasTable('table_copied')).to.equal(true);
await knex('table_copied')
.columnInfo()
.then((res) => {
expect(Object.keys(res)).to.have.all.members(['id', 'data']);
});
});
});
describe('increments types - postgres', () => {
if (!isPgBased(knex)) {
return Promise.resolve();

View File

@ -38,6 +38,17 @@ describe('MSSQL SchemaBuilder', function () {
);
});
it('create table like another', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users_like', 'users')
.toSQL();
equal(1, tableSql.length);
expect(tableSql[0].sql).to.equal(
'SELECT * INTO [users_like] FROM [users] WHERE 0=1'
);
});
it('test basic create table with incrementing without primary key', function () {
tableSql = client.schemaBuilder().createTable('users', function (table) {
table.increments('id', { primaryKey: false });

View File

@ -34,6 +34,17 @@ module.exports = function (dialect) {
);
});
it('create table like another', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users_like', 'users')
.toSQL();
equal(1, tableSql.length);
expect(tableSql[0].sql).to.equal(
'create table `users_like` like `users`'
);
});
it('test basic create table with incrementing without primary key', function () {
tableSql = client
.schemaBuilder()

View File

@ -41,6 +41,17 @@ describe('Oracle SchemaBuilder', function () {
);
});
it('create table like another', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users_like', 'users')
.toSQL();
equal(1, tableSql.length);
expect(tableSql[0].sql).to.equal(
'create table "users_like" as (select * from "users" where 0=1)'
);
});
it('test basic create table if not exists', function () {
tableSql = client
.schemaBuilder()

View File

@ -25,6 +25,15 @@ describe('OracleDb SchemaBuilder', function () {
);
});
it('test basic create table like', function () {
tableSql = client.schemaBuilder().createTableLike('users_like', 'users');
equal(1, tableSql.toSQL().length);
expect(tableSql.toSQL()[0].sql).to.equal(
'create table "users_like" as (select * from "users" where 0=1)'
);
});
it('test basic create table if not exists', function () {
tableSql = client
.schemaBuilder()

View File

@ -112,6 +112,17 @@ describe('PostgreSQL SchemaBuilder', function () {
);
});
it('create table like another', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users_like', 'users')
.toSQL();
equal(1, tableSql.length);
expect(tableSql[0].sql).to.equal(
'create table "users_like" (like "users" including all)'
);
});
it('basic alter table', function () {
tableSql = client
.schemaBuilder()

View File

@ -26,6 +26,17 @@ describe('Redshift SchemaBuilder', function () {
);
});
it('create table like another', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users_like', 'users')
.toSQL();
equal(1, tableSql.length);
expect(tableSql[0].sql).to.equal(
'create table "users_like" (like "users")'
);
});
it('basic alter table', function () {
tableSql = client
.schemaBuilder()

View File

@ -36,6 +36,19 @@ describe('SQLite SchemaBuilder', function () {
);
});
it('basic create table like', function () {
tableSql = client
.schemaBuilder()
.createTableLike('users', 'users_like')
.toSQL();
equal(1, tableSql.length);
equal(
tableSql[0].sql,
'create table `users` as select * from `users_like` where 0=1'
);
});
it('create json table', function () {
tableSql = client
.schemaBuilder()

7
types/index.d.ts vendored
View File

@ -1841,6 +1841,11 @@ export declare namespace Knex {
tableName: string,
callback: (tableBuilder: CreateTableBuilder) => any
): SchemaBuilder;
createTableLike(
tableName: string,
tableNameLike: string,
callback: (tableBuilder: CreateTableBuilder) => any
): SchemaBuilder;
createSchema(schemaName: string): SchemaBuilder;
createSchemaIfNotExists(schemaName: string): SchemaBuilder;
alterTable(
@ -2479,7 +2484,7 @@ export declare namespace Knex {
queryCompiler(builder: any): any;
schemaBuilder(): SchemaBuilder;
schemaCompiler(builder: SchemaBuilder): any;
tableBuilder(type: any, tableName: any, fn: any): TableBuilder;
tableBuilder(type: any, tableName: any, tableNameLike: any, fn: any): TableBuilder;
tableCompiler(tableBuilder: any): any;
columnBuilder(tableBuilder: any, type: any, args: any): ColumnBuilder;
columnCompiler(tableBuilder: any, columnBuilder: any): any;