tina4-nodejs 3.13.14 → 3.13.18

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.
@@ -1,5 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
- import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult } from "./types.js";
2
+ import type { DatabaseAdapter, DatabaseResult as DatabaseWriteResult, ColumnInfo, FieldDefinition } from "./types.js";
3
3
  import { DatabaseResult } from "./databaseResult.js";
4
4
 
5
5
  /**
@@ -22,6 +22,109 @@ export function stripTrailingSemicolons(sql: string): string {
22
22
  return stripped;
23
23
  }
24
24
 
25
+ /**
26
+ * Adapter bridge helpers (v3.14.0, Option A).
27
+ *
28
+ * The public Database/BaseModel/QueryBuilder API is async so it works on the
29
+ * async adapters (PostgreSQL/MySQL/MSSQL/Firebird/Mongo). SQLite implements
30
+ * only the synchronous methods (`node:sqlite` is sync); the async adapters
31
+ * implement only the `*Async` variants and make the sync methods throw.
32
+ *
33
+ * Each helper prefers the adapter's `*Async` method when present and awaits it,
34
+ * otherwise falls back to the sync method. For SQLite the fallback resolves
35
+ * instantly; for async adapters the awaited promise does the real work. This is
36
+ * the single chokepoint every public read/write flows through.
37
+ */
38
+ export async function adapterFetch<T = Record<string, unknown>>(
39
+ adapter: DatabaseAdapter, sql: string, params?: unknown[], limit?: number, skip?: number,
40
+ ): Promise<T[]> {
41
+ return (adapter as any).fetchAsync
42
+ ? await (adapter as any).fetchAsync(sql, params, limit, skip)
43
+ : adapter.fetch<T>(sql, params, limit, skip);
44
+ }
45
+
46
+ export async function adapterQuery<T = Record<string, unknown>>(
47
+ adapter: DatabaseAdapter, sql: string, params?: unknown[],
48
+ ): Promise<T[]> {
49
+ return (adapter as any).queryAsync
50
+ ? await (adapter as any).queryAsync(sql, params)
51
+ : adapter.query<T>(sql, params);
52
+ }
53
+
54
+ export async function adapterFetchOne<T = Record<string, unknown>>(
55
+ adapter: DatabaseAdapter, sql: string, params?: unknown[],
56
+ ): Promise<T | null> {
57
+ return (adapter as any).fetchOneAsync
58
+ ? await (adapter as any).fetchOneAsync(sql, params)
59
+ : adapter.fetchOne<T>(sql, params);
60
+ }
61
+
62
+ export async function adapterExecute(
63
+ adapter: DatabaseAdapter, sql: string, params?: unknown[],
64
+ ): Promise<unknown> {
65
+ return (adapter as any).executeAsync
66
+ ? await (adapter as any).executeAsync(sql, params)
67
+ : adapter.execute(sql, params);
68
+ }
69
+
70
+ export async function adapterStartTransaction(adapter: DatabaseAdapter): Promise<void> {
71
+ if ((adapter as any).startTransactionAsync) await (adapter as any).startTransactionAsync();
72
+ else adapter.startTransaction();
73
+ }
74
+
75
+ export async function adapterCommit(adapter: DatabaseAdapter): Promise<void> {
76
+ if ((adapter as any).commitAsync) await (adapter as any).commitAsync();
77
+ else adapter.commit();
78
+ }
79
+
80
+ export async function adapterRollback(adapter: DatabaseAdapter): Promise<void> {
81
+ if ((adapter as any).rollbackAsync) await (adapter as any).rollbackAsync();
82
+ else adapter.rollback();
83
+ }
84
+
85
+ export async function adapterTableExists(adapter: DatabaseAdapter, name: string): Promise<boolean> {
86
+ return (adapter as any).tableExistsAsync
87
+ ? await (adapter as any).tableExistsAsync(name)
88
+ : adapter.tableExists(name);
89
+ }
90
+
91
+ export async function adapterTables(adapter: DatabaseAdapter): Promise<string[]> {
92
+ return (adapter as any).tablesAsync
93
+ ? await (adapter as any).tablesAsync()
94
+ : adapter.tables();
95
+ }
96
+
97
+ export async function adapterColumns(adapter: DatabaseAdapter, table: string): Promise<ColumnInfo[]> {
98
+ return (adapter as any).columnsAsync
99
+ ? await (adapter as any).columnsAsync(table)
100
+ : adapter.columns(table);
101
+ }
102
+
103
+ export async function adapterCreateTable(
104
+ adapter: DatabaseAdapter, name: string, columns: Record<string, FieldDefinition>,
105
+ ): Promise<void> {
106
+ if ((adapter as any).createTableAsync) await (adapter as any).createTableAsync(name, columns);
107
+ else adapter.createTable(name, columns);
108
+ }
109
+
110
+ /**
111
+ * Extract the engine-assigned auto-increment id from an `execute()` result.
112
+ *
113
+ * SQLite returns `{ lastInsertRowid }`. PostgreSQL (pg) returns a result whose
114
+ * `rows[0].id` holds the value when the statement had a `RETURNING` clause
115
+ * (insertAsync adds one). MySQL/MSSQL adapters set the adapter's lastInsertId,
116
+ * so callers fall back to `adapter.lastInsertId()` when the result has neither.
117
+ */
118
+ export function extractLastInsertId(result: unknown): number | bigint | null {
119
+ if (result && typeof result === "object") {
120
+ const r = result as any;
121
+ if (r.lastInsertRowid !== undefined && r.lastInsertRowid !== null) return r.lastInsertRowid;
122
+ if (r.rows?.[0]?.id !== undefined && r.rows[0].id !== null) return r.rows[0].id;
123
+ if (r.lastInsertId !== undefined && r.lastInsertId !== null) return r.lastInsertId;
124
+ }
125
+ return null;
126
+ }
127
+
25
128
  let activeAdapter: DatabaseAdapter | null = null;
