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

Create multiple PKs with increments #4903

Merged
merged 8 commits into from Jan 3, 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
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