@onreza/prisma-adapter-bun 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/conversion.ts +170 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onreza/prisma-adapter-bun",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Prisma 7+ driver adapter for Bun.sql — use Bun's built-in PostgreSQL client instead of pg",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/conversion.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { type ArgType, type ColumnType, ColumnTypeEnum } from "@prisma/driver-adapter-utils";
2
2
 
3
3
  // Top-level regex constants (biome: useTopLevelRegex)
4
- const RE_TIMESTAMPTZ_OFFSET = /([+-]\d{2})$/;
4
+ const RE_TIMESTAMPTZ_OFFSET = /([+-]\d{2})(:\d{2})?$/;
5
5
  const RE_TIMETZ_STRIP = /[+-]\d{2}(:\d{2})?$/;
6
- const RE_MONEY_SYMBOL = /^\$/;
6
+ const RE_MONEY_SYMBOL = /\$/g;
7
7
  const RE_PG_ESCAPE_BACKSLASH = /\\/g;
8
8
  const RE_PG_ESCAPE_QUOTE = /"/g;
9
+ const RE_INT8_STRING = /^-?\d+$/;
10
+ const RE_NUMERIC_STRING = /^-?\d+\.\d+$/;
11
+ const RE_UUID_STRING = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
+ const RE_TIME_STRING = /^(\d{2}:\d{2}:\d{2})(\.\d+)?([+-]\d{2}(:\d{2})?)?$/;
13
+ const RE_MONEY_STRING = /^-?\$[\d,]+\.\d{2}$/;
14
+ const RE_BIT_STRING = /^[01]+$/;
9
15
 
10
16
  // PostgreSQL OIDs (from pg_type system catalog)
11
17
  export const PgOid = {
@@ -200,7 +206,8 @@ function normalizeTimestamptz(value: unknown): unknown {
200
206
  }
201
207
  if (typeof value === "string") {
202
208
  // Normalize various timezone formats to +00:00
203
- return value.replace(" ", "T").replace(RE_TIMESTAMPTZ_OFFSET, "$1:00");
209
+ // Only add :00 if not already present (e.g., "+03" "+03:00", but "+03:00" stays as is)
210
+ return value.replace(" ", "T").replace(RE_TIMESTAMPTZ_OFFSET, "$1$2");
204
211
  }
205
212
  return value;
206
213
  }
@@ -224,6 +231,10 @@ function normalizeTime(value: unknown): unknown {
224
231
  if (value instanceof Date) {
225
232
  return formatTime(value);
226
233
  }
234
+ // Bun.sql returns TIME as string (e.g., "14:30:00")
235
+ if (typeof value === "string") {
236
+ return value;
237
+ }
227
238
  return value;
228
239
  }
229
240
 
