DMOSignatureHelper.java

/*
** Module   : DMOSignatureHelper.java
** Abstract : A collection of methods used for handling DMO signatures.
**
** Copyright (c) 2020-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 AIL 20201125 First revision.
**         20201126 Added unique constraint encoding and checking.
**         20201210 Added explicit signature for DMO operations in which the property names matter.
**         20201214 Fixed unique constraint checking.
**         20201216 Added support for loose-copy-mode.
**     CA  20210508 Added a signature for the DMO schema, used by shared buffer/temp-table match.
**     AIL 20210510 Used type encoding for schema signaure; made typeMapping thread-safe.
**     IAS 20210515 Fixed potential NPE in the buildSchemaSignature() method (could happen
**                  for the denormalized extent fields)
**     OM  20220727 FieldId and PropertyId are different for denormalized extent fields.
**     CA  20220606 Fixed a bug in uniqueConstraintsMatch, wrong index was used for dstConstraints.
**     HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
**                  for performance.
** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 003 SR  20230704 Added sqlSignatured used for copy-temp-table.
** 004 DDF 20250131 Removed no-undo status from the schema signature.
**     DDF 20250210 Added back the no-undo status and added simpleSchemaSignature to DmoSignature
**                  constructor.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.orm;

import java.util.*;
import java.util.logging.*;
import java.util.regex.Pattern;

import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;

/**
 * Compute a string based signature for a DMO, which contains information regarding the 
 * type of each field, extents and eventually some restrictions like mandatory or unique.
 * In order to have the prefix-code property for each generated string, the following 
 * standards should be respected:
 * - The properties are represented in the order they are provided.
 * - A data type representation should be a single upper case character.
 * - In case the field has an extent, the length of the extent will follow the data type 
 * representation as a number (no matter the number of digits)
 * - At the end of the representation of one property, there will be a sequence of lower-case
 * characters showing which restrictions are imposed on this property (mandatory). The
 * order of these is important.
 * This means that one property will have the following format: 
 * UpperCaseType ExtentDigit* LowerCaseConstraint*
 * At the end of the property representation, we will have unique constraint representations,
 * which is sequence of delimited enumerations of properties id.
 */
public class DMOSignatureHelper
{
   /** 
    * A map statically initialized with the classic JVM data type single-char representation.
    * This can also contain other mappings created at the runtime.
    */
   private static final Map<Class<? extends BaseDataType>, Character> typeMapping;
   
   /** The character used for representing a mandatory property */
   private static final char CHAR_MANDANTORY = 'm';
   
   /** 
    * The character used as delimiter between unique constraints which are encoded 
    * as a sequence of property ids 
    */
   private static final char CHAR_UNIQUE_CONSTRAINT_DELIMITER = '|';
   
   /** The character used as delimiter between the property ids in the same group (ex: unique constraint) */
   private static final char CHAR_PROPERTY_ID_DELIMITER = ',';
   
   /** The character used before appending a property name to the signature. */
   private static final char PROPERTY_NAME_DELIMITER_START = '(';

   /** The character used after appending a property name to the signature. */
   private static final char PROPERTY_NAME_DELIMITER_FINISH = ')';
   
   /** A string based delimiter (regex friendly) used for splitting a signature into separate constraints */
   private static final String FORMATTED_CONSTRAINT_DELIMITER;
   
   /** 
    * A string based delimiter (regex friendly) used for splitting a propriety id group 
    * into separate property ids 
    */
   private static final String FORMATTED_PROPERTY_DELIMITER;
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(DMOSignatureHelper.class.getName());
   
   /** A helper to select a suitable unused character for a specified data type. */
   private static final TypeIdentifierProvider findSuitableCandidate;

   /** A helper to select any unused character for a specified data type. */
   private static final TypeIdentifierProvider findAnyCandidate;
   
