text: 'use strict';
  var { CompilationError, hasErrors, sortMessages } = require('../base/messages');
  const { forEachDefinition, forEachMemberRecursively, forEachMember, forEachGeneric, setProp,
          cloneWithTransformations }  = require('../base/model');
  const deepCopy = require('../base/deepCopy');
  const { isAssocOrComposition, isStructuredElement, isManagedAssociationElement,
          foreachPath, printableName, mergeOptions, hasBoolAnnotation, getTopLevelArtifactNameOf,
          isElementWithType, addStringAnnotationTo, getArtifactDatabaseNameOf,
          getElementDatabaseNameOf } = require('../model/modelUtils');
  const transformUtils = require('./transformUtils');
  const { checkTypeParameters } = require('../checks/checkElements');
  const alerts = require('../base/alerts');
  const { translateAssocsToJoins, constructPathNode, walkQuery } = require('./translateAssocsToJoins');
  
  /**
   * If a mixin association is published, return the mixin association.
   *
   * @param {object} artifact Artifact to check
   * @param {object} association Association (Element) published by the view
   * @returns {object} The mixin association
   */
  function getMixinAssocIfPublished(artifact, association){
    return getMixinAssocOfQueryIfPublished(artifact.query, association);
  }
  
  
  function getMixinAssocOfQueryIfPublished(query, association){
    if(query && query.mixin){
      for(let elem of Object.keys(query.mixin)){
        const mixin_element= query.mixin[elem];
        if(mixin_element.name.id === association.name.id && mixin_element.name.absolute === association.name.absolute){
          return mixin_element;
        }
      }
    }
    return undefined;
  }
  
  /**
   * Check wether the given artifact uses the given mixin association.
   *
   * @param {object} artifact Artifact to check
   * @param {object} association Mixin association (Element) to check for
   * @returns {Boolean} True if used
   */
  function usesMixinAssociation(artifact, association){
    if(artifact.elements){
      for(let elem of Object.keys(artifact.elements)){
        const element = artifact.elements[elem];
        if(element.value && element.value.path && element.value.path.length > 1 && element.value.path[0].id === association.name.id){
          return true;
        }
      }
    }
    return false;
  }
  
  
  /**
   * Return a copy of augmented CSN 'model' with a number of transformations made for rendering
   * in HANA CDS style, used by 'toHana', toSql' and 'toRename'.
   * The behavior is controlled by the following options:
   * options = {
   *    forHana.names                               // See the behavior of 'names' in toHana, toSql and toRename
   *    forHana.associations                        // See the behavior of 'associations' in toHana and toSql
   *                                                // (please note that toSql will always set at least 'mixin')
   *    forHana.keepNamespaces                      // Do not transform namespaces to contexts (to be used for
   *                                                // producing HANA-CDS compatible names with 'toHana', 'toSql' ...)
   *    forHana.keepStructsAssocs                   // Do not flatten structs, do not convert managed assocs to
   *                                                // unmanaged ones, do not convert assocs to joins (to be used
   *                                                // for rendering strictly HANA-CDS compatible CDS source with
   *                                                // 'toHana')
   *    forHana.alwaysResolveDerivedTypes           // Always resolve derived type chains (by default, this is only
   *                                                // done for 'quoted' names). FIXME: Should always be done in general.
   * }
   * The result model will always have 'options.forHana' set, to indivate that these transformations have happened.
   * The following transformations are made:
   * - (000) Some primitive type names are mapped to HANA type names (e.g. DateTime => UTCDateTime,
   *         Date => LocalDate, ...).The primitive type 'UUID' is renamed to 'String' (see also 060 below).
   * - (001) Add a temporal where condition to views where applicable before assoc2join
   * - (010) (not for 'keepStructsAssocs'): Transform associations to joins if requested by option
   *         'forHana.associations'
   * - (015) Draft shadow entities are generated for entities/views annotated with '@odata.draft.enabled'.
   * - (020) Check: in "plain" mode, quoted ids are not allowed.
   *         (a) check in namespace declarations
   *         (b) check in artifact/element definitions.
   * - (030) ("plain" only) For all elements, derived types are replaced by their final base type.
   * - (040) Abstract entities and entities 'implemented in' something are ignored, as well
   *         as entities annotated with '@cds.persistence.skip' or '@cds.persistence.exists'.
   * - (045) The query is stripped from entities that are annotated with '@cds.persistence.table',
   *         essentially converting views to entities.
   * - (050) Checks on the hierarchical model (pre-flattening)
   *         array of, @cds.valid.from/to
   * - (060) Users of primitive type 'UUID' (which is renamed to 'String' in 000) get length 36'.
   * - (070) Default type facets
   * - (080) Annotation definitions are ignored (note that annotation assignments are filtered out by toCdl).
   * - (090) Compositions become associations.
   * - (100) 'masked' is ignored (a), and attribute 'localized' is removed (b)
   * - (110) Actions and functions (bound or unbound) are ignored.
   * - (120) (a) Services become contexts.
   *         (b) (not for 'keepNamespaces'): Namespaces become contexts.
   * - (130) (not for 'keepStructsAssocs'): Elements having structured types are flattened into
   *         multiple elements (using '_' or '.' as name separator, depending on 'forHana.names').
   * - (140) (not for 'keepStructsAssocs'): Managed associations get explicit ON-conditions, with
   *         generated foreign key elements (also using '_' or '.' as name separator, depending on 'forHana.names').
   * - (150) (a) Elements from inherited (included) entities are copied into the receiving entity
   *         (b) The 'include' property is removed from entities.
   * - (160) Projections become views, with MIXINs for association elements (adding $projection where
   *         appropriate for ON-conditions).
   * - (170) ON-conditions referring to '$self' are transformed to compare explicit keys instead.
   * - (180) In projections and views, ...
   *         (a) association elements that are mixins must not be explicitly redirected
   *         (b) MIXINs are created for association elements in the select list that are not mixins by themselves.
   * - (190) For all enum types, ...
   *         (a) enum constants in defaults are replaced by their values (assuming a matching enum as element type)
   *         (b) the enum-ness is stripped off (i.e. the enum type is replaced by its final base type).
   * - (200) The 'key' property is removed from all elements of types.
   * - (210) (not for 'keepStructsAssocs'): Managed associations in GROUP BY and ORDER BY are
   *         replaced by by their foreign key fields.
   * - (220) Contexts that contain no artifacts or only ignored artifacts are ignored.
   * - (230) (only for 'keepStructsAssocs'): The following are rejected in views
   *         (a) Structured elements
   *         (b) Managed association elements
   *         (c) Managed association entries in GROUP BY
   *         (d) Managed association entries in ORDER BY
   * - (240) All artifacts (a), elements, foreign keys, parameters (b) that have a DB representation are annotated
   *         with their database name (as '@cds.persistence.name') according to the naming convention chosen
   *         in 'options.forHana.names'.
   *
   * @param {XSN.Model}   inputModel
   * @param {CSN.Options} [options]
   */
  function transformForHana(inputModel, options) {
    // Work on a copy of the input model
    const { error, warning, info, signal } = alerts(inputModel);
  
    let model = deepCopy(inputModel);
    // if the input model is not an augmented csn -> augment it
    // FIXME: only in beta mode at the moment
    if (!model.sources && options.betaMode) {
      let main = require('../main');
      model = main.compileSources({ '<stdin>.json' : JSON.stringify(model, null, 2) }, options);
    }
  
    model.messages = inputModel.messages;
  
    // Tell consumers that this model has undergone the transformations for HANA, and merge options into model
    model.options = mergeOptions({ forHana: true }, inputModel.options, options);
    options = model.options;
  
  
    const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_';
  
    const { addDefaultTypeFacets, flattenStructuredElement, flattenForeignKeys, checkForeignKeys,
      flattenStructStepsInPath, createForeignKeyElement, toFinalBaseType,
      isAssociationOperand, isDollarSelfOrProjectionOperand,
      createAndAddDraftAdminDataProjection, createScalarElement, createAssociationElement,
      addElement,copyAndAddElement, createAssociationPathComparison,
      extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments } = transformUtils.getTransformers(model, pathDelimiter);
  
    // (000) Rename primitive types, make UUID a String
    renamePrimitiveTypesAndUuid();
  
    // (001) Add a temporal where condition to views where applicable before assoc2join
    //       assoc2join eventually rewrites the table aliases
    //       Temporal only in beta-mode
    forEachDefinition(model, a => {
      addTemporalWhereConditionToView(a);
  
      // Make sure that all source artifacts and association targets reach the database
      // (otherwise the view can't be activated), but only if the source artifact is NOT activated against the database
      if(!isNoDbArtifact(a) && !hasBoolAnnotation(a, '@cds.persistence.table') && a.$queries && a.$queries.length > 0) {
        let env = {
          aliasCount: 0,
          walkover: { from: true, onCondFrom:true, select:true, filter: true },
          callback: (pathDict, env) => {
            if(env.location === 'from') {
              pathDict.path.forEach((ps) => {
                let art = ps._artifact.target ? ps._artifact.target._artifact : ps._artifact;
                if(isNoDbArtifact(art)) {
                  signal(error`"${a.name.absolute}": "${art.name.absolute}" in path step "${ps.id}" of "${pathDict.path.map(p=>p.id).join('.')}" is either 'abstract' or '@cds.persistence.skip'`, pathDict.location);
                }
              })
            }
            else { // in the select clause (assoc path step must not be last in path)
              if(pathDict.path[0]._navigation && pathDict.path[0]._navigation.kind !== '$tableAlias') {
                pathDict.path.forEach((ps, pos) => {
                  if(ps._artifact.target && isNoDbArtifact(ps._artifact.target._artifact) && pos < pathDict.path.length-1) {
                    let art = ps._artifact.target._artifact;
                    signal(error`"${a.name.absolute}": "${art.name.absolute}" in path step "${ps.id}" of "${pathDict.path.map(p=>p.id).join('.')}" is either 'abstract' or '@cds.persistence.skip'`, pathDict.location);
                  }
                })
              }
            }
          }
        };
        a.$queries.forEach(q => walkQuery(q, env));
      }
    } );
  
    // (010) If requested, translate associations to joins
    if (!options.forHana.keepStructsAssocs && options.forHana.associations !== 'assocs') {
      model = translateAssocsToJoins(model);
    }
  
    // (020 a) In "plain" mode, quoted ids are not allowed.
    // Here, check namespace declarations (only stored in model.sources).
    if (options.forHana.names === 'plain' && model.sources) {
      for (let s in model.sources) {
        if (model.sources[s].namespace && model.sources[s].namespace.path) {
          model.sources[s].namespace.path.filter(x => x.quoted).forEach(errorQuotedIdentifier);
        }
      }
    }
  
    // (030) - only for 'plain' names
    // For all elements, replace derived types by final base type
    // Needs to happen before setLengthForFormerUuid()
    // FIXME: Actually, we should simply do this always
    if (options.forHana.alwaysResolveDerivedTypes || options.forHana.names === 'plain') {
      forEachDefinition(model, artifact => {
        forEachMemberRecursively(artifact, member => {
          toFinalBaseType(member);
        });
      });
    }
  
    // Process all artifacts (pass 1)
    forEachDefinition(model, artifact => {
      // (040) Ignore entities and views that are abstract or implemented
      // or carry the annotation cds.persistence.skip/exists
      // These entities are not removed from the model, but flagged as "to be ignored"
      if (artifact.kind === 'entity' || artifact.kind === 'view') {
        if (isNoDbArtifact(artifact) || hasBoolAnnotation(artifact, '@cds.persistence.exists')) {
          artifact._ignore = true;
        }
        // issue #3450 HANA CDS can not handle external artifacts which are part of a HANA CDS context
        if (options.forHana.names==='quoted'
            && hasBoolAnnotation(artifact, '@cds.persistence.exists')) {
          let fullname = artifact.name.absolute;
          let firstPath = fullname.split('.')[0];
          let topParent = model.definitions[firstPath];
          // namespaces, contexts and services become contexts in HANA CDS
          if(topParent && ['namespace','context','service'].includes(topParent.kind))
            signal(warning`"${fullname}": external definition belongs to ${topParent.kind} "${topParent.name.absolute}"`, artifact.location);
        }
      }
  
      const stripQueryish = artifact.query && hasBoolAnnotation(artifact, '@cds.persistence.table');
      // (045) Strip all query-ish properties from views and projections annotated with '@cds.persistence.table',
      // and make them entities
      if (stripQueryish) {
        artifact.kind = 'entity';
        delete artifact.query;
        delete artifact.$queries;
        delete artifact.$from;
        delete artifact.source;
      }
  
      // (050) Checks on the hierarchical model (pre-flattening)
      if(artifact.kind === 'entity' && !artifact.query) {
        // only loop over the top-level members -> We do the recursive checking in the checkForArrayOf function.
        forEachMember(artifact, member => {
          checkForArrayOf(artifact, member);
  
        })
      }
  
      let validFrom = [], validTo = [], validKey = [];
      forEachMemberRecursively(artifact, member => {
        // Collect elements with @cds.valid.from, @cds.valid.to
        let [f,t,k] = extractValidFromToKeyElement(member);
        validFrom.push(...f);
        validTo.push(...t);
        validKey.push(...k);
  
        // All elements must have a type for this to work
        if (stripQueryish && member.kind === 'element' && !isElementWithType(member)) {
          signal(error`"${artifact.name.absolute}.${member.name.element}": All elements must have a type when entity is annotated with "@cds.persistence.table"`, member.location);
        }
      });
  
      // (050) Check @cds.valid.from/to only on entity
      //       Views are checked in (001), unbalanced valid.from/to's or mismatching origins
      if(artifact.kind === 'entity' && !artifact.query) {
        validFrom.forEach(e => checkAssignment('@cds.valid.from', e, artifact));
        validTo.forEach(e => checkAssignment('@cds.valid.to', e, artifact));
        validKey.forEach(e => checkAssignment('@cds.valid.key', e, artifact));
        checkMultipleAssignments(validFrom, '@cds.valid.from', artifact);
        checkMultipleAssignments(validTo, '@cds.valid.to', artifact, true);
        checkMultipleAssignments(validKey, '@cds.valid.key', artifact);
      }
  
      // if there is an cds.valid.key, make this the only primary key
      // otherwise add all cds.valid.from to primary key tuple
      if(validKey.length) {
        if(!validFrom.length || !validTo.length){
          signal(error`@cds.valid.key was used but @cds.valid.from and @cds.valid.to are missing`, artifact.location);
        }
        forEachMember(artifact, member => {
          if(member.key){
            member.unique = { val: true };
            delete member.key;
          }
        });
        validKey.forEach(member => {
          member.key = { val: true }
        });
  
        validFrom.forEach(member => {
          member.unique = { val: true };
        });
      }
      else {
        validFrom.forEach(member => {
          member.key = { val: true }
        });
      }
  
      // (120 a) Services become contexts
      if (artifact.kind === 'service') {
        artifact.kind = 'context';
      }
  
      // Do things specific for entities and views (pass 1)
      if (artifact.kind === 'entity' || artifact.kind === 'view') {
        transformEntityOrViewPass1(artifact);
      }
      // (080) Ignore annotation declarations
      if (artifact.kind === 'annotation') {
        artifact._ignore = true;
      }
  
      // For generating DB stuff:
      // - table-entity with parameters: not allowed
      // - view with parameters: ok on HANA, not allowed otherwise
      // (don't complain about action/function with parameters)
      if (!artifact._ignore && artifact.params && (artifact.kind === 'entity' || artifact.kind === 'view')) {
        if (!artifact.query) { // table entity with params
          let loc = Object.keys(artifact.params)[0] && artifact.params[Object.keys(artifact.params)[0]].location || null;
          signal(error`"${artifact.name.absolute}": Table-like entities with parameters are not supported for conversion to ${options.toSql?'SQL':'HANA CDS'}`, loc);
        }
        else if (options.forHana.dialect !== 'hana') { // view with params
          signal(error`"${artifact.name.absolute}": Entities with parameters are only supported on HANA`, artifact.location);
        }
        else  {
          for (let pname in artifact.params) {
            if (pname.match(/\W/g) || pname.match(/^\d/) || pname.match(/^_/)) { // parameter name must be regular SQL identifier
              signal(warning`"${artifact.name.absolute}", parameter "${pname}": is not a regular SQL identifier`, artifact.params[pname].location);
            }
            else if (options.forHana.names !== 'plain' && pname.toUpperCase() !== pname) { // not plain mode: param name must be all upper
              signal(warning`"${artifact.name.absolute}", parameter "${pname}": in naming mode "${options.forHana.names}" parameter names must be all uppercase`, artifact.params[pname].location);
            }
          }
        }
      }
  
      // Process the artifact's  members
      forEachMemberRecursively(artifact, member => {
        // (100 a) Ignore the property 'masked' itself (but not its effect on projections)
        if (member.masked) {
          member._ignoreMasked = true;
        }
        // Barf if a key is explicitly (!) nullable
        if (member.key && member.key.val && member.notNull && !member.notNull.val) {
          signal(error`"${artifact.name.absolute}.${member.name.element}": Key elements cannot be nullable in HANA CDS`, member.notNull.location);
        }
  
        // For HANA: Report an error on
        // - view with parameters that has an element of type association/composition
        // - association that points to entity with parameters
        if (options.forHana.dialect === 'hana' && isAssocOrComposition(member.type) && !options.betaMode) {
          if (artifact.params) {
            signal(error`"${artifact.name.absolute}.${member.name.element}": Associations are not allowed in entities with parameters`, member.location);
          }
          else if(hasBoolAnnotation(artifact, '@cds.persistence.udf') || hasBoolAnnotation(artifact, '@cds.persistence.calcview')) {
            signal(error`${artifact.name.absolute}.${member.name.element}": Associations are not allowed in entities annotated with @cds.persistence { udf, calcview }`, member.location);
          }
  
          if (member.target._artifact.params) {
            signal(error`"${artifact.name.absolute}.${member.name.element}": Associations cannot point to entities with parameters`, member.location);
          }
          else if(hasBoolAnnotation(artifact, '@cds.persistence.udf') || hasBoolAnnotation(artifact, '@cds.persistence.calcview')) {
            // SAP DBTech JDBC: [7]: feature not supported: parameterized sql view cannot support association: line 1 col 1 (at pos 0)
            // UDF/CVs w/o params don't support 'WITH ASSOCIATIONS'
            signal(error`"${artifact.name.absolute}.${member.name.element}": Associations cannot point to entities annotated with @cds.persistence { udf, calcview }`, member.location);
          }
        }
      });
    });
  
    // Add $inferred: query to the key of view elements tagged with @cds.valid.from
    forEachDefinition(model, a => addInferredToKeys(a));
  
    // (170) Transform '$self' in backlink associations to appropriate key comparisons
    // Must happen before draft processing because the artificial ON-conditions in generated
    // draft shadow entities have crooked '_artifact' links, confusing the backlink processing.
    // But it must also happen after flattenForeignKeys has been called for all artifacts,
    // because otherwise we would produce wrong ON-conditions for the keys involved. Sigh ...
    forEachDefinition(model, artifact => {
      // Fixme: For toHana mixins must be transformed, for toSql -d hana
      // mixin elements must be transformed, why can't toSql also use mixins?
      doit(artifact.elements);
      if(artifact.query && artifact.query.mixin) {
        doit(artifact.query.mixin);
      }
      function doit(dict) {
        for (let elemName in dict) {
          let elem = dict[elemName];
          if (isAssocOrComposition(elem.type) && elem.onCond) {
            processBacklinkAssoc(elem, artifact);
          }
        }
      }
    });
  
    // (015) Generate artificial draft shadow entities if requested
    // Note that this needs to happen after implicit redirection has been performed, because it checks
    // for all draft nodes (additional artifacts reachable via compositions) to be part of a service.
    // This is typically achieved only by means of implicit redirection.
    forEachDefinition(model, artifact => {
      if ((artifact.kind === 'entity' || artifact.kind === 'view') && hasBoolAnnotation(artifact, '@odata.draft.enabled')) {
        // Ignore if not part of a service
        if (!artifact._service) {
          let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location;
          signal(warning`Ignoring annotation "@odata.draft.enabled" - artifact "${artifact.name.absolute}" is not part of a service`, location);
          return;
        }
        // Determine the set of target draft nodes belonging to this draft root (the draft root
        // itself plus all its transitively composition-reachable targets)
        let draftNodes = Object.create(null);
        collectDraftNodesInto(artifact, artifact, draftNodes);
        // Draft-enable all of them
        for (let name in draftNodes) {
          generateDraftForHana(draftNodes[name], artifact);
        }
        // Redirect associations/compositions between draft shadow nodes
        for (let name in draftNodes) {
          let shadowNode = model.definitions[name + '_drafts'];
          // Might not exist because of previous errors
          if (shadowNode) {
            redirectDraftTargets(model.definitions[name + '_drafts'], draftNodes);
          }
        }
      }
    });
  
    /**
     * Check if the given artifact should be turned into a context.
     *
     * @param {any} art Artifact
     * @param {any} artName Artifact Name
     * @returns {Boolean}
     */
    function shouldBeContext(art, artName){
      return ( !options.forHana.keepNamespaces &&
        art.kind === 'namespace' &&
        ((artName !== 'cds' || model.definitions['cds.foundation']) &&
        (artName !== 'localized' || (art.artifacts && Object.keys(art.artifacts).length > 0))));
    }
  
    /**
     * Turn namespaces in .artifacts of a context into contexts as well.
     *
     * @param {any} art Art to transform
     */
    function contextifySubnamespaces(art){
      if(!art.artifacts){
        return;
      }
  
      for (let childName in art.artifacts || {}) {
        let child = art.artifacts[childName];
        if(shouldBeContext(child, childName)){
          art.artifacts[childName].kind = 'context';
          contextifySubnamespaces(child);
        }
  
      }
    }
    // Process all artifacts (pass 2)
    // Perform HANA-specific checks that require the whole model to be processed
    // FIXME: Ideally, we could do this on-the-fly while processing artifacts, but because we do struct
    // flattening etc non-recursively, we need to wait till all artifacts are processed
    forEachDefinition(model, (artifact, name) => {
      // (120 b) Namespaces become contexts (unless explicitly asked to keep them), except
      // the 'cds' namespace if not "extended" by cds.foundation
      if (shouldBeContext(artifact, name)) {
        artifact.kind = 'context';
        contextifySubnamespaces(artifact);
      }
      if (!artifact._ignore) {
        // Some transformation for all kinds of artifacts
        transformCommon(artifact);
        // (150 b) Strip inheritance
        // Note that this should happen after implicit redirection, because includes are required for that
        delete artifact.includes;
        // (240 a) Annotate artifacts with their DB names
        // Skip artifacts that have no DB equivalent anyway
        if (!['service', 'context', 'namespace', 'annotation', 'action', 'function'].includes(artifact.kind)) {
          addStringAnnotationTo('@cds.persistence.name', getArtifactDatabaseNameOf(artifact.name.absolute, options.forHana.names, model), artifact);
        }
  
        // Mark associations with target '@cds.persistence.skip' or abstract to be ignored before the transformEntityOrViewPass2
        if(!isNoDbArtifact(artifact)) {
          if(artifact.query && artifact.query.mixin)
            forEachGeneric(artifact.query, 'mixin', ignoreAssociationToSkippedTarget);
          forEachMemberRecursively(artifact, ignoreAssociationToSkippedTarget);
        }
        // Do things specific for entities and views (pass 2)
        if (artifact.kind === 'entity' || artifact.kind === 'view') {
          transformEntityOrViewPass2(artifact);
        }
  
        forEachMemberRecursively(artifact, (member, memberName) => {
          transformCommon(member);
          // (240 b) Annotate elements, foreign keys, parameters etc with their DB names
          // Virtual elements in entities and types are not annotated, as they have no DB representation.
          // In vies they are, as we generate a null expression for them (null as <colname>)
          if (['element', 'key', 'param'].includes(member.kind) && (!member.virtual || artifact.query)) {
            addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.forHana.names), member);
          }
          // (20 a) If we keep associations as they are (hdbcds naming convention), we cannot have structured
          // view elements (we could enumerate the elements but we can't give them the names one would expect)
          if (options.forHana.keepStructsAssocs
              && artifact.query
              && isStructuredElement(member)) {
            signal(error`With "hdbcds" naming, structured elements cannot be used in a view`, member.location);
            return;
          }
          if (!member._ignore && isAssocOrComposition(member.type)) {
            // Check foreign keys of redirected associations,
            checkForeignKeys(member);
          }
          // (200) Strip 'key' property from type elements
          if (artifact.kind === 'type' && member.key) {
            delete member.key;
          }
        });
  
        // (210) Fix GROUP BY, ORDER BY in views
        if (artifact.query) {
          replaceAssociationsInGroupByOrderBy(artifact);
        }
      }
    });
  
    forEachDefinition(model, (artifact) => {
      // (220) Weed out empty top-level contexts recursively (i.e. those that only have ignored content)
      if (artifact.kind === 'context' && getTopLevelArtifactNameOf(artifact.name.absolute, model) == artifact.name.absolute) {
        ignoreContextsWithIgnoredContent(artifact);
      }
    })
  
    // (190 b) Replace enum types by their final base type
    forEachDefinition(model, (artifact) => {
      replaceEnumByBaseType(artifact);
      forEachMemberRecursively(artifact, (member) => {
        replaceEnumByBaseType(member);
        if (options.forHana.alwaysResolveDerivedTypes || options.forHana.names === 'plain') {
          toFinalBaseType(member);
        }
      });
    });
  
  
    /**
     * Add $inferred : query to key elements of the view if they are annotated
     * with @cds.valid.from.
     *
     * Otherwise, key: true will be generated in the columns of the CSN.
     *
     * @param {any} artifact
     */
    function addInferredToKeys(artifact) {
      if(artifact.query) {
        for(const elemName in artifact.elements){
          const element = artifact.elements[elemName];
  
          if((hasBoolAnnotation(element, '@cds.valid.key') || hasBoolAnnotation(element, '@cds.valid.from')) && element.key && element.key.val === true && !element.key.$inferred){
            element.key.$inferred = 'query';
          }
        }
      }
    }
  
    /**
     * Add a where condition to views that
     * - are annotated with @cds.valid.from and @cds.valid.to,
     * - have only one @cds.valid.from and @cds.valid.to,
     * - and both annotations come from the same entity
     *
     * If the view has one of the annotations but the other conditions are not met, an error will be raised.
     *
     * @param {any} artifact
     */
    function addTemporalWhereConditionToView(artifact) {
      if(artifact.query) {
        // We need to check $combined because:
        // - the "temporal" fields might not be part of the select list AND we don't want to force users to do it
        // - we cannot know on which entity to put the "where" condition
        let [from, to] = getFromToElements(artifact.query.$combined);
        // exactly one validFrom & validTo
        if(from.length == 1 && to.length==1)
        {
          // and both are from the same origin
          if(from[0].origin._artifact._main === to[0].origin._artifact._main) {
  
            let fromPath = [
              { id: from[0]._parent.name.id, _artifact: from[0].origin._artifact._main, _navigation: from[0]._parent },
              { id: from[0].origin._artifact.name.id,       _artifact: from[0].origin._artifact } ];
  
  
            let toPath = [
              { id: to[0]._parent.name.id, _artifact: to[0].origin._artifact._main, _navigation: to[0]._parent },
              { id: to[0].origin._artifact.name.id,        _artifact: to[0].origin._artifact } ];
  
            let atFrom = [ { id: '$at', _artifact: null }, { id: 'from', _artifact: null }];
            let atTo =   [ { id: '$at', _artifact: null }, { id: 'to',   _artifact: null }];
  
            let fromTerm = { op: { val: '<' }, args: [constructPathNode(fromPath, undefined, false), constructPathNode(atTo)] };
            let toTerm =   { op: { val: '>' }, args: [constructPathNode(toPath, undefined, false), constructPathNode(atFrom)] };
  
            let cond = { op: { val: 'and' }, args: [fromTerm, toTerm] };
  
            let where = artifact.query.where;
  
            if(where) { // if there is an existing where-clause, extend it by adding 'and (temporal clause)'
              if(Array.isArray(where))
                where = where[0];
              if(where.op.val === 'and') {
                where.args.push([cond]);
              }
              else {
                where = { op: {val: 'and' }, args: [ [where], [cond]] };
              }
  
              artifact.query.where = where;
            }
            else {
              artifact.query.where = [cond];
            }
          }
          else {
            signal(info`No temporal WHERE clause added as "${from[0].origin._artifact._main.name.absolute}.${from[0].origin._artifact.name.id}" and "${to[0].origin._artifact._main.name.absolute}.${to[0].origin._artifact.name.id}" are not of same origin`, artifact.location);
          }
        }
        else if(from.length > 0 || to.length > 0) {
          signal(info`No temporal WHERE clause added due to unbalanced @cds.valid.from/to`, artifact.location);
        }
      }
    }
  
    /**
     * Get all elements tagged with @cds.valid.from/to from the union of all entities of the from-clause.
     *
     * @param {any} combined union of all entities of the from-clause
     * @returns {Array[]} Array where first field is array of elements with @cds.valid.from, second field is array of elements with @cds.valid.to.
     */
    function getFromToElements(combined) {
      let from = [], to = [];
      for(let name in combined) {
        let elt = combined[name];
        if(!Array.isArray(elt))
          elt = [elt];
        elt.forEach(e => {
          let [f,t] = extractValidFromToKeyElement(e.origin._artifact);
          if(f.length>0)
            from.push(e);
          if(t.length>0)
            to.push(e);
        });
      }
  
      return [from,to];
    }
  
    /**
     * Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors
     *
     * @param {any} model The model
     * @returns {Array} Reclassified messages-Array
     */
    function reclassifyWarnings(model){
      return model.messages.map(message => {
        switch(message.messageId){
          case 'to-many-no-on':
            message.severity = 'Error';
            break;
          case 'empty-entity':
          case 'empty-type':
            message.severity = 'Error';
            break;
        }
        return message;
      })
    }
  
    if(model.messages)
      model.messages = reclassifyWarnings(model);
    // Throw exception in case of errors
    if (hasErrors(model.messages)) {
      throw new CompilationError( sortMessages(model.messages), model)
    }
  
    return model;
  
    // Associations that target a @cds.persistence.skip artifact must be removed from the persistence model
    function ignoreAssociationToSkippedTarget(member) {
      if(options.forHana.dialect === 'hana' && !member._ignore && isAssocOrComposition(member.type) && isUnreachableAssociationTarget(member.target._artifact)) {
        signal(info`${member.type._artifact.name.absolute.replace('cds.', '')} "${member.name.element || member.name.id}" is removed, as Target "${member.target._artifact.name.absolute}" is either 'abstract' or annotated with '@cds.persistence.skip/exists'`, member.location);
        member._ignore = true;
      }
    }
  
  
    // Traverse artifact 'art', set '_ignore' for all contexts that only contain ignored artifacts
    function ignoreContextsWithIgnoredContent(art) {
      // We only care for non-ignored contexts
      if (art._ignore || art.kind !== 'context') {
        return;
      }
      // Descend into children, note if any is non-ignored
      let hasNonIgnoredChildren = false;
      for (let childName in art.artifacts || {}) {
        let child = art.artifacts[childName];
        ignoreContextsWithIgnoredContent(child);
        if (!child._ignore) {
          hasNonIgnoredChildren = true;
        }
      }
      if (!hasNonIgnoredChildren) {
        // If we get here, the context was either empty or everything in it was ignored.
        // We currently preserve empty contexts - might also ignore them if we later decide so
        if (art.artifacts && Object.keys(art.artifacts).length > 0) {
          art._ignore = true;
        }
      }
    }
  
    // Perform transformations common to artifacts and members (in place)
    // Note that this should happen after implicit redirection and draft enabling, because
    // it would take away information required by those.
    function transformCommon(obj) {
      // (020 b) In "plain" mode, quoted ids are not allowed. Here, check for artifact and
      // element definitions (ignore elements that have been added by postprocessing steps.)
      if (options.forHana.names === 'plain' && !obj.$viaTransform) {
        if (obj.name.path) {
          obj.name.path.filter(x => x.quoted).forEach(errorQuotedIdentifier);
        }
        else if (obj.name.quoted) {
          errorQuotedIdentifier(obj.name);
        }
      }
      // (060) Set length 36 for former type UUID (now String after 000)
      setLengthForFormerUuid(obj);
      setLengthForFormerUuid(obj.items);
      // (070) Supply default type facets
      addDefaultTypeFacets(obj);
      addDefaultTypeFacets(obj.items);
      // (090) Compositions become associations
      if (obj.type && obj.type._artifact.name.absolute && obj.type._artifact.name.absolute === 'cds.Composition') {
        obj.type.path = [{id: obj.type.absolute}];
        setProp(obj.type, '_artifact', model.definitions['cds.Association']);
        setProp(obj.type.path[0], '_artifact', model.definitions['cds.Association']);
      }
      // (100 b) Remove attribute 'localized'
      delete obj.localized;
      // (080) and (110) Ignore annotation declarations, actions and functions
      if (obj.kind === 'annotation' || obj.kind === 'action' || obj.kind === 'function') {
        obj._ignore = true;
      }
      // Check type parameters (length, precision, scale ...)
      if (!obj._ignore && obj.type) {
        checkTypeParameters(obj, model);
      }
      if (!obj._ignore && obj.items && obj.items.type) {
        checkTypeParameters(obj.items, model);
      }
      // (190 a) Replace enum symbols by their value (if found)
      replaceEnumSymbolsByValues(obj);
    }
  
    // Issue an error for a quoted identifier in toHana, plain mode
    function errorQuotedIdentifier(obj) {
      signal(error`Quoted identifiers are not allowed in plain mode of toHana: "${obj.id}"`, obj.location);
    }
  
    // Change the names of those builtin types that have different names in HANA.
    // (do that directly in the model where the builtin types are defined, so that
    // all users of the types benefit from it). Also add the type parameter 'length'
    // to 'UUID' (which becomes a string).
    // TODO: there is no benefit at all - it is fundamentally wrong
    function renamePrimitiveTypesAndUuid() {
      const hanaNames = {
        'cds.DateTime' : 'cds.UTCDateTime',
        'cds.Timestamp' : 'cds.UTCTimestamp',
        'cds.Date' : 'cds.LocalDate',
        'cds.Time' : 'cds.LocalTime',
        'cds.UUID' : 'cds.String'
      }
      for (let name in hanaNames) {
        let type = model.definitions[name];
        type.name.absolute = hanaNames[name];
        type.name.$renamed = name;
      }
      let uuid = model.definitions['cds.UUID'];
      uuid.length = { literal: 'number', val: 36 }; // Note: Still need 'setLengthForFormerUuid'
    }
  
    // If 'obj' has final type 'cds.UUID' (renamed to String in 000), set its length to 36.
    function setLengthForFormerUuid(obj) {
      if (!obj)
        return;
      let type = obj._finalType || obj.type && obj.type._artifact;
      if (type === model.definitions['cds.UUID'] || type && type.type && type.type._artifact === model.definitions['cds.UUID']) {
        obj.length = { literal: 'number', val: 36 };
      }
    }
  
    // Perform first pass of all transformations required for an entity or a projection entity or a view (in place)
    function transformEntityOrViewPass1(art) {
      // (130) First walk through the entity elements: Flatten structs (might result in new elements)
      // (unless explicitly asked to keep structs)
      if (!options.forHana.keepStructsAssocs) {
        for (let elemName in art.elements) {
          let elem = art.elements[elemName];
          if (isStructuredElement(elem)) {
            // Ignore the structured element, replace it by its flattened form
            elem._ignore = true;
            let flatElems = flattenStructuredElement(elem);
            for (let flatElemName in flatElems) {
              if (art.elements[flatElemName]) {
                signal(error`"${art.name.absolute}.${elemName}": Flattened struct element name conflicts with existing element: "${flatElemName}"`, elem.location);
              }
              art.elements[flatElemName] = flatElems[flatElemName];
              // FIXME: Should also adapt indexNo of the newly added elements and all subsequent ones
            }
          }
        }
      }
  
      // Flatten structs in indexes and partitions (unless explicitly asked to keep structs)
      let tc = art.technicalConfig;
      if (!options.forHana.keepStructsAssocs) {
        if (tc) {
          // Secondary and fulltext indexes
          for (let name in tc.indexes) {
            let index = tc.indexes[name];
            // Array of indexes is an error: multiple index definitions with same name.
            // However, if it has survived until here, behave correctly
            if (Array.isArray(index)) {
              index.forEach(idx => {
                checkFTIColumns(idx);
                flattenStructuredColumnPaths(idx.columns);
              });
            }
            else {
              checkFTIColumns(index);
              flattenStructuredColumnPaths(index.columns);
            }
          }
          if (tc.fzindexes) {
            tc.fzindexes.forEach(idx =>  {
              flattenStructuredColumnPaths(idx.columns);
            });
          }
          // Partition columns
          if (tc.partition) {
            tc.partition.specs.forEach(spec => flattenStructuredColumnPaths(spec.columns));
          }
        }
      }
  
      function checkFTIColumns(index) {
        if(index.kind === 'fulltextindex') {
          index.columns.filter(col => isStructuredElement(col._artifact)).forEach(col => {
            signal(error`"${art.name.absolute}": A fulltext index cannot be defined on a structured element "${col._artifact.name.absolute}`,
            col.location);
          });
        }
      }
  
      // Flatten structs used in paths (unless explicitly asked to keep structs)
      if (!options.forHana.keepStructsAssocs) {
        foreachPath(art, (path, pathOwner) => {
          pathOwner.path = flattenStructStepsInPath(path);
        });
      }
  
      // after all paths have been flattened "a_b_c", assign fuzzy index definitions
      // to the flattened element
      // FIXME: This will no longer be necessary once we have moved to toSqlNew (not preserved in compact CSN anyway)
      if (tc && tc.fzindexes) {
        tc.fzindexes.forEach(idx =>  {
          idx.columns.forEach(col => {
            if(!col._ignore) {
              setProp(art.elements[col.path[0].id], '_fzindex', idx); // last one wins on multiple occurences
            }
          });
        });
      }
  
      // Second walk through the entity elements: Deal with associations (might also result in new elements)
      for (let elemName in art.elements) {
        let elem = art.elements[elemName];
        // (140) Generate foreign key elements and ON-condition for managed associations
        // (unless explicitly asked to keep assocs unchanged)
        if (!options.forHana.keepStructsAssocs) {
          if (isManagedAssociationElement(elem)) {
            // Aliased foreign keys in managed associations not yet allowed (only check for non-projections, to avoid duplicate errors)
            if (!elem._main.query) {
              for (let name in elem.foreignKeys) {
                let foreignKey = elem.foreignKeys[name]
                if (!foreignKey.calculated && !foreignKey.name.$inferred && !foreignKey.name.calculated) {
                  signal(error`"${elem.name.absolute}.${elem.name.element}": Aliases for foreign keys are not supported yet with --to-hana`,
                         elem.foreignKeys[name].location);
                }
              }
            }
            // Flatten foreign keys (replacing foreign keys that are managed associations by their respective foreign keys)
            elem.foreignKeys = flattenForeignKeys(elem.foreignKeys);
            // Generate foreign key elements for managed associations, and assemble an ON-condition with them
            let onCondParts = [];
            for (let name in elem.foreignKeys) {
              let foreignKey = elem.foreignKeys[name]
              // Sanity check
              if (!foreignKey.targetElement.path) {
                throw Error('Expecting foreign key ' + printableName(foreignKey) + ' to have a path');
              }
              // Assemble left hand side of 'assoc.key = fkey'
              let assocKeyArg = {
                path: [ { id: elem.name.id } ].concat(foreignKey.targetElement.path),
              }
              setProp(assocKeyArg, '_artifact', foreignKey._artifact);
              // Assemble right hand side of 'assoc.key = fkey'
              let foreignKeyElement = createForeignKeyElement(elem, foreignKey, art);
              let fKeyArg = {
                path: [ { id: foreignKeyElement.name.id } ],
              }
              setProp(fKeyArg, '_artifact', foreignKey._artifact);
              onCondParts.push({ op: {val:'=', location: elem.location}, args : [ assocKeyArg, fKeyArg ]});
            }
            if (onCondParts.length == 1) {
              // Only one key-fkey comparison
              elem.onCond = onCondParts[0];
            } else {
              // Multiple key-fkey comparisons - join with 'and'
              elem.onCond = { op: {val:'and', location: elem.location}, args: onCondParts };
            }
  
            // If the managed association has a 'key' property => remove it as unmanaged assocs cannot be keys
            // TODO: Are there other modifiers (like 'key') that are valid for managed, but not valid for unmanaged assocs?
            if (elem.key && elem.key.val) {
              delete elem.key;
            }
  
            // If the managed association has a 'not null' property => remove it
            if (elem.notNull && elem.notNull.val) {
              delete elem.notNull;
            }
  
            // The association is now unmanaged, i.e. actually it should no longer have foreign keys
            // at all. But the processing of backlink associations below expects to have them, so
            // we don't delete them (but mark them as implicit so that toCdl does not render them)
            elem.implicitForeignKeys = true;
          }
        }
  
        // Workaround for bugzilla 176495
        // If a select item of a cdx view contains an expression, the result type cannot be computed
        // but must be explicitly specified. This is important for the OData channel, which doesn't
        // work if the type is missing (for HANA channel an explicit type is not required, as HANA CDS
        // can compute the result type).
        // Due to bug in HANA CDS, providing explicit type 'LargeString' or 'LargeBinary' causes a
        // diserver crash. Until a fix in HANA CDS is available, we allow to suppress the explicit
        // type in the HANA channel via an annotation.
        if (art.query && elem['@cds.workaround.noExplicitTypeForHANA']) {
          delete elem._typeIsExplicit;
        }
      }
  
      /*
        For each path node of the columns array do:
          1) Store current path node in newColumns
          2) If the column path terminates in a structured type:
             (but not a managed association, this requires much more effort!)
            2a) Generate all leaf paths originating from this structured type.
                Every path step of the new leaf paths has an _artifact reference to
                it's corresponding definition element.
                Prepend the column path as prefix to all leaf paths and create a new path object
                with attributes { path, absolute, element, _artifact, calculated: true }
            2b) Propagate all properties from original path node to the resulting flattened path
                node, except for [ path, absolute, element, location, calculated ].
            2c) Set leaf _artifact as _artifact to flattened path node.
            2d) Add the new path node to newColumns.
            2e) Mark the original path node with _ignore=true.
          3) Replace the content of original columns with the content of newColumns if required
      */
      function flattenStructuredColumnPaths(columns) {
        if (columns === undefined) {
          return;
        }
        let newColumns = [];
        columns.forEach(col => {
          newColumns.push(col);
          if (col._artifact.elements) {
            let flattenedPaths = getLeafPathsForStructuredElement(col._artifact, col.path);
            flattenedPaths.forEach(fp => {
              let flatCol = { path: fp, absolute: col.absolute,
                element: fp.map(f=>f.id).join('.'), calculated: true };
              Object.keys(col).forEach(k => {
                if (!['path', 'absolute', 'element', 'location', 'calculated'].includes(k))
                  flatCol[k] = col[k];
              });
              setProp(flatCol, '_artifact', fp[fp.length-1]._artifact);
              newColumns.push(flatCol);
            });
            col._ignore = true;
          }
        });
        if (columns.length < newColumns.length) {
          columns.splice(0, columns.length, ...newColumns);
        }
  
        function getLeafPathsForStructuredElement(elt, prefixPath=[]) {
          let paths = [];
          if (elt.elements) {
            for (let name in elt.elements) {
              let sub = elt.elements[name];
              paths = paths.concat(getLeafPathsForStructuredElement(sub, [ newPathStep(sub) ]) );
            }
            return paths.map(p => prefixPath.concat(p));
          }
          else {
            return prefixPath;
          }
  
          function newPathStep(elt) {
            let node = { id: elt.name.id };
            setProp(node, '_artifact', elt);
            return node;
          }
        }
      }
    }
  
    // Perform all transformations required for an entity or a projection entity or a view (in place)
    function transformEntityOrViewPass2(art) {
      // Check for generation to HANA CDS or to HANA SQL, don't check for SQLLite
      if(art.query && ( options.forHana || options.toSql)){
        // check all (sub)queries for assoc publishing inside of unions -> forbidden in hdbcds
        walkUnionSelect(art.query, checkAssocPublishingInUnion());
        // TODO: for HANA SQL, check that assocs with the same name
        // also point to the same target when joined via UNION
        // { toF: assoc to E } UNION { toF: assoc to F} => WITH ASSOCIATIONS (... JOIN E)
      }
  
      let hasNonAssocElements = false;
      // Second walk through the entity elements: Deal with associations (might also result in new elements)
      for (let elemName in art.elements) {
        let elem = art.elements[elemName];
  
        // Views must have at least one element that is not an unmanaged assoc
        if (!elem.onCond && !elem._ignore) {
          hasNonAssocElements = true;
        }
  
        // (230 b) If we keep associations as they are (hdbcds naming convention), we cannot have managed associations
        // as view elements (their foreign keys cannot be addressed in the view)
        if (options.forHana.keepStructsAssocs
            && art.query
            && isAssocOrComposition(elem.type)
            && !elem.onCond) {
          signal(error`With "hdbcds" naming, managed association elements cannot be used in a view`, elem.location);
          continue;
        }
  
        // (180 a) An association element in a projection or a view that is defined in a mixin must not
        // be explicitly redirected
        // TODO: consider moving this check to the compiler or a general check phase
        if (art.query
            && isAssocOrComposition(elem.type)
            && (elem.value && elem.value.path && elem.value.path.length == 1 && art.query && art.query.mixin && art.query.mixin[elem.value.path[0].id])
            && elem.redirected) {
          signal(error`Association elements defined in a mixin must not be redirected: "${elem.value.path[0].id}"`, elem.location);
        }
  
        // (180 b) Create MIXINs for association elements in projections or views (those that are not mixins by themselves)
        // CDXCORE-585: Allow mixin associations to be used and published in parallel
        if (art.query !== undefined && isAssocOrComposition(elem.type)) {
          const isNotMixinByItself = !(elem.value && elem.value.path && elem.value.path.length == 1 && art.query && art.query.mixin && art.query.mixin[elem.value.path[0].id]);
          const correspondingMixin = getMixinAssocIfPublished(art, elem);
          if(isNotMixinByItself || correspondingMixin !== undefined){
            // If the mixin is only published and not used, only display the __ clone. Ignore the "original".
            if(correspondingMixin !== undefined && !usesMixinAssociation(art, elem)){
              correspondingMixin._ignore = true;
            }
            delete elem.redirected;
            delete elem._typeIsExplicit;
            // Create an unused alias name for the MIXIN
            let mixinElemName = '__' + elemName;
            while (art.elements[mixinElemName]) {
              mixinElemName = '_' + mixinElemName;
            }
            // Copy the association element to the MIXIN clause under its alias name
            // (shallow copy is sufficient, just fix name and value)
            let mixinElem = Object.assign({}, elem);
            mixinElem.name = Object.assign({}, elem.name, { id: mixinElemName, element: mixinElemName });
            setProp(mixinElem, '_finalType', elem._finalType);
            setProp(mixinElem, '_main', elem._main);
            setProp(mixinElem, '_parent', elem._parent);
            mixinElem.value = Object.assign({}, elem.value);
            if (elem.value._artifact)
              setProp( mixinElem.value, '_artifact', elem.value._artifact );
            // FIXME: Remove once the compactor no longer renders 'origin'
            mixinElem.origin = mixinElem.value;
            // Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
            transformCommon(mixinElem);
            if (!art.query.mixin) {
              art.query.mixin = Object.create(null);
            }
            art.query.mixin[mixinElemName] = mixinElem;
            // Let the original association element use the newly generated MIXIN name as value and alias
            delete elem.viaAll;
            delete elem.name.calculated;
            delete elem.name.$inferred;
            if (!elem.value) {
              elem.value = {
                // TODO: keep the following, needed by toSql ?
                absolute : art.name.absolute
              };
            }
            elem.value.path = [{ id : mixinElemName }];
            elem.value.element = mixinElemName;  // TODO: needed by toSql ?
            // FIXME: Remove once the compactor no longer renders 'origin'
            elem.origin = elem.value;
            // Clone 'onCond', prepending '$projection' to paths where appropriate,
            // and fixing the association alias just created
            if (mixinElem.onCond) {
              mixinElem.onCond = cloneWithTransformations(mixinElem.onCond, {
                path: (path) => {
                  // Clone the path, without any transformations
                  let clonedPath = cloneWithTransformations(path, {}, true);
                  // Prepend '$projection' to the path, unless the first path step is the (mixin) element itself or starts with '$')
                  if (clonedPath[0].id == elemName) {
                    clonedPath[0].id = mixinElemName;
                  } else if (!clonedPath[0].id.startsWith('$')) {
                    let projectionId = { id: '$projection' };
                    setProp(projectionId, '_artifact', art);
                    clonedPath.unshift(projectionId);
                  }
                  return clonedPath;
                },
                func: (func) => {
                  // Unfortunately, function names are disguised as paths, so we would prepend a '$projection'
                  // above (no way to distinguish that in the callback for 'path' above). We can only pluck it
                  // off again here ... sigh
                  if (func.path && func.path[0] && func.path[0].id === '$projection') {
                    func.path = func.path.slice(1);
                  }
                  return func;
                }
              }, true);
            }
          }
        }
      }
  
      // (160) Projections now finally become views
      if (art.query) {
        art.$syntax = 'view';
  
        // Complain if there are no elements other than unmanaged associations
        if (!hasNonAssocElements) {
          signal(error`"${art.name.absolute}": For HANA CDS or SQL, a view or projection must have at least one element that is not an unmanaged association`, art.location);
        }
      }
  
      // FIXME: Move this somewhere more appropriate
      if (art.$queries) {
        for (let query of art.$queries) {
          // Make the to-csn.js to use elements instead of columns
          query.columns = null;
          for (let elemName in query.elements) {
            // If following an association, explicitly set the implicit alias
            // due to an issue with HANA
            const element = query.elements[elemName];
            if(element.value && element.value.path){
              if(element.value.path.some((pathStep) => pathStep && pathStep._artifact && pathStep._artifact.target))
                element.name.$inferred = false;
            }
            delete query.elements[elemName].viaAll;
          }
          // There is no 'excluding' in HANA CDS
          delete query.exclude;
        }
      }
    }
  
    // If 'elem' has a default that is an enum constant, replace that by its value. Complain
    // if not found or not an enum type,
    function replaceEnumSymbolsByValues(elem) {
      // (190 a) Replace enum symbols by their value (if found)
      if (elem.default && elem.default.literal === 'enum') {
        if (!elem._finalType.enum) {
          // Not an enum at all
          signal(error`Enum literal "#${elem.default.symbol.id}" can only be used with an enum type in HANA CDS`, elem.default.location);
        }
        else {
          // Try to get the corresponding enum symbol from the element's type
          let enumSymbol = elem._finalType.enum[elem.default.symbol.id];
          if (!enumSymbol) {
            signal(error`Enum literal "#${elem.default.symbol.id}" not found in "${elem._finalType.name.absolute}" for HANA CDS`, elem.default.location);
          }
          else if (enumSymbol.value) {
            // Replace default with enum value
            elem.default = {
              literal: enumSymbol.value.literal,
              val: enumSymbol.value.val,
            };
          }
          else {
            // Enum symbol without explicit value - replace default by the symbol in string form
            elem.default = {
              literal: 'string',
              val: enumSymbol.name.id,
            };
          }
        }
      }
    }
  
    // If 'node' has an enum type, change node's type to be the enum's base type
    // and strip off the 'enum' property.
    function replaceEnumByBaseType(node) {
      if (node.items) {
        replaceEnumByBaseType(node.items);
      }
      // (190 b) Replace enum types by their final base type (must happen after 190 a)
      if (node && node._finalType && (node.enum || node._finalType.enum)) {
        // node.type = node._finalType.type
        // node.type = node._finalType.type._artifact._finalType.type;
        // if (node._finalType.length) {
        //   node.length = node._finalType.length;
        // }
        setProp(node, '_finalType', node._finalType.type._artifact);
        delete node.enum;
      }
    }
  
    // If the association element 'elem' of 'art' is a backlink association, massage its ON-condition
    // (in place) so that it
    // - compares the generated foreign key fields of the corresponding forward
    //   association with their respective keys in 'art' (for managed forward associations)
    // - contains the corresponding forward association's ON-condition in "reversed" form,
    //   i.e. as seen from 'elem' (for unmanaged associations)
    // Otherwise, do nothing.
    function processBacklinkAssoc(elem, art) {
      if (elem.onCond.op && elem.onCond.op.val === 'xpr') {
        // This is an ON-condition in token-stream form
        let xprArgs = elem.onCond.args;
        let result = [];
        let i = 0;
        while (i < xprArgs.length) {
          // Only token tripel `<path>, '=', <path>` are of interest here
          if (i < xprArgs.length - 2 && xprArgs[i + 1] === '=') {
            // Check if one side is $self and the other an association
            // (if so, replace all three tokens with the condition generated from the other side, in parentheses)
            if (isDollarSelfOrProjectionOperand(xprArgs[i]) && isAssociationOperand(xprArgs[i + 2])) {
              result.push('(');
              result.push(transformDollarSelfComparison(xprArgs[i + 2], elem, art, xprArgs[i].location));
              result.push(')');
              i += 3;
            }
            else if (isDollarSelfOrProjectionOperand(xprArgs[i + 2]) && isAssociationOperand(xprArgs[i])) {
              result.push('(');
              result.push(transformDollarSelfComparison(xprArgs[i], elem, art, xprArgs[i + 2].location));
              result.push(')');
              i += 3;
            }
            // Otherwise take one (!) token unchanged
            else {
              result.push(xprArgs[i]);
              i++;
            }
          }
          // Take all other tokens unchanged
          else {
            result.push(xprArgs[i]);
            i++;
          }
        }
        elem.onCond.args = result;
      }
      else {
        // This is an ON-condition in tree form
        elem.onCond = transformOpsInExpr(elem.onCond, expr => {
          // Only '=' comparisons are of interest here
          if (!expr.op || expr.op.val !== '=' || expr.args.length != 2) {
            // Nothing to do
            return expr;
          }
          // Check if one side is $self (if so, replace the whole expression with the condition
          // generated from the other side, in parentheses). Note that we have already checked that the other
          // side is an association in 'checkExpressions' (for tree expressions only).
          if (isDollarSelfOrProjectionOperand(expr.args[0])) {
            return [transformDollarSelfComparison(expr.args[1], elem, art, expr.location)];
          }
          else if (isDollarSelfOrProjectionOperand(expr.args[1])) {
            return [transformDollarSelfComparison(expr.args[0], elem, art, expr.location)];
          }
          else {
            // Nothing to do
            return expr;
          }
        });
        // Avoid redundant parentheses
        if (elem.onCond instanceof Array && elem.onCond.length == 1) {
          elem.onCond = elem.onCond[0];
        }
      }
  
      // Return the condition to replace the comparison `<assocOp> = $self` in the ON-condition
      // of element <elem> of artifact 'art'. If there is anything to complain, use location <loc>
      function transformDollarSelfComparison(assocOp, elem, art, loc) {
        // Sanity checks
        if (!isAssociationOperand(assocOp)) {
          throw new Error(`Expected an association operand: ${JSON.stringify(assocOp)}`);
        }
  
        // Check: The forward link <assocOp> must point back to this artifact
        // FIXME: Unfortunately, we can currently only check this for non-views (because when a view selects
        // a backlink association element from an entity, the forward link will point to the entity,
        // not to the view).
        // FIXME: This also means that corresponding key fields should be in the select list etc ...
        if (!art.query && assocOp._artifact.target._artifact && assocOp._artifact.target._artifact != art) {
          signal(error`Only an association that points back to this artifact can be compared to "$self"`, loc);
        }
  
        // Check: The forward link <assocOp> must not contain '$self' in its own ON-condition
        if (assocOp._artifact.onCond) {
          let containsDollarSelf = false;
          transformOpsInExpr(assocOp._artifact.onCond, expr => {
            if (expr.args.some(isDollarSelfOrProjectionOperand)) {
              containsDollarSelf = true;
            }
            return expr;
          });
          if (containsDollarSelf) {
            signal(error`An association that uses "$self" in its ON-condition cannot be compared to "$self"`, loc);
          }
        }
  
        // Transform comparison of $self to managed association into AND-combined foreign key comparisons
        if (assocOp._artifact.foreignKeys) {
          return transformDollarSelfComparisonWithManagedAssoc(assocOp, elem.name.element || elem.name.id, loc);
        }
        // Transform comparison of $self to unmanaged association into "reversed" ON-condition
        else if (assocOp._artifact.onCond) {
          return transformDollarSelfComparisonWithUnmanagedAssoc(assocOp, elem.name.element || elem.name.id, loc);
        } else {
          throw new Error(`Expected either managed or unmanaged association in $self-comparison: ${JSON.stringify(elem.onCond)}`);
        }
      }
  
      // Return an expression where 'expr' is replaced by 'callback(expr)' for each operation
      // (recursively) in 'expr'. Leave expressions unchanged that are not operations.
      function transformOpsInExpr(expr, callback) {
        if (expr instanceof Array) {
          return expr.map(item => transformOpsInExpr(item, callback));
        }
        if (expr.args) {
          expr.args = expr.args.map(arg => transformOpsInExpr(arg, callback));
        }
        if (expr.op) {
          return callback(expr);
        }
        return expr;
      }
  
      // For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
      // where <assoc> is a managed association, return a condition comparing the generated
      // foreign key elements <elemName>.<assoc>_<fkey1..n> of <assoc> to the corresponding
      // keys in this artifact.
      // For example, `ON elem.ass = $self` becomes `ON elem.ass_key1 = key1 AND elem.ass_key2 = key2`
      // (assuming that `ass` has the foreign keys `key1` and `key2`)
      function transformDollarSelfComparisonWithManagedAssoc(assoc, elemName, loc) {
        let result = {};
        let keys = Object.keys(assoc._artifact.foreignKeys);
        // if the element was structured then it was flattened => change of the delimiter from '.' to '_'
        // this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
        let assocName = assoc._artifact.name.element.replace(/\./g, pathDelimiter);
        elemName = elemName.replace(/\./g, pathDelimiter);
        if (keys.length > 1) {
          result.op = {val:'and',location:loc};
          result.args = [];
        } else {
          result.op = {val:'=',location:loc};
        }
        keys.forEach(k => {
          // Depending on naming conventions, the foreign key may two path steps (hdbcds) or be a single path step with a flattened name (plain, quoted)
          let fKeyPath = options.forHana.keepStructsAssocs ? [{ id: assocName, location: loc },
                                                              { id: k, location: loc} ]
                                                           : [{ id: `${assocName}${pathDelimiter}${k}`, location: loc }]
          // FIXME: _artifact to the args ???
          let a = [
            {
              path: [{ id: elemName, location: loc }, ...fKeyPath], location: loc
            },
            {
              path: [{ id: k, location: loc }], location: loc
            }
          ];
          if (keys.length > 1) {
            result.args.push({
              op: {val:'=',location:loc},
              args: a,
              location: loc
            });
          }
          else {
            result.args = a;
          }
        });
        return result;
      }
  
      // For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
      // where <assoc> is an unmanaged association, return the ON-condition of <assoc> as it would
      // be written from the perspective of the artifact containing association <elemName>.
      // For example, `ON elem.ass = $self` becomes `ON a = elem.x AND b = elem.y`
      // (assuming that `ass` has the ON-condition `ON ass.a = x AND ass.b = y`)
      function transformDollarSelfComparisonWithUnmanagedAssoc(assoc, elemName, loc) {
        // if the element was structured then it may have been flattened => change of the delimiter from '.' to '_'
        // this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
        elemName = elemName.replace(/\./g, pathDelimiter);
        let assocName = assoc._artifact.name.element.replace(/\./g, pathDelimiter);
        // clone the onCond for later use in the path transformation,
        // also assign the _artifact elements of the path elements to the copy
        let newOnCond = cloneWithTransformations(assoc._artifact.onCond, {
          path: (value, node, resultNode) => {
            resultNode = cloneWithTransformations(value, {});
            resultNode.forEach((element, index) => {
              setProp(element, '_artifact', value[index]._artifact);
            });
            return resultNode;
          }
        });
        // goes through the the newOnCond and transform all the 'path' elements
        foreachPath(newOnCond, (path) => {
          if (path[0].id === assocName) // we are in the "path" from the forwarding assoc => need to remove the first part of the path
            path.shift();
          else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
            path.unshift({ id: elemName, location: loc });
            // if there was a $self identifier in the forwarding association onCond
            // we do not need it any more, as we prepended in the previous step the back association's id
            if (path[1].id === '$self')
              path.splice(1, 1);
          }
        });
        return newOnCond;
      }
    }
  
    // Replace (formerly) managed associations in GROUP BY and ORDER BY of 'view' by their foreign key fields.
    // FIXME: We should also complain about unmanaged assocs?
    // FIXME: We should also check for structured entries for 'keepStructsAssocs'
    function replaceAssociationsInGroupByOrderBy(view) {
      for (let query of view.$queries) {
        if (query.groupBy) {
          let result = [];
          // Walk the GROUP BY entries, replace assocs by their foreign keys
          for (let groupByEntry of query.groupBy) {
            if (groupByEntry._artifact && groupByEntry._artifact.foreignKeys) {
              // This is (or used to be before transformation) a managed assoc
              // (230 c) If we keep associations as they are (hdbcds naming convention), we can't have associations in GROUP BY
              if (options.forHana.keepStructsAssocs) {
                signal(error`With "hdbcds" naming, managed association elements cannot be used in the GROUP BY clause of a view`, groupByEntry.location);
                continue;
              }
  
              // Walk the managed assoc's (original) fkeys (apparently, the _artifact of
              // the GROUP-BY entry points to something in the view which does have all properties of the original
              // fkeys, so we need to dig up '_finalType' ...)
              // Sanity check
              if (!groupByEntry._artifact._finalType || !groupByEntry._artifact._finalType.foreignKeys) {
                throw new Error("Expecting GROUP BY entry's artifact's _finalType to have foreign keys");
              }
              let foreignKeys = groupByEntry._artifact._finalType.foreignKeys;
              for (let foreignKeyName in foreignKeys) {
                let foreignKey = foreignKeys[foreignKeyName];
                // Sanity check
                if (!foreignKey.generatedFieldName) {
                  throw new Error('Expecting generated field name for foreign key: ' + JSON.stringify(foreignKey));
                }
                // For each fkey, assemble a new GROUP BY entry that looks like one with the assoc, replacing the last path step
                let newPath = groupByEntry.path.slice(0, -1);
                newPath.push({
                  id : foreignKey.generatedFieldName,
                });
                result.push(Object.assign({}, groupByEntry, { path : newPath, element : foreignKey.generatedFieldName }));
                // FIXME: We don't bother to reconstruct _artifact links here (hopefully they won't be needed)
              }
            } else {
              // Not a formerly managed association - take as it is
              result.push(groupByEntry);
            }
          }
          query.groupBy = result;
        }
        // Unfortunately, ORDER BY entries look slightly different, so we can't easily reuse what we have above
        // FIXME: Maybe simplify this one day ... ugly!
        if (query.orderBy) {
          let result = [];
          // Walk the ORDER BY entries, replace assocs by their foreign keys
          for (let orderByEntry of query.orderBy) {
            if (orderByEntry.value && orderByEntry.value._artifact && orderByEntry.value._artifact.foreignKeys) {
              // This is (or used to be before transformation) a managed assoc
              // (230 d) If we keep associations as they are (hdbcds naming convention), we can't have associations in ORDER BY
              if (options.forHana.keepStructsAssocs) {
                signal(error`With "hdbcds" naming, managed association elements cannot be used in the ORDER BY clause of a view`, orderByEntry.value.location);
                continue;
              }
              // Walk the assoc's  (original) fkeys (apparently, the _artifact of
              // the ORDER-BY entry points to something in the view which does have all properties of the original
              // fkeys, so we need to dig up 'origin' ...)
              // Sanity check
              if (!orderByEntry.value._artifact._finalType || !orderByEntry.value._artifact._finalType.foreignKeys) {
                throw new Error("Expecting ORDER BY entry's artifact's _finalType to have foreign keys");
              }
              let foreignKeys = orderByEntry.value._artifact._finalType.foreignKeys;
              for (let foreignKeyName in foreignKeys) {
                let foreignKey = foreignKeys[foreignKeyName];
                // Sanity check
                if (!foreignKey.generatedFieldName) {
                  throw new Error('Expecting generated field name for foreign key: ' + JSON.stringify(foreignKey));
                }
                // For each fkey, assemble a new ORDER BY entry that looks like one with the assoc, replacing the last path step
                let newPath = orderByEntry.value.path.slice(0, -1);
                newPath.push({
                  id : foreignKey.generatedFieldName,
                });
                let newValue = Object.assign({}, orderByEntry.value, { path : newPath, element : foreignKey.generatedFieldName });
                result.push(Object.assign({}, orderByEntry, { value: newValue }));
                // FIXME: We don't bother to reconstruct _artifact links here (hopefully they won't be needed)
              }
            } else {
              // Not a formerly managed association - take as it is
              result.push(orderByEntry);
            }
          }
          query.orderBy = result;
        }
      }
    }
  
    // Collect all artifacts that are transitively reachable via compositions from 'artifact' into 'draftNodes'.
    // 'rootArtifact' is the root artifact where composition traversal started.
    // Check that no artifact other than the root node has '@odata.draft.enabled'
    function collectDraftNodesInto(artifact, rootArtifact, draftNodes) {
      // Collect the artifact itself
      draftNodes[artifact.name.absolute] = artifact;
      // Follow all composition targets in elements of 'artifact'
      for (let elemName in artifact.elements) {
        let elem = artifact.elements[elemName];
        if (elem.target && elem._finalType.type._artifact.name.absolute === 'cds.Composition') {
          let draftNode = elem.target._artifact;
          // Sanity check
          if (!draftNode) {
            throw new Error('Expecting target to be resolved: ' + JSON.stringify(elem, null, 2));
          }
          // Ignore composition if not part of a service
          if (!draftNode._service) {
            signal(warning`Target "${draftNode.name.absolute}" of composition "${elem.name.absolute}.${elem.name.element}" cannot be a draft node because it is not part of a service`, elem.location);
            continue;
          }
          // Barf if a draft node other than the root has @odata.draft.enabled itself
          if (draftNode != rootArtifact && hasBoolAnnotation(draftNode, '@odata.draft.enabled')) {
            signal(error`"${elem.name.absolute}.${elem.name.element}": Composition in draft-enabled entity cannot lead to another entity with "@odata.draft.enabled"`, elem.location);
            delete draftNodes[draftNode.name.absolute];
            continue;
          }
          // Recurse unless already known
          if (!hasBoolAnnotation(draftNode, '@odata.draft.enabled', false) && !draftNodes[draftNode.name.absolute]) {
            collectDraftNodesInto(draftNode, rootArtifact, draftNodes);
          }
        }
      }
    }
  
    // Generate all that is required in HANA CDS for draft enablement of 'artifact'.
    function generateDraftForHana(artifact, draftRoot) {
      // console.error(`generateDraftForHana(${artifact.name.absolute})`);
      // Sanity check
      if (!artifact._service) {
        throw new Error('Expecting artifact to be part of a service: ' + JSON.stringify(artifact));
      }
  
      // The name of the draft shadow entity we should generate
      let draftsArtifactName = artifact.name.absolute + '_drafts';
  
  
      // extract keys for UUID inspection
      let keys = [];
      forEachGeneric(artifact, 'elements', elt => {
        if(elt.key && elt.key.val)
          keys.push(elt);
      });
  
      // In contrast to EDM, the DB entity may have more than one technical keys but should have idealy exactly one key of type cds.UUID
      if (keys.length !== 1) {
        signal(warning`"${artifact.name.absolute}": "@odata.draft.enabled" - Entity should have exactly one key element`, artifact.location);
      }
      let uuidCount = keys.reduce((uuidCount, k) => {
        return((k._finalType.name.absolute === 'cds.UUID' && k._finalType.name.$renamed === 'cds.UUID') ? ++uuidCount : uuidCount);
      }, 0);
      if (uuidCount === 0) {
        signal(warning`"${artifact.name.absolute}": "@odata.draft.enabled" - Entity key element should be of type "cds.UUID"`, artifact.location);
      }
  
      // Generate the DraftAdministrativeData projection into the service, unless there is already one
      let draftAdminDataProjectionName = artifact._service.name.absolute + '.' + 'DraftAdministrativeData';
      let draftAdminDataProjection = model.definitions[draftAdminDataProjectionName];
      if (!draftAdminDataProjection) {
        draftAdminDataProjection = createAndAddDraftAdminDataProjection(artifact._service);
      }
      // Barf if it is not an entity or not what we expect
      if (draftAdminDataProjection.kind !== 'entity' || !draftAdminDataProjection.elements['DraftUUID']) {
        signal(error`Generated entity "${draftAdminDataProjectionName}" conflicts with existing artifact`, draftAdminDataProjection.location);
      }
  
      // Duplicate the artifact as a draft shadow entity
      if (model.definitions[draftsArtifactName]) {
        let definingDraftRoot = model.definitions[draftsArtifactName]._draftRoot;
        if(!definingDraftRoot) {
          signal(error`"${draftsArtifactName}": Generated entity name conflicts with existing entity`, model.definitions[draftsArtifactName].location);
        } else {
          signal(error`"${draftsArtifactName}": Entity already generated by draft root "${definingDraftRoot.name.absolute}"`, draftRoot.location);
        }
        return;
      }
      let draftsArtifact = {
        name: {
          path: [
            { id: draftsArtifactName },
          ],
          id : artifact.name.id + '_drafts',
          absolute: draftsArtifactName,
        },
        kind: 'entity',
        elements: Object.create(null),
      };
      setProp(draftsArtifact, '_service', artifact._service);
      setProp(draftsArtifact, '_finalType', draftsArtifact);
      setProp(draftsArtifact, '_draftRoot', draftRoot);
      // Copy all elements
      for (let elemName in artifact.elements) {
        let elem = copyAndAddElement(artifact.elements[elemName], draftsArtifact, elemName);
        // explicitly set nullable if not key and not unmanaged association
        if ((!elem.key || !elem.key.val) && !elem.onCond) {
          elem.notNull = {
            val: false,
          }
        }
      }
  
      // Generate the additional elements into the draft-enabled artifact
  
      // key IsActiveEntity : Boolean default true
      let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', false);
      addElement(isActiveEntity, draftsArtifact);
  
      // HasActiveEntity : Boolean default false
      let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false);
      addElement(hasActiveEntity, draftsArtifact);
  
      // HasDraftEntity : Boolean default false;
      let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false);
      addElement(hasDraftEntity, draftsArtifact);
  
      // DraftAdministrativeData : Association to one DraftAdministrativeData not null;
      let draftAdministrativeData = createAssociationElement('DraftAdministrativeData', draftAdminDataProjection, true);
      draftAdministrativeData.cardinality = {
        targetMax: {
          literal: 'number',
          val: 1,
        },
      };
      draftAdministrativeData.notNull = {
        val: true,
      }
      addElement(draftAdministrativeData, draftsArtifact);
      // Note that we may need to do the HANA transformation steps for managed associations
      // (foreign key field generation, generatedFieldName, creating ON-condition) by hand,
      // because the corresponding transformation steps have already been done on all artifacts
      // when we come here). Only for 'keepStructsAssocs' this is not required.
      if (!options.forHana.keepStructsAssocs && draftAdministrativeData.foreignKeys['DraftUUID']) {
        createForeignKeyElement(draftAdministrativeData, draftAdministrativeData.foreignKeys['DraftUUID'], draftsArtifact);
        draftAdministrativeData.onCond
          = createAssociationPathComparison(draftAdministrativeData,
                                            draftAdminDataProjection.elements['DraftUUID'],
                                            '=',
                                            draftsArtifact.elements['DraftAdministrativeData' + pathDelimiter + 'DraftUUID']);
        // The notNull has been transferred to the foreign key field and must be removed on the association
        delete draftAdministrativeData.notNull;
  
        // The association is now unmanaged, i.e. actually it should no longer have foreign keys
        // at all. But the processing of backlink associations below expects to have them, so
        // we don't delete them (but mark them as implicit so that toCdl does not render them)
        draftAdministrativeData.implicitForeignKeys = true;
      }
  
      // Add draft shadow entity to the model
      model.definitions[draftsArtifactName] = draftsArtifact;
    }
  
    // Redirect all association/composition targets in 'artifact' that point to targets in
    // the dictionary 'draftNodes' to their corresponding draft shadow artifacts.
    function redirectDraftTargets(artifact, draftNodes) {
      for (let elemName in artifact.elements) {
        let elem = artifact.elements[elemName];
        if (elem.target) {
          let target = elem.target._artifact;
          // Sanity check
          if (!target) {
            throw new Error('Expecting target to be resolved: ' + JSON.stringify(elem, null, 2));
          }
          // Nothing to do if target is not a draft node
          if (!draftNodes[target.name.absolute]) {
            continue;
          }
          // Redirect the composition/association in this draft shadow entity to the target draft shadow entity
          // console.error(`Redirecting target of ${elemName} in ${artifact.name.absolute} to ${target.name.absolute + '_drafts'}`);
          let shadowTarget = getDraftShadowEntityFor(target);
          // Might not exist because of previous errors
          if (shadowTarget) {
            elem.target.path[elem.target.path.length -1].id = shadowTarget.name.id;
            setProp(elem.target.path[elem.target.path.length -1], '_artifact', shadowTarget);
            setProp(elem.target, '_artifact', shadowTarget);
            // FIXME: Strictly speaking, we would also need to replace the foreign keys' _artifact links,
            // but since their content is identical anyway, we simply omit that for now.
          }
        }
      }
  
      // Returns the corresponding draft shadow artifact for draft node 'draftNode'.
      function getDraftShadowEntityFor(draftNode) {
        // Sanity check
        if (!draftNodes[draftNode.name.absolute]) {
          throw new Error(`Not a draft node: ${draftNode.name.absolute}`);
        }
        return model.definitions[draftNode.name.absolute + '_drafts'];
      }
    }
  
   /**
   * Check if the given element of the given element uses 'array of'.
   *
   * It checks the element itself and any types used or "subelements".
   *
   * @param {any} artifact The artifact
   * @param {any} element The element to check
   * @returns {void}
   */
    function checkForArrayOf(artifact, element) {
      /**
       * Show the error message for the given element name
       *
       * @param {any} eltName Name of the element
       * @returns {void}
       */
      function show_error(eltName) {
        signal(error`"${artifact.name.absolute}.${eltName}": Array types are not allowed as table types`, element.location);
      }
  
      /**
       * Recursively check the if the given element contains array of
       * and is really going to the database (no @cds.persistence.skip/exists: true)
       * @param {any} e Element to check
       * @param {any} [current_path] Current path for better error messages
       * @returns {void}
       */
      function check(e, current_path, skip_name = false) {
        // Only append element names to the path, skip names for typeof, since those take the following route:
        // a: x; b: typeof a; b -> a -> x. We only want to log b -> x
        if (e.name.element && !skip_name) {
          if (!current_path) {
            current_path = e.name.id;
          } else {
            current_path += '.' + e.name.id;
          }
        }
  
        // E is undefined -> return. Should not happen, just in case.
        if (!e) return;
        else if (e.$inferred) return;
        // Element has items -> This means it is an array of, show error and return.
        else if (e.items) show_error(current_path);
        // Element has elements -> Check each sub-element.
        else if (e.elements) {
          for (let innerElement of Object.keys(e.elements)) {
            check(e.elements[innerElement], current_path);
          }
        }
        // Element has a type -> Check if the type is/contains an array
        else if (e.type && !e.type.$inferred && e.type._artifact && (!e.type._artifact.builtin || e.type._artifact.builtin !== true)) {
          check(e.type._artifact, current_path, e.type.absolute === artifact.name.absolute); // skip names for typeOf
        }
        else return;
      }
      // Start the recursive checking (if table is really about to be created)
      // FIXME: same expression as in checkElements => utility function?
      const createTable = !(isNoDbArtifact(artifact) || hasBoolAnnotation(artifact, '@cds.persistence.exists'));
  
      if (createTable)
        check(element);
    }
  
    /**
     * Get a callback that checks assocs published in Unions
     *
     * @param {any} noErrorJustIgnore Don't error, just ignore the assoc
     * @returns {Function}
     */
    function checkAssocPublishingInUnion(noErrorJustIgnore=false){
      /**
      * Check if the given query publishes associations inside a UNION
      * Intended as a callback for walkUnionSelect
      *
      * @param {any} query Current Query
      * @param {any} state State - inUnion
      * @returns {object} The updated state
      */
      return function(query, state){
        if(query.op && query.op.val === 'union'){
          state.inUnion = true;
        }
  
        if(state.inUnion){
          for (let elemName in query.elements) {
            let elem = query.elements[elemName];
            if(elem.target){
              if(noErrorJustIgnore){
                elem._ignore = true;
                signal(warning`Ignoring published association inside a union - will be added via WITH ASSOCIATIONS`, elem.location || query.location);
              } else {
                signal(error`"${query.name.absolute}"/query:${query.name.query}/"${elemName}": Associations cannot be published in a UNION`, elem.location || query.location);
              }
            }
          }
        }
  
        return state;
      }
    }
    /**
     * Naive implementation of a query walker - should be able to walk UNION-SELECT-constructs
     *
     * @param {any} query Current query to walk
     * @param {any} callback Callback to call for each query
     *                       will be called with query and state
     *                       return value will be used as new state
     * @param {any} [state={}] State - stuff you've already seen etc.
     */
    function walkUnionSelect(query, callback, state={}){
      state = callback(query, state);
      if(query.args){
        for(const q of query.args){
          if(Array.isArray(q)){
            for(const subq of q){
              walkUnionSelect(subq, callback, state);
            }
          } else {
            walkUnionSelect(q, callback, state);
          }
        }
      }
    }
  
    /*
      If the artifact is either abstract or assigned '@cds.persistence.skip' it never reaches the Database layer
    */
    function isNoDbArtifact(art) {
      return ['entity', 'view'].includes(art.kind) && (art.abstract || hasBoolAnnotation(art, '@cds.persistence.skip'));
    }
  
    function isUnreachableAssociationTarget(art) {
      return isNoDbArtifact(art) || hasBoolAnnotation(art, '@cds.persistence.exists');
    }
  
  }
  
  module.exports = {
    transformForHana
  };
  