depot / kysely-planetscale

A Kysely dialect for PlanetScale Serverless
https://depot.dev/blog/kysely-dialect-planetscale
MIT License
350 stars 15 forks source link

Numbers returned as strings #30

Closed twiddler closed 6 months ago

twiddler commented 6 months ago

In the following test suite, when using PlanetScaleDialect and receiving more than one row, what should be numbers is parsed as strings instead:

import { Kysely, MysqlDialect } from "kysely";
import { PlanetScaleDialect } from "kysely-planetscale";
import { createPool } from "mysql2";
import { strict as assert } from "node:assert";
import { describe, test } from "node:test";
import { fetch } from "undici";

describe("mysql", function () {
  const db = new Kysely({
    dialect: new MysqlDialect({
      pool: createPool({
        database: "mysql",
        host: "localhost",
        user: "root",
        password: "",
      }),
    }),
  });

  test("one row", async function () {
    const q = await db
      .selectFrom([
        db.selectNoFrom((eb) => eb.val(0).as("num_column")).as("foo"),
      ])
      .selectAll()
      .execute();

    assert.deepStrictEqual(q, [{ num_column: 0 }]); // ✅
  });

  test("two rows", async function () {
    const q = await db
      .selectFrom([
        db
          .selectNoFrom((eb) => eb.val(0).as("num_column"))
          .unionAll(db.selectNoFrom((eb) => eb.val(0).as("num_column")))
          .as("foo"),
      ])
      .selectAll()
      .execute();

    assert.deepStrictEqual(q, [{ num_column: 0 }, { num_column: 0 }]); // ✅
  });
});

describe("planetscale", function () {
  const db = new Kysely({
    dialect: new PlanetScaleDialect({
      url: process.env.PLANETSCALE_URL,
      fetch,
    }),
  });

  test("one row", async function () {
    const q = await db
      .selectFrom([
        db.selectNoFrom((eb) => eb.val(0).as("num_column")).as("foo"),
      ])
      .selectAll()
      .execute();

    assert.deepStrictEqual(q, [{ num_column: 0 }]); // ✅
  });

  test("two rows", async function () {
    const q = await db
      .selectFrom([
        db
          .selectNoFrom((eb) => eb.val(0).as("num_column"))
          .unionAll(db.selectNoFrom((eb) => eb.val(0).as("num_column")))
          .as("foo"),
      ])
      .selectAll()
      .execute();

    assert.deepStrictEqual(q, [{ num_column: 0 }, { num_column: 0 }]); // ❌ actual: [ { num_column: '0' }, { num_column: '0' } ]
  });
});

Gist for reproducing: https://gist.github.com/twiddler/8f98b48ec61c75eaa5422249c1edebc9. Please set PLANETSCALE_URL in .env and use Node 21 or later.

jacobwgillespie commented 6 months ago

Hey! So this adapter doesn't change the types returned from the database driver (https://github.com/planetscale/database-js) - what you're seeing here is the DB driver trying to determine if the database number is representable as a JavaScript number or not. Specifically, 64-bit numbers are not representable as JS numbers so they get returned as strings. Depending on the query, the PlanetScale driver may not be able to determine if the number is safe, so it gets returned as a string.

If you have any questions about how that's working, you may want to open an issue at https://github.com/planetscale/database-js. You can also intentionally modify the types from the DB driver with a custom cast function if you'd like: https://github.com/depot/kysely-planetscale#custom-cast-function.

twiddler commented 6 months ago

Thanks for the quick answer. I'll take it there.