26
129
  const namedAdapters: Map<string, DatabaseAdapter> = new Map();
27
130
 
@@ -422,21 +525,38 @@ export class Database {
422
525
  this.close();
423
526
  }
424
527
 
425
- /** Query rows with optional pagination. Returns a DatabaseResult wrapper. */
426
- fetch(sql: string, params?: unknown[], limit?: number, offset?: number): DatabaseResult {
528
+ /** Query rows with optional pagination. Returns a DatabaseResult wrapper.
529
+ *
530
+ * Async since v3.14.0 (Option A): the public API awaits the adapter's
531
+ * `*Async` method when present (PostgreSQL/MySQL/MSSQL/Firebird/Mongo) and
532
+ * falls back to the synchronous method for SQLite (`node:sqlite` is sync, so
533
+ * the fallback resolves instantly). This is the breaking change that makes
534
+ * the wrapper work uniformly across every engine.
535
+ */
536
+ async fetch(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<DatabaseResult> {
427
537
  // v3.13.12: strip trailing `;` before the adapter wraps with COUNT(*)
428
538
  // or appends LIMIT/OFFSET. Without this, `"SELECT * FROM t;"` becomes
429
539
  // `"SELECT * FROM t; LIMIT 100 OFFSET 0"` — a syntax error.
430
540
  sql = stripTrailingSemicolons(sql);
431
541
  const adapter = this.getNextAdapter();
432
- const rows = adapter.fetch<Record<string, unknown>>(sql, params, limit, offset);
433
- return new DatabaseResult(rows, undefined, undefined, limit, offset, adapter, sql);
542
+ try {
543
+ const rows = await adapterFetch(adapter, sql, params, limit, offset);
544
+ this.lastError = null;
545
+ return new DatabaseResult(rows, undefined, undefined, limit, offset, adapter, sql);
546
+ } catch (e: any) {
547
+ // v3.13.11 #49.2: fetch() records last_error like execute() does.
548
+ this.lastError = e?.message ?? String(e);
549
+ throw e;
550
+ }
434
551
  }
435
552
 
436
553
  /** Fetch a single row or null. */
437
- fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
554
+ async fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
438
555
  sql = stripTrailingSemicolons(sql);
439
- return this.getNextAdapter().fetchOne<T>(sql, params);
556
+ const adapter = this.getNextAdapter();
557
+ return (adapter as any).fetchOneAsync
558
+ ? await (adapter as any).fetchOneAsync<T>(sql, params)
559
+ : adapter.fetchOne<T>(sql, params);
440
560
  }
441
561
 
442
562
  /**
@@ -452,20 +572,20 @@ export class Database {
452
572
  * Returns `[]` (not `null`) when no rows match. Cross-framework parity
453
573
  * with Python `db.fetch_all()`, PHP `$db->fetchAll()`, and Ruby `db.fetch_all`.
454
574
  */
455
- fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number): T[] {
456
- return this.fetch(sql, params, limit, offset).records as T[];
575
+ async fetchAll<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, offset?: number): Promise<T[]> {
576
+ return (await this.fetch(sql, params, limit, offset)).records as T[];
457
577
  }
