perf: single-pass single-buffer tablet serialization, BigInt-free int64 writes (#16)

CPU profiling of the write path (insertTablet-heavy workload) showed the
event loop 79.5% busy with serializeTabletValues at 26% self time and GC
at 24.5% — almost entirely allocation churn in the serializer. This
rewrites the fast (default) write path:

- serializeTabletValues: new single-pass serializeTabletValuesFast()
  writes all columns AND null bitmaps into ONE exactly-sized buffer.
  Eliminates the rows->columns transpose (one array per column), the
  per-column boolean[] null bitmaps (built then discarded when a column
  has no nulls), the per-column intermediate buffers, the per-column
  1-byte Buffer.from([flag]) allocations, and the trailing Buffer.concat
  (which re-copied the whole payload). Fixed-width sizes come from the
  schema; variable-width columns get one byteLength pre-scan.
- INT64/TIMESTAMP writes: writeBigInt64BE(BigInt(v)) replaced with a
  sign-correct hi/lo 32-bit pair (hi = floor(v / 2^32), lo = v >>> 0),
  avoiding one BigInt allocation per value. BigInt inputs and non-safe
  integers still take the exact writeBigInt64BE path.
- TEXT/STRING columns: single-entry encode cache — TAG columns repeat
  one string per tablet, so consecutive identical values reuse the
  encoded Buffer instead of re-encoding UTF-8 every row.
- BufferPool: removed from the write path. It was acquire-only (zero
  release() calls anywhere in src/), i.e. a guaranteed 0% hit rate and
  pure overhead over Buffer.allocUnsafe. The class stays exported
  (public API) but is deprecated.

Wire format is unchanged — existing serialization unit tests pass
untouched, and new golden tests assert the fast path output equals the
legacy path byte-for-byte for mixed-type tablets with nulls, all-null
columns, bitmap-width edge cases (17 rows), BLOB/DATE, and negative /
MIN_SAFE_INTEGER / BigInt int64 values. The legacy path
(enableFastSerialization=false) is untouched.
diff --git a/src/client/Session.ts b/src/client/Session.ts
index 5c8cffc..73d180d 100644
--- a/src/client/Session.ts
+++ b/src/client/Session.ts
@@ -30,11 +30,10 @@
 import { RowRecord } from "./RowRecord";
 import { BaseColumnDecoder, ColumnEncoding, Column } from "./ColumnDecoder";
 import { RedirectException } from "../utils/Errors";
-import { 
-  serializeColumnFast, 
-  serializeTimestamps 
+import {
+  serializeTabletValuesFast,
+  serializeTimestamps
 } from "../utils/FastSerializer";
-import { globalBufferPool } from "../utils/BufferPool";
 import { parseDateToInt, parseIntToDate } from "../utils/DataTypes";
 
 const ttypes = require("../thrift/generated/client_types");
@@ -611,7 +610,15 @@
     dataTypes: number[],
     rowCount: number,
   ): Buffer {
-    // Serialize tablet values based on data types
+    // Fast path (default): single-pass, single-buffer serialization of all
+    // columns + null bitmaps — no transpose, no per-column intermediate
+    // buffers, no trailing Buffer.concat. Wire format is identical to the
+    // legacy path below.
+    if (this.config.enableFastSerialization) {
+      return serializeTabletValuesFast(values, dataTypes, rowCount);
+    }
+
+    // Legacy path: per-column serialization + Buffer.concat
     // Format: all columns data, then bitmap for null values
     const buffers: Buffer[] = [];
     const bitMaps: (boolean[] | null)[] = [];
@@ -635,10 +642,7 @@
         }
       }
 
-      // Use fast serialization if enabled, otherwise fall back to legacy
-      const buffer = this.config.enableFastSerialization
-        ? serializeColumnFast(columnValues, dataType)
-        : this.serializeColumn(columnValues, dataType);
+      const buffer = this.serializeColumn(columnValues, dataType);
       buffers.push(buffer);
       bitMaps.push(hasNull ? nullBitmap : null);
     }
diff --git a/src/index.ts b/src/index.ts
index 76a2965..2504833 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -49,7 +49,11 @@
 export { RedirectCache } from "./client/RedirectCache";
 export { enableGlobalCleanup } from "./utils/ProcessCleanup";
 export { BufferPool, globalBufferPool } from "./utils/BufferPool";
