Skip to content

Commit

Permalink
Create table like support (#1373) (#4728)
Browse files Browse the repository at this point in the history
  • Loading branch information
OlivierCavadenti committed Oct 15, 2021
1 parent 2b1e37c commit 7c52de9
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 43 deletions.
4 changes: 2 additions & 2 deletions lib/client.js
Expand Up @@ -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) {
Expand Down
30 changes: 18 additions & 12 deletions lib/dialects/mssql/schema/mssql-tablecompiler.js
Expand Up @@ -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 ' : ', ') +
')';

this.pushQuery(sql);
createQuery(columns, ifNot, like) {
let createStatement = ifNot
? `if object_id('${this.tableName()}', 'U') is null `
: '';

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);
Expand Down
12 changes: 8 additions & 4 deletions lib/dialects/mysql/schema/mysql-tablecompiler.js
Expand Up @@ -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) {
Expand All @@ -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}`;

Expand Down
9 changes: 7 additions & 2 deletions lib/dialects/oracle/schema/oracle-tablecompiler.js
Expand Up @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions lib/dialects/postgres/schema/pg-tablecompiler.js
Expand Up @@ -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({
Expand Down
9 changes: 7 additions & 2 deletions lib/dialects/redshift/schema/redshift-tablecompiler.js
Expand Up @@ -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({
Expand Down
21 changes: 12 additions & 9 deletions lib/dialects/sqlite3/schema/sqlite-tablecompiler.js
Expand Up @@ -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);
}

Expand Down
1 change: 1 addition & 0 deletions lib/schema/builder.js
Expand Up @@ -45,6 +45,7 @@ class SchemaBuilder extends EventEmitter {
[
'createTable',
'createTableIfNotExists',
'createTableLike',
'createSchema',
'createSchemaIfNotExists',
'dropSchema',
Expand Down
24 changes: 20 additions & 4 deletions lib/schema/compiler.js
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
10 changes: 7 additions & 3 deletions lib/schema/tablebuilder.js
Expand Up @@ -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`'
Expand All @@ -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();
}

Expand Down
21 changes: 19 additions & 2 deletions lib/schema/tablecompiler.js
Expand Up @@ -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');

Expand All @@ -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();
Expand All @@ -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.
Expand Down Expand Up @@ -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 || [];
Expand Down
51 changes: 51 additions & 0 deletions test/integration2/schema/misc.spec.js
Expand Up @@ -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();
Expand Down
11 changes: 11 additions & 0 deletions test/unit/schema-builder/mssql.js
Expand Up @@ -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 });
Expand Down

0 comments on commit 7c52de9

Please sign in to comment.