@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
621 lines (573 loc) • 30.1 kB
JavaScript
'use strict';
const { forEachDefinition } = require('../../base/model');
const { applyTransformations, getResultingName, hasPersistenceSkipAnnotation } = require('../../model/csnUtils');
const { forEach, forEachKey } = require('../../utils/objectUtils');
const { CompilerAssertion } = require('../../base/error');
const COMPOSITION = 'cds.Composition';
const ASSOCIATION = 'cds.Association';
/**
* Create referential constraints for foreign keys mentioned in on-conditions of associations and compositions.
* The referential constraints will be attached to the csn.Artifacts.
*
* @param {CSN.Model} csn
* @param {CSN.Options} options are used to modify the validate / enforced flag on the constraints
*/
function createReferentialConstraints( csn, options ) {
const isTenant = options.tenantDiscriminator === true;
let validated = true;
let enforced = true;
if (options.integrityNotValidated)
validated = false;
if (options.integrityNotEnforced)
enforced = false;
// prepare the functions with the compositions and associations across all entities first
// and execute it afterwards.
// compositions must be processed first, as the <up_> links for them must result in `ON DELETE CASCADE`
const compositions = [];
const associations = [];
applyTransformations(csn, {
elements: (art, prop, elements, path) => {
// Step I: iterate compositions, enrich dependent keys for <up_> association in target entity of composition
for (const elementName in elements) {
const element = elements[elementName];
const ePath = path.concat([ 'elements', elementName ]); // Save a copy in this scope for the late callback
if (element.type === COMPOSITION && element.$selfOnCondition) {
compositions.push({
fn: () => {
foreignKeyConstraintForUpLinkOfComposition(element, art, ePath);
},
});
}
}
// Step II: iterate associations, enrich dependent keys (in entity containing the association)
for (const elementName in elements) {
const element = elements[elementName];
const ePath = path.concat([ 'elements', elementName ]); // Save a copy in this scope for the late callback
if (element.keys && isToOne(element) && element.type === ASSOCIATION || element.type === COMPOSITION && treatCompositionLikeAssociation(element)) {
associations.push({
fn: () => {
// parent entity of assoc becomes dependent table
foreignKeyConstraintForAssociation( element, art, ePath );
},
});
}
// for `texts` compositions, we may generate foreign key constraints even w/o `up_`
else if (elementName === 'texts' && element.target === `${ path[path.length - 1] }.texts`) {
const { on } = element;
const textsEntity = csn.definitions[element.target];
// `texts` entities have a key named "locale"
const targetSideHasLocaleKey = textsEntity.elements.locale?.key;
if (targetSideHasLocaleKey && !skipConstraintGeneration(art, textsEntity, { /* there is no assoc */ })) {
// Note: a `texts` composition in `Foo` will co-modify `Foo.texts` and create the constraints over there
const { dependentKeys, parentKeys } = extractKeys(on, textsEntity.elements, elements);
const keysInParent = Object.values(elements).filter(e => e.key);
if (keysInParent.length !== dependentKeys.length)
continue;
// `texts` entities have all the keys the original entity has
const allElementsAreKeysAndHaveTheSameName = parentKeys.length &&
parentKeys
.every(
([ targetKey, e ]) => e.key &&
dependentKeys.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
);
if (allElementsAreKeysAndHaveTheSameName)
attachConstraintsToDependentKeys(dependentKeys, parentKeys, path[path.length - 1], 'texts', { texts: true });
}
}
}
},
}, [], { skipIgnore: false, skipArtifact: a => !!(a.query || a.kind !== 'entity' ) });
// create constraints on foreign keys
// always process unmanaged first, up_ links must be flagged
// before they are processed
compositions.forEach(composition => composition.fn());
associations.forEach(association => association.fn());
// Step III: Create the final referential constraints from all dependent key <-> parent key pairs stemming from the same $sourceAssociation
forEachDefinition(csn, collectAndAttachReferentialConstraints);
/**
* Retrieve the <up_> link of an `cds.Composition` used in an on-condition like `$self = <comp>.<up_>`
* and calculate a foreign key constraint for this association if it is constraint compliant.
* The constraint will have an `ON DELETE CASCADE`.
*
* @param {CSN.Element} composition which might has the `$self = <comp>.<up_>` on-condition
* @param {CSN.Artifact} parent artifact containing the composition
* @param {CSN.Path} path
*/
function foreignKeyConstraintForUpLinkOfComposition( composition, parent, path ) {
const dependent = csn.definitions[composition.target];
if (skipConstraintGeneration(parent, dependent, composition))
return;
const onCondition = composition.on;
if (composition.$selfOnCondition && composition.$selfOnCondition.up_.length === 1) {
const upLinkName = composition.$selfOnCondition.up_[0];
const up_ = csn.definitions[composition.target].elements[upLinkName];
if (up_.keys && isToOne(up_)) // no constraint for unmanaged / to-many up_ links
foreignKeyConstraintForAssociation(up_, dependent, [ 'definitions', composition.target, 'elements', upLinkName ], path[path.length - 1] );
}
else if (!onCondition && composition.keys.length > 0) {
throw new CompilerAssertion('Debug me, an on-condition was expected here, but only found keys');
}
}
/**
* Calculate referential constraints for dependent keys in the entity where the cds.Associations is defined.
* The DELETE rule for a referential constraint stemming from a cds.Association will be 'RESTRICT'
* If the association is used as an <up_> link in a compositions on-condition, the ON DELETE rule will be `CASCADE`
*
* @param {CSN.Association} association for that a constraint should be generated
* @param {CSN.Entity} dependent the entity for which a constraint will be generated
* @param {CSN.Path} path
* @param {CSN.PathSegment} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
*/
function foreignKeyConstraintForAssociation( association, dependent, path, upLinkFor = null ) {
const parent = csn.definitions[association.target];
if (skipConstraintGeneration(parent, dependent, association))
return;
const { elements } = dependent;
if (association.keys) {
// 1. cds.Association has constraint compliant on-condition
// mark each dependent key - in the entity containing the association - referenced in the on-condition
const parentKeys = [];
const dependentKeys = [];
association.keys.forEach( (k) => {
dependentKeys.push( [ k.$generatedFieldName, elements[k.$generatedFieldName] ] );
const parentKey = parent.elements[k.ref[0]];
if (parentKey.key) // only keys are valid references in foreign key constraints
parentKeys.push( [ k.ref[0], parent.elements[k.ref[0]] ] );
});
if (isTenant && elements.tenant && parent.elements.tenant) { // `tenant` is not part of on
dependentKeys.push([ 'tenant', elements.tenant ]);
parentKeys.push([ 'tenant', parent.elements.tenant ]);
}
const allKeysCovered = parentKeys.length === Object.values(parent.elements).filter(e => e.key).length;
// sanity check; do not generate constraints for on-conditions like "dependent.idOne = id AND dependent.idTwo = id"
if (allKeysCovered && dependentKeys.length === parentKeys.length)
attachConstraintsToDependentKeys(dependentKeys, parentKeys, association.target, path[path.length - 1], upLinkFor);
}
}
/**
* Extracts dependent keys and their parent keys based on an 'on' condition.
*
* @param {CSN.OnCondition} on - on condition from which dependent keys and their parent keys are extracted.
* @param {CSN.Elements} elements - The elements of the dependent entity, containing the foreign keys.
* @param {CSN.Elements} parentElements - The elements of the parent entity, containing the referenced parent keys.
* @returns {object} An object containing dependent keys and the parent keys which they reference.
*/
function extractKeys( on, elements, parentElements ) {
const dependentKeys = Array.from(elementsOfSourceSide(on, elements));
const parentKeys = Array.from(elementsOfTargetSide(on, parentElements));
if (isTenant && elements.tenant && parentElements.tenant) { // `tenant` is not part of on
dependentKeys.push([ 'tenant', elements.tenant ]);
parentKeys.push([ 'tenant', parentElements.tenant ]);
}
return { dependentKeys, parentKeys };
}
/**
* Attach constraints to individual foreign key elements
* The $foreignKeyConstraint property will later be collected from the foreign keys
* and attached to the $tableConstraints property of the respective entity.
*
* @param {Array} dependentKeys array holding dependent keys in the format [['key1', 'value1'], [...], ...]
* @param {Array} parentKeys array holding parent keys in the format [['key1', 'value1'], [...], ...]
* @param {CSN.PathSegment} parentTable the sql-table where the foreign key constraints will be pointing to
* @param {CSN.PathSegment} sourceAssociation the name of the association from which the constraint originates
* @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
* it is used for a comment in the constraint, which is only printed out in test-mode
*/
function attachConstraintsToDependentKeys(
dependentKeys,
parentKeys,
parentTable,
sourceAssociation,
upLinkFor = null
) {
while (dependentKeys.length > 0) {
const dependentKeyValuePair = dependentKeys.pop();
const dependentKey = dependentKeyValuePair[1];
// if it already has a dependent key assigned, do not overwrite it.
// this is the case for <up_> associations in on-conditions of compositions
const { $foreignKeyConstraint } = dependentKey;
// in contrast to foreign keys which stem from managed associations,
// a tenant foreign key column may have multiple parent keys as partners
const tenantForeignKey = isTenant && dependentKeyValuePair[0] === 'tenant';
if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor))
return;
const parentKeyValuePair = parentKeys.pop();
const parentKeyName = parentKeyValuePair[0];
const constraint = {
parentKey: parentKeyName,
parentTable,
upLinkFor,
sourceAssociation,
onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT',
validated,
enforced,
};
if (tenantForeignKey) {
const dontOverwriteUp = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === sourceAssociation && c.parentTable === parentTable);
const dontOverwriteTexts = dependentKey.$foreignKeyConstraint && dependentKey.$foreignKeyConstraint.some(c => c.sourceAssociation === 'texts' && c.upLinkFor.texts);
if (!dontOverwriteUp && !dontOverwriteTexts)
dependentKey.$foreignKeyConstraint = dependentKey.$foreignKeyConstraint ? [ ...dependentKey.$foreignKeyConstraint, constraint ] : [ constraint ];
}
else {
dependentKey.$foreignKeyConstraint = constraint;
}
}
}
/**
* Skip referential constraint if the parent table (association target, or artifact where composition is defined)
* of the relation is:
* - a query
* - annotated with '@cds.persistence.skip:true'
*
* The following decision table reflects the current implementation:
*
* +-----------------+--------------------+-------------------+----------+
* | Global Switch: | Global Check Type: | `@assert.integrity` | Generate |
* |"assertIntegrity"| "assertIntegrityType"| | Constraint|
* +-----------------+--------------------+-------------------+----------+
* | on | RT | false | no |
* +-----------------+--------------------+-------------------+----------+
* | on | RT | true/not set | no |
* +-----------------+--------------------+-------------------+----------+
* | on | RT | RT | no |
* +-----------------+--------------------+-------------------+----------+
* | on | RT | DB | yes |
* +-----------------+--------------------+-------------------+----------+
* | | | | |
* +-----------------+--------------------+-------------------+----------+
* | on | DB | false | no |
* +-----------------+--------------------+-------------------+----------+
* | on | DB | true/not set | yes |
* +-----------------+--------------------+-------------------+----------+
* | on | DB | RT | no |
* +-----------------+--------------------+-------------------+----------+
* | on | DB | DB | yes |
* +-----------------+--------------------+-------------------+----------+
* | | | | |
* +-----------------+--------------------+-------------------+----------+
* | off | don't care | don't care | no |
* +-----------------+--------------------+-------------------+----------+
* | | | | |
* +-----------------+--------------------+-------------------+----------+
* | individual | RT | true | no |
* +-----------------+--------------------+-------------------+----------+
* | individual | DB | true | yes |
* +-----------------+--------------------+-------------------+----------+
* | individual | don't care | RT | no |
* +-----------------+--------------------+-------------------+----------+
* | individual | don't care | DB | yes |
* +-----------------+--------------------+-------------------+----------+
* | individual | don't care | false/not set | no |
* +-----------------+--------------------+-------------------+----------+
*
* @param {CSN.Definition} parent entity where the foreign key reference will point at
* @param {CSN.Definition} dependent entity where the constraint will be defined on
* @param {CSN.Association} element the composition or association
* @returns {boolean}
*/
function skipConstraintGeneration( parent, dependent, element ) {
// if set to 'off' don't even bother, just skip all constraints
if (options.assertIntegrity === false || options.assertIntegrity === 'false')
return true;
if (parent.query)
return true;
// no constraint if either dependent or parent is not persisted
if (
hasPersistenceSkipAnnotation(parent) ||
hasPersistenceSkipAnnotation(dependent) ||
parent['@cds.persistence.exists'] ||
dependent['@cds.persistence.exists']
)
return true;
// some commonly used string literals
const RT = 'RT';
const DB = 'DB';
const CREATE_FOR_UP = '$createReferentialConstraintForUp_';
const SKIP_FOR_UP = '$skipReferentialConstraintForUp_';
// if the element itself is explicitly excluded from being checked
// skip the constraint for it (and its backlink)
if (isAssertIntegrityAnnotationSetTo(false) ||
isAssertIntegrityAnnotationSetTo(RT) ||
element[SKIP_FOR_UP]
) {
// for "auto-generated" associations like for the up_ of a composition of aspects,
// the annotation on the composition influences the referential constraint for the
// up_ association
if (element.$selfOnCondition && element.targetAspect)
assignPropOnBacklinkIfPossible(SKIP_FOR_UP, true);
return true;
}
const runtimeChecks = options.assertIntegrityType && options.assertIntegrityType.toUpperCase() === RT;
const compilerChecks = options.assertIntegrityType && options.assertIntegrityType.toUpperCase() === DB;
if ((!options.assertIntegrity || options.assertIntegrity === true || options.assertIntegrity === 'true') &&
(!options.assertIntegrityType || runtimeChecks))
return assertForIntegrityTypeRT();
if ((!options.assertIntegrity || options.assertIntegrity === true || options.assertIntegrity === 'true') &&
compilerChecks)
return assertForIntegrityTypeDB();
if ((options.assertIntegrity === 'individual'))
return assertForIndividual();
// The default for the assertIntegrityType is 'RT', no constraints in that case
if ((!options.assertIntegrity || options.assertIntegrity === true) &&
(!options.assertIntegrityType || runtimeChecks))
return true;
if (!element.keys || !isToOne(element))
return true;
return false;
/**
* if global checks are 'individual' we evaluate every association,
* we create db constraints if it is annotated with `@assert.integrity`: 'DB' (or true)
*
* @returns {boolean}
*/
function assertForIndividual() {
if (isAssertIntegrityAnnotationSetTo(DB) || element[CREATE_FOR_UP]) {
// if this is a $self comparison, the up_ link should then result in a constraint
assignPropOnBacklinkIfPossible(CREATE_FOR_UP, true);
return false;
}
if (options.assertIntegrityType === DB && isAssertIntegrityAnnotationSetTo(true)) {
// if this is a $self comparison, the up_ link should then result in a constraint
assignPropOnBacklinkIfPossible(CREATE_FOR_UP, true);
return false;
}
// individual and no ('DB') annotation on constraint --> skip
return true;
}
/**
* if global check type is 'RT' (or not provided) only generate DB constraint if element
* is explicitly annotated "@assert.integrity: 'DB'"
*
* @returns {boolean}
*/
function assertForIntegrityTypeRT() {
// for "auto-generated" associations like for the up_ of a composition of aspects,
// the annotation on the composition influences the referential constraint for the
// up_ association
if (isAssertIntegrityAnnotationSetTo(DB)) {
if (element.targetAspect)
assignPropOnBacklinkIfPossible(CREATE_FOR_UP, true);
return false;
}
if (element[CREATE_FOR_UP])
return false;
return true;
}
/**
* if global checks are on and global integrity check type is 'DB'
* we create db constraints in any case except if annotated
* with `@assert.integrity: 'RT'` (or false, but that is rejected earlier)
*
* @returns {boolean}
*/
function assertForIntegrityTypeDB() {
return isAssertIntegrityAnnotationSetTo(RT);
}
/**
* Convenience to check if value of element's `@assert.integrity` annotation
* is the same as a given value. `@assert.integrity`-value checks do not use the "truthy"-semantics,
* since string values _and_ booleans are allowed, but are treated differently.
*
* @param {string|boolean} value
* @returns {boolean}
*/
function isAssertIntegrityAnnotationSetTo( value ) {
if (typeof element['@assert.integrity'] === 'string' && typeof value === 'string')
return element['@assert.integrity'].toUpperCase() === value.toUpperCase();
return element['@assert.integrity'] === value;
}
/**
* Assigns a helper key-value pair on the up_ association for a $self comparison
* for the current 'element', if applicable
*
* @param {string} prop
* @param {object} val
*/
function assignPropOnBacklinkIfPossible( prop, val ) {
if (!element.$selfOnCondition)
return;
const target = csn.definitions[element.target];
const backlink = target.elements[element.$selfOnCondition.up_[0]];
backlink[prop] = val;
}
}
/**
* If we have a managed composition with a target cardinality of one, we will treat it like
* a regular association when it comes to referential constraints.
* The constraint will thus be generated for the foreign key we create in the source entity.
*
* @param {CSN.Composition} composition the composition which might be treated like an association
* @returns {boolean} true if the composition should be treated as an association in regards to foreign key constraints
*/
function treatCompositionLikeAssociation( composition ) {
return Boolean(isToOne(composition) && composition.keys);
}
/**
* returns true if the association/composition has a max target cardinality of one
*
* @param {CSN.Association|CSN.Composition} assocOrComposition
* @returns {boolean}
*/
function isToOne( assocOrComposition ) {
const { min, max } = assocOrComposition.cardinality || {};
return !min && !max || max === 1;
}
/**
* Finds and returns elementNames and elements of target side mentioned in on-condition.
*
* @param {CSN.OnCondition} on
* @param {CSN.Elements} targetElements elements of association/composition target entity
* @returns {Map} of target elements with their name as key
*/
function elementsOfTargetSide( on, targetElements ) {
const elements = new Map();
const findElements = (tokenStream) => {
tokenStream
.forEach((element) => {
if (typeof element === 'object' && element.ref?.length > 1 && targetElements[element.ref[element.ref.length - 1]])
elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
else if (element.xpr)
findElements(element.xpr);
});
};
findElements(on);
return elements;
}
/**
* Finds and return elementNames and elements of source side mentioned in on-condition.
*
* @param {CSN.OnCondition} on the on-condition
* @param {CSN.Elements} sourceElements elements of source entity where the association/composition is defined.
* @returns {Map} of source elements with their name as key
*/
function elementsOfSourceSide( on, sourceElements ) {
const elements = new Map();
const findElements = (tokenStream) => {
tokenStream
.forEach((element) => {
if (typeof element === 'object' && element.ref?.length === 1 && sourceElements[element.ref[0]])
elements.set(element.ref[0], sourceElements[element.ref[0]]);
else if (element.xpr)
findElements(element.xpr);
});
};
findElements(on);
return elements;
}
/**
* Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same sourceAssociation
* and attaches it to the given artifact.
*
* Go over all elements with $foreignKeyConstraint property:
* - Find all other elements in artifact with the same sourceAssociation
* - Create constraints with the information supplied by $parentKey, $parentTable and $onDelete
*
* @param {CSN.Artifact} artifact
* @param {string} artifactName
*/
function collectAndAttachReferentialConstraints( artifact, artifactName ) {
const referentialConstraints = Object.create(null);
// tenant foreign keys may have multiple parent keys
// process tenant foreign key first
if (isTenant && artifact.elements?.tenant) {
const element = artifact.elements.tenant;
if (element.$foreignKeyConstraint) {
const tenantConstraints = element.$foreignKeyConstraint;
delete element.$foreignKeyConstraint;
// create (multiple) foreign key constraint(s) for the tenant column with each association in the dependent entity
tenantConstraints.forEach((c) => {
createReferentialConstraints(c, 'tenant');
});
}
}
for (const elementName in artifact.elements) {
const element = artifact.elements[elementName];
if (!element.$foreignKeyConstraint)
continue;
// copy constraint property, and delete it from the element
const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint);
delete element.$foreignKeyConstraint;
createReferentialConstraints($foreignKeyConstraint, elementName);
}
if (Object.keys(referentialConstraints).length) {
if (!('$tableConstraints' in artifact))
artifact.$tableConstraints = Object.create(null);
artifact.$tableConstraints.referential = referentialConstraints;
}
/**
* Creates referential constraints for database relationships. This function constructs constraints based on foreign key information and element names,
* and determines deletion rules based on the existing constraints and options. It manages dependencies and names for constraints dynamically during
* execution.
*
* @param {object} $foreignKeyConstraint - An object encapsulating details about the foreign key constraint
* @param {string} elementName - The name of the dependent element or table that is linked by the foreign key.
*/
function createReferentialConstraints($foreignKeyConstraint, elementName) {
const { parentTable } = $foreignKeyConstraint;
const parentKey = [ $foreignKeyConstraint.parentKey ];
const dependentKey = [ elementName ];
const onDeleteRules = new Set();
onDeleteRules.add($foreignKeyConstraint.onDelete);
forEach(artifact.elements, (foreignKeyName, foreignKey) => {
// find all other `$foreignKeyConstraint`s with same `sourceAssociation` and same `parentTable`
const matchingForeignKeyFound = foreignKey.$foreignKeyConstraint &&
foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation &&
foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable;
if (!matchingForeignKeyFound)
return;
const $foreignKeyConstraintCopy = Object.assign({}, foreignKey.$foreignKeyConstraint);
delete foreignKey.$foreignKeyConstraint;
parentKey.push($foreignKeyConstraintCopy.parentKey);
dependentKey.push(foreignKeyName);
onDeleteRules.add($foreignKeyConstraintCopy.onDelete);
});
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > CASCADE
const onDelete = onDeleteRules.has('RESTRICT') ? 'RESTRICT' : 'CASCADE';
let onDeleteRemark = null;
// comments in sqlite files are causing the JDBC driver to throw an error on deployment
if (options.testMode && onDelete === 'CASCADE') {
if ($foreignKeyConstraint.upLinkFor?.texts)
onDeleteRemark = `Constraint originates from localized composition ”${ $foreignKeyConstraint.parentTable }:texts“`;
else
onDeleteRemark = `Up_ link for Composition "${ $foreignKeyConstraint.upLinkFor }" implies existential dependency`;
}
// constraint identifier usually start with `c__` to avoid name clashes
let identifier = options.pre2134ReferentialConstraintNames ? '' : 'c__';
identifier += `${ getResultingName(csn, options.sqlMapping, artifactName) }_${ $foreignKeyConstraint.sourceAssociation }`;
referentialConstraints[`${ getResultingName(csn, 'quoted', artifactName) }_${ $foreignKeyConstraint.sourceAssociation }`] = {
identifier,
foreignKey: dependentKey,
parentKey,
dependentTable: artifactName,
parentTable,
onDelete,
onDeleteRemark, // explain why this particular rule is chosen
validated: $foreignKeyConstraint.validated,
enforced: $foreignKeyConstraint.enforced,
};
}
}
}
/**
* If the artifact has both, unique- and foreign key constraints, it is possible that the constraints have the same identifier.
* This would end in table which can't be activated.
*
* @param {CSN.Artifact} artifact
* @param {string} artifactName
* @param {CSN.Path} path
* @param {Function} error
*/
function assertConstraintIdentifierUniqueness( artifact, artifactName, path, error ) {
// can only happen if referential & unique constraints are present
if (!(artifact.$tableConstraints && artifact.$tableConstraints.referential && artifact.$tableConstraints.unique))
return;
forEachKey(artifact.$tableConstraints.unique, (uniqueConstraintKey) => {
const uniqueConstraintIdentifier = `${ artifactName }_${ uniqueConstraintKey }`; // final unique constraint identifier will be generated in renderer likewise
if (artifact.$tableConstraints.referential[uniqueConstraintIdentifier]) {
error(null, path,
{ name: uniqueConstraintIdentifier, art: artifactName },
'Duplicate constraint name $(NAME) in artifact $(ART)');
}
});
}
module.exports = { createReferentialConstraints, assertConstraintIdentifierUniqueness };