-export { serializeColumnFast, serializeTimestamps } from "./utils/FastSerializer";
+export {
+  serializeColumnFast,
+  serializeTimestamps,
+  serializeTabletValuesFast,
+} from "./utils/FastSerializer";
 
 // Concurrent execution utilities for Node.js optimization
 export {
diff --git a/src/utils/BufferPool.ts b/src/utils/BufferPool.ts
index 11e32e0..f477f2d 100644
--- a/src/utils/BufferPool.ts
+++ b/src/utils/BufferPool.ts
@@ -22,7 +22,13 @@
 /**
  * Buffer pool for reusing buffers to reduce GC pressure
  * Inspired by pg nodejs client's buffer management strategy
- * 
+ *
+ * @deprecated No longer used internally. The write path never called
+ * release(), so the pool had a 0% hit rate and acquire() was pure overhead
+ * over Buffer.allocUnsafe. Kept only because it is part of the public API
+ * surface (exported from src/index.ts); may be removed in a future major
+ * version.
+ *
  * Key design principles:
  * 1. Size classes to minimize waste (powers of 2)
  * 2. Maximum pool size to prevent memory bloat
diff --git a/src/utils/Config.ts b/src/utils/Config.ts
index 5ca7da1..05016f9 100644
--- a/src/utils/Config.ts
+++ b/src/utils/Config.ts
@@ -63,9 +63,10 @@
   enableSSL?: boolean;
   sslOptions?: SSLOptions;
   /**
-   * Enable optimized fast serialization with buffer pooling.
-   * Improves performance by 2-3x but may increase memory usage slightly.
-   * Inspired by pg nodejs client's buffer management.
+   * Enable the optimized tablet serialization path: single-pass,
+   * single-buffer writes with inline null bitmaps (no intermediate
+   * per-column buffers or Buffer.concat), BigInt-free int64 writes.
+   * Produces byte-identical wire output to the legacy path.
    * 
    * @default true
    */
diff --git a/src/utils/FastSerializer.ts b/src/utils/FastSerializer.ts
index 76ae9ed..f0d45f5 100644
--- a/src/utils/FastSerializer.ts
+++ b/src/utils/FastSerializer.ts
@@ -17,26 +17,68 @@
  * under the License.
  */
 
-import { globalBufferPool } from "./BufferPool";
 import { parseDateToInt } from "./DataTypes";
 
 /**
  * Fast serialization utilities for IoTDB data types
  * Optimized for performance with:
- * - Pre-allocated buffers
- * - Buffer pooling
- * - Single-pass serialization
+ * - Single exact-size buffer allocation per tablet (no Buffer.concat re-copy)
+ * - BigInt-free int64 writes (hi/lo 32-bit pair)
+ * - Single-entry string encode cache (TAG columns repeat the same value)
  * - Minimal object allocation
- * 
- * Inspired by pg nodejs client's serialization strategy
  */
 
 /**
+ * Write a 64-bit big-endian signed integer without allocating a BigInt.
+ *
+ * For any safe integer v (positive or negative):
+ *   hi = Math.floor(v / 2^32)  — arithmetic shift, sign-correct
+ *   lo = v >>> 0               — ToUint32 performs the mod-2^32 reduction
+ * so that hi * 2^32 + lo === v, which is exactly the two's complement
+ * representation split into two 32-bit halves.
+ *
+ * Falls back to writeBigInt64BE for BigInt inputs and non-safe-integer
+ * numbers (preserves legacy error behavior for invalid values like 1.5).
+ */
+export function writeInt64BE(buffer: Buffer, v: number | bigint, offset: number): void {
+  if (typeof v === "number" && Number.isSafeInteger(v)) {
+    buffer.writeInt32BE(Math.floor(v / 0x100000000), offset);
+    buffer.writeUInt32BE(v >>> 0, offset + 4);
+  } else {
+    buffer.writeBigInt64BE(typeof v === "bigint" ? v : BigInt(v as any), offset);
+  }
+}
+
+/**
+ * Byte length of a BLOB cell value without materializing a Buffer.
+ * Accepts Buffer, Uint8Array, UTF-8 string, or array-like of bytes
+ * (the same inputs the legacy serializer accepted via Buffer.from).
+ */
+function blobByteLength(v: any): number {
+  if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
+    return v.length;
+  }
+  if (typeof v === "string") {
+    return Buffer.byteLength(v, "utf8");
+  }
+  if (isArrayBufferLike(v)) {
+    return v.byteLength; // ArrayBuffer / SharedArrayBuffer have no .length
+  }
+  return v.length; // array-like of bytes
+}
+
+/** ArrayBuffer / SharedArrayBuffer (raw memory, no .length property). */
+function isArrayBufferLike(v: any): v is ArrayBufferLike {
+  return (
+    v instanceof ArrayBuffer ||
+    (typeof SharedArrayBuffer !== "undefined" && v instanceof SharedArrayBuffer)
+  );
+}
+
+/**
  * Serialize BOOLEAN column (1 byte per value)
- * Optimized: Single buffer allocation
  */
 export function serializeBooleanColumn(values: any[]): Buffer {
-  // For small buffers, direct allocation is faster than pooling
   const buffer = Buffer.allocUnsafe(values.length);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
@@ -47,152 +89,150 @@
 
 /**
  * Serialize INT32 column (4 bytes per value, big-endian)
- * Optimized: Single buffer allocation with direct writes
  */
 export function serializeInt32Column(values: any[]): Buffer {
-  const size = values.length * 4;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(values.length * 4);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
     buffer.writeInt32BE(v === null || v === undefined ? 0 : v, i * 4);
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize INT64 column (8 bytes per value, big-endian)
- * Optimized: Single buffer allocation with direct writes
+ * Optimized: BigInt-free hi/lo writes for number inputs
  */
 export function serializeInt64Column(values: any[]): Buffer {
-  const size = values.length * 8;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(values.length * 8);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
-    buffer.writeBigInt64BE(
-      v === null || v === undefined ? BigInt(0) : BigInt(v),
-      i * 8
-    );
+    if (v === null || v === undefined) {
+      buffer.writeInt32BE(0, i * 8);
+      buffer.writeUInt32BE(0, i * 8 + 4);
+    } else {
+      writeInt64BE(buffer, v, i * 8);
+    }
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize FLOAT column (4 bytes per value, big-endian)
- * Optimized: Single buffer allocation with direct writes
  */
 export function serializeFloatColumn(values: any[]): Buffer {
-  const size = values.length * 4;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(values.length * 4);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
     buffer.writeFloatBE(v === null || v === undefined ? 0.0 : v, i * 4);
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize DOUBLE column (8 bytes per value, big-endian)
- * Optimized: Single buffer allocation with direct writes
  */
 export function serializeDoubleColumn(values: any[]): Buffer {
-  const size = values.length * 8;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(values.length * 8);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
     buffer.writeDoubleBE(v === null || v === undefined ? 0.0 : v, i * 8);
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize TEXT/STRING column (4 bytes length + UTF-8 bytes per value)
- * Optimized: Two-pass approach to pre-calculate total size
+ * Optimized: single-entry encode cache — TAG columns repeat the same string
+ * for every row of a tablet, so the encoded Buffer is reused on consecutive
+ * identical values instead of re-encoding.
  */
 export function serializeTextColumn(values: any[]): Buffer {
-  // Phase 1: Calculate total size and prepare string buffers
-  const strBuffers: Buffer[] = [];
+  // Phase 1: Calculate total size (byteLength only — no Buffer allocation)
   let totalSize = 0;
-  
+  let lastSizeStr: string | null = null;
+  let lastSizeLen = 0;
+
   for (const v of values) {
-    const str = v === null || v === undefined ? "" : String(v);
-    const strBytes = Buffer.from(str, "utf8");
-    strBuffers.push(strBytes);
-    totalSize += 4 + strBytes.length;
+    if (v === null || v === undefined) {
+      totalSize += 4;
+      continue;
+    }
+    const str = typeof v === "string" ? v : String(v);
+    if (str !== lastSizeStr) {
+      lastSizeLen = Buffer.byteLength(str, "utf8");
+      lastSizeStr = str;
+    }
+    totalSize += 4 + lastSizeLen;
   }
-  
-  // Phase 2: Single allocation and copy
-  const result = totalSize >= 1024 ? globalBufferPool.acquire(totalSize) : Buffer.allocUnsafe(totalSize);
+
+  // Phase 2: Single allocation, encode with single-entry cache
+  const result = Buffer.allocUnsafe(totalSize);
   let offset = 0;
-  
-  for (const strBytes of strBuffers) {
-    result.writeInt32BE(strBytes.length, offset);
+  let lastStr: string | null = null;
+  let lastBuf: Buffer | null = null;
+
+  for (const v of values) {
+    if (v === null || v === undefined) {
+      result.writeInt32BE(0, offset);
+      offset += 4;
+      continue;
+    }
+    const str = typeof v === "string" ? v : String(v);
+    if (str !== lastStr || lastBuf === null) {
+      lastBuf = Buffer.from(str, "utf8");
+      lastStr = str;
+    }
+    result.writeInt32BE(lastBuf.length, offset);
     offset += 4;
-    strBytes.copy(result, offset);
-    offset += strBytes.length;
+    lastBuf.copy(result, offset);
+    offset += lastBuf.length;
   }
-  
-  return result.subarray(0, totalSize);
+
+  return result;
 }
 
 /**
  * Serialize TIMESTAMP column (8 bytes per value, big-endian)
  * Handles both Date objects and numeric timestamps
- * Optimized: Single buffer allocation with direct writes
+ * Optimized: BigInt-free hi/lo writes
  */
 export function serializeTimestampColumn(values: any[]): Buffer {
-  const size = values.length * 8;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(values.length * 8);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
-    let timestamp = BigInt(0);
-    if (v !== null && v !== undefined) {
-      if (v instanceof Date) {
-        timestamp = BigInt(v.getTime());
-      } else {
-        timestamp = BigInt(v);
-      }
+    if (v === null || v === undefined) {
+      buffer.writeInt32BE(0, i * 8);
+      buffer.writeUInt32BE(0, i * 8 + 4);
+    } else {
+      writeInt64BE(buffer, v instanceof Date ? v.getTime() : v, i * 8);
     }
-    buffer.writeBigInt64BE(timestamp, i * 8);
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize DATE column (4 bytes per value, INT32 yyyyMMdd encoding)
- * Optimized: Single buffer allocation with direct writes
  */
 export function serializeDateColumn(values: any[]): Buffer {
-  const size = values.length * 4;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-
+  const buffer = Buffer.allocUnsafe(values.length * 4);
   for (let i = 0; i < values.length; i++) {
     const v = values[i];
     const encoded = v === null || v === undefined ? 0 : parseDateToInt(v);
     buffer.writeInt32BE(encoded, i * 4);
   }
-
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
  * Serialize BLOB column (4 bytes length + binary data per value)
- * Optimized: Two-pass approach to pre-calculate total size
+ * Two-pass approach to pre-calculate total size
  */
 export function serializeBlobColumn(values: any[]): Buffer {
   // Phase 1: Calculate total size and prepare blob buffers
   const blobBuffers: Buffer[] = [];
   let totalSize = 0;
-  
+
   for (const v of values) {
     const blob = v === null || v === undefined
       ? Buffer.alloc(0)
@@ -202,38 +242,37 @@
     blobBuffers.push(blob);
     totalSize += 4 + blob.length;
   }
-  
+
   // Phase 2: Single allocation and copy
-  const result = totalSize >= 1024 ? globalBufferPool.acquire(totalSize) : Buffer.allocUnsafe(totalSize);
+  const result = Buffer.allocUnsafe(totalSize);
   let offset = 0;
-  
+
   for (const blob of blobBuffers) {
     result.writeInt32BE(blob.length, offset);
     offset += 4;
     blob.copy(result, offset);
     offset += blob.length;
   }
-  
-  return result.subarray(0, totalSize);
+
+  return result;
 }
 
 /**
  * Serialize timestamps array to buffer (used by insertTablet)
- * Optimized: Direct conversion to BigInt buffer
+ * Optimized: BigInt-free hi/lo writes (timestamps are safe integers)
  */
 export function serializeTimestamps(timestamps: number[]): Buffer {
-  const size = timestamps.length * 8;
-  const buffer = size >= 1024 ? globalBufferPool.acquire(size) : Buffer.allocUnsafe(size);
-  
+  const buffer = Buffer.allocUnsafe(timestamps.length * 8);
   for (let i = 0; i < timestamps.length; i++) {
     const t = timestamps[i];
     if (typeof t !== "number" || !Number.isFinite(t)) {
       throw new Error(`Invalid timestamp at index ${i}: ${t}`);
     }
-    buffer.writeBigInt64BE(BigInt(Math.floor(t)), i * 8);
+    const v = Math.floor(t);
+    buffer.writeInt32BE(Math.floor(v / 0x100000000), i * 8);
+    buffer.writeUInt32BE(v >>> 0, i * 8 + 4);
   }
-  
-  return buffer.subarray(0, size);
+  return buffer;
 }
 
 /**
@@ -265,3 +304,259 @@
       throw new Error(`Unsupported data type: ${dataType}`);
   }
 }
+
+/**
+ * Serialize a whole tablet's values (all columns + null bitmaps) into ONE buffer.
+ *
+ * Wire format (identical to the legacy per-column path):
+ *   [col0 data][col1 data]...[colN data]
+ *   then per column: [hasNull flag byte][bitmap bytes, only when flag=1]
+ *   Bitmap: ceil(rowCount/8) bytes, LSB-first, bit=1 means NULL.
+ *
+ * Optimizations over the legacy path:
+ * - No rows→columns transpose (reads values[row][col] directly)
+ * - No per-column intermediate buffers or boolean[] bitmaps
+ * - No trailing Buffer.concat (exact data size pre-computed; bitmap section
+ *   allocated worst-case and trimmed with a zero-copy subarray)
+ * - Null bitmaps are packed inline during the value pass; the flag byte
+ *   stays 0 and no bitmap bytes are emitted when a column has no nulls
+ *
+ * @param values Row-major tablet values: values[rowIndex][colIndex]
+ * @param dataTypes TSDataType code per column
+ * @param rowCount Number of rows
+ */
+export function serializeTabletValuesFast(
+  values: any[][],
+  dataTypes: number[],
+  rowCount: number,
+): Buffer {
+  const numCols = dataTypes.length;
+  const bitmapBytes = Math.ceil(rowCount / 8);
+
+  // ---- Pass 1: exact data-section size (fixed widths from schema; one
+  // byteLength scan for variable-width columns, memoizing repeated strings) ----
+  let dataSize = 0;
+  for (let c = 0; c < numCols; c++) {
+    switch (dataTypes[c]) {
+      case 0: // BOOLEAN
+        dataSize += rowCount;
+        break;
+      case 1: // INT32
+      case 3: // FLOAT
+      case 9: // DATE
+        dataSize += rowCount * 4;
+        break;
+      case 2: // INT64
+      case 4: // DOUBLE
+      case 8: // TIMESTAMP
+        dataSize += rowCount * 8;
+        break;
+      case 5: // TEXT
+      case 11: { // STRING
+        let lastStr: string | null = null;
+        let lastLen = 0;
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            dataSize += 4;
+            continue;
+          }
+          const str = typeof v === "string" ? v : String(v);
+          if (str !== lastStr) {
+            lastLen = Buffer.byteLength(str, "utf8");
+            lastStr = str;
+          }
+          dataSize += 4 + lastLen;
+        }
+        break;
+      }
+      case 10: { // BLOB
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          dataSize += 4 + (v === null || v === undefined ? 0 : blobByteLength(v));
+        }
+        break;
+      }
+      default:
+        throw new Error(`Unsupported data type: ${dataTypes[c]}`);
+    }
+  }
+
+  // Single allocation: data section + worst-case bitmap section
+  // (flag byte per column + bitmap per column); trimmed at the end.
+  const buffer = Buffer.allocUnsafe(dataSize + numCols * (1 + bitmapBytes));
+
+  // ---- Pass 2: write values and pack null bitmaps inline ----
+  let off = 0; // data-section write offset
+  let bmOff = dataSize; // bitmap-section write offset (compact)
+
+  for (let c = 0; c < numCols; c++) {
+    const flagPos = bmOff;
+    buffer[flagPos] = 0; // hasNull flag; flipped on first null
+    let hasNull = false;
+
+    // Marks row r as NULL: lazily initializes this column's bitmap region.
+    const markNull = (r: number): void => {
+      if (!hasNull) {
+        hasNull = true;
+        buffer[flagPos] = 1;
+        buffer.fill(0, flagPos + 1, flagPos + 1 + bitmapBytes);
+      }
+      buffer[flagPos + 1 + (r >>> 3)] |= 1 << (r & 7);
+    };
+
+    switch (dataTypes[c]) {
+      case 0: // BOOLEAN
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer[off] = 0;
+          } else {
+            buffer[off] = v ? 1 : 0;
+          }
+          off += 1;
+        }
+        break;
+      case 1: // INT32
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+          } else {
+            buffer.writeInt32BE(v, off);
+          }
+          off += 4;
+        }
+        break;
+      case 2: // INT64
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+            buffer.writeUInt32BE(0, off + 4);
+          } else {
+            writeInt64BE(buffer, v, off);
+          }
+          off += 8;
+        }
+        break;
+      case 3: // FLOAT
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeFloatBE(0.0, off);
+          } else {
+            buffer.writeFloatBE(v, off);
+          }
+          off += 4;
+        }
+        break;
+      case 4: // DOUBLE
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeDoubleBE(0.0, off);
+          } else {
+            buffer.writeDoubleBE(v, off);
+          }
+          off += 8;
+        }
+        break;
+      case 5: // TEXT
+      case 11: { // STRING
+        // Single-entry encode cache: TAG columns repeat one string per tablet
+        let lastStr: string | null = null;
+        let lastBuf: Buffer | null = null;
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+            off += 4;
+            continue;
+          }
+          const str = typeof v === "string" ? v : String(v);
+          if (str !== lastStr || lastBuf === null) {
+            lastBuf = Buffer.from(str, "utf8");
+            lastStr = str;
+          }
+          buffer.writeInt32BE(lastBuf.length, off);
+          off += 4;
+          lastBuf.copy(buffer, off);
+          off += lastBuf.length;
+        }
+        break;
+      }
+      case 8: // TIMESTAMP
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+            buffer.writeUInt32BE(0, off + 4);
+          } else {
+            writeInt64BE(buffer, v instanceof Date ? v.getTime() : v, off);
+          }
+          off += 8;
+        }
+        break;
+      case 9: // DATE (INT32 yyyyMMdd encoding)
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+          } else {
+            buffer.writeInt32BE(parseDateToInt(v), off);
+          }
+          off += 4;
+        }
+        break;
+      case 10: { // BLOB
+        for (let r = 0; r < rowCount; r++) {
+          const v = values[r][c];
+          if (v === null || v === undefined) {
+            markNull(r);
+            buffer.writeInt32BE(0, off);
+            off += 4;
+            continue;
+          }
+          // Write directly into the target buffer — no intermediate
+          // Buffer.from() materialization (sizing used blobByteLength).
+          const len = blobByteLength(v);
+          buffer.writeInt32BE(len, off);
+          off += 4;
+          if (Buffer.isBuffer(v)) {
+            v.copy(buffer, off);
+          } else if (v instanceof Uint8Array) {
+            buffer.set(v, off);
+          } else if (typeof v === "string") {
+            buffer.write(v, off, "utf8");
+          } else if (isArrayBufferLike(v)) {
+            // Raw memory: wrap in a zero-copy Uint8Array view to bulk-copy.
+            buffer.set(new Uint8Array(v), off);
+          } else {
+            // Array-like of bytes (legacy accepted input)
+            for (let i = 0; i < len; i++) {
+              buffer[off + i] = v[i];
+            }
+          }
+          off += len;
+        }
+        break;
+      }
+      default:
+        throw new Error(`Unsupported data type: ${dataTypes[c]}`);
+    }
+
+    bmOff = flagPos + 1 + (hasNull ? bitmapBytes : 0);
+  }
+
+  // Zero-copy trim: unused worst-case bitmap space is dropped
+  return buffer.subarray(0, bmOff);
+}
diff --git a/tests/unit/FastSerializer.test.ts b/tests/unit/FastSerializer.test.ts
index f6e673d..1e83a97 100644
--- a/tests/unit/FastSerializer.test.ts
+++ b/tests/unit/FastSerializer.test.ts
@@ -29,6 +29,7 @@
   serializeBlobColumn,
   serializeTimestamps,
   serializeColumnFast,