   /** Static initialization of the type mapping */
   static 
   {
      typeMapping = new IdentityHashMap<>();

      typeMapping.put(character.class, 'C');
      typeMapping.put(decimal.class, 'F');
      typeMapping.put(integer.class, 'I');
      typeMapping.put(int64.class, 'J');
      typeMapping.put(logical.class, 'Z');
      
      FORMATTED_CONSTRAINT_DELIMITER = Pattern.quote(String.valueOf(CHAR_UNIQUE_CONSTRAINT_DELIMITER));
      FORMATTED_PROPERTY_DELIMITER = Pattern.quote(String.valueOf(CHAR_PROPERTY_ID_DELIMITER));
      
      findSuitableCandidate = (type) -> 
      {
         synchronized (typeMapping)
         {
            Set<Character> values = new HashSet<Character>(typeMapping.values());
            for (char raw : type.getName().toCharArray())
            {
               char candidate = Character.toUpperCase(raw);
               if (isType(candidate) && !values.contains(candidate))
               {
                  typeMapping.put(type, candidate);
                  return candidate;
               }
            }
            return null;
         }
      };
      
      findAnyCandidate = (type) -> 
      {
         synchronized (typeMapping)
         {
            Set<Character> values = new HashSet<Character>(typeMapping.values());
            for (char candidate = 'A'; candidate <= 'Z'; candidate++)
            {
               if (!values.contains(candidate))
               {
                  typeMapping.put(type, candidate);
                  return candidate;
               }
            }
            return null;
         }
      };
   }
   
   /**
    * This builds a signature which represents a DMO. The DMOs which are compatible under certain
    * circumstances (structural, by property name etc.) should compatible string signatures.
    * 
    * @param   meta
    *          The DMO meta instance.  WARNING: this is called from the {@link DmoMeta#DmoMeta c'tor}. 
    *          
    * @return  A DMO signature representing the a DMO having the specified properties and unique constraints.
    */
   static DmoSignature buildSignature(DmoMeta meta)
   {
      // build a collection of lists containing properties part of the same unique constraint
      // this will be used when encoding unique constraints in the signature
      List<List<Property>> uniqueConstraints = meta.getUniqueConstraintsAsProps();
      IteratorProvider<Property> itProvider = () -> meta.getFields(false);
      
      Map<Integer, Integer> explicitIdToPos = new HashMap<>();
      String quickSignature = buildQuickSignature(itProvider, uniqueConstraints, meta.tempTable);
      String explicitSignature = buildExplicitSignature(itProvider, 
                                                        uniqueConstraints, 
                                                        meta.tempTable,
                                                        explicitIdToPos);
      
      // The simpleSchemaSignature relies on the schemaSignature, the number of characters
      // used to add the no-undo flag and the separator should be removed from the start of
      // the schemaSignature to get the simple version (no options, e.g. no-undo).
      String schemaSignature = buildSchemaSignature(meta);
      String simpleSchemaSignature = schemaSignature.substring(2);
      
      String sqlSignature = buildSqlSignature(itProvider,
                                              uniqueConstraints,
                                              meta.tempTable,
                                              explicitIdToPos);
      
      return new DmoSignature(schemaSignature,
                              simpleSchemaSignature,
                              quickSignature,
                              explicitSignature,
                              explicitIdToPos,
                              sqlSignature);
   }
   
   /**
    * Check if two signatures are compatible from properties name order point of view. This works only
    * if both the source and destination have explicit signatures - as the property names are relevant here.
    * 
    * @param   srcSignature
    *          The source signature which should be checked.
    * @param   dstSignature
    *          The destination signature which should be checked.
    *          
    * @return  {@code true} if the DMO signatures shuffled the property names in the explicit signatures
    *          in the same order. This means that, if the explicit signature match and this returns 
    *          {@code true}, it means that they have the same order of the properties name/types/extents/
    *          constraints etc. Otherwise, they only have the same property names but in different order,
    *          case in which this returns {@code false}.
    */
   public static boolean exactPropertyOrder(DmoSignature srcSignature, DmoSignature dstSignature)
   {
      if (srcSignature == null || dstSignature == null)
      {
         return false;
      }
      
      String srcOrder = srcSignature.getIdToPosAsString();
      String dstOrder = dstSignature.getIdToPosAsString();
      
      if (srcOrder == null || dstOrder == null)
      {
         return false;
      }
      
      return srcOrder.equals(dstOrder);
   }

