repo_description: A fast, local first, reactive Database for JavaScript Applications https://rxdb.info/
repo_topics: angular browser-database couchdb crdt database firebase graphql indexeddb ionic local-first localstorage nodejs nosql postgres pouchdb react-native realtime realtime-database rxdb sqlite
code: /**
   * For some RxStorage implementations,
   * we need to use our custom crafted indexes
   * so we can easily iterate over them. And sort plain arrays of document data.
   *
   * We really often have to craft an index string for a given document.
   * Performance of everything in this file is very important
   * which is why the code sometimes looks strange.
   * Run performance tests before and after you touch anything here!
   */
  
  import {
      getSchemaByObjectPath
  } from './rx-schema-helper.ts';
  import type {
      JsonSchema,
      RxDocumentData,
      RxJsonSchema
  } from './types/index.d.ts';
  import {
      ensureNotFalsy,
      objectPathMonad,
      ObjectPathMonadFunction
  } from './plugins/utils/index.ts';
  import {
      INDEX_MAX,
      INDEX_MIN
  } from './query-planner.ts';
  import {
      newRxError
  } from './rx-error.ts';
  
  
  /**
   * Prepare all relevant information
   * outside of the returned function
   * from getIndexableStringMonad()
   * to save performance when the returned
   * function is called many times.
   */
  type IndexMetaField<RxDocType> = {
      fieldName: string;
      schemaPart: JsonSchema;
      /*
       * Only in number fields.
       */
      parsedLengths?: ParsedLengths;
      getValue: ObjectPathMonadFunction<RxDocType>;
      getIndexStringPart: (docData: RxDocumentData<RxDocType>) => string;
  };
  
  export function getIndexMeta<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      index: string[]
  ): IndexMetaField<RxDocType>[] {
      const fieldNameProperties: IndexMetaField<RxDocType>[] = index.map(fieldName => {
          const schemaPart = getSchemaByObjectPath(
              schema,
              fieldName
          );
          if (!schemaPart) {
              throw newRxError('CI1', { fieldName });
          }
          const type = schemaPart.type;
          let parsedLengths: ParsedLengths | undefined;
          if (type === 'number' || type === 'integer') {
              parsedLengths = getStringLengthOfIndexNumber(
                  schemaPart
              );
          }
  
          const getValue = objectPathMonad(fieldName);
          const maxLength = schemaPart.maxLength ? schemaPart.maxLength : 0;
  
          let getIndexStringPart: (docData: RxDocumentData<RxDocType>) => string;
          if (type === 'string') {
              getIndexStringPart = docData => {
                  let fieldValue = getValue(docData);
                  if (!fieldValue) {
                      fieldValue = '';
                  }
                  return fieldValue.padEnd(maxLength, ' ');
              };
          } else if (type === 'boolean') {
              getIndexStringPart = docData => {
                  const fieldValue = getValue(docData);
                  return fieldValue ? '1' : '0';
              };
          } else { // number
              /**
               * @performance
               * Inline the number index string generation to avoid
               * function call overhead and redundant boundary checks.
               * Document data in the hot path is assumed to be valid.
               */
              const pLengths = parsedLengths as ParsedLengths;
              const pMin = pLengths.minimum;
              const pMax = pLengths.maximum;
              const pRoundedMin = pLengths.roundedMinimum;
              const pNonDecimals = pLengths.nonDecimals;
              const pDecimals = pLengths.decimals;
              const pMultiplier = pLengths.multiplier;
              if (pDecimals === 0) {
                  getIndexStringPart = docData => {
                      let fieldValue = getValue(docData);
                      if (typeof fieldValue === 'undefined') {
                          fieldValue = 0;
                      }
                      if (fieldValue < pMin) {
                          fieldValue = pMin;
                      }
                      if (fieldValue > pMax) {
                          fieldValue = pMax;
                      }
                      return (Math.floor(fieldValue) - pRoundedMin).toString().padStart(pNonDecimals, '0');
                  };
              } else {
                  getIndexStringPart = docData => {
                      let fieldValue = getValue(docData);
                      if (typeof fieldValue === 'undefined') {
                          fieldValue = 0;
                      }
                      if (fieldValue < pMin) {
                          fieldValue = pMin;
                      }
                      if (fieldValue > pMax) {
                          fieldValue = pMax;
                      }
                      const flooredValue = Math.floor(fieldValue);
                      const shifted = Math.min(
                          Math.round((fieldValue - flooredValue) * pMultiplier),
                          pMultiplier - 1
                      );
                      const str = (flooredValue - pRoundedMin).toString().padStart(pNonDecimals, '0');
                      return str + shifted.toString().padStart(pDecimals, '0');
                  };
              }
          }
  
          const ret: IndexMetaField<RxDocType> = {
              fieldName,
              schemaPart,
              parsedLengths,
              getValue,
              getIndexStringPart
          };
          return ret;
      });
      return fieldNameProperties;
  }
  
  
  /**
   * Crafts an indexable string that can be used
   * to check if a document would be sorted below or above
   * another documents, dependent on the index values.
   * @monad for better performance
   *
   * IMPORTANT: Performance is really important here
   * which is why we code so 'strange'.
   * Always run performance tests when you want to
   * change something in this method.
   */
  export function getIndexableStringMonad<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      index: string[]
  ): (docData: RxDocumentData<RxDocType>) => string {
      const fieldNameProperties = getIndexMeta(schema, index);
      const fieldNamePropertiesAmount = fieldNameProperties.length;
      const indexPartsFunctions = fieldNameProperties.map(r => r.getIndexStringPart);
  
      /**
       * @hotPath Performance of this function is very critical!
       * Specialize for common field counts to avoid loop overhead.
       */
      if (fieldNamePropertiesAmount === 1) {
          return indexPartsFunctions[0];
      }
      if (fieldNamePropertiesAmount === 2) {
          const fn0 = indexPartsFunctions[0];
          const fn1 = indexPartsFunctions[1];
          return (docData: RxDocumentData<RxDocType>): string => fn0(docData) + fn1(docData);
      }
      if (fieldNamePropertiesAmount === 3) {
          const fn0 = indexPartsFunctions[0];
          const fn1 = indexPartsFunctions[1];
          const fn2 = indexPartsFunctions[2];
          return (docData: RxDocumentData<RxDocType>): string => fn0(docData) + fn1(docData) + fn2(docData);
      }
  
      const ret = function (docData: RxDocumentData<RxDocType>): string {
          let str = '';
          for (let i = 0; i < fieldNamePropertiesAmount; ++i) {
              str += indexPartsFunctions[i](docData);
          }
          return str;
      };
      return ret;
  }
  
  
  declare type ParsedLengths = {
      minimum: number;
      maximum: number;
      nonDecimals: number;
      decimals: number;
      roundedMinimum: number;
      /**
       * Pre-computed Math.pow(10, decimals) to avoid
       * recomputing on every getNumberIndexString call.
       */
      multiplier: number;
  };
  export function getStringLengthOfIndexNumber(
      schemaPart: JsonSchema
  ): ParsedLengths {
      const minimum = Math.floor(schemaPart.minimum as number);
      const maximum = Math.ceil(schemaPart.maximum as number);
      const multipleOf: number = schemaPart.multipleOf as number;
  
      const valueSpan = maximum - minimum;
      const nonDecimals = valueSpan.toString().length;
  
      const multipleOfParts = multipleOf.toString().split('.');
      let decimals = 0;
      if (multipleOfParts.length > 1) {
          decimals = multipleOfParts[1].length;
      }
      return {
          minimum,
          maximum,
          nonDecimals,
          decimals,
          roundedMinimum: minimum,
          multiplier: Math.pow(10, decimals)
      };
  }
  
  export function getIndexStringLength<RxDocType>(
      schema: RxJsonSchema<RxDocumentData<RxDocType>>,
      index: string[]
  ): number {
      const fieldNameProperties = getIndexMeta(schema, index);
      let length = 0;
      fieldNameProperties.forEach(props => {
          const schemaPart = props.schemaPart;
          const type = schemaPart.type;
  
          if (type === 'string') {
              length += schemaPart.maxLength as number;
          } else if (type === 'boolean') {
              length += 1;
          } else {
              const parsedLengths = props.parsedLengths as ParsedLengths;
              length = length + parsedLengths.nonDecimals + parsedLengths.decimals;
          }
  
      });
      return length;
  }
  
  
  export function getPrimaryKeyFromIndexableString(
      indexableString: string,
      primaryKeyLength: number
  ): string {
      const paddedPrimaryKey = indexableString.slice(primaryKeyLength * -1);
      // we can safely trim here because the primary key is not allowed to start or end with a space char.
      const primaryKey = paddedPrimaryKey.trim();
      return primaryKey;
  }
  
  
  export function getNumberIndexString(
      parsedLengths: ParsedLengths,
      fieldValue: number
  ): string {
      /**
       * Ensure that the given value is in the boundaries
       * of the schema, otherwise it would create a broken index string.
       * This can happen for example if you have a minimum of 0
       * and run a query like
       * selector {
       *  numField: { $gt: -1000 }
       * }
       */
      if (typeof fieldValue === 'undefined') {
          fieldValue = 0;
      }
      if (fieldValue < parsedLengths.minimum) {
          fieldValue = parsedLengths.minimum;
      }
      if (fieldValue > parsedLengths.maximum) {
          fieldValue = parsedLengths.maximum;
      }
  
      const nonDecimalsValueAsString = (Math.floor(fieldValue) - parsedLengths.roundedMinimum).toString();
      let str = nonDecimalsValueAsString.padStart(parsedLengths.nonDecimals, '0');
  
      if (parsedLengths.decimals > 0) {
          /**
           * @performance
           * Use math to extract decimal digits instead of toString().split('.')
           * which creates intermediate strings and arrays.
           * multiplier is pre-computed in ParsedLengths to avoid Math.pow() per call.
           */
          const multiplier = parsedLengths.multiplier;
          const shifted = Math.min(
              Math.round((fieldValue - Math.floor(fieldValue)) * multiplier),
              multiplier - 1
          );
          const decimalPart = shifted.toString();
          str += decimalPart.padStart(parsedLengths.decimals, '0');
      }
      return str;
  }
  
  export function getStartIndexStringFromLowerBound(
      schema: RxJsonSchema<any>,
      index: string[],
      lowerBound: (string | boolean | number | null | undefined)[]
  ): string {
      let str = '';
      index.forEach((fieldName, idx) => {
          const schemaPart = getSchemaByObjectPath(
              schema,
              fieldName
          );
          const bound = lowerBound[idx];
          const type = schemaPart.type;
  
          switch (type) {
              case 'string':
                  const maxLength = ensureNotFalsy(schemaPart.maxLength, 'maxLength not set');
                  if (typeof bound === 'string') {
                      if (bound === INDEX_MAX) {
                          str += ''.padEnd(maxLength, INDEX_MAX);
                      } else {
                          str += (bound as string).padEnd(maxLength, ' ');
                      }
                  } else {
                      /**
                       * Use the null character (\x00) as the absolute minimum
                       * instead of space (\x20). Strings can contain characters
                       * with codepoints below space (e.g. \t = 9, \n = 10)
                       * which would sort before space-padded bounds and be
                       * silently excluded from query results.
                       */
                      str += ''.padEnd(maxLength, '\x00');
                  }
                  break;
              case 'boolean':
                  if (bound === null) {
                      str += '0';
                  } else if (bound === INDEX_MIN) {
                      str += '0';
                  } else if (bound === INDEX_MAX) {
                      str += '1';
                  } else {
                      const boolToStr = bound ? '1' : '0';
                      str += boolToStr;
                  }
                  break;
              case 'number':
              case 'integer':
                  const parsedLengths = getStringLengthOfIndexNumber(
                      schemaPart
                  );
                  if (bound === null || bound === INDEX_MIN) {
                      const fillChar = '0';
                      str += fillChar.repeat(parsedLengths.nonDecimals + parsedLengths.decimals);
                  } else if (bound === INDEX_MAX) {
                      str += getNumberIndexString(
                          parsedLengths,
                          parsedLengths.maximum
                      );
                  } else {
                      const add = getNumberIndexString(
                          parsedLengths,
                          bound as number
                      );
                      str += add;
                  }
                  break;
              default:
                  throw newRxError('CI2', { type: type as string });
          }
      });
      return str;
  }
  
  
  export function getStartIndexStringFromUpperBound(
      schema: RxJsonSchema<any>,
      index: string[],
      upperBound: (string | boolean | number | null | undefined)[]
  ): string {
      let str = '';
      index.forEach((fieldName, idx) => {
          const schemaPart = getSchemaByObjectPath(
              schema,
              fieldName
          );
          const bound = upperBound[idx];
          const type = schemaPart.type;
  
          switch (type) {
              case 'string':
                  const maxLength = ensureNotFalsy(schemaPart.maxLength, 'maxLength not set');
                  if (typeof bound === 'string' && bound !== INDEX_MAX) {
                      str += (bound as string).padEnd(maxLength, ' ');
                  } else if (bound === INDEX_MIN) {
                      /**
                       * Use the null character (\x00) as the absolute minimum
                       * to match the lower bound fix. This ensures that compound
                       * index queries with exclusive bounds on earlier fields
                       * correctly exclude documents whose later string fields
                       * contain characters with codepoints below space.
                       */
                      str += ''.padEnd(maxLength, '\x00');
                  } else {
                      str += ''.padEnd(maxLength, INDEX_MAX);
                  }
                  break;
              case 'boolean':
                  if (bound === null || bound === INDEX_MAX) {
                      str += '1';
                  } else if (bound === INDEX_MIN) {
                      str += '0';
                  } else {
                      const boolToStr = bound ? '1' : '0';
                      str += boolToStr;
                  }
                  break;
              case 'number':
              case 'integer':
                  const parsedLengths = getStringLengthOfIndexNumber(
                      schemaPart
                  );
                  if (bound === null || bound === INDEX_MAX) {
                      const fillChar = '9';
                      str += fillChar.repeat(parsedLengths.nonDecimals + parsedLengths.decimals);
                  } else if (bound === INDEX_MIN) {
                      const fillChar = '0';
                      str += fillChar.repeat(parsedLengths.nonDecimals + parsedLengths.decimals);
                  } else {
                      str += getNumberIndexString(
                          parsedLengths,
                          bound as number
                      );
                  }
                  break;
              default:
                  throw newRxError('CI2', { type: type as string });
          }
      });
      return str;
  }
  
  /**
   * Used in storages where it is not possible
   * to define inclusiveEnd/inclusiveStart
   */
  export function changeIndexableStringByOneQuantum(str: string, direction: 1 | -1): string {
      const lastChar = str.slice(-1);
      let charCode = lastChar.charCodeAt(0);
      charCode = charCode + direction;
      const withoutLastChar = str.slice(0, -1);
      return withoutLastChar + String.fromCharCode(charCode);
  }
  