Range of integers/floats/dates in programming languages when using BIGINT in PostgreSQL

Was trying to migrate a database from MySQL to PostgreSQL recently when I “discovered” that UNSIGNED is not supported in PostgreSQL, as it is not part of the ANSI SQL standard which it follows closely. Other findings were that non-aggregate columns are not allowed in queries with GROUP BY clauses (which MySQL/SQLite “allows” by selecting the value from a random record in each group), and that the auto-increment of the primary key goes out of sync if any record is inserted with the value for the primary key specified (see this article on how to manually re-sync it).

BIGINT has a range of -9223372036854775808 to 9223372036854775807, while BIGINT UNSIGNED has a range of 0 to 18446744073709551615 (supported in MySQL/SQLite but not in PostgreSQL). This got me wondering if programming languages can even support such large numbers out of the box, without use of special third party libraries.

Did up code samples for JavaScript (applies to Node.js as well), PHP and Python to test the following:

  • Set variable to integer 9223372036854775807 (2^63 – 1)
  • Cast string to integer
  • Integer arithmetic
  • Set variable to float using UNIX timestamp with microsecond precision in decimals, set beyond year 2038
  • Cast string to float
  • Float arithmetic
  • Convert textual representation of date (beyond year 2038) to Date object
  • Convert UNIX timestamp with microsecond precision in decimals (beyond year 2038) to Date object

JavaScript / Node.js:

Failed integer tests. Note that the PostgreSQL driver for Node.js returns strings for BIGINT datatype and while integers can be returned by overriding the default type parser, values larger than 9007199254740991 (1000x smaller than BIGINT max value) will still cause parseInt() to fail.

  let sprintf = function (format) {
      let result = format;
      for (let i = 1; i < arguments.length; i++) {
          result = result.replace('%s', arguments[i]);
      }

      return result;
  };

  // Smaller range may cause issues for large database record IDs or values
  let maxInt = Number.MAX_SAFE_INTEGER; // 2^53 - 1
  let intValue = 9223372036854775807; // 2^63 - 1
  let intStr = '9223372036854775807';
  let intStrAsInt = parseInt(intStr);
  let intSum = 9223372036854775806 + 1;
  console.log(sprintf("Integer Test:\n%s\n%s\n%s\n%s\n\n", maxInt, intValue, intStrAsInt, intSum));
  // Integer Test:
  // 9007199254740991
  // 9223372036854776000
  // 9223372036854776000
  // 9223372036854776000

  let isoDate = '2040-01-02T03:04:05.678901Z';
  let floatValue = 2209086245.678901; // UNIX timestamp for ISO date
  let floatStr = '2209086245.678901';
  let floatStrAsFloat = parseFloat(floatStr);
  let floatSum = 2209086244.677900 + 1.001001;
  console.log(sprintf("Float Test:\n%s\n%s\n%s\n\n", floatValue, floatStrAsFloat, floatSum));
  // Float Test:
  // 2209086245.678901
  // 2209086245.678901
  // 2209086245.6789007

  let dateFromIsoDate = new Date(isoDate);
  let dateFromEpoch = new Date(floatValue * 1000);
  console.log(sprintf(
      "Date Test:\n%s\n%s\n%s\n%s\n%s\n%s\n\n",
      isoDate,
      floatValue,
      dateFromIsoDate.toISOString(),
      (dateFromIsoDate.getTime() / 1000),
      dateFromEpoch.toISOString(),
      (dateFromEpoch.getTime() / 1000),
  ));
  // Date Test:
  // 2040-01-02T03:04:05.678901Z
  // 2209086245.678901
  // 2040-01-02T03:04:05.678Z
  // 2209086245.678
  // 2040-01-02T03:04:05.678Z
  // 2209086245.678

PHP:

  <?php
  $maxInt = PHP_INT_MAX; // 2^63 - 1 on 64-bit systems
  $intValue = 9223372036854775807; // 2^63 - 1
  $intStr = '9223372036854775807';
  $intStrAsInt = intval($intStr);
  $intSum = 9223372036854775806 + 1;
  printf("Integer Test:\n%d\n%d\n%d\n%d\n\n", $maxInt, $intValue, $intStrAsInt, $intSum);
  // Integer Test:
  // 9223372036854775807
  // 9223372036854775807
  // 9223372036854775807
  // 9223372036854775807

  // Only up to 4 decimal places shown if %s format specifier is used
  // Prints 2209086245.6789007 if %.7f format specifier is used
  $isoDate = '2040-01-02T03:04:05.678901Z';
  $floatValue = 2209086245.678901; // UNIX timestamp for ISO date
  $floatStr = '2209086245.678901';
  $floatStrAsFloat = floatval($floatStr);
  $floatSum = 2209086244.677900 + 1.001001;
  printf("Float Test:\n%f\n%f\n%f\n\n", $floatValue, $floatStrAsFloat, $floatSum);
  // Float Test:
  // 2209086245.678901
  // 2209086245.678901
  // 2209086245.678901

  $dateFromIsoDate = new DateTimeImmutable($isoDate);
  $dateFromEpoch = DateTimeImmutable::createFromFormat('U.u', $floatValue);
  $format = 'Y-m-d\TH:i:s.up';
  printf(
      "Date Test:\n%s\n%.6f\n%s\n%s\n%s\n%s\n\n",
      $isoDate,
      $floatValue,
      $dateFromIsoDate->format($format),
      $dateFromIsoDate->format('U.u'),
      $dateFromEpoch->format($format),
      $dateFromEpoch->format('U.u'),
  );
  // Date Test:
  // 2040-01-02T03:04:05.678901Z
  // 2209086245.678901
  // 2040-01-02T03:04:05.678901Z
  // 2209086245.678901
  // 2040-01-02T03:04:05.678900+00:00
  // 2209086245.678900

Python:

  from datetime import datetime
  import sys

  # No more sys.maxint in Python 3 as there is no longer a limit to the value of integers
  max_int = sys.maxsize; # 2^63 - 1
  int_value = 9223372036854775807; # 2^63 - 1
  int_str = '9223372036854775807';
  int_str_as_int = int(int_str);
  int_sum = 9223372036854775806 + 1;
  print("Integer Test:\n{0}\n{1}\n{2}\n{3}\n\n".format(max_int, int_value, int_str_as_int, int_sum));
  # Integer Test:
  # 9223372036854775807
  # 9223372036854775807
  # 9223372036854775807
  # 9223372036854775807

  iso_date = '2040-01-02T03:04:05.678901Z';
  float_value = 2209086245.678901; # UNIX timestamp for ISO date
  float_str = '2209086245.678901';
  float_str_as_float = float(float_str);
  float_sum = 2209086244.677900 + 1.001001;
  print("Float Test:\n{0}\n{1}\n{2}\n\n".format(float_value, float_str_as_float, float_sum));
  # Float Test:
  # 2209086245.678901
  # 2209086245.678901
  # 2209086245.6789007

  date_from_iso_date = datetime.fromisoformat(iso_date.replace('Z', '+00:00')); # does not work with Z hence replace
  date_from_epoch = datetime.fromtimestamp(float_value);
  print("Date Test:\n{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n\n".format(
      iso_date,
      float_value,
      date_from_iso_date.isoformat(),
      date_from_iso_date.timestamp(),
      date_from_epoch.isoformat(),
      date_from_epoch.timestamp(),
  ));
  # Date Test:
  # 2040-01-02T03:04:05.678901Z
  # 2209086245.678901
  # 2040-01-02T03:04:05.678901+00:00
  # 2209086245.678901
  # 2040-01-02T11:04:05.678901
  # 2209086245.678901