Repository: rocicorp/mono
Pull request #4361: fix(z2s): Fix handling of pg types with params (char, varchar, numeric, etc).
Author: login: grgbkr
Created: 2025-05-12T18:29:26Z | Merged: 2025-05-13T23:48:07Z



Diff:
diff --git a/packages/z2s/src/compiler.output.test.ts b/packages/z2s/src/compiler.output.test.ts
index 417de102ab..750a6f5357 100644
--- a/packages/z2s/src/compiler.output.test.ts
+++ b/packages/z2s/src/compiler.output.test.ts
@@ -178,7 +178,7 @@ beforeEach(() => {
 test('limit', () => {
   expect(formatPgInternalConvert(limit(10))).toMatchInlineSnapshot(`
     {
-      "text": "LIMIT $1::text::numeric",
+      "text": "LIMIT $1::text::double precision",
       "values": [
         "10",
       ],
@@ -722,7 +722,7 @@ test('simple', () => {
     ),
   ).toMatchInlineSnapshot(`
     {
-      "text": ""user"."age" > $1::text::numeric",
+      "text": ""user"."age" > $1::text::double precision",
       "values": [
         "21",
       ],
@@ -747,7 +747,7 @@ test('simple', () => {
     ),
   ).toMatchInlineSnapshot(`
     {
-      "text": ""user"."age" >= $1::text::numeric",
+      "text": ""user"."age" >= $1::text::double precision",
       "values": [
         "21",
       ],
@@ -772,7 +772,7 @@ test('simple', () => {
     ),
   ).toMatchInlineSnapshot(`
     {
-      "text": ""user"."age" < $1::text::numeric",
+      "text": ""user"."age" < $1::text::double precision",
       "values": [
         "21",
       ],
@@ -797,7 +797,7 @@ test('simple', () => {
     ),
   ).toMatchInlineSnapshot(`
     {
-      "text": ""user"."age" <= $1::text::numeric",
+      "text": ""user"."age" <= $1::text::double precision",
       "values": [
         "21",
       ],
diff --git a/packages/z2s/src/compiler.ts b/packages/z2s/src/compiler.ts
index 28892a7ddd..79a7eb4d66 100644
--- a/packages/z2s/src/compiler.ts
+++ b/packages/z2s/src/compiler.ts
@@ -34,6 +34,7 @@ import {
   parse as parseBigIntJson,
 } from '../../zero-cache/src/types/bigint-json.ts';
 import {hasOwn} from '../../shared/src/has-own.ts';
+import {pgToZqlStringTypeMap} from '../../zero-cache/src/types/pg.ts';
 
 type Table = {
   zql: string;
@@ -164,16 +165,12 @@ export function orderBy(
 }
 
 function maybeCollate(serverColumnSchema: ServerColumnSchema): SQLQuery {
-  if (
-    serverColumnSchema.type === 'text' ||
-    serverColumnSchema.type === 'char' ||
-    serverColumnSchema.type === 'varchar'
-  ) {
-    return sql` COLLATE ${sql.ident(Z2S_COLLATION)}`;
-  }
   if (serverColumnSchema.type === 'uuid' || serverColumnSchema.isEnum) {
     return sql`::text COLLATE ${sql.ident(Z2S_COLLATION)}`;
   }
+  if (Object.hasOwn(pgToZqlStringTypeMap, serverColumnSchema.type)) {
+    return sql` COLLATE ${sql.ident(Z2S_COLLATION)}`;
+  }
 
   return sql``;
 }
diff --git a/packages/z2s/src/sql.test.ts b/packages/z2s/src/sql.test.ts
index bdf973c23d..b325077013 100644
--- a/packages/z2s/src/sql.test.ts
+++ b/packages/z2s/src/sql.test.ts
@@ -56,7 +56,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
       {
-        "text": "SELECT * FROM "user" WHERE "id" = $1::text::numeric",
+        "text": "SELECT * FROM "user" WHERE "id" = $1::text::double precision",
         "values": [
           "1",
         ],
@@ -88,7 +88,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
       {
-        "text": "SELECT * FROM "user" WHERE "id" = $1::text::numeric OR "other_id" = $1::text::numeric",
+        "text": "SELECT * FROM "user" WHERE "id" = $1::text::double precision OR "other_id" = $1::text::double precision",
         "values": [
           "1",
         ],
@@ -168,7 +168,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
       {
-        "text": "SELECT * FROM "foo" WHERE "jsonb" = $1::text::jsonb OR "numeric" = $2::text::numeric
+        "text": "SELECT * FROM "foo" WHERE "jsonb" = $1::text::jsonb OR "numeric" = $2::text::double precision
               OR "str" = $3::text COLLATE "ucs_basic" OR "boolean" = $4::text::boolean OR "uuid"::text = $5::text COLLATE "ucs_basic" OR "enum"::text = $6::text COLLATE "ucs_basic" OR "timestamp" = to_timestamp($7::text::bigint / 1000.0) AT TIME ZONE 'UTC' OR "timestampz" = to_timestamp($7::text::bigint / 1000.0)",
         "values": [
           "{}",
@@ -194,7 +194,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
       {
-        "text": "SELECT * FROM "foo" WHERE $1::text::numeric AND $2::text::numeric AND $3::text::text COLLATE "ucs_basic" AND $4::text::boolean AND $5",
+        "text": "SELECT * FROM "foo" WHERE $1::text::double precision AND $2::text::double precision AND $3::text::text COLLATE "ucs_basic" AND $4::text::boolean AND $5",
         "values": [
           "1",
           "1.1",
@@ -249,7 +249,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
       {
-        "text": "INSERT INTO "foo" VALUES ($1::text::numeric, $2::text::numeric, $3::text, $4::text::boolean)",
+        "text": "INSERT INTO "foo" VALUES ($1::text::numeric, $2::text::numeric, $3::text::text, $4::text::boolean)",
         "values": [
           "1",
           "1.1",
diff --git a/packages/z2s/src/sql.ts b/packages/z2s/src/sql.ts
index 6c28791c03..fac64635bb 100644
--- a/packages/z2s/src/sql.ts
+++ b/packages/z2s/src/sql.ts
@@ -7,6 +7,7 @@ import {
 import {assert, unreachable} from '../../shared/src/asserts.ts';
 import type {ServerColumnSchema} from './schema.ts';
 import type {LiteralValue} from '../../zero-protocol/src/ast.ts';
+import {isPgNumberType, isPgStringType} from '../../zero-cache/src/types/pg.ts';
 
 export const Z2S_COLLATION = 'ucs_basic';
 
@@ -29,15 +30,17 @@ const sqlConvert = Symbol('fromJson');
 export type LiteralType = 'boolean' | 'number' | 'string' | 'null';
 export type PluralLiteralType = Exclude<LiteralType, 'null'>;
 
+type ColumnSqlConvertArg = {
+  [sqlConvert]: 'column';
+  type: string;
+  isEnum: boolean;
+  value: unknown;
+  plural: boolean;
+  isComparison: boolean;
+};
+
 type SqlConvertArg =
-  | {
-      [sqlConvert]: 'column';
-      type: string;
-      isEnum: boolean;
-      value: unknown;
-      plural: boolean;
-      isComparison: boolean;
-    }
+  | ColumnSqlConvertArg
   | {
       [sqlConvert]: 'literal';
       type: LiteralType;
@@ -122,13 +125,7 @@ function stringify(arg: SqlConvertArg): string | null {
   }
   if (
     arg[sqlConvert] === 'column' &&
-    (arg.isEnum ||
-      arg.type === 'uuid' ||
-      arg.type === 'bpchar' ||
-      arg.type === 'character' ||
-      arg.type === 'text' ||
-      arg.type === 'character varying' ||
-      arg.type === 'varchar')
+    (arg.isEnum || isPgStringType(arg.type))
   ) {
     return arg.value as string;
   }
@@ -148,103 +145,89 @@ class SQLConvertFormat implements FormatConfig {
     const key = value.value;
     if (this.#seen.has(key)) {
       return {
-        placeholder: this.#createPlaceholder(this.#seen.get(key)!, value),
+        placeholder: createPlaceholder(this.#seen.get(key)!, value),
         value: PREVIOUSLY_SEEN_VALUE,
       };
     }
     this.#seen.set(key, this.#seen.size + 1);
     return {
-      placeholder: this.#createPlaceholder(this.#seen.size, value),
+      placeholder: createPlaceholder(this.#seen.size, value),
       value: stringify(value),
     };
   };
+}
 
-  #createPlaceholder(index: number, arg: SqlConvertArg) {
-    // Ok, so what is with all the `::text` casts
-    // before the final cast?
-    // This is to force the statement to describe its arguments
-    // as being text. Without the text cast the args are described as
-    // being bool/json/numeric/whatever and the bindings try to coerce
-    // the inputs to those types.
-    if (arg.type === 'null') {
-      assert(arg.value === null, "Args of type 'null' must have value null");
-      assert(!arg.plural, "Args of type 'null' must not be plural");
-      return `$${index}`;
-    }
+function createPlaceholder(index: number, arg: SqlConvertArg) {
+  if (arg.type === 'null') {
+    assert(arg.value === null, "Args of type 'null' must have value null");
+    assert(!arg.plural, "Args of type 'null' must not be plural");
+    return `$${index}`;
+  }
 
-    if (arg[sqlConvert] === 'literal') {
-      const collate =
-        arg.type === 'string' ? ` COLLATE "${Z2S_COLLATION}"` : '';
-      const {value} = arg;
-      if (Array.isArray(value)) {
-        const elType = pgTypeForLiteralType(arg.type);
-        return formatPlural(index, `value::${elType}${collate}`);
-      }
-      return `$${index}::text::${pgTypeForLiteralType(arg.type)}${collate}`;
-    }
-
-    const collate = arg.isComparison ? ` COLLATE "${Z2S_COLLATION}"` : '';
-    if (!arg.plural) {
-      if (arg.isEnum) {
-        if (arg.isComparison) {
-          return `$${index}::text${collate}`;
-        }
-        return `$${index}::text::"${arg.type}"`;
-      }
-      switch (arg.type) {
-        case 'date':
-        case 'timestamp':
-        case 'timestamp without time zone':
-          return `to_timestamp($${index}::text::bigint / 1000.0) AT TIME ZONE 'UTC'`;
-        case 'timestamptz':
-        case 'timestamp with time zone':
-          return `to_timestamp($${index}::text::bigint / 1000.0)`;
-        case 'text':
-          return `$${index}::text${collate}`;
-        case 'bpchar':
-        case 'character':
-        case 'character varying':
-        case 'varchar':
-          return `$${index}::text::${arg.type}${collate}`;
-        // uuid doesn't support collation, so we compare as text
-        case 'uuid':
-          return arg.isComparison
-            ? `$${index}::text${collate}`
-            : `$${index}::text::uuid`;
-        default:
-          return `$${index}::text::${arg.type}`;
-      }
+  if (arg[sqlConvert] === 'literal') {
+    const collate = arg.type === 'string' ? ` COLLATE "${Z2S_COLLATION}"` : '';
+    const {value} = arg;
+    if (Array.isArray(value)) {
+      const elType = pgTypeForLiteralType(arg.type);
+      return formatPlural(index, `value::${elType}${collate}`);
     }
+    return `$${index}::text::${pgTypeForLiteralType(arg.type)}${collate}`;
+  }
 
-    if (arg.isEnum && arg.isComparison) {
-      if (arg.isComparison) {
-        return formatPlural(index, `value::text${collate}`);
-      }
-      return formatPlural(index, `value::${arg.type}`);
-    }
+  const common = formatCommonToSingularAndPlural(index, arg);
+  return arg.plural ? formatPlural(index, common) : common;
+}
 
-    switch (arg.type) {
-      case 'date':
-      case 'timestamp':
-      case 'timestamptz':
-      case 'timestamp with time zone':
-      case 'timestamp without time zone':
-        return formatPlural(index, `to_timestamp(value::bigint / 1000.0)`);
-      case 'bpchar':
-      case 'character':
-      case 'character varying':
-      case 'text':
-      case 'varchar':
-        return formatPlural(index, `value::${arg.type}${collate}`);
-      // uuid doesn't support collation, so we compare as text
-      case 'uuid':
-        return arg.isComparison
-          ? formatPlural(index, `value::text${collate}`)
-          : formatPlural(index, `value::${arg.type}`);
-      default:
-        return formatPlural(index, `value::${arg.type}`);
-    }
+function formatCommonToSingularAndPlural(
+  index: number,
+  arg: ColumnSqlConvertArg,
+) {
+  // Ok, so what is with all the `::text` casts
+  // before the final cast?
+  // This is to force the statement to describe its arguments
+  // as being text. Without the text cast the args are described as
+  // being bool/json/numeric/whatever and the bindings try to coerce
+  // the inputs to those types.
+  const valuePlaceholder = arg.plural ? 'value' : `$${index}`;
+  switch (arg.type) {
+    case 'date':
+    case 'timestamp':
+    case 'timestamp without time zone':
+      return `to_timestamp(${valuePlaceholder}::text::bigint / 1000.0) AT TIME ZONE 'UTC'`;
+    case 'timestamptz':
+    case 'timestamp with time zone':
+      return `to_timestamp(${valuePlaceholder}::text::bigint / 1000.0)`;
+    // uuid doesn't support collation, so we compare as text
+    case 'uuid':
+      return arg.isComparison
+        ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+        : `${valuePlaceholder}::text::uuid`;
+  }
+  if (arg.isEnum) {
+    return arg.isComparison
+      ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+      : `${valuePlaceholder}::text::"${arg.type}"`;
+  }
+  if (isPgStringType(arg.type)) {
+    // For comparison cast to the general `text` type, not the
+    // specific column type (i.e. `arg.type`), because we don't want to
+    // force the value being compared to the size/max-size of the column
+    // type before comparison.
+    return arg.isComparison
+      ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+      : `${valuePlaceholder}::text::${arg.type}`;
+  }
+  if (isPgNumberType(arg.type)) {
+    // For comparison cast to `double precision` which uses IEEE 754 (the same
+    // representation as JavaScript numbers which will accurately
+    // represent any number value from zql) not the specific column type
+    // (i.e. `arg.type`), because we don't want to force the value being
+    // compared to the range and precision of the column type before comparison.
+    return arg.isComparison
+      ? `${valuePlaceholder}::text::double precision`
+      : `${valuePlaceholder}::text::${arg.type}`;
   }
+  return `${valuePlaceholder}::text::${arg.type}`;
 }
 
 function formatPlural(index: number, select: string) {
@@ -258,7 +241,10 @@ function pgTypeForLiteralType(type: Exclude<LiteralType, 'null'>) {
     case 'boolean':
       return 'boolean';
     case 'number':
-      return 'numeric';
+      // `double precision` uses IEEE 754, the same representation as JavaScript
+      // numbers, and so this will accurately represent any number value
+      // from zql
+      return 'double precision';
     case 'string':
       return 'text';
     default:
diff --git a/packages/zero-cache/src/types/pg.ts b/packages/zero-cache/src/types/pg.ts
index 1e5f9094d4..920cd6ae75 100644
--- a/packages/zero-cache/src/types/pg.ts
+++ b/packages/zero-cache/src/types/pg.ts
@@ -202,8 +202,7 @@ export const typeNameByOID: Record<number, string> = Object.fromEntries(
 
 Object.freeze(typeNameByOID);
 
-export const pgToZqlTypeMap = Object.freeze({
-  // Numeric types
+export const pgToZqlNumericTypeMap = Object.freeze({
   'smallint': 'number',
   'integer': 'number',
   'int': 'number',
@@ -224,6 +223,28 @@ export const pgToZqlTypeMap = Object.freeze({
   'float': 'number',
   'float4': 'number',
   'float8': 'number',
+});
+
+export function isPgNumberType(pgType: string) {
+  return Object.hasOwn(pgToZqlNumericTypeMap, formatTypeForLookup(pgType));
+}
+
+export const pgToZqlStringTypeMap = Object.freeze({
+  'bpchar': 'string',
+  'character': 'string',
+  'character varying': 'string',
+  'text': 'string',
+  'uuid': 'string',
+  'varchar': 'string',
+});
+
+export function isPgStringType(pgType: string) {
+  return Object.hasOwn(pgToZqlStringTypeMap, formatTypeForLookup(pgType));
+}
+
+export const pgToZqlTypeMap = Object.freeze({
+  // Numeric types
+  ...pgToZqlNumericTypeMap,
 
   // Date/Time types
   'date': 'number',
@@ -233,12 +254,7 @@ export const pgToZqlTypeMap = Object.freeze({
   'timestamp without time zone': 'number',
 
   // String types
-  'bpchar': 'string',
-  'character': 'string',
-  'character varying': 'string',
-  'text': 'string',
-  'uuid': 'string',
-  'varchar': 'string',
+  ...pgToZqlStringTypeMap,
 
   // Boolean types
   'bool': 'boolean',
@@ -256,10 +272,19 @@ export function dataTypeToZqlValueType(
   isEnum: boolean,
 ): ValueType | undefined {
   const valueType = (pgToZqlTypeMap as Record<string, ValueType>)[
-    pgType.toLocaleLowerCase()
+    formatTypeForLookup(pgType)
   ];
   if (valueType === undefined && isEnum) {
     return 'string';
   }
   return valueType;
 }
+
+// Strips args (i.e. (32) in char(32)) and lowercases.
+function formatTypeForLookup(pgType: string): string {
+  const startOfArgs = pgType.indexOf('(');
+  if (startOfArgs === -1) {
+    return pgType.toLocaleLowerCase();
+  }
+  return pgType.toLocaleLowerCase().substring(0, startOfArgs);
+}
diff --git a/packages/zero-pg/src/custom.pg-test.ts b/packages/zero-pg/src/custom.pg-test.ts
index ee896fb9c9..a65d1c17e8 100644
--- a/packages/zero-pg/src/custom.pg-test.ts
+++ b/packages/zero-pg/src/custom.pg-test.ts
@@ -43,6 +43,24 @@ describe('makeSchemaCRUD', () => {
 
   const basicRow = {id: '1', a: 2, b: 'foo', c: true};
 
+  const typesWithParamsRow = {
+    id: '1',
+    char: 'hello',
+    varchar: 'goodbye',
+    numeric: 10.1234,
+    decimal: 5.01,
+  };
+
+  const typesWithParamsExpectedRow = {
+    id: '1',
+    // char gets padded to 10
+    char: 'hello'.padEnd(10),
+    varchar: 'goodbye',
+    // NUMERIC(8, 3) gets truncated to 3 decimal places
+    numeric: 10.123,
+    decimal: 5.01,
+  };
+
   const uuidAndEnumRow = {
     id: '123e4567-e89b-12d3-a456-426614174000',
     reference_id: '987fcdeb-a89b-12d3-a456-426614174000',
@@ -65,6 +83,7 @@ describe('makeSchemaCRUD', () => {
         crud.dateTypes.insert(timeRow),
         crud.jsonCases.insert(jsonRow),
         crud.jsonbCases.insert(jsonRow),
+        crud.typesWithParams.insert(typesWithParamsRow),
         crud.uuidAndEnum.insert(uuidAndEnumRow),
         crud.alternate_basic.insert(basicRow),
       ]);
@@ -83,6 +102,7 @@ describe('makeSchemaCRUD', () => {
         checkDb(tx, 'dateTypes', [timeRow]),
         checkDb(tx, 'jsonCases', [jsonRow]),
         checkDb(tx, 'jsonbCases', [jsonRow]),
+        checkDb(tx, 'types_with_params', [typesWithParamsExpectedRow]),
         checkDb(tx, 'uuidAndEnum', [uuidAndEnumRow]),
         checkDb(tx, 'alternate_schema.basic', [basicRow]),
       ]);
@@ -152,6 +172,7 @@ describe('makeSchemaCRUD', () => {
         crud.dateTypes.upsert(timeRow),
         crud.jsonCases.upsert(jsonRow),
         crud.jsonbCases.upsert(jsonRow),
+        crud.typesWithParams.upsert(typesWithParamsRow),
         crud.uuidAndEnum.upsert(uuidAndEnumRow),
         crud.alternate_basic.upsert(basicRow),
       ]);
@@ -170,6 +191,7 @@ describe('makeSchemaCRUD', () => {
         checkDb(tx, 'dateTypes', [timeRow]),
         checkDb(tx, 'jsonCases', [jsonRow]),
         checkDb(tx, 'jsonbCases', [jsonRow]),
+        checkDb(tx, 'types_with_params', [typesWithParamsExpectedRow]),
         checkDb(tx, 'uuidAndEnum', [uuidAndEnumRow]),
         checkDb(tx, 'alternate_schema.basic', [basicRow]),
       ]);
@@ -197,6 +219,13 @@ describe('makeSchemaCRUD', () => {
           obj: {foo: 'baz'},
           arr: ['d', 'e', 'f'],
         }),
+        crud.typesWithParams.upsert({
+          id: '1',
+          char: 'foo',
+          varchar: 'bar',
+          decimal: 100.5,
+          numeric: 5.001,
+        }),
         crud.uuidAndEnum.upsert({
           ...uuidAndEnumRow,
           status: 'inactive',
@@ -246,6 +275,15 @@ describe('makeSchemaCRUD', () => {
             arr: ['d', 'e', 'f'],
           },
         ]),
+        checkDb(tx, 'types_with_params', [
+          {
+            id: '1',
+            char: 'foo'.padEnd(10),
+            varchar: 'bar',
+            decimal: 100.5,
+            numeric: 5.001,
+          },
+        ]),
         checkDb(tx, 'uuidAndEnum', [
           {
             ...uuidAndEnumRow,
@@ -275,6 +313,7 @@ describe('makeSchemaCRUD', () => {
         crud.dateTypes.insert(timeRow),
         crud.jsonCases.insert(jsonRow),
         crud.jsonbCases.insert(jsonRow),
+        crud.typesWithParams.insert(typesWithParamsRow),
         crud.uuidAndEnum.insert(uuidAndEnumRow),
         crud.alternate_basic.insert(basicRow),
       ]);
@@ -301,6 +340,13 @@ describe('makeSchemaCRUD', () => {
           obj: {foo: 'baz'},
           arr: ['d', 'e', 'f'],
         }),
+        crud.typesWithParams.update({
+          id: '1',
+          char: 'foo',
+          varchar: 'bar',
+          decimal: 100.5,
+          numeric: 5.001,
+        }),
         crud.uuidAndEnum.update({
           id: uuidAndEnumRow.id,
           status: 'pending',
@@ -349,6 +395,15 @@ describe('makeSchemaCRUD', () => {
             arr: ['d', 'e', 'f'],
           },
         ]),
+        checkDb(tx, 'types_with_params', [
+          {
+            id: '1',
+            char: 'foo'.padEnd(10),
+            varchar: 'bar',
+            decimal: 100.5,
+            numeric: 5.001,
+          },
+        ]),
         checkDb(tx, 'uuidAndEnum', [
           {
             ...uuidAndEnumRow,
@@ -379,6 +434,7 @@ describe('makeSchemaCRUD', () => {
         crud.dateTypes.insert(timeRow),
         crud.jsonCases.insert(jsonRow),
         crud.jsonbCases.insert(jsonRow),
+        crud.typesWithParams.insert(typesWithParamsRow),
         crud.uuidAndEnum.insert(uuidAndEnumRow),
         crud.alternate_basic.insert(basicRow),
       ]);
@@ -390,6 +446,7 @@ describe('makeSchemaCRUD', () => {
         crud.dateTypes.delete({ts: timeRow.ts}),
         crud.jsonCases.delete({str: jsonRow.str}),
         crud.jsonbCases.delete({str: jsonRow.str}),
+        crud.typesWithParams.delete({id: typesWithParamsRow.id}),
         crud.uuidAndEnum.delete({id: uuidAndEnumRow.id}),
         crud.alternate_basic.delete({id: '1'}),
       ]);
@@ -401,6 +458,7 @@ describe('makeSchemaCRUD', () => {
         checkDb(tx, 'dateTypes', []),
         checkDb(tx, 'jsonCases', []),
         checkDb(tx, 'jsonbCases', []),
+        checkDb(tx, 'types_with_params', []),
         checkDb(tx, 'uuidAndEnum', []),
         checkDb(tx, 'alternate_schema.basic', []),
       ]);
diff --git a/packages/zero-pg/src/schema.test.ts b/packages/zero-pg/src/schema.test.ts
index ef6ffd0152..b85613069f 100644
--- a/packages/zero-pg/src/schema.test.ts
+++ b/packages/zero-pg/src/schema.test.ts
@@ -318,6 +318,18 @@ describe('checkSchemasAreCompatible', () => {
       };
     });
 
+    // Generate expected errors
+    const expectedErrors: SchemaIncompatibilityError[] = Object.entries(
+      pgToZqlTypeMap,
+    ).map(([pgType, zqlType]) => ({
+      type: 'typeError' as const,
+      table: 'test',
+      column: `${pgType.replace(/\s+/g, '_')}_col`,
+      pgType,
+      declaredType: getWrongType(zqlType).schema.type,
+      requiredType: zqlType,
+    }));
+
     // Add enum type separately since it's not in pgToZqlTypeMap
     const enumColName = 'enum_col';
     incompatibleColumns[enumColName] = boolean();
@@ -327,6 +339,77 @@ describe('checkSchemasAreCompatible', () => {
       isEnum: true,
     };
 
+    // Add enum error
+    expectedErrors.push({
+      type: 'typeError' as const,
+      table: 'test',
+      column: 'enum_col',
+      pgType: 'test_enum',
+      declaredType: 'boolean',
+      requiredType: 'string',
+    });
+
+    // Add string type variants with args
+    for (const pgType of [
+      'bpchar',
+      'character',
+      'character varying',
+      'varchar',
+    ]) {
+      const pgTypeWithArg = `${pgType}(10)`;
+      const columnName = `${pgTypeWithArg.replace(/\s+/g, '_')}_col`;
+
+      // Add to incompatible schema with wrong type
+      const wrongType = getWrongType('string');
+      incompatibleColumns[columnName] = wrongType;
+
+      // Add to compatible schema with correct type
+      compatibleColumns[columnName] = getCorrectType('string');
+
+      // Add to server schema
+      serverColumns[columnName] = {
+        type: pgTypeWithArg,
+        isEnum: false,
+      };
+
+      expectedErrors.push({
+        type: 'typeError' as const,
+        table: 'test',
+        column: columnName,
+        pgType: pgTypeWithArg,
+        declaredType: wrongType.schema.type,
+        requiredType: 'string',
+      });
+    }
+
+    // Add number type variants with args
+    for (const pgType of ['numeric', 'decimal']) {
+      const pgTypeWithArg = `${pgType}(10, 5)`;
+      const columnName = `${pgTypeWithArg.replace(/\s+/g, '_')}_col`;
+
+      // Add to incompatible schema with wrong type
+      const wrongType = getWrongType('number');
+      incompatibleColumns[columnName] = wrongType;
+
+      // Add to compatible schema with correct type
+      compatibleColumns[columnName] = getCorrectType('number');
+
+      // Add to server schema
+      serverColumns[columnName] = {
+        type: pgTypeWithArg,
+        isEnum: false,
+      };
+
+      expectedErrors.push({
+        type: 'typeError' as const,
+        table: 'test',
+        column: columnName,
+        pgType: pgTypeWithArg,
+        declaredType: wrongType.schema.type,
+        requiredType: 'number',
+      });
+    }
+
     // Create schemas
     const incompatibleSchema = createSchema({
       tables: [
@@ -350,28 +433,6 @@ describe('checkSchemasAreCompatible', () => {
       serverSchema,
     );
 
-    // Generate expected errors
-    const expectedErrors: SchemaIncompatibilityError[] = Object.entries(
-      pgToZqlTypeMap,
-    ).map(([pgType, zqlType]) => ({
-      type: 'typeError' as const,
-      table: 'test',
-      column: `${pgType.replace(/\s+/g, '_')}_col`,
-      pgType,
-      declaredType: getWrongType(zqlType).schema.type,
-      requiredType: zqlType,
-    }));
-
-    // Add enum error
-    expectedErrors.push({
-      type: 'typeError' as const,
-      table: 'test',
-      column: 'enum_col',
-      pgType: 'test_enum',
-      declaredType: 'boolean',
-      requiredType: 'string',
-    });
-
     expect(incompatibleErrors).toEqual(expectedErrors);
 
     // Test compatible schema
diff --git a/packages/zero-pg/src/schema.ts b/packages/zero-pg/src/schema.ts
index e18224f720..327cbcec12 100644
--- a/packages/zero-pg/src/schema.ts
+++ b/packages/zero-pg/src/schema.ts
@@ -10,6 +10,9 @@ export type ServerSchemaRow = {
   table: string;
   column: string;
   type: string;
+  length: string | null;
+  precision: string | null;
+  scale: string | null;
   enum: string;
   enumtype: string;
 };
@@ -52,6 +55,9 @@ export async function getServerSchema<S extends Schema>(
           c.table_name::text AS table,
           c.column_name::text AS column,
           c.data_type::text AS type,
+          c.character_maximum_length AS length,
+          c.numeric_precision AS precision,
+          c.numeric_scale AS scale,
           (t.typtype = 'e')::text AS enum,
           t.typname::text AS enumtype
       FROM
@@ -80,8 +86,28 @@ export async function getServerSchema<S extends Schema>(
       serverSchema[tableName] = tableSchema;
     }
     const isEnum = row.enum.toLowerCase().startsWith('t');
+    let type = row.type.toLocaleLowerCase();
+    if (isEnum) {
+      type = row.enumtype;
+    }
+    if (
+      (type === 'bpchar' ||
+        type === 'character' ||
+        type === 'character varying' ||
+        type === 'varchar') &&
+      row.length
+    ) {
+      type = `${type}(${row.length})`;
+    }
+    if (
+      (type === 'numeric' || type === 'decimal') &&
+      row.precision !== null &&
+      row.scale !== null
+    ) {
+      type = `${type}(${row.precision}, ${row.scale})`;
+    }
     tableSchema[row.column] = {
-      type: isEnum ? row.enumtype : row.type,
+      type,
       isEnum,
     };
   }
diff --git a/packages/zero-pg/src/test/schema.ts b/packages/zero-pg/src/test/schema.ts
index d67bd37ea2..fb62cf4198 100644
--- a/packages/zero-pg/src/test/schema.ts
+++ b/packages/zero-pg/src/test/schema.ts
@@ -59,6 +59,16 @@ export const schema = createSchema({
       })
       .primaryKey('str'),
     table('jsonbCases').columns(jsonCols).primaryKey('str'),
+    table('typesWithParams')
+      .from('types_with_params')
+      .columns({
+        id: string(),
+        char: string(),
+        varchar: string(),
+        numeric: number(),
+        decimal: number(),
+      })
+      .primaryKey('id'),
     table('uuidAndEnum')
       .columns({
         id: string(),
@@ -131,6 +141,15 @@ CREATE TABLE "jsonCases" (
   PRIMARY KEY ("str")
 );
 
+CREATE TABLE types_with_params (
+  id TEXT PRIMARY KEY,
+  char CHAR(10),
+  varchar VARCHAR(20),
+  numeric NUMERIC(8, 3),
+  decimal DECIMAL(10, 5)
+);
+
+
 CREATE TYPE "statusEnum" AS ENUM ('active', 'inactive', 'pending');
 CREATE TYPE type_enum AS ENUM ('user', 'system', 'admin');
 
diff --git a/packages/zql-integration-tests/src/compiler-bigint.pg-test.ts b/packages/zql-integration-tests/src/compiler-bigint.pg-test.ts
index 844ea2e457..caa76a5774 100644
--- a/packages/zql-integration-tests/src/compiler-bigint.pg-test.ts
+++ b/packages/zql-integration-tests/src/compiler-bigint.pg-test.ts
@@ -27,6 +27,8 @@ import {
   newQueryDelegate,
 } from '../../zqlite/src/test/source-factory.ts';
 import './helpers/comparePg.ts';
+import {getServerSchema} from '../../zero-pg/src/schema.ts';
+import {Transaction} from '../../zero-pg/src/test/util.ts';
 
 const lc = createSilentLogContext();
 
@@ -78,24 +80,17 @@ const schema = createSchema({
 });
 type Schema = typeof schema;
 
-const serverSchema: ServerSchema = {
-  issue: {
-    id: {type: 'text', isEnum: false},
-    title: {type: 'text', isEnum: false},
-  },
-  comment: {
-    id: {type: 'text', isEnum: false},
-    issueId: {type: 'text', isEnum: false},
-    hash: {type: 'bigint', isEnum: false},
-  },
-} as const;
-
 let issueQuery: Query<Schema, 'issue'>;
+let serverSchema: ServerSchema;
 
 beforeAll(async () => {
   pg = await testDBs.create(DB_NAME, undefined, false);
   await pg.unsafe(createTableSQL);
 
+  serverSchema = await pg.begin(tx =>
+    getServerSchema(new Transaction(tx), schema),
+  );
+
   sqlite = new Database(lc, ':memory:');
   const testData = {
     issue: Array.from({length: 3}, (_, i) => ({
diff --git a/packages/zql-integration-tests/src/compiler-types-with-args.pg-test.ts b/packages/zql-integration-tests/src/compiler-types-with-args.pg-test.ts
new file mode 100644
index 0000000000..25a5b28b25
--- /dev/null
+++ b/packages/zql-integration-tests/src/compiler-types-with-args.pg-test.ts
@@ -0,0 +1,488 @@
+import {Client} from 'pg';
+import {afterAll, beforeAll, describe, expect, test} from 'vitest';
+import {testLogConfig} from '../../otel/src/test-log-config.ts';
+import type {JSONValue} from '../../shared/src/json.ts';
+import {createSilentLogContext} from '../../shared/src/logging-test-utils.ts';
+import {compile, extractZqlResult} from '../../z2s/src/compiler.ts';
+import type {ServerSchema} from '../../z2s/src/schema.ts';
+import {formatPgInternalConvert} from '../../z2s/src/sql.ts';
+import {initialSync} from '../../zero-cache/src/services/change-source/pg/initial-sync.ts';
+import {getConnectionURI, testDBs} from '../../zero-cache/src/test/db.ts';
+import {type PostgresDB} from '../../zero-cache/src/types/pg.ts';
+import {type Row} from '../../zero-protocol/src/data.ts';
+import {relationships} from '../../zero-schema/src/builder/relationship-builder.ts';
+import {createSchema} from '../../zero-schema/src/builder/schema-builder.ts';
+import {
+  number,
+  string,
+  table,
+} from '../../zero-schema/src/builder/table-builder.ts';
+import {clientToServer} from '../../zero-schema/src/name-mapper.ts';
+import {completedAST, newQuery} from '../../zql/src/query/query-impl.ts';
+import {type Query} from '../../zql/src/query/query.ts';
+import {Database} from '../../zqlite/src/db.ts';
+import {fromSQLiteTypes} from '../../zqlite/src/table-source.ts';
+import {
+  mapResultToClientNames,
+  newQueryDelegate,
+} from '../../zqlite/src/test/source-factory.ts';
+import './helpers/comparePg.ts';
+import {getServerSchema} from '../../zero-pg/src/schema.ts';
+import {Transaction} from '../../zero-pg/src/test/util.ts';
+
+const lc = createSilentLogContext();
+
+const DB_NAME = 'compiler-types-with-params';
+
+let pg: PostgresDB;
+let nodePostgres: Client;
+let sqlite: Database;
+
+export const createTableSQL = /*sql*/ `
+CREATE TABLE IF NOT EXISTS "issue" (
+  "id" TEXT PRIMARY KEY,
+  "title" TEXT NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS "comment" (
+  "id" TEXT PRIMARY KEY,
+  "issueId" TEXT NOT NULL,
+  "hash" char(6) NOT NULL,
+  "weight" numeric(10, 2) NOT NULL
+);
+`;
+
+const issue = table('issue')
+  .columns({
+    id: string(),
+    title: string(),
+  })
+  .primaryKey('id');
+
+const comment = table('comment')
+  .columns({
+    id: string(),
+    issueId: string(),
+    hash: string(),
+    weight: number(),
+  })
+  .primaryKey('id');
+
+const issueRelationships = relationships(issue, ({many}) => ({
+  comments: many({
+    sourceField: ['id'],
+    destField: ['issueId'],
+    destSchema: comment,
+  }),
+}));
+
+const schema = createSchema({
+  tables: [issue, comment],
+  relationships: [issueRelationships],
+});
+type Schema = typeof schema;
+
+let serverSchema: ServerSchema;
+
+let issueQuery: Query<Schema, 'issue'>;
+
+beforeAll(async () => {
+  pg = await testDBs.create(DB_NAME, undefined, false);
+  await pg.unsafe(createTableSQL);
+
+  serverSchema = await pg.begin(tx =>
+    getServerSchema(new Transaction(tx), schema),
+  );
+
+  sqlite = new Database(lc, ':memory:');
+  const testData = {
+    issue: Array.from({length: 3}, (_, i) => ({
+      id: `issue${i + 1}`,
+      title: `Test Issue ${i + 1}`,
+    })),
+    comment: Array.from({length: 6}, (_, i) => ({
+      id: `comment${i + 1}`,
+      issueId: `issue${Math.ceil((i + 1) / 2)}`,
+      // all but comment6 have values < Number.MAX_SAFE_INTEGER
+      hash: `hash-${i + 1}`,
+      weight: Number(`${i + 1}.${i + 1}${i + 1}`),
+    })),
+  };
+
+  const mapper = clientToServer(schema.tables);
+  for (const [table, rows] of Object.entries(testData)) {
+    const columns = Object.keys(rows[0]);
+    const forPg = rows.map(row =>
+      columns.reduce(
+        (acc, c) => ({
+          ...acc,
+          [mapper.columnName(table, c)]: row[c as keyof typeof row],
+        }),
+        {} as Record<string, unknown>,
+      ),
+    );
+    await pg`INSERT INTO ${pg(mapper.tableName(table))} ${pg(forPg)}`;
+  }
+  await initialSync(
+    lc,
+    {appID: 'compiler_pg_test', shardNum: 0, publications: []},
+    sqlite,
+    getConnectionURI(pg),
+    {tableCopyWorkers: 1, rowBatchSize: 10000},
+  );
+
+  const queryDelegate = newQueryDelegate(lc, testLogConfig, sqlite, schema);
+
+  issueQuery = newQuery(queryDelegate, schema, 'issue');
+
+  // Check that PG, SQLite, and test data are in sync
+  const [issuePgRows, commentPgRows] = await Promise.all([
+    pg`SELECT * FROM "issue"`,
+    pg`SELECT * FROM "comment"`,
+  ]);
+  expect(mapResultToClientNames(issuePgRows, schema, 'issue')).toEqual(
+    testData.issue,
+  );
+  expect(
+    mapResultToClientNames(commentPgRows.map(mapWeight), schema, 'comment'),
+  ).toEqual(testData.comment);
+
+  const [issueLiteRows, commentLiteRows] = [
+    mapResultToClientNames(
+      sqlite.prepare('SELECT * FROM "issue"').all<Row>(),
+      schema,
+      'issue',
+    ) as Schema['tables']['issue'][],
+    mapResultToClientNames(
+      sqlite.prepare('SELECT * FROM "comment"').all<Row>(),
+      schema,
+      'comment',
+    ) as Schema['tables']['comment'][],
+  ];
+  expect(
+    issueLiteRows.map(row => fromSQLiteTypes(schema.tables.issue.columns, row)),
+  ).toEqual(testData.issue);
+  expect(
+    commentLiteRows.map(row =>
+      fromSQLiteTypes(schema.tables.comment.columns, row),
+    ),
+  ).toEqual(testData.comment);
+
+  const {host, port, user, pass} = pg.options;
+  nodePostgres = new Client({
+    user,
+    host: host[0],
+    port: port[0],
+    password: pass ?? undefined,
+    database: DB_NAME,
+  });
+  await nodePostgres.connect();
+});
+
+afterAll(async () => {
+  await nodePostgres.end();
+});
+
+describe('compiling ZQL to SQL', () => {
+  describe('postgres.js', () => {
+    t((query: string, args: unknown[]) =>
+      pg.unsafe(query, args as JSONValue[]),
+    );
+  });
+  describe('node-postgres', () => {
+    t(
+      async (query: string, args: unknown[]) =>
+        (await nodePostgres.query(query, args as JSONValue[])).rows,
+    );
+  });
+  function t(
+    runPgQuery: (query: string, args: unknown[]) => Promise<unknown[]>,
+  ) {
+    test('basic', async () => {
+      const query = issueQuery.related('comments');
+      const c = compile(serverSchema, schema, completedAST(query));
+      const sqlQuery = formatPgInternalConvert(c);
+      const pgResult = extractZqlResult(
+        await runPgQuery(sqlQuery.text, sqlQuery.values as JSONValue[]),
+      );
+      const zqlResult = mapResultToClientNames(await query, schema, 'issue');
+      expect(zqlResult).toEqualPg(pgResult);
+      expect(zqlResult).toMatchInlineSnapshot(`
+        [
+          {
+            "comments": [
+              {
+                "hash": "hash-1",
+                "id": "comment1",
+                "issueId": "issue1",
+                "weight": 1.11,
+              },
+              {
+                "hash": "hash-2",
+                "id": "comment2",
+                "issueId": "issue1",
+                "weight": 2.22,
+              },
+            ],
+            "id": "issue1",
+            "title": "Test Issue 1",
+          },
+          {
+            "comments": [
+              {
+                "hash": "hash-3",
+                "id": "comment3",
+                "issueId": "issue2",
+                "weight": 3.33,
+              },
+              {
+                "hash": "hash-4",
+                "id": "comment4",
+                "issueId": "issue2",
+                "weight": 4.44,
+              },
+            ],
+            "id": "issue2",
+            "title": "Test Issue 2",
+          },
+          {
+            "comments": [
+              {
+                "hash": "hash-5",
+                "id": "comment5",
+                "issueId": "issue3",
+                "weight": 5.55,
+              },
+              {
+                "hash": "hash-6",
+                "id": "comment6",
+                "issueId": "issue3",
+                "weight": 6.66,
+              },
+            ],
+            "id": "issue3",
+            "title": "Test Issue 3",
+          },
+        ]
+      `);
+    });
+
+    test('order by', async () => {
+      const query = issueQuery
+        .related('comments', q => q.orderBy('hash', 'asc'))
+        .limit(2);
+      const c = compile(serverSchema, schema, completedAST(query));
+      const sqlQuery = formatPgInternalConvert(c);
+      const pgResult = extractZqlResult(
+        await runPgQuery(sqlQuery.text, sqlQuery.values as JSONValue[]),
+      );
+      const zqlResult = mapResultToClientNames(await query, schema, 'issue');
+      expect(zqlResult).toEqualPg(pgResult);
+      expect(zqlResult).toMatchInlineSnapshot(`
+              [
+                {
+                  "comments": [
+                    {
+                      "hash": "hash-1",
+                      "id": "comment1",
+                      "issueId": "issue1",
+                      "weight": 1.11,
+                    },
+                    {
+                      "hash": "hash-2",
+                      "id": "comment2",
+                      "issueId": "issue1",
+                      "weight": 2.22,
+                    },
+                  ],
+                  "id": "issue1",
+                  "title": "Test Issue 1",
+                },
+                {
+                  "comments": [
+                    {
+                      "hash": "hash-3",
+                      "id": "comment3",
+                      "issueId": "issue2",
+                      "weight": 3.33,
+                    },
+                    {
+                      "hash": "hash-4",
+                      "id": "comment4",
+                      "issueId": "issue2",
+                      "weight": 4.44,
+                    },
+                  ],
+                  "id": "issue2",
+                  "title": "Test Issue 2",
+                },
+              ]
+            `);
+    });
+
+    test('comparison operators with char(6)', async () => {
+      // Here we test that while 'hash-51' is longer than the char(6) limit of
+      // the hash column, we do not truncate it to 'hash-5' before comparison
+      // thus we do not incorrectly include 'hash-5' in the results.
+      const query = issueQuery.related('comments', q =>
+        q.where('hash', '>=', 'hash-51'),
+      );
+      const c = compile(serverSchema, schema, completedAST(query));
+      const sqlQuery = formatPgInternalConvert(c);
+      const pgResult = extractZqlResult(
+        await runPgQuery(sqlQuery.text, sqlQuery.values as JSONValue[]),
+      );
+      const zqlResult = mapResultToClientNames(await query, schema, 'issue');
+      expect(zqlResult).toEqualPg(pgResult);
+      expect(zqlResult).toMatchInlineSnapshot(`
+      [
+        {
+          "comments": [],
+          "id": "issue1",
+          "title": "Test Issue 1",
+        },
+        {
+          "comments": [],
+          "id": "issue2",
+          "title": "Test Issue 2",
+        },
+        {
+          "comments": [
+            {
+              "hash": "hash-6",
+              "id": "comment6",
+              "issueId": "issue3",
+              "weight": 6.66,
+            },
+          ],
+          "id": "issue3",
+          "title": "Test Issue 3",
+        },
+      ]
+    `);
+
+      const q2 = issueQuery.related('comments', q =>
+        q.where('hash', '=', 'hash-1'),
+      );
+      const c2 = compile(serverSchema, schema, completedAST(q2));
+      const sqlQuery2 = formatPgInternalConvert(c2);
+      const pgResult2 = extractZqlResult(
+        await runPgQuery(sqlQuery2.text, sqlQuery2.values as JSONValue[]),
+      );
+      const zqlResult2 = mapResultToClientNames(await q2, schema, 'issue');
+      expect(zqlResult2).toEqualPg(pgResult2);
+      expect(zqlResult2).toMatchInlineSnapshot(`
+              [
+                {
+                  "comments": [
+                    {
+                      "hash": "hash-1",
+                      "id": "comment1",
+                      "issueId": "issue1",
+                      "weight": 1.11,
+                    },
+                  ],
+                  "id": "issue1",
+                  "title": "Test Issue 1",
+                },
+                {
+                  "comments": [],
+                  "id": "issue2",
+                  "title": "Test Issue 2",
+                },
+                {
+                  "comments": [],
+                  "id": "issue3",
+                  "title": "Test Issue 3",
+                },
+              ]
+            `);
+    });
+
+    test('comparison operators with numeric(10, 2)', async () => {
+      // Here we test that while '6.661' has more decimal places than
+      // numeric(10, 2), we do not truncate it to 5.55 before comparison
+      // thus we do not incorrectly include weight 5.55 in the results.
+      const query = issueQuery.related('comments', q =>
+        q.where('weight', '>=', 5.551),
+      );
+      const c = compile(serverSchema, schema, completedAST(query));
+      const sqlQuery = formatPgInternalConvert(c);
+      const pgResult = extractZqlResult(
+        await runPgQuery(sqlQuery.text, sqlQuery.values as JSONValue[]),
+      );
+      const zqlResult = mapResultToClientNames(await query, schema, 'issue');
+      expect(zqlResult).toEqualPg(pgResult);
+      expect(zqlResult).toMatchInlineSnapshot(`
+              [
+                {
+                  "comments": [],
+                  "id": "issue1",
+                  "title": "Test Issue 1",
+                },
+                {
+                  "comments": [],
+                  "id": "issue2",
+                  "title": "Test Issue 2",
+                },
+                {
+                  "comments": [
+                    {
+                      "hash": "hash-6",
+                      "id": "comment6",
+                      "issueId": "issue3",
+                      "weight": 6.66,
+                    },
+                  ],
+                  "id": "issue3",
+                  "title": "Test Issue 3",
+                },
+              ]
+            `);
+
+      const q2 = issueQuery.related('comments', q =>
+        q.where('weight', '=', 1.11),
+      );
+      const c2 = compile(serverSchema, schema, completedAST(q2));
+      const sqlQuery2 = formatPgInternalConvert(c2);
+      const pgResult2 = extractZqlResult(
+        await runPgQuery(sqlQuery2.text, sqlQuery2.values as JSONValue[]),
+      );
+      const zqlResult2 = mapResultToClientNames(await q2, schema, 'issue');
+      expect(zqlResult2).toEqualPg(pgResult2);
+      expect(zqlResult2).toMatchInlineSnapshot(`
+              [
+                {
+                  "comments": [
+                    {
+                      "hash": "hash-1",
+                      "id": "comment1",
+                      "issueId": "issue1",
+                      "weight": 1.11,
+                    },
+                  ],
+                  "id": "issue1",
+                  "title": "Test Issue 1",
+                },
+                {
+                  "comments": [],
+                  "id": "issue2",
+                  "title": "Test Issue 2",
+                },
+                {
+                  "comments": [],
+                  "id": "issue3",
+                  "title": "Test Issue 3",
+                },
+              ]
+            `);
+    });
+  }
+});
+
+function mapWeight(value: Row): Row {
+  return {
+    ...value,
+    weight: Number(value['weight']),
+  };
+}
diff --git a/packages/zql/src/query/test/test-schemas.ts b/packages/zql/src/query/test/test-schemas.ts
index 6bccf3bded..f2dc7f6ea5 100644
--- a/packages/zql/src/query/test/test-schemas.ts
+++ b/packages/zql/src/query/test/test-schemas.ts
@@ -152,7 +152,7 @@ const labelRelationships = relationships(label, ({many}) => ({
   ),
 }));
 
-export const schema = createSchema({
+export const schemaOptions = {
   tables: [issue, user, comment, revision, label, issueLabel],
   relationships: [
     issueRelationships,
@@ -161,7 +161,9 @@ export const schema = createSchema({
     revisionRelationships,
     labelRelationships,
   ],
-});
+};
+
+export const schema = createSchema(schemaOptions);
 
 export const issueSchema = schema.tables.issue;
 export const commentSchema = schema.tables.comment;


Reviews:
user: login: tantaman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:05:15Z

Review comments:
diff_hunk: @@ -722,7 +722,7 @@ test('simple', () => {
     ),
   ).toMatchInlineSnapshot(`
path: packages/z2s/src/compiler.output.test.ts
user: login: tantaman
body: an extra quote snuck in there somehow `double precision""`
created_at: 2025-05-13T18:52:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -249,7 +249,7 @@ describe('string arg packing', () => {
       ),
     ).toMatchInlineSnapshot(`
path: packages/z2s/src/sql.test.ts
user: login: tantaman
body: the `insert` case is casting to `numeric` rather than `double precision`.
created_at: 2025-05-13T19:04:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -148,103 +149,89 @@ class SQLConvertFormat implements FormatConfig {
     const key = value.value;
     if (this.#seen.has(key)) {
       return {
-        placeholder: this.#createPlaceholder(this.#seen.get(key)!, value),
+        placeholder: createPlaceholder(this.#seen.get(key)!, value),
         value: PREVIOUSLY_SEEN_VALUE,
       };
     }
     this.#seen.set(key, this.#seen.size + 1);
     return {
-      placeholder: this.#createPlaceholder(this.#seen.size, value),
+      placeholder: createPlaceholder(this.#seen.size, value),
       value: stringify(value),
     };
   };
+}
 
-  #createPlaceholder(index: number, arg: SqlConvertArg) {
-    // Ok, so what is with all the `::text` casts
-    // before the final cast?
-    // This is to force the statement to describe its arguments
-    // as being text. Without the text cast the args are described as
-    // being bool/json/numeric/whatever and the bindings try to coerce
-    // the inputs to those types.
-    if (arg.type === 'null') {
-      assert(arg.value === null, "Args of type 'null' must have value null");
-      assert(!arg.plural, "Args of type 'null' must not be plural");
-      return `$${index}`;
-    }
-
-    if (arg[sqlConvert] === 'literal') {
-      const collate =
-        arg.type === 'string' ? ` COLLATE "${Z2S_COLLATION}"` : '';
-      const {value} = arg;
-      if (Array.isArray(value)) {
-        const elType = pgTypeForLiteralType(arg.type);
-        return formatPlural(index, `value::${elType}${collate}`);
-      }
-      return `$${index}::text::${pgTypeForLiteralType(arg.type)}${collate}`;
-    }
+function createPlaceholder(index: number, arg: SqlConvertArg) {
+  if (arg.type === 'null') {
+    assert(arg.value === null, "Args of type 'null' must have value null");
+    assert(!arg.plural, "Args of type 'null' must not be plural");
+    return `$${index}`;
+  }
 
-    const collate = arg.isComparison ? ` COLLATE "${Z2S_COLLATION}"` : '';
-    if (!arg.plural) {
-      if (arg.isEnum) {
-        if (arg.isComparison) {
-          return `$${index}::text${collate}`;
-        }
-        return `$${index}::text::"${arg.type}"`;
-      }
-      switch (arg.type) {
-        case 'date':
-        case 'timestamp':
-        case 'timestamp without time zone':
-          return `to_timestamp($${index}::text::bigint / 1000.0) AT TIME ZONE 'UTC'`;
-        case 'timestamptz':
-        case 'timestamp with time zone':
-          return `to_timestamp($${index}::text::bigint / 1000.0)`;
-        case 'text':
-          return `$${index}::text${collate}`;
-        case 'bpchar':
-        case 'character':
-        case 'character varying':
-        case 'varchar':
-          return `$${index}::text::${arg.type}${collate}`;
-        // uuid doesn't support collation, so we compare as text
-        case 'uuid':
-          return arg.isComparison
-            ? `$${index}::text${collate}`
-            : `$${index}::text::uuid`;
-        default:
-          return `$${index}::text::${arg.type}`;
-      }
+  if (arg[sqlConvert] === 'literal') {
+    const collate = arg.type === 'string' ? ` COLLATE "${Z2S_COLLATION}"` : '';
+    const {value} = arg;
+    if (Array.isArray(value)) {
+      const elType = pgTypeForLiteralType(arg.type);
+      return formatPlural(index, `value::${elType}${collate}`);
     }
+    return `$${index}::text::${pgTypeForLiteralType(arg.type)}${collate}`;
+  }
 
-    if (arg.isEnum && arg.isComparison) {
-      if (arg.isComparison) {
-        return formatPlural(index, `value::text${collate}`);
-      }
-      return formatPlural(index, `value::${arg.type}`);
-    }
+  const common = formatCommonToSingularAndPlural(index, arg);
+  return arg.plural ? formatPlural(index, common) : common;
+}
 
-    switch (arg.type) {
-      case 'date':
-      case 'timestamp':
-      case 'timestamptz':
-      case 'timestamp with time zone':
-      case 'timestamp without time zone':
-        return formatPlural(index, `to_timestamp(value::bigint / 1000.0)`);
-      case 'bpchar':
-      case 'character':
-      case 'character varying':
-      case 'text':
-      case 'varchar':
-        return formatPlural(index, `value::${arg.type}${collate}`);
-      // uuid doesn't support collation, so we compare as text
-      case 'uuid':
-        return arg.isComparison
-          ? formatPlural(index, `value::text${collate}`)
-          : formatPlural(index, `value::${arg.type}`);
-      default:
-        return formatPlural(index, `value::${arg.type}`);
-    }
+function formatCommonToSingularAndPlural(
+  index: number,
+  arg: ColumnSqlConvertArg,
+) {
+  // Ok, so what is with all the `::text` casts
+  // before the final cast?
+  // This is to force the statement to describe its arguments
+  // as being text. Without the text cast the args are described as
+  // being bool/json/numeric/whatever and the bindings try to coerce
+  // the inputs to those types.
+  const valuePlaceholder = arg.plural ? 'value' : `$${index}`;
+  switch (arg.type) {
+    case 'date':
+    case 'timestamp':
+    case 'timestamp without time zone':
+      return `to_timestamp(${valuePlaceholder}::text::bigint / 1000.0) AT TIME ZONE 'UTC'`;
+    case 'timestamptz':
+    case 'timestamp with time zone':
+      return `to_timestamp(${valuePlaceholder}::text::bigint / 1000.0)`;
+    // uuid doesn't support collation, so we compare as text
+    case 'uuid':
+      return arg.isComparison
+        ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+        : `${valuePlaceholder}::text::uuid`;
+  }
+  if (arg.isEnum) {
+    return arg.isComparison
+      ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+      : `${valuePlaceholder}::text::"${arg.type}"`;
+  }
+  if (isPgStringType(arg.type)) {
+    // For comparison cast to the general `text` type, not the
+    // specific column type (i.e. `arg.type`), because we don't want to
+    // force the value being compared to the size/max-size of the column
+    // type before comparison.
+    return arg.isComparison
+      ? `${valuePlaceholder}::text COLLATE "${Z2S_COLLATION}"`
+      : `${valuePlaceholder}::text::${arg.type}`;
+  }
+  if (isPgNumberType(arg.type)) {
+    // For comparison cast to `double precision` which uses IEEE 754 (the same
+    // representation as JavaScript numbers which will accurately
+    // represent any number value from zql)not the specific column type
+    // (i.e. `arg.type`), because we don't want to force the value being
+    // compared to the range and precision of the column type before comparison.
+    return arg.isComparison
+      ? `${valuePlaceholder}::text::double precision"`
path: packages/z2s/src/sql.ts
user: login: tantaman
body: ```suggestion
      ? `${valuePlaceholder}::text::double precision`
```
created_at: 2025-05-13T19:05:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-12T18:29:31Z
body: [vc]: #Cuzta9+LciZUOYS22YnYt630JGrhYHGz2VegKhanhCA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyZXBsaWNhY2hlLWRvY3MiLCJyb290RGlyZWN0b3J5IjpudWxsLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9jaWNvcnAvcmVwbGljYWNoZS1kb2NzLzJNaW5Uc0x5cGg0TE1xN2t2VzhiZHZSNDlxNXYiLCJwcmV2aWV3VXJsIjoicmVwbGljYWNoZS1kb2NzLWdpdC1ncmdia3ItejJzLWNoYXItcm9jaWNvcnAudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJyZXBsaWNhY2hlLWRvY3MtZ2l0LWdyZ2Jrci16MnMtY2hhci1yb2NpY29ycC52ZXJjZWwuYXBwIn19LHsibmFtZSI6InpidWdzIiwicm9vdERpcmVjdG9yeSI6ImFwcHMvemJ1Z3MiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9jaWNvcnAvemJ1Z3MvemJhU1dSdEwyRGZ6ak42Q2gzdjdDY0FEbWEyNCIsInByZXZpZXdVcmwiOiJ6YnVncy1naXQtZ3JnYmtyLXoycy1jaGFyLXJvY2ljb3JwLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiemJ1Z3MtZ2l0LWdyZ2Jrci16MnMtY2hhci1yb2NpY29ycC52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **replicache-docs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/replicache-docs/2MinTsLyph4LMq7kvW8bdvR49q5v)) | [Visit Preview](https://replicache-docs-git-grgbkr-z2s-char-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/replicache-docs-git-grgbkr-z2s-char-rocicorp.vercel.app?via=pr-comment-feedback-link) | May 13, 2025 11:44pm |
| **zbugs** | ✅ Ready ([Inspect](https://vercel.com/rocicorp/zbugs/zbaSWRtL2DfzjN6Ch3v7CcADma24)) | [Visit Preview](https://zbugs-git-grgbkr-z2s-char-rocicorp.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/zbugs-git-grgbkr-z2s-char-rocicorp.vercel.app?via=pr-comment-feedback-link) | May 13, 2025 11:44pm |



author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T18:31:11Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/8cf6220f-8051-492d-8e0f-4b743d954fa9?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/grgbkr-z2s-char?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">grgbkr/z2s-char</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/linux?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Linux</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/file-size?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">File Size</a></th><th>Benchmark Result<br />kilobytes (KB)<br />(Result Δ%)</th><th>Upper Boundary<br />kilobytes (KB)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-package-tgz?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero-package.tgz</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=851c67cc-562f-4cf2-ad2a-fbd79e66d4f4&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=2bcdd867-a170-440b-8c18-ee15b6476b90&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1744587896000&end_time=1747179896000&report=8cf6220f-8051-492d-8e0f-4b743d954fa9&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/e6468f04-5af4-4583-a37e-8037b5fe3076?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>1,165.95 KB<br /><details><summary>(+0.03%)</summary>Baseline: 1,165.55 KB</details></td><td>1,188.86 KB<br />(98.07%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=851c67cc-562f-4cf2-ad2a-fbd79e66d4f4&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=4f172dbf-1d31-4809-9c2d-12c79ed4c9d4&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1744587896000&end_time=1747179896000&report=8cf6220f-8051-492d-8e0f-4b743d954fa9&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/e6468f04-5af4-4583-a37e-8037b5fe3076?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>194.59 KB<br /><details><summary>(0.00%)</summary>Baseline: 194.59 KB</details></td><td>198.48 KB<br />(98.04%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/zero-js-br?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">zero.js.br</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=851c67cc-562f-4cf2-ad2a-fbd79e66d4f4&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=f9d12b7c-f48c-41fc-a662-5019d902c9f9&measures=8d8dbe98-8c7a-4838-b351-cd9d621320fe&start_time=1744587896000&end_time=1747179896000&report=8cf6220f-8051-492d-8e0f-4b743d954fa9&upper_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/e6468f04-5af4-4583-a37e-8037b5fe3076?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>54.58 KB<br /><details><summary>(0.00%)</summary>Baseline: 54.58 KB</details></td><td>55.67 KB<br />(98.04%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/8cf6220f-8051-492d-8e0f-4b743d954fa9?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/size"></div>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T18:31:54Z
body: <h2><a href="https://bencher.dev/perf/zero/reports/b009fe56-1d6e-48ab-87bf-ede027e81ddc?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/zero/branches/grgbkr-z2s-char?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">grgbkr/z2s-char</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/zero/testbeds/linux?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Linux</a></td></tr></table><details><summary>Click to view all benchmark results</summary><br /><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/zero/measures/throughput?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">Throughput</a></th><th>Benchmark Result<br />operations / second (ops/s)<br />(Result Δ%)</th><th>Lower Boundary<br />operations / second (ops/s)<br />(Limit %)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-custom-bench-ts-big-schema?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/custom.bench.ts > big schema</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=85c30cc0-d6ac-4f5e-9f62-0939be7ecdf0&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=da25d28a-0b6b-4baa-8550-fb143072856d&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1744587946000&end_time=1747179946000&report=b009fe56-1d6e-48ab-87bf-ede027e81ddc&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0b8e8376-e73a-4646-86e3-09d87399a2b8?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>331,533.69 ops/s<br /><details><summary>(+61.88%)</summary>Baseline: 204,799.43 ops/s</details></td><td>-12,788.97 ops/s<br />(-3.86%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-basics-all-1000-rows-x-10-colu-rypc9f4e?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > basics > All 1000 rows x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=85c30cc0-d6ac-4f5e-9f62-0939be7ecdf0&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=8ff168e6-e598-4cc2-a9c7-e5d09c4aefec&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1744587946000&end_time=1747179946000&report=b009fe56-1d6e-48ab-87bf-ede027e81ddc&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0b8e8376-e73a-4646-86e3-09d87399a2b8?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>1,587.68 ops/s<br /><details><summary>(+66.33%)</summary>Baseline: 954.53 ops/s</details></td><td>-15.56 ops/s<br />(-0.98%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-pk-compare-pk-n?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > pk compare > pk = N</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=85c30cc0-d6ac-4f5e-9f62-0939be7ecdf0&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=ed1c4f7e-ef3f-4184-9f6d-61c76f3bd686&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1744587946000&end_time=1747179946000&report=b009fe56-1d6e-48ab-87bf-ede027e81ddc&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0b8e8376-e73a-4646-86e3-09d87399a2b8?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>31,172.00 ops/s<br /><details><summary>(+32.92%)</summary>Baseline: 23,452.05 ops/s</details></td><td>8,426.31 ops/s<br />(27.03%)</td></tr><tr><td><a href="https://bencher.dev/perf/zero/benchmarks/src-client-zero-bench-ts-with-filter-lower-rows-500-x-1-d9xjs9n1?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">src/client/zero.bench.ts > with filter > Lower rows 500 x 10 columns (numbers)</a></td><td>📈 <a href="https://bencher.dev/perf/zero?branches=133db6b4-9873-4425-87f9-090f5e8975cc&heads=85c30cc0-d6ac-4f5e-9f62-0939be7ecdf0&testbeds=7274290c-8365-4158-ba73-19904b7ef4ae&benchmarks=ce024352-2a47-400d-b489-b9951bf428ac&measures=fc802276-c2ed-462d-9fa5-0c1fcf2deb8e&start_time=1744587946000&end_time=1747179946000&report=b009fe56-1d6e-48ab-87bf-ede027e81ddc&lower_boundary=true">view plot</a><br />🚷 <a href="https://bencher.dev/perf/zero/thresholds/0b8e8376-e73a-4646-86e3-09d87399a2b8?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">view threshold</a></td><td>2,480.51 ops/s<br /><details><summary>(+65.80%)</summary>Baseline: 1,496.09 ops/s</details></td><td>-48.71 ops/s<br />(-1.96%)</td></tr></tbody></table></details><a href="https://bencher.dev/perf/zero/reports/b009fe56-1d6e-48ab-87bf-ede027e81ddc?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr%2Bcomments&utm_term=zero">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/zero/id/perf"></div>
author_association: NONE
user: login: grgbkr
created_at: 2025-05-13T23:23:09Z
body: @tantaman I did another pass on this, mainly adding in the type args and extending tests.
author_association: CONTRIBUTOR