+  writeInt64BE,
 } from "../../src/utils/FastSerializer";
 import { globalBufferPool } from "../../src/utils/BufferPool";
 
@@ -80,6 +81,110 @@
     });
   });
 
+  describe("BigInt-free INT64 hi/lo writes", () => {
+    const roundTrip = (v: number | bigint): bigint => {
+      const buf = Buffer.allocUnsafe(8);
+      writeInt64BE(buf, v, 0);
+      return buf.readBigInt64BE(0);
+    };
+
+    it("should write positive numbers correctly", () => {
+      expect(roundTrip(0)).toBe(BigInt(0));
+      expect(roundTrip(1)).toBe(BigInt(1));
+      expect(roundTrip(0xffffffff)).toBe(BigInt(0xffffffff)); // 32-bit boundary
+      expect(roundTrip(0x100000000)).toBe(BigInt("4294967296"));
+      expect(roundTrip(Number.MAX_SAFE_INTEGER)).toBe(BigInt(Number.MAX_SAFE_INTEGER));
+    });
+
+    it("should write negative numbers correctly (two's complement)", () => {
+      expect(roundTrip(-1)).toBe(BigInt(-1));
+      expect(roundTrip(-2000)).toBe(BigInt(-2000));
+      expect(roundTrip(-0x100000000)).toBe(BigInt("-4294967296"));
+      expect(roundTrip(-0x100000001)).toBe(BigInt("-4294967297"));
+      expect(roundTrip(Number.MIN_SAFE_INTEGER)).toBe(BigInt(Number.MIN_SAFE_INTEGER));
+    });
+
+    it("should handle BigInt inputs", () => {
+      expect(roundTrip(BigInt(1000))).toBe(BigInt(1000));
+      expect(roundTrip(BigInt(-2000))).toBe(BigInt(-2000));
+      expect(roundTrip(BigInt("-9223372036854775808"))).toBe(BigInt("-9223372036854775808")); // i64 min
+      expect(roundTrip(BigInt("9223372036854775807"))).toBe(BigInt("9223372036854775807")); // i64 max
+    });
+
+    it("should match legacy BigInt output for INT64 columns with negatives", () => {
+      const values = [
+        Number.MIN_SAFE_INTEGER,
+        -1,
+        -0x100000000,
+        0,
+        0xffffffff,
+        Number.MAX_SAFE_INTEGER,
+        BigInt(-42),
+      ];
+      const buffer = serializeInt64Column(values);
+      const legacy = Buffer.alloc(values.length * 8);
+      values.forEach((v, i) => legacy.writeBigInt64BE(BigInt(v), i * 8));
+      expect(buffer.equals(legacy)).toBe(true);
+    });
+
+    it("should serialize negative timestamps (pre-1970) correctly", () => {
+      const values = [-1, -86400000, 0, 1700000000000];
+      const buffer = serializeTimestampColumn(values);
+      values.forEach((v, i) => {
+        expect(buffer.readBigInt64BE(i * 8)).toBe(BigInt(v));
+      });
+      // serializeTimestamps (time column) too
+      const tsBuffer = serializeTimestamps(values as number[]);
+      values.forEach((v, i) => {
+        expect(tsBuffer.readBigInt64BE(i * 8)).toBe(BigInt(v));
+      });
+    });
+  });
+
+  describe("String encode cache", () => {
+    it("should reuse encoding for repeated strings (TAG-like column)", () => {
+      const values = Array.from({ length: 100 }, () => "device_001");
+      const buffer = serializeTextColumn(values);
+      let offset = 0;
+      for (let i = 0; i < 100; i++) {
+        const len = buffer.readInt32BE(offset);
+        offset += 4;
+        expect(buffer.toString("utf8", offset, offset + len)).toBe("device_001");
+        offset += len;
+      }
+      expect(offset).toBe(buffer.length);
+    });
+
+    it("should not wrongly reuse cache for different strings", () => {
+      const values = ["aaa", "aaa", "bbb", "aaa", "cc", "你好", "你好", "cc"];
+      const buffer = serializeTextColumn(values);
+      let offset = 0;
+      for (const v of values) {
+        const expected = Buffer.from(v, "utf8");
+        const len = buffer.readInt32BE(offset);
+        offset += 4;
+        expect(len).toBe(expected.length);
+        expect(buffer.toString("utf8", offset, offset + len)).toBe(v);
+        offset += len;
+      }
+      expect(offset).toBe(buffer.length);
+    });
+
+    it("should handle nulls interleaved with cached strings", () => {
+      const values = ["x", null, "x", undefined, "y"];
+      const buffer = serializeTextColumn(values);
+      let offset = 0;
+      const expected = ["x", "", "x", "", "y"];
+      for (const v of expected) {
+        const len = buffer.readInt32BE(offset);
+        offset += 4;
+        expect(buffer.toString("utf8", offset, offset + len)).toBe(v);
+        offset += len;
+      }
+      expect(offset).toBe(buffer.length);
+    });
+  });
+
   describe("FLOAT Serialization", () => {
     it("should serialize FLOAT values correctly", () => {
       const values = [1.5, -2.5, 0.0, null, undefined];
@@ -273,20 +378,20 @@
   });
 
   describe("Buffer Pool Integration", () => {
-    it("should track buffer pool statistics", () => {
+    it("should no longer use the deprecated buffer pool", () => {
       const statsInitial = globalBufferPool.getStats();
-      
-      // Allocate buffers larger than 1KB to trigger pooling
+
+      // Large buffers used to trigger pooling; serializers now allocate directly
       const largeValues = Array.from({ length: 300 }, (_, i) => i); // 300 * 4 = 1200 bytes
-      serializeInt32Column(largeValues); // This should use the pool
-      serializeDoubleColumn(largeValues); // 300 * 8 = 2400 bytes, uses pool
-      
+      serializeInt32Column(largeValues);
+      serializeDoubleColumn(largeValues); // 300 * 8 = 2400 bytes
+
       const statsFinal = globalBufferPool.getStats();
-      
-      // Should have some allocations (or hits if pool was already populated)
-      const totalActivity = statsFinal.allocations + statsFinal.hits;
-      const initialActivity = statsInitial.allocations + statsInitial.hits;
-      expect(totalActivity).toBeGreaterThan(initialActivity);
+
+      // The pool is deprecated: no acquire() calls should be made
+      expect(statsFinal.allocations + statsFinal.hits).toBe(
+        statsInitial.allocations + statsInitial.hits,
+      );
     });
   });
 });
diff --git a/tests/unit/TabletSerialization.test.ts b/tests/unit/TabletSerialization.test.ts
index 620a432..a3ddfa8 100644
--- a/tests/unit/TabletSerialization.test.ts
+++ b/tests/unit/TabletSerialization.test.ts
@@ -288,6 +288,117 @@
     });
   });
 