   /**
    * Check if the source and destination are compatible for a fast buffer-copy. 
    * 
    * @param   src
    *          The signature of the source DMO.
    * @param   dst
    *          The signature of the destination DMO.
    *          
    * @return  {@code true} if the explicit signatures are equal, so they have the same property names
    *          (not always in the same order).
    */
   public static boolean validBufferCopy(DmoSignature src, DmoSignature dst)
   {
      if (src == null || dst == null)
      {
         return false;
      }
      
      String srcSignature = src.getExplicitSignature();
      String dstSignature = dst.getExplicitSignature();
      
      if (srcSignature == null || dstSignature == null)
      {
         return false;
      }
      
      return srcSignature.equals(dstSignature);
   }
   
   /**
    * Check if the destination signature is less restrictive than the source signature. This checks
    * each field for type and extent compatibility; afterwards it checks if all destination constraints
    * can be validated by source's constraints. If the signatures are compatible in this way, 
    * we can allow a fast copy. Note that looseCopy mode will relax the constraints regarding property
    * order; all we care about are the unique constraints.
    * 
    * @param   src
    *          The signature of the source DMO.
    * @param   dst
    *          The signature of the destination DMO.
    * @param   looseCopy
    *          Flag which indicates if the DMO signatures are compatible in loose-copy-mode.
    *          
    * @return  {@code true} if the signatures are compatible.
    */
   public static boolean validTempTableBulkCopy(DmoSignature src, 
                                                DmoSignature dst, 
                                                boolean looseCopy)
   {
      if (src == null || dst == null)
      {
         return false;
      }
      
      String srcSignature = src.getQuickSignature();
      String dstSignature = dst.getQuickSignature();
      
      if (srcSignature == null || dstSignature == null)
      {
         return false;
      }
      
      if (srcSignature.equals(dstSignature))
      {
         return true;
      }
      
      // fall back to an in-detail analysis of the signatures
      String[] srcChunks = srcSignature.split(FORMATTED_CONSTRAINT_DELIMITER, 2);
      String[] dstChunks = dstSignature.split(FORMATTED_CONSTRAINT_DELIMITER, 2);

      // verify the properties first: split after the unique constraint delimiter and get the first chunk
      // in loose-copy-mode, this step is skipped
      if (!looseCopy && !propertiesMatch(srcChunks[0], dstChunks[0]))
      {
         return false;
      }

      // verify unique constraints after: take the second chuck and match the unique constraints
      // both have unique constraints so resolve through special method
      if (srcChunks.length > 1 && dstChunks.length > 1 && !uniqueConstraintsMatch(srcChunks[1], dstChunks[1]))
      {
         return false;
      }
      
      // the destination has unique constraints, but the source doesn't, so this is not a safe copy
      if (srcChunks.length <= 1 && dstChunks.length > 1)
      {
         return false;
      }
      
      return true;
   }
   
   /**
    * Build a signature to match the DMO schema with another one.
    * <p>
    * The rules use the field's data-type in the order they are defined, without extent or mandatory.
    * <p>
    * For indexes, the signature will contain each index, in order of definition, and each will have: 
    * the index type (unique, primary, word), followed by the components (which are the field index in the
    * definition), sort order, and field's case-sensitivity, in case of character. 
    * 
    * @param     meta
    *            The DMO meta.
    *
    * @return    The schema signature.
    */
   private static String buildSchemaSignature(DmoMeta meta)
   {
      char sep = '|';
      char sep2 = '#';
      
      Map<String, Integer> l2i = new HashMap<>();
      StringBuilder sig = new StringBuilder();
      sig.append(meta.noUndo ? 'N' : 'U').append(sep); // no-undo status
      sig.append(meta.legacyTable.toLowerCase()).append(sep); // always lowercase
      sig.append(meta.propsByName.size()).append(sep2);
      int idx = 0;
      for (Property field : meta.propsByName.values())
      {
         if (field instanceof ReservedProperty)
         {
            continue;
         }
         // field extent is not used, just data type
         Character ch = getTypeEncoding((Class<? extends BaseDataType>) field._fwdType);
         sig.append(ch != null ? ch : field._fwdType.getSimpleName()).append(sep); // data-type
         l2i.put(field.legacyLower, idx); // always lowercase
         idx = idx + 1;
      }
      sig.append(sep2);
      for (Index index : meta.indexes)
      {
         sig.append(index.legacy().toLowerCase()).append(sep); // always lowercase
         // index-tyoe: WORD, PRIMARY or UNIQUE
         sig.append(index.word() ? "W" : "")
            .append(index.primary() ? "P" : "")
            .append(index.unique() ? "U" : "")
            .append(sep);
         for (IndexComponent ixc : index.components())
         {
            String fname = ixc.legacy().toLowerCase(); // always lowercase
            sig.append(l2i.get(fname)).append(sep);  // field index.
            Property field = meta.propsByName.get(ixc.name());
            if (field == null)
            {
               field = meta.propsByName.values().stream().
                                       filter(p-> fname.equals(p.legacy)).findFirst().orElse(null);
            }
            if (field != null && field.caseSensitive)
            {
               sig.append("CS").append(sep); // field case-sensitivity is used only if there is an index on it
            }
            sig.append(ixc.descending() ? 'D' : 'A').append(sep2); // sort order
         }
      }
      return sig.toString();
   }