@@ -233,6 +244,8 @@ function normalizeNumeric(value: unknown): unknown {
233
244
 
234
245
  function normalizeMoney(value: unknown): unknown {
235
246
  const s = String(value);
247
+ // Remove $ symbol, preserving minus sign if present
248
+ // Handles: "$100.50" -> "100.50", "-$100.50" -> "-100.50"
236
249
  return s.replace(RE_MONEY_SYMBOL, "");
237
250
  }
238
251
 
@@ -259,7 +272,19 @@ function normalizeArray(value: unknown, elementNormalizer: (v: unknown) => unkno
259
272
 
260
273
  type Normalizer = (value: unknown) => unknown;
261
274
 
275
+ /**
276
+ * Normalize INT8 string to BigInt.
277
+ * Bun.sql returns BIGINT as string, but Prisma expects BigInt for Int64 columns.
278
+ */
279
+ function normalizeInt8(value: unknown): unknown {
280
+ if (typeof value === "string") {
281
+ return BigInt(value);
282
+ }
283
+ return value;
284
+ }
285
+
262
286
  export const resultNormalizers: Record<number, Normalizer> = {
287
+ [PgOid.INT8]: normalizeInt8,
263
288
  [PgOid.NUMERIC]: normalizeNumeric,
264
289
  [PgOid.MONEY]: normalizeMoney,
265
290
  [PgOid.TIME]: normalizeTime,
@@ -270,8 +295,14 @@ export const resultNormalizers: Record<number, Normalizer> = {
270
295
  [PgOid.JSON]: normalizeJson,
271
296
  [PgOid.JSONB]: normalizeJson,
272
297
  [PgOid.BYTEA]: normalizeBytes,
298
+ // BIT/VARBIT are returned as strings by Bun.sql
299
+ [PgOid.BIT]: String,
300
+ [PgOid.VARBIT]: String,
301
+ // UUID is returned as string by Bun.sql
302
+ [PgOid.UUID]: String,
273
303
 
274
304
  // Array normalizers
305
+ [PgOid.INT8_ARRAY]: (v) => normalizeArray(v, normalizeInt8),
275
306
  [PgOid.NUMERIC_ARRAY]: (v) => normalizeArray(v, normalizeNumeric),
276
307
  [PgOid.MONEY_ARRAY]: (v) => normalizeArray(v, normalizeMoney),
277
308
  [PgOid.TIME_ARRAY]: (v) => normalizeArray(v, normalizeTime),
@@ -284,18 +315,124 @@ export const resultNormalizers: Record<number, Normalizer> = {
284
315
  [PgOid.BYTEA_ARRAY]: (v) => normalizeArray(v, normalizeBytes),
285
316
  [PgOid.BIT_ARRAY]: (v) => normalizeArray(v, String),
286
317
  [PgOid.VARBIT_ARRAY]: (v) => normalizeArray(v, String),
318
+ [PgOid.UUID_ARRAY]: (v) => normalizeArray(v, String),
287
319
  [PgOid.XML_ARRAY]: (v) => normalizeArray(v, String),
288
320
  };
289
321
 
290
322
  // --- Type inference (fallback when .columns metadata is unavailable) ---
291
323
 
324
+ // INT32 range: -2,147,483,648 to 2,147,483,647
325
+ const INT32_MIN = -2147483648;
326
+ const INT32_MAX = 2147483647;
327
+
328
+ /**
329
+ * Check if a number fits in INT32 range.
330
+ */
331
+ function isInt32(num: number): boolean {
332
+ return Number.isInteger(num) && num >= INT32_MIN && num <= INT32_MAX;
333
+ }
334
+
335
+ /**
336
+ * Check if a string represents an INT8 (bigint) value.
337
+ * Used to distinguish BIGINT columns (returned as strings by Bun.sql)
338
+ * from plain TEXT columns.
339
+ */
340
+ function isInt8String(value: string): boolean {
341
+ if (!RE_INT8_STRING.test(value)) return false;
342
+ // Check if value is outside INT32 range or has more than 10 digits
343
+ // (indicating it's likely a BIGINT, not INT4)
344
+ // Note: account for minus sign when checking length
345
+ const signOffset = value.startsWith("-") ? 1 : 0;
346
+ if (value.length - signOffset > 10) return true;
347
+ // For values within 10 digits, check the actual numeric range
348
+ const num = Number(value);
349
+ if (!Number.isFinite(num)) return true; // Very large number, treat as INT8
350
+ return num < INT32_MIN || num > INT32_MAX;
351
+ }
352
+
353
+ /**
354
+ * Check if a string represents a NUMERIC/DECIMAL value.
355
+ * Bun.sql returns NUMERIC as string (e.g., "99.99").
356
+ */
357
+ function isNumericString(value: string): boolean {
358
+ return RE_NUMERIC_STRING.test(value);
359
+ }
360
+
361
+ /**
362
+ * Check if a string represents a UUID value.
363
+ * Bun.sql returns UUID as string (e.g., "550e8400-e29b-41d4-a716-446655440000").
364
+ */
365
+ function isUuidString(value: string): boolean {
366
+ return RE_UUID_STRING.test(value);
367
+ }
368
+
369
+ /**
370
+ * Check if a string represents a TIME value (without timezone).
371
+ * Bun.sql returns TIME as string (e.g., "14:30:00").
372
+ */
373
+ function isTimeString(value: string): boolean {
374
+ return RE_TIME_STRING.test(value) && !value.includes("+") && !value.includes("-");
375
+ }
376
+
377
+ /**
378
+ * Check if a string represents a TIMETZ value (with timezone).
379
+ * Bun.sql returns TIMETZ as string (e.g., "14:30:00+03").
380
+ */
381
+ function isTimetzString(value: string): boolean {
382
+ return RE_TIME_STRING.test(value) && (value.includes("+") || value.includes("-"));
383
+ }
384
+
385
+ /**
386
+ * Check if a string represents a MONEY value.
387
+ * Bun.sql returns MONEY as string (e.g., "$100.50").
388
+ */
389
+ function isMoneyString(value: string): boolean {
390
+ return RE_MONEY_STRING.test(value);
391
+ }
392
+
393
+ /**
394
+ * Check if a string represents a BIT/VARBIT value.
395
+ * Bun.sql returns BIT as string of 0s and 1s (e.g., "10101010").
396
+ * Important: must check before isInt8String to avoid misclassifying
397
+ * bit strings like "101010101010" as BIGINT.
398
+ */
399
+ function isBitString(value: string): boolean {
400
+ // Must be non-empty, only 0s and 1s
401
+ if (value.length === 0) return false;
402
+ if (!RE_BIT_STRING.test(value)) return false;
403
+ // Exclude values that look like valid integers 2-9 (single digits that are not 0 or 1)
404
+ // Bit strings are typically longer or contain only 0/1
405
+ if (value.length === 1) return value === "0" || value === "1";
406
+ return true;
407
+ }
408
+
409
+ /**
410
+ * Infer OID for a string element in an array.
411
+ * Separated to reduce complexity of inferArrayOid.
412
+ */
413
+ function inferStringArrayOid(value: string): number {
414
+ // Order matters: check more specific patterns first
415
+ if (isBitString(value)) return PgOid.BIT_ARRAY;
416
+ if (isUuidString(value)) return PgOid.UUID_ARRAY;
417
+ // TIMETZ must be checked before TIME
418
+ if (isTimetzString(value)) return PgOid.TIMETZ_ARRAY;
419
+ if (isTimeString(value)) return PgOid.TIME_ARRAY;
420
+ if (isMoneyString(value)) return PgOid.MONEY_ARRAY;
421
+ if (isInt8String(value)) return PgOid.INT8_ARRAY;
422
+ if (isNumericString(value)) return PgOid.NUMERIC_ARRAY;
423
+ return PgOid.TEXT_ARRAY;
424
+ }
425
+
292
426
  function inferArrayOid(arr: unknown[]): number {
293
427
  if (arr.length === 0) return PgOid.TEXT_ARRAY;
294
428
  const first = arr.find((v) => v !== null && v !== undefined);
295
429
  if (first === undefined) return PgOid.TEXT_ARRAY;
296
430
  if (typeof first === "number") {
297
- return Number.isInteger(first) ? PgOid.INT8_ARRAY : PgOid.FLOAT8_ARRAY;
431
+ if (!Number.isInteger(first)) return PgOid.FLOAT8_ARRAY;
432
+ return isInt32(first) ? PgOid.INT4_ARRAY : PgOid.INT8_ARRAY;
298
433
  }
434
+ if (typeof first === "bigint") return PgOid.INT8_ARRAY;
435
+ if (typeof first === "string") return inferStringArrayOid(first);
299
436
  if (typeof first === "boolean") return PgOid.BOOL_ARRAY;
300
437
  if (first instanceof Date) return PgOid.TIMESTAMPTZ_ARRAY;
301
438
  if (first instanceof Uint8Array || first instanceof Buffer) return PgOid.BYTEA_ARRAY;
@@ -323,6 +460,31 @@ function isJsonString(value: string): boolean {
323
460
  }
324
461
  }
325
462
 
463
+ /**
464
+ * Infer OID for a string value.
465
+ * Separated to reduce cognitive complexity of inferOidFromValue.
466
+ */
467
+ function inferStringOid(value: string): number {
468
+ if (isJsonString(value)) return PgOid.JSON;
469
+ // Order matters: check more specific patterns before generic ones
470
+ // BIT strings (e.g., "10101010") - must check before INT8 to avoid misclassification
471
+ if (isBitString(value)) return PgOid.BIT;
472
+ // UUID strings (e.g., "550e8400-e29b-41d4-a716-446655440000")
473
+ if (isUuidString(value)) return PgOid.UUID;
474
+ // TIMETZ strings (e.g., "14:30:00+03") - must check before TIME
475
+ if (isTimetzString(value)) return PgOid.TIMETZ;
476
+ // TIME strings (e.g., "14:30:00")
477
+ if (isTimeString(value)) return PgOid.TIME;
478
+ // MONEY strings (e.g., "$100.50")
479
+ if (isMoneyString(value)) return PgOid.MONEY;
480
+ // BIGINT columns are returned as strings by Bun.sql
481
+ // We need to detect them to return correct ColumnType (Int64, not Text)
482
+ if (isInt8String(value)) return PgOid.INT8;
483
+ // NUMERIC/DECIMAL columns are returned as strings by Bun.sql
484
+ if (isNumericString(value)) return PgOid.NUMERIC;
485
+ return PgOid.TEXT;
486
+ }
487
+
326
488
  /**
327
489
  * Infer a PostgreSQL OID from a JavaScript value.
328
490
  * Used as a fallback when Bun.sql doesn't expose column metadata.
@@ -333,13 +495,15 @@ export function inferOidFromValue(value: unknown): number {
333
495
  if (typeof value === "boolean") return PgOid.BOOL;
334
496
  if (typeof value === "bigint") return PgOid.INT8;
335
497
  if (typeof value === "number") {
336
- return Number.isInteger(value) ? PgOid.INT8 : PgOid.FLOAT8;
498
+ if (!Number.isInteger(value)) return PgOid.FLOAT8;
499
+ // Distinguish INT4 vs INT8 based on value range
500
+ return isInt32(value) ? PgOid.INT4 : PgOid.INT8;
337
501
  }
338
502
  if (value instanceof Date) return PgOid.TIMESTAMPTZ;
339
503
  if (value instanceof Uint8Array || value instanceof Buffer) return PgOid.BYTEA;
340
504
  if (Array.isArray(value)) return inferArrayOid(value);
341
505
  if (typeof value === "object") return PgOid.JSONB;
342
- if (typeof value === "string" && isJsonString(value)) return PgOid.JSON;
506
+ if (typeof value === "string") return inferStringOid(value);
343
507
  return PgOid.TEXT;
344
508
  }
345
509