cube-js/cube

Consistently return numeric values as strings in REST API and GraphQL API responses

igorlukanin opened this issue · 0 comments

Is your feature request related to a problem? Please describe.
It has become a known issue or design choice that the REST API returns numeric values either as numbers or strings, depending on multiple factors, and GraphQL API always returns numeric values as numbers. (Regarding the REST API, it will always return numeric values as strings if a pre-aggregation is used or if a measure is calculated.)

This makes it slightly inconvenient to handle numeric values at the data presentation layer, because it should both handle numbers and strings and properly cast them to desired types.

There's the castNumerics option in the JavaScript SDK that would cast all numeric values to numbers automatically, however, this might lead to precision loss since such a conversion would not be correct in case of numbers more than Number.MAX_SAFE_INTEGER or less than Number.MIN_SAFE_INTEGER as they can't be represented as JavaScript Number.

Describe the solution you'd like
Both REST API and GraphQL API should always return numeric values as strings so that the data presentation layer can parse and cast them consistently.

Potentially, these APIs can also return the native type (integer, float, or decimal) as part of the dataset or metadata, so that users who don't want to use castNumerics can use that information for the type conversion.

Describe alternatives you've considered

Additional context
Here's an example of how Cube currently works, as of v0.36.2.

Data model:

cubes:
  - name: numerics
    sql: >
      SELECT
        CAST(1.23 AS FLOAT64) AS value_float64,
        CAST(4.56 AS NUMERIC) AS value_numeric,
        CAST(789 AS INT64) AS value_int64,
        TRUE AS without_pre_aggregation

    dimensions:
      - name: value_float64
        sql: value_float64
        type: number

      - name: value_numeric
        sql: value_numeric
        type: number

      - name: value_int64
        sql: value_int64
        type: number

      - name: without_pre_aggregation
        sql: without_pre_aggregation
        type: boolean
        
    measures:
      - name: max_float64
        sql: value_float64
        type: max

      - name: max_numeric
        sql: value_numeric
        type: max

      - name: max_int64
        sql: value_int64
        type: max
          
      - name: calculated_float64
        sql: "1.0 * {max_float64}"
        type: number
          
      - name: calculated_numeric
        sql: "1.0 * {max_numeric}"
        type: number
          
      - name: calculated_int64
        sql: "1.0 * {max_int64}"
        type: number

    pre_aggregations:
      - name: main
        dimensions:
          - value_float64
          - value_numeric
          - value_int64
        measures:
          - max_float64
          - max_numeric
          - max_int64

Example query:

{
  "limit": 5000,
  "dimensions": [
    "numerics.value_float64",
    "numerics.value_int64",
    "numerics.value_numeric",
    "numerics.without_pre_aggregation"
  ],
  "measures": [
    "numerics.max_float64",
    "numerics.max_int64",
    "numerics.max_numeric",
    "numerics.calculated_float64",
    "numerics.calculated_int64",
    "numerics.calculated_numeric"
  ]
}

REST API, query that does not use a pre-aggregation:

{
    "numerics.value_float64": 1.23,
    "numerics.value_int64": 789,
    "numerics.value_numeric": "4.56",
    "numerics.without_pre_aggregation": true,
    "numerics.max_float64": 1.23,
    "numerics.max_int64": 789,
    "numerics.max_numeric": "4.56",
    "numerics.calculated_float64": 1.23,
    "numerics.calculated_int64": 789,
    "numerics.calculated_numeric": "4.56"
  }

REST API, query that uses a pre-aggregation:

  {
    "numerics.value_float64": "1.23",
    "numerics.value_int64": "789",
    "numerics.value_numeric": "4.56",
    "numerics.max_float64": "1.23",
    "numerics.max_int64": "789",
    "numerics.max_numeric": "4.56",
    "numerics.calculated_float64": "1.23",
    "numerics.calculated_int64": "789",
    "numerics.calculated_numeric": "4.56"
  }

GraphQL API, any query:

{
  "data": {
    "cube": [
      {
        "numerics": {
          "calculated_float64": 1.23,
          "calculated_int64": 789,
          "calculated_numeric": 4.56,
          "max_float64": 1.23,
          "max_int64": 789,
          "max_numeric": 4.56,
          "value_float64": 1.23,
          "value_int64": 789,
          "value_numeric": 4.56,
          "without_pre_aggregation": "true"
        }
      }
    ]
  }
}