   /**
    * This builds a string which structurally represents a DMO. The DMOs which are compatible,
    * from strcture's point of view, should have the same signature. This is useful for quickly
    * checking compatibility (example: copy-temp-table).
    * 
    * @param   it
    *          An iterator to the whole collection of properties for the processed DMO.
    * @param   uniqueConstraints 
    *          A list of multiple properties which together should be unique.
    * @param   tempTable 
    *          A flag to indicate that that the properties are for a temp-table.
    *          
    * @return  A string representing the signature for a DMO having the specified properties.
    */
   private static String buildQuickSignature(IteratorProvider<Property> it, 
                                             List<List<Property>> uniqueConstraints, 
                                             boolean tempTable)
   {
      StringBuilder sb = new StringBuilder();
      
      if (!buildPropertySignature(it, tempTable, false, null, sb, false))
      {
         return null;
      }      
      
      if (!buildUniqueConstraintsSignature(uniqueConstraints, null, sb))
      {
         return null;
      }
      
      return sb.toString();
   }
   
   private static String buildExplicitSignature(IteratorProvider<Property> it, 
                                               List<List<Property>> uniqueConstraints, 
                                               boolean tempTable,
                                               Map<Integer, Integer> idToPos)
   {
      StringBuilder sb = new StringBuilder();
      
      if (!buildPropertySignature(it, tempTable, true, idToPos, sb, false))
      {
         return null;
      }       
      
      if (!buildUniqueConstraintsSignature(uniqueConstraints, idToPos, sb))
      {
         return null;
      }     
      
      return sb.toString();
   }
   
   private static String buildSqlSignature(IteratorProvider<Property> it, 
                                           List<List<Property>> uniqueConstraints, 
                                           boolean tempTable,
                                           Map<Integer, Integer> idToPos)
   {
      StringBuilder sb = new StringBuilder();
      
      if (!buildPropertySignature(it, tempTable, true, idToPos, sb, true))
      {
         return null;
      }       
      
      if (!buildUniqueConstraintsSignature(uniqueConstraints, idToPos, sb))
      {
         return null;
      }     
      
      return sb.toString();
   }
   
   /**
    * Check if two signatures are compatible in terms of properties data types, extent and field-level
    * constraints.
    * 
    * @param   src
    *          The signature of the source DMO.
    * @param   dst
    *          The signature of the destination DMO.
    *          
    * @return  {@code true} if the signatures are compatible.
    */
   private static boolean propertiesMatch(String src, String dst)
   {
      if (src == null || dst == null)
      {
         return false;
      }
      if (src.equals(dst))
      {
         return true;
      }
      
      char[] srcChars = src.toCharArray();
      char[] dstChars = dst.toCharArray();
      
      int srcLength = srcChars.length;
      int dstLength = dstChars.length;
      
      int i, j;
      for (i = 0, j = 0; i < srcLength && j < dstLength;)
      {
         if (!isType(srcChars[i]) || !isType(dstChars[j]))
         {
            if (LOG.isLoggable(Level.SEVERE))
            {
               LOG.log(Level.SEVERE, "DMO signatures are malformed; "
                        + "some tokens are not parsed when validating copy");
            }
            return false;
         }
         
         // check type
         if (srcChars[i++] != dstChars[j++])
         {
            return false;
         }
         
         // check extent
         while (i < srcLength && j < dstLength && isDigit(srcChars[i]) && isDigit(dstChars[j]))
         {
            if (srcChars[i++] != dstChars[j++])
            {
               return false;
            }
         }
         
         if ((i < srcLength && isDigit(srcChars[i])) || (j < dstLength && isDigit(dstChars[j])))
         {
            return false;
         }
                  
         // check constraints
         while (j < dstLength && isConstraint(dstChars[j]))
         {
            while (i < srcLength && isConstraint(srcChars[i]) && srcChars[i] != dstChars[j])
            {
               i++;
            }
            
            if (i >= srcLength || !isConstraint(srcChars[i]))
            {
               return false;
            }
            
            // the source has a constraint which matches the current destination constraint
            i++;
            j++;
         }
      }
      
      return i == srcLength && j == dstLength;
   }
   