+  describe('Fast vs Legacy Tablet Serialization (golden wire-format test)', () => {
+    let legacySession: Session;
+
+    beforeEach(() => {
+      legacySession = new Session({
+        host: 'localhost',
+        port: 6667,
+        username: 'root',
+        password: 'root',
+        enableFastSerialization: false,
+      });
+    });
+
+    const compare = (values: any[][], dataTypes: number[], rowCount: number) => {
+      const fast: Buffer = (session as any).serializeTabletValues(values, dataTypes, rowCount);
+      const legacy: Buffer = (legacySession as any).serializeTabletValues(values, dataTypes, rowCount);
+      expect(fast.length).toBe(legacy.length);
+      expect(fast.equals(legacy)).toBe(true);
+    };
+
+    test('mixed-type tablet with nulls matches legacy byte-for-byte', () => {
+      // 10 rows × 8 columns covering every type, nulls scattered
+      const dataTypes = [
+        TSDataType.BOOLEAN,
+        TSDataType.INT32,
+        TSDataType.INT64,
+        TSDataType.FLOAT,
+        TSDataType.DOUBLE,
+        TSDataType.TEXT,
+        TSDataType.TIMESTAMP,
+        TSDataType.STRING,
+      ];
+      const values: any[][] = [];
+      for (let r = 0; r < 10; r++) {
+        values.push([
+          r % 3 === 0 ? null : r % 2 === 0,
+          r === 1 ? null : r * 100 - 500,
+          r === 4 ? null : BigInt(-1000000000000) + BigInt(r),
+          r === 7 ? undefined : r * 1.5 - 3,
+          r === 2 ? null : r * 2.5e10,
+          r === 5 ? null : `tag_${r % 2}`, // repeated strings exercise the cache
+          r === 8 ? null : 1700000000000 + r,
+          r === 0 ? null : '设备_' + (r % 3),
+        ]);
+      }
+      compare(values, dataTypes, 10);
+    });
+
+    test('tablet with no nulls matches legacy byte-for-byte', () => {
+      const dataTypes = [TSDataType.DOUBLE, TSDataType.INT64, TSDataType.TEXT];
+      const values: any[][] = [];
+      for (let r = 0; r < 17; r++) {
+        // 17 rows: bitmap byte-count edge (ceil(17/8)=3)
+        values.push([r * 1.1, -r * 12345678901, 'constant_tag']);
+      }
+      compare(values, dataTypes, 17);
+    });
+
+    test('tablet with all-null column matches legacy byte-for-byte', () => {
+      const dataTypes = [TSDataType.INT32, TSDataType.TEXT, TSDataType.BOOLEAN];
+      const values: any[][] = [];
+      for (let r = 0; r < 9; r++) {
+        values.push([null, r % 2 === 0 ? 's' : null, true]);
+      }
+      compare(values, dataTypes, 9);
+    });
+
+    test('BLOB and DATE tablet matches legacy byte-for-byte', () => {
+      const dataTypes = [TSDataType.BLOB, TSDataType.DATE];
+      const values: any[][] = [
+        [Buffer.from([1, 2, 3]), new Date('2024-01-01')],
+        [null, 20240102],
+        [Buffer.alloc(0), null],
+        [Buffer.from([0xff]), new Date('2025-06-15')],
+      ];
+      compare(values, dataTypes, 4);
+    });
+
+    test('non-Buffer BLOB inputs (Uint8Array, byte array, string) match legacy', () => {
+      const dataTypes = [TSDataType.BLOB];
+      const values: any[][] = [
+        [new Uint8Array([0x01, 0x02, 0x03, 0x04])],
+        [[0x10, 0x20, 0x30]],
+        ['hello-blob'],
+        [null],
+        [new Uint8Array(0)],
+      ];
+      compare(values, dataTypes, 5);
+    });
+
+    test('ArrayBuffer and SharedArrayBuffer BLOB inputs match legacy', () => {
+      const dataTypes = [TSDataType.BLOB];
+      const ab = new Uint8Array([0x01, 0x02, 0xff]).buffer;
+      const sab = new SharedArrayBuffer(4);
+      new Uint8Array(sab).set([0xaa, 0xbb, 0xcc, 0xdd]);
+      const values: any[][] = [
+        [ab],
+        [sab],
+        [new ArrayBuffer(0)],
+        [null],
+      ];
+      compare(values, dataTypes, 4);
+    });
+
+    test('single-row and empty-ish edge cases match legacy', () => {
+      compare([[42]], [TSDataType.INT32], 1);
+      compare([[null]], [TSDataType.DOUBLE], 1);
+      compare([[9007199254740991, -9007199254740991]].map((r) => r), [TSDataType.INT64, TSDataType.INT64], 1);
+    });
+  });
+
   describe('Column Serialization - Other Data Types', () => {
     test('should serialize BOOLEAN column', () => {
       const values = [true, false, true, false];