Ask for value when changing nullable to not-nullable (#5400)

* Add ContainsNullValues exception abstraction

* Add dialog for null values when disabling non-null

Fixes #2934

* Add translation for CONTAINS_NULL_VALUE error

* Make dialog title translated
This commit is contained in:
Rijk van Zanten
2021-04-30 15:27:18 -04:00
committed by GitHub
parent c62d30b816
commit f3574deae0
10 changed files with 171 additions and 9 deletions

View File

@@ -0,0 +1,12 @@
import { BaseException } from '../base';
type Exceptions = {
collection: string;
field: string;
};
export class ContainsNullValuesException extends BaseException {
constructor(field: string, exceptions?: Exceptions) {
super(`Field "${field}" contains null values.`, 400, 'CONTAINS_NULL_VALUES', exceptions);
}
}

View File

@@ -1,4 +1,5 @@
import database from '../../../database';
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
import { RecordNotUniqueException } from '../record-not-unique';
@@ -134,6 +135,10 @@ function notNullViolation(error: MSSQLError) {
const collection = bracketMatches[0].slice(1, -1);
const field = quoteMatches[0].slice(1, -1);
if (error.message.includes('Cannot insert the value NULL into column')) {
return new ContainsNullValuesException(field, { collection, field });
}
return new NotNullViolationException(field, {
collection,
field,

View File

@@ -1,3 +1,4 @@
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
import { RecordNotUniqueException } from '../record-not-unique';
@@ -11,6 +12,8 @@ enum MySQLErrorCodes {
ER_DATA_TOO_LONG = 'ER_DATA_TOO_LONG',
NOT_NULL_VIOLATION = 'ER_BAD_NULL_ERROR',
FOREIGN_KEY_VIOLATION = 'ER_NO_REFERENCED_ROW_2',
ER_INVALID_USE_OF_NULL = 'ER_INVALID_USE_OF_NULL',
WARN_DATA_TRUNCATED = 'WARN_DATA_TRUNCATED',
}
export function extractError(error: MySQLError): MySQLError | Error {
@@ -25,7 +28,12 @@ export function extractError(error: MySQLError): MySQLError | Error {
return notNullViolation(error);
case MySQLErrorCodes.FOREIGN_KEY_VIOLATION:
return foreignKeyViolation(error);
// Note: MariaDB throws data truncated for null value error
case MySQLErrorCodes.ER_INVALID_USE_OF_NULL:
case MySQLErrorCodes.WARN_DATA_TRUNCATED:
return containsNullValues(error);
}
return error;
}
@@ -132,3 +140,17 @@ function foreignKeyViolation(error: MySQLError) {
invalid,
});
}
function containsNullValues(error: MySQLError) {
const betweenTicks = /`([^`]+)`/g;
// Normally, we shouldn't read from the executed SQL. In this case, we're altering a single
// column, so we shouldn't have the problem where multiple columns are altered at the same time
const tickMatches = error.sql.match(betweenTicks);
if (!tickMatches) return error;
const field = tickMatches[1].slice(1, -1);
return new ContainsNullValuesException(field);
}

View File

@@ -1,3 +1,28 @@
export function extractError(error: Error): Error {
return error;
import { ContainsNullValuesException } from '../contains-null-values';
import { OracleError } from './types';
enum OracleErrorCodes {
'CONTAINS_NULL_VALUES' = 2296,
// @TODO extend with other errors
}
export function extractError(error: OracleError): OracleError | Error {
switch (error.errorNum) {
case OracleErrorCodes.CONTAINS_NULL_VALUES:
return containsNullValues(error);
default:
return error;
}
}
function containsNullValues(error: OracleError): OracleError | ContainsNullValuesException {
const betweenQuotes = /"([^"]+)"/g;
const matches = error.message.match(betweenQuotes);
if (!matches) return error;
const collection = matches[0].slice(1, -1);
const field = matches[1].slice(1, -1);
return new ContainsNullValuesException(field, { collection, field });
}

View File

@@ -1,3 +1,4 @@
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
import { RecordNotUniqueException } from '../record-not-unique';
@@ -88,9 +89,12 @@ function valueLimitViolation(error: PostgresError) {
function notNullViolation(error: PostgresError) {
const { table, column } = error;
if (!column) return error;
if (error.message.endsWith('contains null values')) {
return new ContainsNullValuesException(column, { collection: table, field: column });
}
return new NotNullViolationException(column, {
collection: table,
field: column,

View File

@@ -1,3 +1,4 @@
import { ContainsNullValuesException } from '../contains-null-values';
import { InvalidForeignKeyException } from '../invalid-foreign-key';
import { NotNullViolationException } from '../not-null-violation';
import { RecordNotUniqueException } from '../record-not-unique';
@@ -41,6 +42,16 @@ function notNullConstraint(error: SQLiteError) {
const [table, column] = errorParts[errorParts.length - 1].split('.');
if (table && column) {
// Now this gets a little finicky... SQLite doesn't have any native ALTER, so Knex implements
// it by creating a new table, and then copying the data over. That also means we'll never get
// a ContainsNullValues constraint error, as there is no ALTER. HOWEVER, we can hack around
// that by checking for the collection name, as Knex's alter default template name will always
// start with _knex_temp. The best we can do in this case is check for that, and use it to
// decide between NotNullViolation and ContainsNullValues
if (table.startsWith('_knex_temp_alter')) {
return new ContainsNullValuesException(column);
}
return new NotNullViolationException(column, {
collection: table,
field: column,

View File

@@ -31,10 +31,16 @@ export type PostgresError = {
constraint?: string;
};
export type OracleError = {
message: string;
errorNum: number;
offset: number;
};
export type SQLiteError = {
message: string;
errno: number;
code: string;
};
export type SQLError = MSSQLError & MySQLError & PostgresError & SQLiteError & Error;
export type SQLError = MSSQLError & MySQLError & PostgresError & SQLiteError & OracleError & Error;