   /**
    * Check if the second set of unique constraints (encoded in the destination signature) can be implied
    * by the first set of unique constraints. This will happen if, for each unique constraint in the 
    * destination, there is a unique constraint in the source which is stronger (having such constraint 
    * will automatically imply that the destination unique constraint is satisfied).
    * 
    * @param   src
    *          The signature of the source's unique constraints.
    * @param   dst
    *          The signature of the destination's unique constraints.
    *          
    * @return  {@code true} if the constraints in the destination are satisfied by the unique constraints 
    *          in the source.
    */
   private static boolean uniqueConstraintsMatch(String src, String dst)
   {
      if (src == null || dst == null)
      {
         return false;
      }
      
      if (src.equals(dst))
      {
         return true;
      }
      
      String[] srcConstraints = src.split(FORMATTED_CONSTRAINT_DELIMITER);
      String[] dstConstraints = dst.split(FORMATTED_CONSTRAINT_DELIMITER);
      int srcLength = srcConstraints.length;
      int dstLength = dstConstraints.length;
      
      for (int j = 0; j < dstLength; j++)
      {
         boolean foundMatch = false;
         for (int i = 0; i < srcLength; i++)
         {
            if (strongerUniqueConstraint(srcConstraints[i], dstConstraints[j]))
            {
               foundMatch = true;
               break;
            }
         }
         if (!foundMatch)
         {
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Check if having the first unique constraint will automatically determine that the second unique
    * constraint will be satisfied. This happens when all fields in the first unique constraint are
    * included in the second one. From signature's point of view, this will be resolved by 
    * finding all encoded ids in the first unique constraint in the second one. Having same signature,
    * will automatically mean that the first unique constraint is stronger.
    * 
    * @param   firstConstraint
    *          The first unique constraint signature which is checked.
    * @param   secondConstraint
    *          The second unique constraint signature which is checked.
    *          
    * @return  {@code true} if the first unique constraint is included in the second, which means
    *          that is stronger and can imply the second unique constraint.
    */
   private static boolean strongerUniqueConstraint(String firstConstraint, String secondConstraint)
   {
      if (firstConstraint == null || secondConstraint == null)
      {
         return false;
      }
      if (firstConstraint.equals(secondConstraint))
      {
         return true;
      }
      
      String[] ids1 = firstConstraint.split(FORMATTED_PROPERTY_DELIMITER);
      String[] ids2 = secondConstraint.split(FORMATTED_PROPERTY_DELIMITER);
      int length1 = ids1.length;
      int length2 = ids2.length;
      
      // we make use of the fact that the property ids are sorted in a strictly ascending way
      int i, j;
      for (i = 0, j = 0; i < length1 && j < length2; j++)
      {         
         if (ids1[i].equals(ids2[j])) // we found a constraint match for property i
         {
            i++;
         }
      }
      
      // first constraint is stronger, if we could find all its properties in the second constraint
      return i == length1;
   }
   
   /**
    * This is responsible for encoding the properties and append them to the provided string builder.
    * The encoding is a non-delimited sequence of property encodings. Each property encoding is
    * formed out of: data type encoding, extent encoding and field-level constraints.
    * 
    * @param   itProvider
    *          An iterator to a collection of properties which should be encoded.
    * @param   tempTable
    *          A flag indicating that the signature is created for a temp-table specific DMO.
    * @param   explicit
    *          A flag indicating that this property signature is done for an explicit signature.
    *          This means that the property names should be included, and the property ordered.
    * @param   idToPos
    *          A valid map which will be filled with the correspondence between property id and 
    *          their order in the generated signature. This is required only if the explicit
    *          flag is set on true - as only explicit signatures have a different ordering of the props.
    * @param   sb
    *          The string builder onto which the signature should be appended.
    * @param   sqlName
    *          A flag indicating that the signature should also include the SQL name of the fields.
    *          
    * @return  {@code true} if the properties could be encoded in a signature.
    */
   private static boolean buildPropertySignature(IteratorProvider<Property> itProvider, 
                                                 boolean tempTable, 
                                                 boolean explicit,
                                                 Map<Integer, Integer> idToPos,
                                                 StringBuilder sb,
                                                 boolean sqlName)
   {      
      Map<String, String> nameToSignature = new LinkedHashMap<>();
      Iterator<Property> it = itProvider.provide();
      while (it.hasNext())
      {
         StringBuilder sbProp = new StringBuilder();
         Property prop = it.next();
         Class<? extends BaseDataType> type = (Class<? extends BaseDataType>) prop._fwdType;
         Character ch = getTypeEncoding(type);
         
         if (ch == null)
         {
            return false;
         }
         
         sbProp.append(ch);
         
         int extent = prop.extent;
         if (extent > 0)
         {
            sbProp.append(extent);
         }

         // for explicit signatures, we need to add the name of the property
         if (explicit && !processFieldName(prop, tempTable, sbProp, sqlName))
         {
            return false;
         }
         
         if (!processFieldConstraints(prop, tempTable, sbProp))
         {
            return false;
         }
         
         nameToSignature.put(prop.legacy, sbProp.toString());
      }
      
      if (!explicit)
      {
         // this is a quick signature: append the properties in the default order
         for (String signature : nameToSignature.values())
         {
            sb.append(signature);
         }
         return true;
      }
      
      // this is an explicit signature: append the properties sorted by the name
      Map<String, Integer> name2Pos = new HashMap<>();
      int idx = 1;
      for (Map.Entry<String, String> entry : new TreeMap<>(nameToSignature).entrySet())
      {
         String name = entry.getKey();
         String signature = entry.getValue();
         
         sb.append(signature);
         name2Pos.put(name, idx++);
      }
      
      // create a positioning mapping as the fields are not in the same order in the
      // signature as in a record
      it = itProvider.provide();
      while (it.hasNext())
      {
         Property prop = it.next();
         idToPos.put(prop.id, name2Pos.get(prop.legacy));
      }
      
      return true;
   }
   
   /**
    * This is responsible for encoding the unique constraints and append them to the provided string builder.
    * The encoding is a lexicographically sorted sequence of unique constraint signatures, delimited by a
    * defined character. Each unique constraint signature is a sorted sequence of property ids, delimited
    * by a fixed character.
    * 
    * @param   uniqueConstraints
    *          A list of unique constrains (each being a list of properties).
    * @param   idToPos
    *          A mapping from the property ids to their position in the signature. This should be 
    *          {@code null} for quick signatures (as their ids match the position}, which this should be
    *          not {@code null} null in explicit signatures, where the order doesn't always match.
    * @param   sb
    *          The string builder onto which the signature should be appended.
    *          
    * @return  {@code true} if the unique constraints could be converted to a signature
    */
   private static boolean buildUniqueConstraintsSignature(List<List<Property>> uniqueConstraints, 
                                                          Map<Integer, Integer> idToPos,
                                                          StringBuilder sb)
   {
      if (uniqueConstraints == null || uniqueConstraints.isEmpty())
      {
         return true;
      }
      
      List<String> constraintSignatures = new ArrayList<>();
      
      for (List<Property> props : uniqueConstraints)
      {
         if (props == null || props.isEmpty())
         {
            continue;
         }
         
         StringBuilder signature = new StringBuilder();
         List<Integer> ids = new ArrayList<>(props.size());
         
         props.forEach((prop) -> ids.add(idToPos == null ? prop.id : idToPos.get(prop.id)));
         
         // the ids should be sorted in order to do exact matches more easily (string matching)
         Collections.sort(ids);
         for (int i = 0; i < ids.size() - 1; i++)
         {
            signature.append(ids.get(i)).append(CHAR_PROPERTY_ID_DELIMITER);
         }
         signature.append(ids.get(ids.size() - 1));
         
         constraintSignatures.add(signature.toString());
      }

      // the constraints should be sorted in order to do exact matches more easily (string matching)
      Collections.sort(constraintSignatures);
      
      for (int i = 0; i < constraintSignatures.size(); i++)
      {
         sb.append(CHAR_UNIQUE_CONSTRAINT_DELIMITER).append(constraintSignatures.get(i));
      }
      
      return true;
   }
   
   /**
    * Provide a single character encoding for a certain base data type.
    * 
    * @param   type
    *          The base data type for which the encoding is to be retrieved.
    * @return  A character to be used when encoding a base data type into a signature.
    */
   private static Character getTypeEncoding(Class<? extends BaseDataType> type)
   {
      Character ch;
      synchronized (typeMapping)
      {
         ch = typeMapping.get(type);
      }
      
      if (ch == null)
      {
         // for this kind of situations, find a suitable representation for this type
         ch = findSuitableCandidate.provide(type);
      }
      
      if (ch == null)
      {
         // can't find a suitable representation, assign a random character
         ch = findAnyCandidate.provide(type);
      }
      
      if (ch == null && LOG.isLoggable(Level.WARNING))
      {
         LOG.log(Level.WARNING, "Couldn't find a character representation for " + type.getName());
      }
      
      return ch;
   }
   
   /**
    * Check if the provided character is suitable for data type encoding in the signature.
    * 
    * @param   ch
    *          The character which is checked.
    *          
    * @return  {@code true} if the character is suitable.
    */
   private static boolean isType(char ch)
   {
      return 'A' <= ch && ch <= 'Z';
   }
   
   /**
    * Check if the provided character is a base-10 digit.
    * 
    * @param   ch
    *          The character which is checked.
    *          
    * @return  {@code true} if the character is a digit.
    */
   private static boolean isDigit(char ch)
   {
      return '0' <= ch && ch <= '9';
   }
   
   /**
    * Check if the provided character is suitable for constraint encoding in the signature.
    * 
    * @param   ch
    *          The character which is checked.
    *          
    * @return  {@code true} if the character is suitable.
    */
   private static boolean isConstraint(char ch)
   {
      return 'a' <= ch && ch <= 'z';
   }
   
   /**
    * Helper which sets restrictions like mandatory or unique to a specified field.
    * 
    * @param   prop
    *          The property for which the restriction analysis is done.
    * @param   tempTable
    *          Flag which indicates that the property is part of a temporary table.
    * @param   sb
    *          The string builder which should be completed with the found restrictions.
    *          
    * @return  {@code true} if we could process the constraints of the property    
    */
   private static boolean processFieldConstraints(Property prop,
                                                  boolean tempTable,
                                                  StringBuilder sb)
   {
      if (!tempTable && prop.mandatory)
      {
         sb.append(CHAR_MANDANTORY);
      }
      
      return true;
   }
   
   private static boolean processFieldName(Property prop,
                                           boolean tempTable,
                                           StringBuilder sb,
                                           boolean sqlName)
   {
      String name = prop.legacy;
      if (name.indexOf(PROPERTY_NAME_DELIMITER_START)  != -1 || 
          name.indexOf(PROPERTY_NAME_DELIMITER_FINISH) != -1)
      {
         return false;
      }
      
      sb.append(PROPERTY_NAME_DELIMITER_START);
      sb.append(name);
      sb.append(PROPERTY_NAME_DELIMITER_FINISH);
      if(sqlName)
      {
         sb.append("SQL");
         sb.append(PROPERTY_NAME_DELIMITER_START);
         sb.append(prop.column);
         sb.append(PROPERTY_NAME_DELIMITER_FINISH);
      }
      return true;
   }
   
   /**
    * Helper for anonymous functions used in identifying a suitable character for a specified data type. 
    */
   interface TypeIdentifierProvider 
   {
      /** Single provider method for a character based on a base data type class. */
      Character provide(Class<? extends BaseDataType> type);
   }
   
   /**
    * Helper for anonymous functions used in identifying a suitable character for a specified data type. 
    */
   interface IteratorProvider<T>
   {
      /** Single provider method for an iterator */
      Iterator<T> provide();
   }
}