From b6e589d3bad3d443541ec02f39d50cd1eae00d65 Mon Sep 17 00:00:00 2001 From: Oleksii Sholik Date: Wed, 29 Nov 2023 13:34:28 +0200 Subject: [PATCH] feat(electric): Add support for the float4 column type (#657) Co-authored-by: Kevin --- .changeset/young-steaks-return.md | 5 + .../typescript/src/cli/migrations/migrate.ts | 11 +- .../src/client/conversions/sqlite.ts | 15 ++- .../test/client/conversions/input.test.ts | 3 +- .../test/client/conversions/sqlite.test.ts | 23 +++- .../test/client/generated/client/index.d.ts | 42 ++++++- .../typescript/test/client/generated/index.ts | 31 ++++- .../test/client/model/datatype.test.ts | 115 +++++++++++++++++- .../test/client/prisma/schema.prisma | 1 + components/electric/lib/electric/postgres.ex | 2 +- .../lib/electric/satellite/serialization.ex | 44 +++++-- .../test/electric/postgres/extension_test.exs | 6 +- .../electric/satellite/serialization_test.exs | 7 +- .../satellite/ws_validations_test.exs | 51 +++++--- e2e/satellite_client/src/client.ts | 3 +- .../src/generated/client/index.ts | 26 +++- .../src/generated/client/prismaClient.d.ts | 35 +++++- e2e/satellite_client/src/prisma/schema.prisma | 1 + .../03.18_node_satellite_can_sync_float.lux | 95 +++++++++++++++ .../03.18_node_satellite_can_sync_float8.lux | 83 ------------- e2e/tests/_satellite_macros.luxinc | 10 +- .../writeTableSchemas.ts | 28 +++-- 22 files changed, 489 insertions(+), 148 deletions(-) create mode 100644 .changeset/young-steaks-return.md create mode 100644 e2e/tests/03.18_node_satellite_can_sync_float.lux delete mode 100644 e2e/tests/03.18_node_satellite_can_sync_float8.lux diff --git a/.changeset/young-steaks-return.md b/.changeset/young-steaks-return.md new file mode 100644 index 0000000000..e54c8ffcdf --- /dev/null +++ b/.changeset/young-steaks-return.md @@ -0,0 +1,5 @@ +--- +"@core/electric": patch +--- + +[VAX-846, VAX-849] Add support for the REAL / FLOAT4 column type in electrified tables. diff --git a/clients/typescript/src/cli/migrations/migrate.ts b/clients/typescript/src/cli/migrations/migrate.ts index a0c75983ab..ada54edc23 100644 --- a/clients/typescript/src/cli/migrations/migrate.ts +++ b/clients/typescript/src/cli/migrations/migrate.ts @@ -372,14 +372,15 @@ function addValidator(ln: string): string { if (field) { const intValidator = '@zod.number.int().gte(-2147483648).lte(2147483647)' - const float8Validator = '@zod.custom.use(z.number().or(z.nan()))' + const floatValidator = '@zod.custom.use(z.number().or(z.nan()))' // Map attributes to validators const attributeValidatorMapping = new Map([ ['@db.Uuid', '@zod.string.uuid()'], ['@db.SmallInt', '@zod.number.int().gte(-32768).lte(32767)'], ['@db.Int', intValidator], - ['@db.DoublePrecision', float8Validator], + ['@db.DoublePrecision', floatValidator], + ['@db.Real', floatValidator], ]) const attribute = field.attributes .map((a) => a.type) @@ -394,9 +395,9 @@ function addValidator(ln: string): string { ['Int', intValidator], ['Int?', intValidator], ['Int[]', intValidator], - ['Float', float8Validator], - ['Float?', float8Validator], - ['Float[]', float8Validator], + ['Float', floatValidator], + ['Float?', floatValidator], + ['Float[]', floatValidator], ]) const typeValidator = typeValidatorMapping.get(field.type) diff --git a/clients/typescript/src/client/conversions/sqlite.ts b/clients/typescript/src/client/conversions/sqlite.ts index 5a985863a4..b5a7e7339c 100644 --- a/clients/typescript/src/client/conversions/sqlite.ts +++ b/clients/typescript/src/client/conversions/sqlite.ts @@ -29,6 +29,11 @@ export function toSqlite(v: any, pgType: PgType): any { // and deserialise it back to `NaN` when reading from the DB. // cf. https://github.com/WiseLibs/better-sqlite3/issues/1088 return 'NaN' + } else if ( + pgType === PgBasicType.PG_FLOAT4 || + pgType === PgBasicType.PG_REAL + ) { + return Math.fround(v) } else { return v } @@ -46,10 +51,18 @@ export function fromSqlite(v: any, pgType: PgType): any { return deserialiseBoolean(v) } else if ( v === 'NaN' && - (pgType === PgBasicType.PG_FLOAT8 || pgType === PgBasicType.PG_FLOAT4) + (pgType === PgBasicType.PG_FLOAT8 || + pgType === PgBasicType.PG_FLOAT4 || + pgType === PgBasicType.PG_REAL) ) { // it's a serialised NaN return NaN + } else if ( + pgType === PgBasicType.PG_FLOAT4 || + pgType === PgBasicType.PG_REAL + ) { + // convert to float4 in case someone would have written a bigger value to SQLite directly + return Math.fround(v) } else if (pgType === PgBasicType.PG_INT8) { // always return BigInts for PG_INT8 values // because some drivers (e.g. wa-sqlite) return a regular JS number if the value fits into a JS number diff --git a/clients/typescript/test/client/conversions/input.test.ts b/clients/typescript/test/client/conversions/input.test.ts index 504533c275..319103e9eb 100644 --- a/clients/typescript/test/client/conversions/input.test.ts +++ b/clients/typescript/test/client/conversions/input.test.ts @@ -31,7 +31,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" ) db.exec('DROP TABLE IF EXISTS Dummy') @@ -233,6 +233,7 @@ const dateNulls = { int2: null, int4: null, int8: null, + float4: null, float8: null, uuid: null, } diff --git a/clients/typescript/test/client/conversions/sqlite.test.ts b/clients/typescript/test/client/conversions/sqlite.test.ts index e949766c0c..b2d422bd7d 100644 --- a/clients/typescript/test/client/conversions/sqlite.test.ts +++ b/clients/typescript/test/client/conversions/sqlite.test.ts @@ -30,7 +30,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" ) } @@ -183,32 +183,43 @@ test.serial('floats are converted correctly to SQLite', async (t) => { data: [ { id: 1, + float4: 1.234, float8: 1.234, }, { id: 2, + float4: NaN, float8: NaN, }, { id: 3, + float4: Infinity, float8: +Infinity, }, { id: 4, + float4: -Infinity, float8: -Infinity, }, ], }) const rawRes = await electric.db.raw({ - sql: 'SELECT id, float8 FROM DataTypes ORDER BY id ASC', + sql: 'SELECT id, float4, float8 FROM DataTypes ORDER BY id ASC', args: [], }) t.deepEqual(rawRes, [ - { id: 1, float8: 1.234 }, - { id: 2, float8: 'NaN' }, - { id: 3, float8: Infinity }, - { id: 4, float8: -Infinity }, + // 1.234 cannot be stored exactly in a float4 + // hence, there is a rounding error, which is observed when we + // read the float4 value back into a 64-bit JS number + // The value 1.2339999675750732 that we read back + // is also what Math.fround(1.234) returns + // as being the nearest 32-bit single precision + // floating point representation of 1.234 + { id: 1, float4: 1.2339999675750732, float8: 1.234 }, + { id: 2, float4: 'NaN', float8: 'NaN' }, + { id: 3, float4: Infinity, float8: Infinity }, + { id: 4, float4: -Infinity, float8: -Infinity }, ]) }) diff --git a/clients/typescript/test/client/generated/client/index.d.ts b/clients/typescript/test/client/generated/client/index.d.ts index 79a7b6ade5..c8dde2627a 100644 --- a/clients/typescript/test/client/generated/client/index.d.ts +++ b/clients/typescript/test/client/generated/client/index.d.ts @@ -77,6 +77,10 @@ export type DataTypes = { */ int4: number | null int8: bigint | null + /** + * @zod.custom.use(z.number().or(z.nan())) + */ + float4: number | null /** * @zod.custom.use(z.number().or(z.nan())) */ @@ -4803,6 +4807,7 @@ export namespace Prisma { int2: number | null int4: number | null int8: number | null + float4: number | null float8: number | null relatedId: number | null } @@ -4812,6 +4817,7 @@ export namespace Prisma { int2: number | null int4: number | null int8: bigint | null + float4: number | null float8: number | null relatedId: number | null } @@ -4828,6 +4834,7 @@ export namespace Prisma { int2: number | null int4: number | null int8: bigint | null + float4: number | null float8: number | null relatedId: number | null } @@ -4844,6 +4851,7 @@ export namespace Prisma { int2: number | null int4: number | null int8: bigint | null + float4: number | null float8: number | null relatedId: number | null } @@ -4860,6 +4868,7 @@ export namespace Prisma { int2: number int4: number int8: number + float4: number float8: number relatedId: number _all: number @@ -4871,6 +4880,7 @@ export namespace Prisma { int2?: true int4?: true int8?: true + float4?: true float8?: true relatedId?: true } @@ -4880,6 +4890,7 @@ export namespace Prisma { int2?: true int4?: true int8?: true + float4?: true float8?: true relatedId?: true } @@ -4896,6 +4907,7 @@ export namespace Prisma { int2?: true int4?: true int8?: true + float4?: true float8?: true relatedId?: true } @@ -4912,6 +4924,7 @@ export namespace Prisma { int2?: true int4?: true int8?: true + float4?: true float8?: true relatedId?: true } @@ -4928,6 +4941,7 @@ export namespace Prisma { int2?: true int4?: true int8?: true + float4?: true float8?: true relatedId?: true _all?: true @@ -5032,6 +5046,7 @@ export namespace Prisma { int2: number | null int4: number | null int8: bigint | null + float4: number | null float8: number | null relatedId: number | null _count: DataTypesCountAggregateOutputType | null @@ -5067,6 +5082,7 @@ export namespace Prisma { int2?: boolean int4?: boolean int8?: boolean + float4?: boolean float8?: boolean relatedId?: boolean related?: boolean | DummyArgs @@ -6816,6 +6832,7 @@ export namespace Prisma { int2: 'int2', int4: 'int4', int8: 'int8', + float4: 'float4', float8: 'float8', relatedId: 'relatedId' }; @@ -7079,6 +7096,7 @@ export namespace Prisma { int2?: IntNullableFilter | number | null int4?: IntNullableFilter | number | null int8?: BigIntNullableFilter | bigint | number | null + float4?: FloatNullableFilter | number | null float8?: FloatNullableFilter | number | null relatedId?: IntNullableFilter | number | null related?: XOR | null @@ -7096,6 +7114,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder related?: DummyOrderByWithRelationInput @@ -7118,6 +7137,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder _count?: DataTypesCountOrderByAggregateInput @@ -7142,6 +7162,7 @@ export namespace Prisma { int2?: IntNullableWithAggregatesFilter | number | null int4?: IntNullableWithAggregatesFilter | number | null int8?: BigIntNullableWithAggregatesFilter | bigint | number | null + float4?: FloatNullableWithAggregatesFilter | number | null float8?: FloatNullableWithAggregatesFilter | number | null relatedId?: IntNullableWithAggregatesFilter | number | null } @@ -7369,6 +7390,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null related?: DummyCreateNestedOneWithoutDatatypeInput } @@ -7385,6 +7407,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null relatedId?: number | null } @@ -7401,6 +7424,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null related?: DummyUpdateOneWithoutDatatypeNestedInput } @@ -7417,6 +7441,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null relatedId?: NullableIntFieldUpdateOperationsInput | number | null } @@ -7433,6 +7458,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null relatedId?: number | null } @@ -7449,6 +7475,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null } @@ -7464,6 +7491,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null relatedId?: NullableIntFieldUpdateOperationsInput | number | null } @@ -7824,6 +7852,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder } @@ -7833,6 +7862,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder } @@ -7849,6 +7879,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder } @@ -7865,6 +7896,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder } @@ -7874,6 +7906,7 @@ export namespace Prisma { int2?: SortOrder int4?: SortOrder int8?: SortOrder + float4?: SortOrder float8?: SortOrder relatedId?: SortOrder } @@ -8627,6 +8660,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null } @@ -8642,6 +8676,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null } @@ -8686,6 +8721,7 @@ export namespace Prisma { int2?: IntNullableFilter | number | null int4?: IntNullableFilter | number | null int8?: BigIntNullableFilter | bigint | number | null + float4?: FloatNullableFilter | number | null float8?: FloatNullableFilter | number | null relatedId?: IntNullableFilter | number | null } @@ -8730,6 +8766,7 @@ export namespace Prisma { int2?: number | null int4?: number | null int8?: bigint | number | null + float4?: number | null float8?: number | null } @@ -8745,6 +8782,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null } @@ -8760,6 +8798,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null } @@ -8775,6 +8814,7 @@ export namespace Prisma { int2?: NullableIntFieldUpdateOperationsInput | number | null int4?: NullableIntFieldUpdateOperationsInput | number | null int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null + float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null } @@ -8792,4 +8832,4 @@ export namespace Prisma { * DMMF */ export const dmmf: runtime.BaseDMMF -} \ No newline at end of file +} diff --git a/clients/typescript/test/client/generated/index.ts b/clients/typescript/test/client/generated/index.ts index 78763130bd..c6b0dfb848 100644 --- a/clients/typescript/test/client/generated/index.ts +++ b/clients/typescript/test/client/generated/index.ts @@ -17,7 +17,7 @@ import { // ENUMS ///////////////////////////////////////// -export const DataTypesScalarFieldEnumSchema = z.enum(['id','date','time','timetz','timestamp','timestamptz','bool','uuid','int2','int4','int8','float8','relatedId']); +export const DataTypesScalarFieldEnumSchema = z.enum(['id','date','time','timetz','timestamp','timestamptz','bool','uuid','int2','int4','int8','float4','float8','relatedId']); export const DummyScalarFieldEnumSchema = z.enum(['id','timestamp']); @@ -102,6 +102,7 @@ export const DataTypesSchema = z.object({ int2: z.number().int().gte(-32768).lte(32767).nullish(), int4: z.number().int().gte(-2147483648).lte(2147483647).nullish(), int8: z.bigint().nullish(), + float4: z.number().or(z.nan()).nullish(), float8: z.number().or(z.nan()).nullish(), relatedId: z.number().int().nullish(), }) @@ -225,6 +226,7 @@ export const DataTypesSelectSchema: z.ZodType = z.object int2: z.boolean().optional(), int4: z.boolean().optional(), int8: z.boolean().optional(), + float4: z.boolean().optional(), float8: z.boolean().optional(), relatedId: z.boolean().optional(), related: z.union([z.boolean(),z.lazy(() => DummyArgsSchema)]).optional(), @@ -444,6 +446,7 @@ export const DataTypesWhereInputSchema: z.ZodType = int2: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), int4: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), int8: z.union([ z.lazy(() => BigIntNullableFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), + float4: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), relatedId: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), related: z.union([ z.lazy(() => DummyRelationFilterSchema),z.lazy(() => DummyWhereInputSchema) ]).optional().nullable(), @@ -461,6 +464,7 @@ export const DataTypesOrderByWithRelationInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional(), related: z.lazy(() => DummyOrderByWithRelationInputSchema).optional() @@ -483,6 +487,7 @@ export const DataTypesOrderByWithAggregationInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional(), _count: z.lazy(() => DataTypesCountOrderByAggregateInputSchema).optional(), @@ -507,6 +512,7 @@ export const DataTypesScalarWhereWithAggregatesInputSchema: z.ZodType IntNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), int4: z.union([ z.lazy(() => IntNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), int8: z.union([ z.lazy(() => BigIntNullableWithAggregatesFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), + float4: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), relatedId: z.union([ z.lazy(() => IntNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), }).strict(); @@ -734,6 +740,7 @@ export const DataTypesCreateInputSchema: z.ZodType int2: z.number().int().gte(-32768).lte(32767).optional().nullable(), int4: z.number().int().gte(-2147483648).lte(2147483647).optional().nullable(), int8: z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]).optional().nullable(), + float4: z.number().or(z.nan()).optional().nullable(), float8: z.number().or(z.nan()).optional().nullable(), related: z.lazy(() => DummyCreateNestedOneWithoutDatatypeInputSchema).optional() }).strict(); @@ -750,6 +757,7 @@ export const DataTypesUncheckedCreateInputSchema: z.ZodType int2: z.union([ z.number().int().gte(-32768).lte(32767),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number().int().gte(-2147483648).lte(2147483647),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), related: z.lazy(() => DummyUpdateOneWithoutDatatypeNestedInputSchema).optional() }).strict(); @@ -782,6 +791,7 @@ export const DataTypesUncheckedUpdateInputSchema: z.ZodType NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number().int().gte(-2147483648).lte(2147483647),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), relatedId: z.union([ z.number().int(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -798,6 +808,7 @@ export const DataTypesCreateManyInputSchema: z.ZodType NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number().int().gte(-2147483648).lte(2147483647),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -829,6 +841,7 @@ export const DataTypesUncheckedUpdateManyInputSchema: z.ZodType NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number().int().gte(-2147483648).lte(2147483647),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), relatedId: z.union([ z.number().int(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -1189,6 +1202,7 @@ export const DataTypesCountOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1198,6 +1212,7 @@ export const DataTypesAvgOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1214,6 +1229,7 @@ export const DataTypesMaxOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1230,6 +1246,7 @@ export const DataTypesMinOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1239,6 +1256,7 @@ export const DataTypesSumOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), int4: z.lazy(() => SortOrderSchema).optional(), int8: z.lazy(() => SortOrderSchema).optional(), + float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1992,6 +2010,7 @@ export const DataTypesCreateWithoutRelatedInputSchema: z.ZodType IntNullableFilterSchema),z.number() ]).optional().nullable(), int4: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), int8: z.union([ z.lazy(() => BigIntNullableFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), + float4: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), relatedId: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), }).strict(); @@ -2095,6 +2116,7 @@ export const DataTypesCreateManyRelatedInputSchema: z.ZodType NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -2125,6 +2148,7 @@ export const DataTypesUncheckedUpdateWithoutRelatedInputSchema: z.ZodType NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -2140,6 +2164,7 @@ export const DataTypesUncheckedUpdateManyWithoutDatatypeInputSchema: z.ZodType

NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int4: z.union([ z.number().int().gte(-2147483648).lte(2147483647),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), + float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -3019,6 +3044,10 @@ export const tableSchemas = { "int8", "INT8" ], + [ + "float4", + "FLOAT4" + ], [ "float8", "FLOAT8" diff --git a/clients/typescript/test/client/model/datatype.test.ts b/clients/typescript/test/client/model/datatype.test.ts index a8154ba43d..af72f0006a 100644 --- a/clients/typescript/test/client/model/datatype.test.ts +++ b/clients/typescript/test/client/model/datatype.test.ts @@ -31,7 +31,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" ) } @@ -565,6 +565,119 @@ test.serial('support null values for int4 type', async (t) => { t.deepEqual(fetchRes, expectedRes) }) +test.serial('support float4 type', async (t) => { + const validFloat1 = 1.402823e36 + const validFloat2 = -1.402823e36 + const floats = [ + { + id: 1, + float4: validFloat1, + }, + { + id: 2, + float4: validFloat2, + }, + { + id: 3, + float4: +Infinity, + }, + { + id: 4, + float4: -Infinity, + }, + { + id: 5, + float4: NaN, + }, + ] + + const res = await tbl.createMany({ + data: floats, + }) + + t.deepEqual(res, { + count: 5, + }) + + // Check that we can read the floats back + const fetchRes = await tbl.findMany({ + select: { + id: true, + float4: true, + }, + orderBy: { + id: 'asc', + }, + }) + + t.deepEqual( + fetchRes, + floats.map((o) => ({ ...o, float4: Math.fround(o.float4) })) + ) +}) + +test.serial('converts numbers outside float4 range', async (t) => { + const tooPositive = 2 ** 150 + const tooNegative = -(2 ** 150) + const tooSmallPositive = 2 ** -150 + const tooSmallNegative = -(2 ** -150) + const floats = [ + { + id: 1, + float4: tooPositive, + }, + { + id: 2, + float4: tooNegative, + }, + { + id: 3, + float4: tooSmallPositive, + }, + { + id: 4, + float4: tooSmallNegative, + }, + ] + + const res = await tbl.createMany({ + data: floats, + }) + + t.deepEqual(res, { + count: 4, + }) + + // Check that we can read the floats back + const fetchRes = await tbl.findMany({ + select: { + id: true, + float4: true, + }, + orderBy: { + id: 'asc', + }, + }) + + t.deepEqual(fetchRes, [ + { + id: 1, + float4: Infinity, + }, + { + id: 2, + float4: -Infinity, + }, + { + id: 3, + float4: 0, + }, + { + id: 4, + float4: 0, + }, + ]) +}) test.serial('support float8 type', async (t) => { const validFloat1 = 1.7976931348623157e308 const validFloat2 = -1.7976931348623157e308 diff --git a/clients/typescript/test/client/prisma/schema.prisma b/clients/typescript/test/client/prisma/schema.prisma index 4cd4f05e80..39f04374a9 100644 --- a/clients/typescript/test/client/prisma/schema.prisma +++ b/clients/typescript/test/client/prisma/schema.prisma @@ -55,6 +55,7 @@ model DataTypes { int2 Int? @db.SmallInt /// @zod.number.int().gte(-32768).lte(32767) int4 Int? /// @zod.number.int().gte(-2147483648).lte(2147483647) int8 BigInt? + float4 Float? @db.Real /// @zod.custom.use(z.number().or(z.nan())) float8 Float? @db.DoublePrecision /// @zod.custom.use(z.number().or(z.nan())) relatedId Int? related Dummy? @relation(fields: [relatedId], references: [id]) diff --git a/components/electric/lib/electric/postgres.ex b/components/electric/lib/electric/postgres.ex index 854fd889fd..ddeb7083e0 100644 --- a/components/electric/lib/electric/postgres.ex +++ b/components/electric/lib/electric/postgres.ex @@ -84,7 +84,7 @@ defmodule Electric.Postgres do ~w[ bool date - float8 + float4 float8 int2 int4 int8 text time diff --git a/components/electric/lib/electric/satellite/serialization.ex b/components/electric/lib/electric/satellite/serialization.ex index 2cff59d607..9757960019 100644 --- a/components/electric/lib/electric/satellite/serialization.ex +++ b/components/electric/lib/electric/satellite/serialization.ex @@ -500,15 +500,11 @@ defmodule Electric.Satellite.Serialization do val end - def decode_column_value!(val, :float8) when val in ["Infinity", "-Infinity", "NaN"], do: val - - def decode_column_value!(val, :float8) do - case Float.parse(val) do - {_, ""} -> :ok - _ -> raise "Unexpected value for float8 colum: #{inspect(val)}" + def decode_column_value!(val, type) when type in [:float4, :float8] do + case String.downcase(val) do + inf_or_nan when inf_or_nan in ~w[inf infinity -inf -infinity nan] -> val + _ -> decode_float_value!(val, type) end - - val end def decode_column_value!(val, type) when type in [:int2, :int4, :int8] do @@ -565,6 +561,15 @@ defmodule Electric.Satellite.Serialization do uuid end + defp decode_float_value!(val, type) do + with {num, ""} <- Float.parse(val), + :ok = assert_float_in_range(num, type) do + val + else + _ -> raise "Unexpected value for #{type} colum: #{inspect(val)}" + end + end + @int2_range -32768..32767 @int4_range -2_147_483_648..2_147_483_647 @int8_range -9_223_372_036_854_775_808..9_223_372_036_854_775_807 @@ -595,4 +600,27 @@ defmodule Electric.Satellite.Serialization do _ = String.to_integer(fs_str) :ok end + + defp assert_float_in_range(_num, :float8), do: :ok + + defp assert_float_in_range(num, :float4) do + conversion_result = + case <> do + <<_sign::1, 0xFF, 0::23>> -> + # The input is rounded up to Infinity when converted to a 32-bit floating point number. + # It should have been encoded as literal "Infinity" by the client. + :error + + <<_sign::1, 0, 0::23>> when num != 0 -> + # The input is rounded down to zero. It should have been encoded as literal "0" by the client. + :error + + _ -> + :ok + end + + with :error <- conversion_result do + raise "Value for float4 column out of range: #{inspect(num)}" + end + end end diff --git a/components/electric/test/electric/postgres/extension_test.exs b/components/electric/test/electric/postgres/extension_test.exs index 872e540e3a..9acbf02607 100644 --- a/components/electric/test/electric/postgres/extension_test.exs +++ b/components/electric/test/electric/postgres/extension_test.exs @@ -436,6 +436,8 @@ defmodule Electric.Postgres.ExtensionTest do num4c INTEGER, num8a INT8, num8b BIGINT, + real4a FLOAT4, + "Real4b" REAL, real8a FLOAT8, real8b DOUBLE PRECISION, ts TIMESTAMP, @@ -459,8 +461,6 @@ defmodule Electric.Postgres.ExtensionTest do c1 CHARACTER, c2 CHARACTER(11), "C3" VARCHAR(11), - real4a FLOAT4, - "Real4b" REAL, created_at TIMETZ ); CALL electric.electrify('public.t1'); @@ -472,8 +472,6 @@ defmodule Electric.Postgres.ExtensionTest do c1 character(1) c2 character(11) "C3" character varying(11) - real4a real - "Real4b" real created_at time with time zone """ |> String.trim() diff --git a/components/electric/test/electric/satellite/serialization_test.exs b/components/electric/test/electric/satellite/serialization_test.exs index c663f8b1af..a66b0cb4b3 100644 --- a/components/electric/test/electric/satellite/serialization_test.exs +++ b/components/electric/test/electric/satellite/serialization_test.exs @@ -145,11 +145,11 @@ defmodule Electric.Satellite.SerializationTest do columns = [ %{name: "f1", type: :float8}, - %{name: "f2", type: :float8}, + %{name: "f2", type: :float4}, %{name: "f3", type: :float8}, - %{name: "f4", type: :float8}, + %{name: "f4", type: :float4}, %{name: "f5", type: :float8}, - %{name: "f6", type: :float8}, + %{name: "f6", type: :float4}, %{name: "f7", type: :float8} ] @@ -167,6 +167,7 @@ defmodule Electric.Satellite.SerializationTest do test "raises when the row contains an invalid value for its type" do test_data = [ {"1.0", :int4}, + {"-.1", :float4}, {"33.", :float8}, {"1000000", :int2}, {"-1000000000000000", :int4}, diff --git a/components/electric/test/electric/satellite/ws_validations_test.exs b/components/electric/test/electric/satellite/ws_validations_test.exs index d4a02e7a88..58b2333a97 100644 --- a/components/electric/test/electric/satellite/ws_validations_test.exs +++ b/components/electric/test/electric/satellite/ws_validations_test.exs @@ -195,23 +195,27 @@ defmodule Electric.Satellite.WsValidationsTest do migrate( ctx.db, vsn, - "CREATE TABLE public.foo (id TEXT PRIMARY KEY, f8 DOUBLE PRECISION)", + "CREATE TABLE public.foo (id TEXT PRIMARY KEY, f4 REAL, f8 DOUBLE PRECISION)", electrify: "public.foo" ) valid_records = [ - %{"id" => "1", "f8" => "+0.0"}, - %{"id" => "2", "f8" => "+0.1"}, - %{"id" => "3", "f8" => "1"}, - %{"id" => "4", "f8" => "-1"}, - %{"id" => "5", "f8" => "7.3e-4"}, - %{"id" => "6", "f8" => "1.23456789E+248"}, - %{"id" => "7", "f8" => "-0.0"}, - %{"id" => "8", "f8" => "-1.0"}, - %{"id" => "9", "f8" => "1e-10"}, - %{"id" => "10", "f8" => "+0"}, - %{"id" => "11", "f8" => "-0"}, - %{"id" => "12", "f8" => "0"} + %{"id" => "1", "f4" => "+0.0", "f8" => "+0.0"}, + %{"id" => "2", "f4" => "+0.1", "f8" => "+0.1"}, + %{"id" => "3", "f4" => "1", "f8" => "1"}, + %{"id" => "4", "f4" => "-1", "f8" => "-1"}, + %{"id" => "5", "f4" => "7.3e-4", "f8" => "7.3e-4"}, + %{"id" => "6", "f4" => "3.4028234663852886e38", "f8" => "1.23456789E+248"}, + %{"id" => "7", "f4" => "-0.0", "f8" => "-0.0"}, + %{"id" => "8", "f4" => "-1.0", "f8" => "-1.0"}, + %{"id" => "9", "f4" => "-1e-10", "f8" => "1e-10"}, + %{"id" => "10", "f4" => "+0", "f8" => "+0"}, + %{"id" => "11", "f4" => "-0", "f8" => "-0"}, + %{"id" => "12", "f4" => "0", "f8" => "0"}, + %{"id" => "13", "f4" => "-3.4028234663852886e38", "f8" => "-2.387561194739013e307"}, + %{"id" => "14", "f4" => "inf", "f8" => "Infinity"}, + %{"id" => "15", "f4" => "-INF", "f8" => "-iNfInItY"}, + %{"id" => "16", "f4" => "nan", "f8" => "nAn"} ] within_replication_context(ctx, vsn, fn conn -> @@ -234,7 +238,26 @@ defmodule Electric.Satellite.WsValidationsTest do %{"id" => "27", "f8" => "20_30"}, %{"id" => "28", "f8" => "0x33"}, %{"id" => "29", "f8" => "0b101011"}, - %{"id" => "30", "f8" => "0o373"} + %{"id" => "30", "f8" => "0o373"}, + %{"id" => "31", "f4" => "five"}, + %{"id" => "32", "f4" => "."}, + %{"id" => "33", "f4" => "-"}, + %{"id" => "34", "f4" => "+"}, + %{"id" => "35", "f4" => "0."}, + %{"id" => "36", "f4" => " 1"}, + %{"id" => "37", "f4" => "20_30"}, + %{"id" => "38", "f4" => "0x33"}, + %{"id" => "39", "f4" => "0b101011"}, + %{"id" => "40", "f4" => "0o373"}, + %{"id" => "41", "f4" => ""}, + %{"id" => "42", "f4" => "1.23456789E+248"}, + %{"id" => "43", "f4" => "-1.23456789e40"}, + %{"id" => "44", "f4" => "0.6e-45"}, + %{"id" => "45", "f8" => "1.8e+308"} + # The following number does not fit into a 64-bit float but there's no way to detect that in Elixir, short of + # writing our own custom parsing for this one edge case. + # Using the built-in string-to-float conversion, the number is parsed as `-0.0`. + # %{"id" => "46", "f8" => "-2.4e-324"} ] Enum.each(invalid_records, fn record -> diff --git a/e2e/satellite_client/src/client.ts b/e2e/satellite_client/src/client.ts index 4187e16d6b..26ff1f1709 100644 --- a/e2e/satellite_client/src/client.ts +++ b/e2e/satellite_client/src/client.ts @@ -212,10 +212,11 @@ export const get_float = (electric: Electric, id: string) => { }) } -export const write_float = (electric: Electric, id: string, f8: number) => { +export const write_float = (electric: Electric, id: string, f4: number, f8: number) => { return electric.db.floats.create({ data: { id, + f4, f8, } }) diff --git a/e2e/satellite_client/src/generated/client/index.ts b/e2e/satellite_client/src/generated/client/index.ts index a66b6e63c9..83d027ab1a 100644 --- a/e2e/satellite_client/src/generated/client/index.ts +++ b/e2e/satellite_client/src/generated/client/index.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import type { Prisma } from '@prisma/client'; +import type { Prisma } from './prismaClient'; import { TableSchema, DbSchema, Relation, ElectricClient, HKT } from 'electric-sql/client/model'; import migrations from './migrations'; @@ -16,7 +16,7 @@ export const BoolsScalarFieldEnumSchema = z.enum(['id','b']); export const DatetimesScalarFieldEnumSchema = z.enum(['id','d','t']); -export const FloatsScalarFieldEnumSchema = z.enum(['id','f8']); +export const FloatsScalarFieldEnumSchema = z.enum(['id','f4','f8']); export const IntsScalarFieldEnumSchema = z.enum(['id','i2','i4','i8']); @@ -128,6 +128,7 @@ export type Ints = z.infer export const FloatsSchema = z.object({ id: z.string(), + f4: z.number().or(z.nan()).nullable(), f8: z.number().or(z.nan()).nullable(), }) @@ -226,6 +227,7 @@ export const IntsSelectSchema: z.ZodType = z.object({ export const FloatsSelectSchema: z.ZodType = z.object({ id: z.boolean().optional(), + f4: z.boolean().optional(), f8: z.boolean().optional(), }).strict() @@ -511,11 +513,13 @@ export const FloatsWhereInputSchema: z.ZodType = z.obje OR: z.lazy(() => FloatsWhereInputSchema).array().optional(), NOT: z.union([ z.lazy(() => FloatsWhereInputSchema),z.lazy(() => FloatsWhereInputSchema).array() ]).optional(), id: z.union([ z.lazy(() => StringFilterSchema),z.string() ]).optional(), + f4: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), f8: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), }).strict(); export const FloatsOrderByWithRelationInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -525,6 +529,7 @@ export const FloatsWhereUniqueInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional(), _count: z.lazy(() => FloatsCountOrderByAggregateInputSchema).optional(), _avg: z.lazy(() => FloatsAvgOrderByAggregateInputSchema).optional(), @@ -538,6 +543,7 @@ export const FloatsScalarWhereWithAggregatesInputSchema: z.ZodType FloatsScalarWhereWithAggregatesInputSchema).array().optional(), NOT: z.union([ z.lazy(() => FloatsScalarWhereWithAggregatesInputSchema),z.lazy(() => FloatsScalarWhereWithAggregatesInputSchema).array() ]).optional(), id: z.union([ z.lazy(() => StringWithAggregatesFilterSchema),z.string() ]).optional(), + f4: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), f8: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), }).strict(); @@ -847,36 +853,43 @@ export const IntsUncheckedUpdateManyInputSchema: z.ZodType = z.object({ id: z.string(), + f4: z.number().or(z.nan()).optional().nullable(), f8: z.number().or(z.nan()).optional().nullable() }).strict(); export const FloatsUncheckedCreateInputSchema: z.ZodType = z.object({ id: z.string(), + f4: z.number().or(z.nan()).optional().nullable(), f8: z.number().or(z.nan()).optional().nullable() }).strict(); export const FloatsUpdateInputSchema: z.ZodType = z.object({ id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + f4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), f8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const FloatsUncheckedUpdateInputSchema: z.ZodType = z.object({ id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + f4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), f8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const FloatsCreateManyInputSchema: z.ZodType = z.object({ id: z.string(), + f4: z.number().or(z.nan()).optional().nullable(), f8: z.number().or(z.nan()).optional().nullable() }).strict(); export const FloatsUpdateManyMutationInputSchema: z.ZodType = z.object({ id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + f4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), f8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); export const FloatsUncheckedUpdateManyInputSchema: z.ZodType = z.object({ id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + f4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), f8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -1239,24 +1252,29 @@ export const FloatNullableFilterSchema: z.ZodType = export const FloatsCountOrderByAggregateInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); export const FloatsAvgOrderByAggregateInputSchema: z.ZodType = z.object({ + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); export const FloatsMaxOrderByAggregateInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); export const FloatsMinOrderByAggregateInputSchema: z.ZodType = z.object({ id: z.lazy(() => SortOrderSchema).optional(), + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); export const FloatsSumOrderByAggregateInputSchema: z.ZodType = z.object({ + f4: z.lazy(() => SortOrderSchema).optional(), f8: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -2771,6 +2789,10 @@ export const tableSchemas = { "id", "TEXT" ], + [ + "f4", + "FLOAT4" + ], [ "f8", "FLOAT8" diff --git a/e2e/satellite_client/src/generated/client/prismaClient.d.ts b/e2e/satellite_client/src/generated/client/prismaClient.d.ts index 7a851f9335..469396e5d1 100644 --- a/e2e/satellite_client/src/generated/client/prismaClient.d.ts +++ b/e2e/satellite_client/src/generated/client/prismaClient.d.ts @@ -98,6 +98,10 @@ export type Ints = { */ export type Floats = { id: string + /** + * @zod.custom.use(z.number().or(z.nan())) + */ + f4: number | null /** * @zod.custom.use(z.number().or(z.nan())) */ @@ -7205,50 +7209,60 @@ export namespace Prisma { } export type FloatsAvgAggregateOutputType = { + f4: number | null f8: number | null } export type FloatsSumAggregateOutputType = { + f4: number | null f8: number | null } export type FloatsMinAggregateOutputType = { id: string | null + f4: number | null f8: number | null } export type FloatsMaxAggregateOutputType = { id: string | null + f4: number | null f8: number | null } export type FloatsCountAggregateOutputType = { id: number + f4: number f8: number _all: number } export type FloatsAvgAggregateInputType = { + f4?: true f8?: true } export type FloatsSumAggregateInputType = { + f4?: true f8?: true } export type FloatsMinAggregateInputType = { id?: true + f4?: true f8?: true } export type FloatsMaxAggregateInputType = { id?: true + f4?: true f8?: true } export type FloatsCountAggregateInputType = { id?: true + f4?: true f8?: true _all?: true } @@ -7342,6 +7356,7 @@ export namespace Prisma { export type FloatsGroupByOutputType = { id: string + f4: number | null f8: number | null _count: FloatsCountAggregateOutputType | null _avg: FloatsAvgAggregateOutputType | null @@ -7366,6 +7381,7 @@ export namespace Prisma { export type FloatsSelect = { id?: boolean + f4?: boolean f8?: boolean } @@ -8106,6 +8122,7 @@ export namespace Prisma { export const FloatsScalarFieldEnum: { id: 'id', + f4: 'f4', f8: 'f8' }; @@ -8467,11 +8484,13 @@ export namespace Prisma { OR?: Enumerable NOT?: Enumerable id?: StringFilter | string + f4?: FloatNullableFilter | number | null f8?: FloatNullableFilter | number | null } export type FloatsOrderByWithRelationInput = { id?: SortOrder + f4?: SortOrder f8?: SortOrder } @@ -8481,6 +8500,7 @@ export namespace Prisma { export type FloatsOrderByWithAggregationInput = { id?: SortOrder + f4?: SortOrder f8?: SortOrder _count?: FloatsCountOrderByAggregateInput _avg?: FloatsAvgOrderByAggregateInput @@ -8494,6 +8514,7 @@ export namespace Prisma { OR?: Enumerable NOT?: Enumerable id?: StringWithAggregatesFilter | string + f4?: FloatNullableWithAggregatesFilter | number | null f8?: FloatNullableWithAggregatesFilter | number | null } @@ -8803,36 +8824,43 @@ export namespace Prisma { export type FloatsCreateInput = { id: string + f4?: number | null f8?: number | null } export type FloatsUncheckedCreateInput = { id: string + f4?: number | null f8?: number | null } export type FloatsUpdateInput = { id?: StringFieldUpdateOperationsInput | string + f4?: NullableFloatFieldUpdateOperationsInput | number | null f8?: NullableFloatFieldUpdateOperationsInput | number | null } export type FloatsUncheckedUpdateInput = { id?: StringFieldUpdateOperationsInput | string + f4?: NullableFloatFieldUpdateOperationsInput | number | null f8?: NullableFloatFieldUpdateOperationsInput | number | null } export type FloatsCreateManyInput = { id: string + f4?: number | null f8?: number | null } export type FloatsUpdateManyMutationInput = { id?: StringFieldUpdateOperationsInput | string + f4?: NullableFloatFieldUpdateOperationsInput | number | null f8?: NullableFloatFieldUpdateOperationsInput | number | null } export type FloatsUncheckedUpdateManyInput = { id?: StringFieldUpdateOperationsInput | string + f4?: NullableFloatFieldUpdateOperationsInput | number | null f8?: NullableFloatFieldUpdateOperationsInput | number | null } @@ -9195,24 +9223,29 @@ export namespace Prisma { export type FloatsCountOrderByAggregateInput = { id?: SortOrder + f4?: SortOrder f8?: SortOrder } export type FloatsAvgOrderByAggregateInput = { + f4?: SortOrder f8?: SortOrder } export type FloatsMaxOrderByAggregateInput = { id?: SortOrder + f4?: SortOrder f8?: SortOrder } export type FloatsMinOrderByAggregateInput = { id?: SortOrder + f4?: SortOrder f8?: SortOrder } export type FloatsSumOrderByAggregateInput = { + f4?: SortOrder f8?: SortOrder } @@ -9627,4 +9660,4 @@ export namespace Prisma { * DMMF */ export const dmmf: runtime.BaseDMMF -} \ No newline at end of file +} diff --git a/e2e/satellite_client/src/prisma/schema.prisma b/e2e/satellite_client/src/prisma/schema.prisma index 357564c25e..e051113be1 100644 --- a/e2e/satellite_client/src/prisma/schema.prisma +++ b/e2e/satellite_client/src/prisma/schema.prisma @@ -68,6 +68,7 @@ model Ints { model Floats { id String @id + f4 Float? @db.Real /// @zod.custom.use(z.number().or(z.nan())) f8 Float? @db.DoublePrecision /// @zod.custom.use(z.number().or(z.nan())) @@map("floats") } diff --git a/e2e/tests/03.18_node_satellite_can_sync_float.lux b/e2e/tests/03.18_node_satellite_can_sync_float.lux new file mode 100644 index 0000000000..bfd18531d1 --- /dev/null +++ b/e2e/tests/03.18_node_satellite_can_sync_float.lux @@ -0,0 +1,95 @@ +[doc NodeJS Satellite correctly syncs float values from and to Electric] +[include _shared.luxinc] +[include _satellite_macros.luxinc] + +[invoke setup] + +[shell proxy_1] + [local sql= + """ + CREATE TABLE public.floats ( + id TEXT PRIMARY KEY, + f4 FLOAT4, + f8 FLOAT8 + ); + ALTER TABLE public.floats ENABLE ELECTRIC; + """] + [invoke migrate_pg 20230908 $sql] + +[invoke setup_client 1 electric_1 5133] + +[shell satellite_1] + [invoke node_await_table "floats"] + [invoke node_sync_table "floats"] + +[shell pg_1] + !INSERT INTO public.floats (id, f4, f8) VALUES ('row1', 1.402e36, 1.797e308); + ??INSERT 0 1 + +[shell satellite_1] + # Wait for the rows to arrive + [invoke node_await_get_float "row1"] + + # JS only has 64 bit floating point numbers. + # Hence, when reading we are storing a 32 bit float in JS 64 bit number + # which makes that we can see the rounding error that was introduced + # e.g. we store JS' 64 bit 1.402e36 number in a 32 bit floating point number + # but this cannot be stored exactly so it is rounded + # if we now read the rounded value in a 64 bit float + # we see the rounding error that was introduced: 1.4020000137922178e+36 + # That's exactly what Math.fround() does, it rounds a 64 bit JS number to a 32 bit float + # that's stored in JS' 64 bit number (as JS only has 64 bit numbers) + [invoke node_get_float "row1" 1.4020000137922178e+36 1.797e+308] + + [invoke node_write_float "row2" -1.402e+36 -1.797e+308] + [invoke node_get_float "row2" -1.4020000137922178e+36 -1.797e+308] + + [invoke node_write_float "row3" -1e10 5.006] + [invoke node_get_float "row3" -10000000000 5.006] + + [invoke node_write_float "row4" 3.402e+39 Infinity] + [invoke node_get_float "row4" Infinity Infinity] + + [invoke node_write_float "row5" -3.402e+39 -1.797e+309] + [invoke node_get_float "row5" -Infinity -Infinity] + + [invoke node_write_float "row6" "2 * {}" NaN] + [invoke node_get_float "row6" NaN NaN] + + [invoke node_write_float "row7" -0 -0] + [invoke node_get_float "row7" 0 0] + +[shell pg_1] + [invoke wait-for "SELECT * FROM public.floats;" "row7" 10 $psql] + + # Postgres stores the float4 numbers as 32-bit floats, + # so we are reading the same float4 numbers we wrote to it + !SELECT * FROM public.floats; + ??row1 | 1.402e+36 | 1.797e+308 + ??row2 | -1.402e+36 | -1.797e+308 + ??row3 | -1e+10 | 5.006 + ??row4 | Infinity | Infinity + ??row5 | -Infinity | -Infinity + ??row6 | NaN | NaN + ??row7 | 0 | 0 + +# Start a new Satellite client and verify that it receives all rows +[invoke setup_client 2 electric_1 5133] + +[shell satellite_2] + [invoke node_await_table "floats"] + [invoke node_sync_table "floats"] + + # Wait for the rows to arrive + [invoke node_await_get_float "row7"] + + [invoke node_get_float "row1" 1.4020000137922178e+36 1.797e+308] + [invoke node_get_float "row2" -1.4020000137922178e+36 -1.797e+308] + [invoke node_get_float "row3" -10000000000 5.006] + [invoke node_get_float "row4" Infinity Infinity] + [invoke node_get_float "row5" -Infinity -Infinity] + [invoke node_get_float "row6" NaN NaN] + [invoke node_get_float "row7" 0 0] + +[cleanup] + [invoke teardown] diff --git a/e2e/tests/03.18_node_satellite_can_sync_float8.lux b/e2e/tests/03.18_node_satellite_can_sync_float8.lux deleted file mode 100644 index 7c61dc0fd1..0000000000 --- a/e2e/tests/03.18_node_satellite_can_sync_float8.lux +++ /dev/null @@ -1,83 +0,0 @@ -[doc NodeJS Satellite correctly syncs float8 values from and to Electric] -[include _shared.luxinc] -[include _satellite_macros.luxinc] - -[invoke setup] - -[shell proxy_1] - [local sql= - """ - CREATE TABLE public.floats ( - id TEXT PRIMARY KEY, - f8 FLOAT8 - ); - ALTER TABLE public.floats ENABLE ELECTRIC; - """] - [invoke migrate_pg 20230908 $sql] - -[invoke setup_client 1 electric_1 5133] - -[shell satellite_1] - [invoke node_await_table "floats"] - [invoke node_sync_table "floats"] - -[shell pg_1] - !INSERT INTO public.floats (id, f8) VALUES ('row1', 1.79769313486231e308); - ??INSERT 0 1 - -[shell satellite_1] - # Wait for the rows to arrive - [invoke node_await_get_float "row1"] - - [invoke node_get_float "row1" 1.79769313486231e+308] - - [invoke node_write_float "row2" -1.79769313486231e308] - [invoke node_get_float "row2" -1.79769313486231e+308] - - [invoke node_write_float "row3" 5.006] - [invoke node_get_float "row3" 5.006] - - [invoke node_write_float "row4" Infinity] - [invoke node_get_float "row4" Infinity] - - [invoke node_write_float "row5" -Infinity] - [invoke node_get_float "row5" -Infinity] - - [invoke node_write_float "row6" NaN] - [invoke node_get_float "row6" NaN] - - [invoke node_write_float "row7" -0] - [invoke node_get_float "row7" 0] - -[shell pg_1] - [invoke wait-for "SELECT * FROM public.floats;" "row7" 10 $psql] - - !SELECT * FROM public.floats; - ??row1 | 1.79769313486231e+308 - ??row2 | -1.79769313486231e+308 - ??row3 | 5.006 - ??row4 | Infinity - ??row5 | -Infinity - ??row6 | NaN - ??row7 | 0 - -# Start a new Satellite client and verify that it receives all rows -[invoke setup_client 2 electric_1 5133] - -[shell satellite_2] - [invoke node_await_table "floats"] - [invoke node_sync_table "floats"] - - # Wait for the rows to arrive - [invoke node_await_get_float "row7"] - - [invoke node_get_float "row1" 1.79769313486231e+308] - [invoke node_get_float "row2" -1.79769313486231e+308] - [invoke node_get_float "row3" 5.006] - [invoke node_get_float "row4" Infinity] - [invoke node_get_float "row5" -Infinity] - [invoke node_get_float "row6" NaN] - [invoke node_get_float "row7" 0] - -[cleanup] - [invoke teardown] diff --git a/e2e/tests/_satellite_macros.luxinc b/e2e/tests/_satellite_macros.luxinc index b0166288a7..b124e479a9 100644 --- a/e2e/tests/_satellite_macros.luxinc +++ b/e2e/tests/_satellite_macros.luxinc @@ -55,10 +55,10 @@ [invoke wait-for "await client.get_float(db, '${id}')" "${id}" 10 $node] [endmacro] -[macro node_write_float id value] +[macro node_write_float id f4_value f8_value] # Can write valid floats to the DB - !await client.write_float(db, '${id}', ${value}) - ??{ id: '${id}', f8: + !await client.write_float(db, '${id}', ${f4_value}, ${f8_value}) + ??{ id: '${id}', ??$node [endmacro] @@ -74,9 +74,9 @@ ??$node [endmacro] -[macro node_get_float id expected_float8] +[macro node_get_float id expected_float4 expected_float8] !await client.get_float(db, '${id}') - ??{ id: '${id}', f8: ${expected_float8} } + ??{ id: '${id}', f4: ${expected_float4}, f8: ${expected_float8} } ??$node [endmacro] diff --git a/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts b/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts index b122ca175b..f5fac010da 100644 --- a/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts +++ b/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts @@ -102,16 +102,18 @@ export function writeFieldsMap( function pgType(field: ExtendedDMMFField, modelName: string): string { const prismaType = field.type const attributes = field.attributes + const getTypeAttribute = () => + attributes.find((a) => a.type.startsWith('@db')) switch (prismaType) { // BigInt, Boolean, Bytes, DateTime, Decimal, Float, Int, JSON, String case 'String': - return stringToPg(attributes) + return stringToPg(getTypeAttribute()) case 'Int': - return intToPg(attributes) + return intToPg(getTypeAttribute()) case 'Boolean': return 'BOOL' case 'DateTime': - return dateTimeToPg(attributes, field.name, modelName) + return dateTimeToPg(getTypeAttribute(), field.name, modelName) case 'BigInt': return 'INT8' case 'Bytes': @@ -119,7 +121,7 @@ function pgType(field: ExtendedDMMFField, modelName: string): string { case 'Decimal': return 'DECIMAL' case 'Float': - return 'FLOAT8' + return floatToPg(getTypeAttribute()) case 'JSON': return 'JSON' default: @@ -127,12 +129,20 @@ function pgType(field: ExtendedDMMFField, modelName: string): string { } } +function floatToPg(pgTypeAttribute: Attribute | undefined): string { + if (!pgTypeAttribute || pgTypeAttribute.type === '@db.DoublePrecision') { + // If Prisma did not add a type attribute then the PG type was FLOAT8 + return 'FLOAT8' + } else { + return 'FLOAT4' + } +} + function dateTimeToPg( - attributes: Array, + a: Attribute | undefined, field: string, model: string ): string { - const a = attributes.find((a) => a.type.startsWith('@db')) const type = a?.type const mapping = new Map([ ['@db.Timestamptz', 'TIMESTAMPTZ'], @@ -159,8 +169,7 @@ function dateTimeToPg( } } -function stringToPg(attributes: Array) { - const pgTypeAttribute = attributes.find((a) => a.type.startsWith('@db')) +function stringToPg(pgTypeAttribute: Attribute | undefined) { if (!pgTypeAttribute || pgTypeAttribute.type === '@db.Text') { // If Prisma does not add a type attribute then the PG type was TEXT return 'TEXT' @@ -171,8 +180,7 @@ function stringToPg(attributes: Array) { } } -function intToPg(attributes: Array) { - const pgTypeAttribute = attributes.find((a) => a.type.startsWith('@db')) +function intToPg(pgTypeAttribute: Attribute | undefined) { if (pgTypeAttribute?.type === '@db.SmallInt') { return 'INT2' } else {