Skip to content

Commit

Permalink
Create multiple PKs with increments (#4903)
Browse files Browse the repository at this point in the history
  • Loading branch information
OlivierCavadenti committed Jan 3, 2022
1 parent 4521040 commit b6775d9
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 39 deletions.
18 changes: 14 additions & 4 deletions lib/dialects/mssql/schema/mssql-columncompiler.js
Expand Up @@ -126,12 +126,22 @@ class ColumnCompiler_MSSQL extends ColumnCompiler {
});
return '';
}

increments(options = { primaryKey: true }) {
return (
'int identity(1,1) not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}

bigincrements(options = { primaryKey: true }) {
return (
'bigint identity(1,1) not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}
}

ColumnCompiler_MSSQL.prototype.increments = ({ primaryKey = true } = {}) =>
'int identity(1,1) not null' + (primaryKey ? ' primary key' : '');
ColumnCompiler_MSSQL.prototype.bigincrements = ({ primaryKey = true } = {}) =>
'bigint identity(1,1) not null' + (primaryKey ? ' primary key' : '');
ColumnCompiler_MSSQL.prototype.bigint = 'bigint';
ColumnCompiler_MSSQL.prototype.mediumint = 'int';
ColumnCompiler_MSSQL.prototype.smallint = 'smallint';
Expand Down
27 changes: 22 additions & 5 deletions lib/dialects/mysql/schema/mysql-columncompiler.js
Expand Up @@ -152,13 +152,30 @@ class ColumnCompiler_MySQL extends ColumnCompiler {
collate(collation) {
return collation && `collate '${collation}'`;
}

increments(options = { primaryKey: true }) {
return (
'int unsigned not null' +
// In MySQL autoincrement are always a primary key. If you already have a primary key, we
// initialize this column as classic int column then modify it later in table compiler
(this.tableCompiler._canBeAddPrimaryKey(options)
? ' auto_increment primary key'
: '')
);
}

bigincrements(options = { primaryKey: true }) {
return (
'bigint unsigned not null' +
// In MySQL autoincrement are always a primary key. If you already have a primary key, we
// initialize this column as classic int column then modify it later in table compiler
(this.tableCompiler._canBeAddPrimaryKey(options)
? ' auto_increment primary key'
: '')
);
}
}

ColumnCompiler_MySQL.prototype.increments = ({ primaryKey = true } = {}) =>
'int unsigned not null auto_increment' + (primaryKey ? ' primary key' : '');
ColumnCompiler_MySQL.prototype.bigincrements = ({ primaryKey = true } = {}) =>
'bigint unsigned not null auto_increment' +
(primaryKey ? ' primary key' : '');
ColumnCompiler_MySQL.prototype.bigint = 'bigint';
ColumnCompiler_MySQL.prototype.mediumint = 'mediumint';
ColumnCompiler_MySQL.prototype.smallint = 'smallint';
Expand Down
22 changes: 21 additions & 1 deletion lib/dialects/mysql/schema/mysql-tablecompiler.js
Expand Up @@ -253,11 +253,31 @@ class TableCompiler_MySQL extends TableCompiler {
constraintName = constraintName
? this.formatter.wrap(constraintName)
: this.formatter.wrap(`${this.tableNameRaw}_pkey`);

const primaryCols = columns;
let incrementsCols = [];
if (this.grouped.columns) {
incrementsCols = this._getIncrementsColumnNames();
if (incrementsCols) {
incrementsCols.forEach((c) => {
if (!primaryCols.includes(c)) {
primaryCols.unshift(c);
}
});
}
}
this.pushQuery(
`alter table ${this.tableName()} add primary key ${constraintName}(${this.formatter.columnize(
columns
primaryCols
)})`
);
if (incrementsCols.length) {
this.pushQuery(
`alter table ${this.tableName()} modify column ${this.formatter.columnize(
incrementsCols
)} int unsigned not null auto_increment`
);
}
}

unique(columns, indexName) {
Expand Down
2 changes: 1 addition & 1 deletion lib/dialects/oracle/schema/internal/trigger.js
Expand Up @@ -64,7 +64,7 @@ const trigger = {
`PK_NAME VARCHAR(200); ` +
`BEGIN` +
` EXECUTE IMMEDIATE ('CREATE SEQUENCE ${schemaQuoted}${sequenceNameQuoted}');` +
` SELECT cols.column_name INTO PK_NAME` +
` SELECT cols.column_name INTO PK_NAME` + // TODO : support autoincrement on table with multiple primary keys
` FROM all_constraints cons, all_cons_columns cols` +
` WHERE cons.constraint_type = 'P'` +
` AND cons.constraint_name = cols.constraint_name` +
Expand Down
14 changes: 10 additions & 4 deletions lib/dialects/oracle/schema/oracle-columncompiler.js
Expand Up @@ -15,14 +15,20 @@ class ColumnCompiler_Oracle extends ColumnCompiler {
this.modifiers = ['defaultTo', 'checkIn', 'nullable', 'comment'];
}

increments({ primaryKey = true } = {}) {
increments(options = { primaryKey: true }) {
createAutoIncrementTriggerAndSequence(this);
return 'integer not null' + (primaryKey ? ' primary key' : '');
return (
'integer not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}

bigincrements({ primaryKey = true } = {}) {
bigincrements(options = { primaryKey: true }) {
createAutoIncrementTriggerAndSequence(this);
return 'number(20, 0) not null' + (primaryKey ? ' primary key' : '');
return (
'number(20, 0) not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}

floating(precision) {
Expand Down
18 changes: 13 additions & 5 deletions lib/dialects/oracle/schema/oracle-tablecompiler.js
Expand Up @@ -85,10 +85,6 @@ class TableCompiler_Oracle extends TableCompiler {
);
}

changeType() {
// alter table + table + ' modify ' + wrapped + '// type';
}

_indexCommand(type, tableName, columns) {
return this.formatter.wrap(
utils.generateCombinedName(this.client.logger, type, tableName, columns)
Expand All @@ -104,9 +100,21 @@ class TableCompiler_Oracle extends TableCompiler {
constraintName = constraintName
? this.formatter.wrap(constraintName)
: this.formatter.wrap(`${this.tableNameRaw}_pkey`);
const primaryCols = columns;
let incrementsCols = [];
if (this.grouped.columns) {
incrementsCols = this._getIncrementsColumnNames();
if (incrementsCols) {
incrementsCols.forEach((c) => {
if (!primaryCols.includes(c)) {
primaryCols.unshift(c);
}
});
}
}
this.pushQuery(
`alter table ${this.tableName()} add constraint ${constraintName} primary key (${this.formatter.columnize(
columns
primaryCols
)})${deferrable}`
);
}
Expand Down
18 changes: 14 additions & 4 deletions lib/dialects/postgres/schema/pg-columncompiler.js
Expand Up @@ -100,12 +100,22 @@ class ColumnCompiler_PG extends ColumnCompiler {
);
}, comment);
}

increments(options = { primaryKey: true }) {
return (
'serial' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}

bigincrements(options = { primaryKey: true }) {
return (
'bigserial' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '')
);
}
}

ColumnCompiler_PG.prototype.bigincrements = ({ primaryKey = true } = {}) =>
'bigserial' + (primaryKey ? ' primary key' : '');
ColumnCompiler_PG.prototype.increments = ({ primaryKey = true } = {}) =>
'serial' + (primaryKey ? ' primary key' : '');
ColumnCompiler_PG.prototype.bigint = 'bigint';
ColumnCompiler_PG.prototype.binary = 'bytea';
ColumnCompiler_PG.prototype.bool = 'boolean';
Expand Down
4 changes: 4 additions & 0 deletions lib/dialects/sqlite3/schema/sqlite-columncompiler.js
Expand Up @@ -26,5 +26,9 @@ ColumnCompiler_SQLite3.prototype.double =
ColumnCompiler_SQLite3.prototype.floating =
'float';
ColumnCompiler_SQLite3.prototype.timestamp = 'datetime';
// autoincrement without primary key is a syntax error in SQLite, so it's necessary
ColumnCompiler_SQLite3.prototype.increments =
ColumnCompiler_SQLite3.prototype.bigincrements =
'integer not null primary key autoincrement';

module.exports = ColumnCompiler_SQLite3;
11 changes: 8 additions & 3 deletions lib/dialects/sqlite3/schema/sqlite-tablecompiler.js
Expand Up @@ -261,9 +261,14 @@ class TableCompiler_SQLite3 extends TableCompiler {
if (constraintName) {
constraintName = ' constraint ' + this.formatter.wrap(constraintName);
}
return `,${constraintName} primary key (${this.formatter.columnize(
columns
)})`;
const needUniqueCols =
this.grouped.columns.filter((t) => t.builder._type === 'increments')
.length > 0;
// SQLite dont support autoincrement columns and composite primary keys (autoincrement is always primary key).
// You need to add unique index instead when you have autoincrement columns (https://stackoverflow.com/a/6154876/1535159)
return `,${constraintName} ${
needUniqueCols ? 'unique' : 'primary key'
} (${this.formatter.columnize(columns)})`;
}
}

Expand Down
16 changes: 12 additions & 4 deletions lib/schema/columncompiler.js
Expand Up @@ -134,6 +134,18 @@ class ColumnCompiler {
defaultTo(value) {
return `default ${formatDefault(value, this.type, this.client)}`;
}

increments(options = { primaryKey: true }) {
return (
'integer not null' +
(this.tableCompiler._canBeAddPrimaryKey(options) ? ' primary key' : '') +
' autoincrement'
);
}

bigincrements(options = { primaryKey: true }) {
return this.increments(options);
}
}

ColumnCompiler.prototype.binary = 'blob';
Expand All @@ -149,10 +161,6 @@ ColumnCompiler.prototype.enu = 'varchar';
ColumnCompiler.prototype.bit = ColumnCompiler.prototype.json = 'text';
ColumnCompiler.prototype.uuid = ({ useBinaryUuid = false } = {}) =>
useBinaryUuid ? 'binary(16)' : 'char(36)';
ColumnCompiler.prototype.increments = ({ primaryKey = true } = {}) =>
'integer not null' + (primaryKey ? ' primary key' : '') + ' autoincrement';
ColumnCompiler.prototype.bigincrements = ({ primaryKey = true } = {}) =>
'integer not null' + (primaryKey ? ' primary key' : '') + ' autoincrement';
ColumnCompiler.prototype.integer =
ColumnCompiler.prototype.smallint =
ColumnCompiler.prototype.mediumint =
Expand Down
17 changes: 17 additions & 0 deletions lib/schema/tablecompiler.js
Expand Up @@ -361,6 +361,23 @@ class TableCompiler {
).toLowerCase();
return this.formatter.wrap(indexName);
}

_getPrimaryKeys() {
return (this.grouped.alterTable || [])
.filter((a) => a.method === 'primary')
.flatMap((a) => a.args)
.flat();
}

_canBeAddPrimaryKey(options) {
return options.primaryKey && this._getPrimaryKeys().length === 0;
}

_getIncrementsColumnNames() {
return this.grouped.columns
.filter((c) => c.builder._type === 'increments')
.map((c) => c.builder._args[0]);
}
}

TableCompiler.prototype.pushQuery = pushQuery;
Expand Down
99 changes: 98 additions & 1 deletion test/integration2/schema/primary-keys.spec.js
Expand Up @@ -31,7 +31,7 @@ describe('Schema', () => {
});

afterEach(async () => {
await knex.schema.dropTable('primary_table');
await knex.schema.dropTableIfExists('primary_table');
});

describe('createPrimaryKey', () => {
Expand Down Expand Up @@ -63,6 +63,103 @@ describe('Schema', () => {
}
});

it('create multiple primary keys with increments on same column', async function () {
await knex.schema.dropTableIfExists('table_multiple_keys');
await knex.schema.createTable('table_multiple_keys', function (t) {
t.primary(['id', 'second_id', 'other_col']);
t.string('second_id', 16).notNullable();
t.integer('other_col').notNullable();
t.increments('id');
});
await knex('table_multiple_keys').insert([
{
second_id: 'abc',
other_col: 2,
},
{
second_id: 'abc',
other_col: 3,
},
]);
expect(() => {
knex('table_multiple_keys').insert([
{
id: 1,
second_id: 'abc',
other_col: 2,
},
]);
}).to.throw;
// It's always ok, the primary key is on three columns.
await knex('table_multiple_keys').insert([
{
second_id: 'abc',
other_col: 2,
},
{
second_id: 'abc',
other_col: 3,
},
]);
expect(() => {
knex('table_multiple_keys').insert([
{
id: 4,
second_id: 'abc',
other_col: 3,
},
]);
}).to.throw;
});

it('create multiple primary keys with increments on other columns', async function () {
await knex.schema.dropTableIfExists('table_multiple_keys');
await knex.schema.createTable('table_multiple_keys', function (t) {
t.primary(['second_id', 'other_col']);
t.string('second_id', 16).notNullable();
t.integer('other_col').notNullable();
t.increments('id');
});
await knex('table_multiple_keys').insert([
{
second_id: 'abc',
other_col: 2,
},
{
second_id: 'abc',
other_col: 3,
},
]);
expect(() => {
knex('table_multiple_keys').insert([
{
second_id: 'abc',
other_col: 2,
},
]);
}).to.throw;
// It's always ok, the primary key is on three columns.
await knex('table_multiple_keys').insert([
{
second_id: 'bcd',
other_col: 2,
},
{
second_id: 'abc',
other_col: 4,
},
]);
expect(() => {
knex('table_multiple_keys').insert([
{
id: 4,
second_id: 'abc',
other_col: 3,
},
]);
}).to.throw;
});

it('creates a primary key with a custom constraint name', async function () {
// CockroachDB 21.1 throws "(72): unimplemented: primary key dropped without subsequent addition of new primary key in same transaction"
if (isCockroachDB(knex)) {
Expand Down

0 comments on commit b6775d9

Please sign in to comment.