458
578
 
459
579
  /**
460
580
  * Execute a write statement. Returns true/false for simple writes.
461
581
  * If SQL contains RETURNING, CALL, EXEC, or SELECT, returns the result set.
462
582
  */
463
- execute(sql: string, params?: unknown[]): boolean | unknown {
583
+ async execute(sql: string, params?: unknown[]): Promise<boolean | unknown> {
464
584
  try {
465
585
  const adapter = this.getNextAdapter();
466
- const result = adapter.execute(sql, params);
586
+ const result = await adapterExecute(adapter, sql, params);
467
587
  if (this.autoCommit) {
468
- try { adapter.commit(); } catch { /* no active transaction */ }
588
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
469
589
  }
470
590
  this.lastError = null;
471
591
  const upper = sql.trim().toUpperCase();
@@ -481,31 +601,37 @@ export class Database {
481
601
  }
482
602
 
483
603
  /** Insert a row into a table. */
484
- insert(table: string, data: Record<string, unknown>): DatabaseWriteResult {
604
+ async insert(table: string, data: Record<string, unknown>): Promise<DatabaseWriteResult> {
485
605
  const adapter = this.getNextAdapter();
486
- const result = adapter.insert(table, data);
606
+ const result = (adapter as any).insertAsync
607
+ ? await (adapter as any).insertAsync(table, data)
608
+ : adapter.insert(table, data);
487
609
  if (this.autoCommit) {
488
- try { adapter.commit(); } catch { /* no active transaction */ }
610
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
489
611
  }
490
612
  return result;
491
613
  }
492
614
 
493
615
  /** Update rows in a table matching filter. */
494
- update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>, params?: unknown[]): DatabaseWriteResult {
616
+ async update(table: string, data: Record<string, unknown>, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult> {
495
617
  const adapter = this.getNextAdapter();
496
- const result = adapter.update(table, data, filter ?? {}, params);
618
+ const result = (adapter as any).updateAsync
619
+ ? await (adapter as any).updateAsync(table, data, filter ?? {}, params)
620
+ : adapter.update(table, data, filter ?? {}, params);
497
621
  if (this.autoCommit) {
498
- try { adapter.commit(); } catch { /* no active transaction */ }
622
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
499
623
  }
500
624
  return result;
501
625
  }
502
626
 
503
627
  /** Delete rows from a table matching filter. */
504
- delete(table: string, filter?: Record<string, unknown>, params?: unknown[]): DatabaseWriteResult {
628
+ async delete(table: string, filter?: Record<string, unknown>, params?: unknown[]): Promise<DatabaseWriteResult> {
505
629
  const adapter = this.getNextAdapter();
506
- const result = adapter.delete(table, filter ?? {}, params);
630
+ const result = (adapter as any).deleteAsync
631
+ ? await (adapter as any).deleteAsync(table, filter ?? {}, params)
632
+ : adapter.delete(table, filter ?? {}, params);
507
633
  if (this.autoCommit) {
508
- try { adapter.commit(); } catch { /* no active transaction */ }
634
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
509
635
  }
510
636
  return result;
511
637
  }
@@ -529,7 +655,7 @@ export class Database {
529
655
  * the whole transaction so executes and the final commit/rollback all run
530
656
  * on the same connection (critical when pool > 0).
531
657
  */
532
- startTransaction(): void {
658
+ async startTransaction(): Promise<void> {
533
659
  // Pick an adapter using the normal selection logic, then pin it.
534
660
  const adapter = this.getNextAdapter();
535
661
  let store = this.txStore.getStore();
@@ -538,33 +664,33 @@ export class Database {
538
664
  } else {
539
665
  this.txStore.enterWith({ adapter });
540
666
  }
541
- adapter.startTransaction();
667
+ await adapterStartTransaction(adapter);
542
668
  }
543
669
 
544
670
  /** Commit the current transaction and release the adapter pin. */
545
- commit(): void {
671
+ async commit(): Promise<void> {
546
672
  const adapter = this.getNextAdapter();
547
- adapter.commit();
673
+ await adapterCommit(adapter);
548
674
  const store = this.txStore.getStore();
549
675
  if (store) store.adapter = null;
550
676
  }
551
677
 
552
678
  /** Rollback the current transaction and release the adapter pin. */
553
- rollback(): void {
679
+ async rollback(): Promise<void> {
554
680
  const adapter = this.getNextAdapter();
555
- adapter.rollback();
681
+ await adapterRollback(adapter);
556
682
  const store = this.txStore.getStore();
557
683
  if (store) store.adapter = null;
558
684
  }
559
685
 
560
686
  /** Check if a table exists. */
561
- tableExists(name: string): boolean {
562
- return this.getNextAdapter().tableExists(name);
687
+ async tableExists(name: string): Promise<boolean> {
688
+ return adapterTableExists(this.getNextAdapter(), name);
563
689
  }
564
690
 
565
691
  /** List all tables in the database. */
566
- getTables(): string[] {
567
- return this.getNextAdapter().tables();
692
+ async getTables(): Promise<string[]> {
693
+ return adapterTables(this.getNextAdapter());
568
694
  }
569
695
 
570
696
  /**
@@ -575,8 +701,8 @@ export class Database {
575
701
  * @param tableName - Name of the table to inspect.
576
702
  * @returns Array of column info objects: { name, type, nullable, default, primaryKey }.
577
703
  */
578
- getColumns(tableName: string): { name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[] {
579
- return this.getNextAdapter().columns(tableName);
704
+ async getColumns(tableName: string): Promise<{ name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[]> {
705
+ return adapterColumns(this.getNextAdapter(), tableName);
580
706
  }
581
707
 
582
708
  /**
@@ -587,18 +713,18 @@ export class Database {
587
713
  * @param paramSets - Array of parameter arrays, one per execution.
588
714
  * @returns Array of results from each execution.
589
715
  */
590
- executeMany(sql: string, paramSets: unknown[][] = []): unknown[] {
716
+ async executeMany(sql: string, paramSets: unknown[][] = []): Promise<unknown[]> {
591
717
  const adapter = this.getNextAdapter();
592
718
  const results: unknown[] = [];
593
719
 
594
- adapter.startTransaction();
720
+ await adapterStartTransaction(adapter);
595
721
  try {
596
722
  for (const params of paramSets) {
597
- results.push(adapter.execute(sql, params));
723
+ results.push(await adapterExecute(adapter, sql, params));
598
724
  }
599
- adapter.commit();
725
+ await adapterCommit(adapter);
600
726
  } catch (e) {
601
- adapter.rollback();
727
+ await adapterRollback(adapter);
602
728
  throw e;
603
729
  }
604
730
 
@@ -639,24 +765,24 @@ export class Database {
639
765
  * Used by sequenceNext() for race-safe ID generation on
640
766
  * SQLite, MySQL, MSSQL, and as a PostgreSQL fallback.
641
767
  */
642
- private ensureSequenceTable(): void {
768
+ private async ensureSequenceTable(): Promise<void> {
643
769
  const adapter = this.getNextAdapter();
644
770
 
645
- if (!adapter.tableExists("tina4_sequences")) {
771
+ if (!(await adapterTableExists(adapter, "tina4_sequences"))) {
646
772
  if (this.dbType === "mssql") {
647
- adapter.execute(
773
+ await adapterExecute(adapter,
648
774
  "CREATE TABLE tina4_sequences (" +
649
775
  "seq_name VARCHAR(200) NOT NULL PRIMARY KEY, " +
650
776
  "current_value INTEGER NOT NULL DEFAULT 0)"
651
777
  );
652
778
  } else {
653
- adapter.execute(
779
+ await adapterExecute(adapter,
654
780
  "CREATE TABLE IF NOT EXISTS tina4_sequences (" +
655
781
  "seq_name VARCHAR(200) NOT NULL PRIMARY KEY, " +
656
782
  "current_value INTEGER NOT NULL DEFAULT 0)"
657
783
  );
658
784
  }
659
- try { adapter.commit(); } catch { /* no active transaction */ }
785
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
660
786
  }
661
787
  }
662
788
 
@@ -666,12 +792,12 @@ export class Database {
666
792
  * If the sequence row doesn't exist yet, seeds it from MAX(pkColumn)
667
793
  * of the given table (or 0 if the table is empty/missing).
668
794
  */
669
- private sequenceNext(seqName: string, table?: string, pkColumn = "id"): number {
670
- this.ensureSequenceTable();
795
+ private async sequenceNext(seqName: string, table?: string, pkColumn = "id"): Promise<number> {
796
+ await this.ensureSequenceTable();
671
797
  const adapter = this.getNextAdapter();
672
798
 
673
799
  // Check if the sequence row exists
674
- const existing = adapter.fetchOne<Record<string, unknown>>(
800
+ const existing = await adapterFetchOne<Record<string, unknown>>(adapter,
675
801
  "SELECT current_value FROM tina4_sequences WHERE seq_name = ?",
676
802
  [seqName]
677
803
  );
@@ -681,7 +807,7 @@ export class Database {
681
807
  let seedValue = 0;
682
808
  if (table) {
683
809
  try {
684
- const maxRow = adapter.fetchOne<Record<string, unknown>>(
810
+ const maxRow = await adapterFetchOne<Record<string, unknown>>(adapter,
685
811
  `SELECT MAX(${pkColumn}) AS max_id FROM ${table}`
686
812
  );
687
813
  if (maxRow?.max_id != null) {
@@ -692,22 +818,22 @@ export class Database {
692
818
  }
693
819
  }
694
820
 
695
- adapter.execute(
821
+ await adapterExecute(adapter,
696
822
  "INSERT INTO tina4_sequences (seq_name, current_value) VALUES (?, ?)",
697
823
  [seqName, seedValue]
698
824
  );
699
- try { adapter.commit(); } catch { /* no active transaction */ }
825
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
700
826
  }
701
827
 
702
828
  // Atomic increment
703
- adapter.execute(
829
+ await adapterExecute(adapter,
704
830
  "UPDATE tina4_sequences SET current_value = current_value + 1 WHERE seq_name = ?",
705
831
  [seqName]
706
832
  );
707
- try { adapter.commit(); } catch { /* no active transaction */ }
833
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
708
834
 
709
835
  // Read the new value
710
- const row = adapter.fetchOne<Record<string, unknown>>(
836
+ const row = await adapterFetchOne<Record<string, unknown>>(adapter,
711
837
  "SELECT current_value FROM tina4_sequences WHERE seq_name = ?",
712
838
  [seqName]
713
839
  );
@@ -724,19 +850,12 @@ export class Database {
724
850
  * (race-safe, replaces old MAX+1).
725
851
  * - Returns 1 if the table is empty or does not exist.
726
852
  */
727
- getNextId(table: string, pkColumn = "id", generatorName?: string): number {
853
+ async getNextId(table: string, pkColumn = "id", generatorName?: string): Promise<number> {
728
854
  const adapter = this.getNextAdapter();
729
855
 
730
- // MongoDB getNextId() is not supported synchronously.
731
- // MongoDB uses ObjectId for primary keys by default.
732
- // For integer sequences, use getNextIdAsync() instead.
733
- if (this.dbType === "mongodb") {
734
- throw new Error(
735
- "getNextId() is not supported for MongoDB (async adapter). " +
736
- "MongoDB uses ObjectId for _id by default. " +
737
- "For integer sequences, use getNextIdAsync() or let MongoDB generate _id automatically.",
738
- );
739
- }
856
+ // MongoDB uses ObjectId for _id by default; integer sequences fall through
857
+ // to the tina4_sequences table strategy below (which works on Mongo too
858
+ // because the adapter implements the SQL-ish methods over collections).
740
859
 
741
860
  // Firebird — use generators (atomic)
742
861
  if (this.dbType === "firebird") {
@@ -744,12 +863,12 @@ export class Database {
744
863
 
745
864
  // Auto-create the generator if it does not exist
746
865
  try {
747
- adapter.execute(`CREATE GENERATOR ${genName}`);
866
+ await adapterExecute(adapter, `CREATE GENERATOR ${genName}`);
748
867
  } catch {
749
868
  // Generator already exists — ignore
750
869
  }
751
870
 
752
- const row = adapter.fetchOne<Record<string, unknown>>(`SELECT GEN_ID(${genName}, 1) AS NEXT_ID FROM RDB$DATABASE`);
871
+ const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT GEN_ID(${genName}, 1) AS NEXT_ID FROM RDB$DATABASE`);
753
872
  return Number(row?.NEXT_ID ?? row?.next_id ?? 1);
754
873
  }
755
874
 
@@ -757,7 +876,7 @@ export class Database {
757
876
  if (this.dbType === "postgres") {
758
877
  const seqName = generatorName ?? `${table.toLowerCase()}_${pkColumn.toLowerCase()}_seq`;
759
878
  try {
760
- const row = adapter.fetchOne<Record<string, unknown>>(`SELECT nextval('${seqName}') AS next_id`);
879
+ const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
761
880
  if (row?.next_id != null) {
762
881
  return Number(row.next_id);
763
882
  }
@@ -767,13 +886,13 @@ export class Database {
767
886
 
768
887
  // Auto-create sequence seeded from MAX
769
888
  try {
770
- const maxRow = adapter.fetchOne<Record<string, unknown>>(
889
+ const maxRow = await adapterFetchOne<Record<string, unknown>>(adapter,
771
890
  `SELECT COALESCE(MAX(${pkColumn}), 0) AS max_id FROM ${table}`
772
891
  );
773
892
  const start = maxRow?.max_id != null ? Number(maxRow.max_id) + 1 : 1;
774
- adapter.execute(`CREATE SEQUENCE ${seqName} START WITH ${start}`);
775
- try { adapter.commit(); } catch { /* no active transaction */ }
776
- const row = adapter.fetchOne<Record<string, unknown>>(`SELECT nextval('${seqName}') AS next_id`);
893
+ await adapterExecute(adapter, `CREATE SEQUENCE ${seqName} START WITH ${start}`);
894
+ try { await adapterCommit(adapter); } catch { /* no active transaction */ }
895
+ const row = await adapterFetchOne<Record<string, unknown>>(adapter, `SELECT nextval('${seqName}') AS next_id`);
777
896
  if (row?.next_id != null) {
778
897
  return Number(row.next_id);
779
898
  }
@@ -26,6 +26,9 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
26
26
  private readonly _sql?: string;
27
27
  private _columnInfoCache?: ColumnInfoResult[];
28
28
 
29
+ // Index signature so `result[0]` type-checks; the Proxy below makes it work.
30
+ [index: number]: Record<string, unknown> | undefined;
31
+
29
32
  constructor(
30
33
  records?: Record<string, unknown>[],
31
34
  columns?: string[],
@@ -43,6 +46,27 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
43
46
  this.offset = offset ?? 0;
44
47
  this._adapter = adapter;
45
48
  this._sql = sql;
49
+
50
+ // Array-like numeric index access: `result[0]` returns records[0].
51
+ // A Proxy forwards integer-string keys to the backing records array so the
52
+ // documented `const first = result[0]` works, while every method/property
53
+ // on the instance still resolves normally. Mirrors Python's __getitem__ and
54
+ // PHP's ArrayAccess on DatabaseResult.
55
+ return new Proxy(this, {
56
+ get(target, prop, receiver) {
57
+ if (typeof prop === "string" && /^-?\d+$/.test(prop)) {
58
+ const i = Number(prop);
59
+ return target.records[i < 0 ? target.records.length + i : i];
60
+ }
61
+ return Reflect.get(target, prop, receiver);
62
+ },
63
+ has(target, prop) {
64
+ if (typeof prop === "string" && /^\d+$/.test(prop)) {
65
+ return Number(prop) < target.records.length;
66
+ }
67
+ return Reflect.has(target, prop);
68
+ },
69
+ });
46
70
  }
47
71
 
48
72
  /** JSON string of records. */
@@ -15,6 +15,12 @@ export { FetchResult } from "./types.js";
15
15
  export { DatabaseResult } from "./databaseResult.js";
16
16
  export type { ColumnInfoResult } from "./databaseResult.js";
17
17
  export { Database, initDatabase, getAdapter, setAdapter, closeDatabase, parseDatabaseUrl, setNamedAdapter, getNamedAdapter, resolveDbPool, stripTrailingSemicolons } from "./database.js";
18
+ export {
19
+ adapterFetch, adapterQuery, adapterFetchOne, adapterExecute,
20
+ adapterStartTransaction, adapterCommit, adapterRollback,
21
+ adapterTableExists, adapterTables, adapterColumns, adapterCreateTable,
22
+ extractLastInsertId,
23
+ } from "./database.js";
18
24
  export type { DatabaseConfig, ParsedDatabaseUrl } from "./database.js";
19
25
  export { discoverModels } from "./model.js";
20
26
  export type { DiscoveredModel } from "./model.js";