forge-sql-orm 2.0.30 → 2.1.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 (46) hide show
  1. package/README.md +1410 -81
  2. package/dist/ForgeSQLORM.js +1456 -60
  3. package/dist/ForgeSQLORM.js.map +1 -1
  4. package/dist/ForgeSQLORM.mjs +1440 -61
  5. package/dist/ForgeSQLORM.mjs.map +1 -1
  6. package/dist/core/ForgeSQLAnalyseOperations.d.ts +1 -1
  7. package/dist/core/ForgeSQLAnalyseOperations.d.ts.map +1 -1
  8. package/dist/core/ForgeSQLCacheOperations.d.ts +119 -0
  9. package/dist/core/ForgeSQLCacheOperations.d.ts.map +1 -0
  10. package/dist/core/ForgeSQLCrudOperations.d.ts +38 -22
  11. package/dist/core/ForgeSQLCrudOperations.d.ts.map +1 -1
  12. package/dist/core/ForgeSQLORM.d.ts +248 -13
  13. package/dist/core/ForgeSQLORM.d.ts.map +1 -1
  14. package/dist/core/ForgeSQLQueryBuilder.d.ts +394 -19
  15. package/dist/core/ForgeSQLQueryBuilder.d.ts.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/lib/drizzle/extensions/additionalActions.d.ts +90 -0
  19. package/dist/lib/drizzle/extensions/additionalActions.d.ts.map +1 -0
  20. package/dist/utils/cacheContextUtils.d.ts +123 -0
  21. package/dist/utils/cacheContextUtils.d.ts.map +1 -0
  22. package/dist/utils/cacheUtils.d.ts +56 -0
  23. package/dist/utils/cacheUtils.d.ts.map +1 -0
  24. package/dist/utils/sqlUtils.d.ts +8 -0
  25. package/dist/utils/sqlUtils.d.ts.map +1 -1
  26. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts +46 -0
  27. package/dist/webtriggers/clearCacheSchedulerTrigger.d.ts.map +1 -0
  28. package/dist/webtriggers/index.d.ts +1 -0
  29. package/dist/webtriggers/index.d.ts.map +1 -1
  30. package/package.json +15 -12
  31. package/src/core/ForgeSQLAnalyseOperations.ts +1 -1
  32. package/src/core/ForgeSQLCacheOperations.ts +195 -0
  33. package/src/core/ForgeSQLCrudOperations.ts +49 -40
  34. package/src/core/ForgeSQLORM.ts +743 -34
  35. package/src/core/ForgeSQLQueryBuilder.ts +456 -20
  36. package/src/index.ts +1 -1
  37. package/src/lib/drizzle/extensions/additionalActions.ts +852 -0
  38. package/src/lib/drizzle/extensions/types.d.ts +99 -10
  39. package/src/utils/cacheContextUtils.ts +212 -0
  40. package/src/utils/cacheUtils.ts +403 -0
  41. package/src/utils/sqlUtils.ts +42 -0
  42. package/src/webtriggers/clearCacheSchedulerTrigger.ts +79 -0
  43. package/src/webtriggers/index.ts +1 -0
  44. package/dist/lib/drizzle/extensions/selectAliased.d.ts +0 -9
  45. package/dist/lib/drizzle/extensions/selectAliased.d.ts.map +0 -1
  46. package/src/lib/drizzle/extensions/selectAliased.ts +0 -72
@@ -1,10 +1,13 @@
1
- import { isTable, sql, eq, and } from "drizzle-orm";
1
+ import { isTable, sql, eq, and, getTableColumns } from "drizzle-orm";
2
2
  import { DateTime } from "luxon";
3
3
  import { isSQLWrapper } from "drizzle-orm/sql/sql";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import { getTableName } from "drizzle-orm/table";
6
+ import * as crypto from "crypto";
7
+ import { kvs, WhereConditions, Filter, FilterConditions } from "@forge/kvs";
4
8
  import { sql as sql$1, migrationRunner } from "@forge/sql";
5
9
  import { drizzle } from "drizzle-orm/mysql-proxy";
6
10
  import { customType, mysqlTable, timestamp, varchar, bigint } from "drizzle-orm/mysql-core";
7
- import { getTableName } from "drizzle-orm/table";
8
11
  const parseDateTime = (value, format) => {
9
12
  let result;
10
13
  if (value instanceof Date) {
@@ -27,6 +30,36 @@ const parseDateTime = (value, format) => {
27
30
  }
28
31
  return result;
29
32
  };
33
+ function formatDateTime(value, format) {
34
+ let dt = null;
35
+ if (value instanceof Date) {
36
+ dt = DateTime.fromJSDate(value);
37
+ } else if (typeof value === "string") {
38
+ for (const parser of [
39
+ DateTime.fromISO,
40
+ DateTime.fromRFC2822,
41
+ DateTime.fromSQL,
42
+ DateTime.fromHTTP
43
+ ]) {
44
+ dt = parser(value);
45
+ if (dt.isValid) break;
46
+ }
47
+ if (!dt?.isValid) {
48
+ const parsed = Number(value);
49
+ if (!isNaN(parsed)) {
50
+ dt = DateTime.fromMillis(parsed);
51
+ }
52
+ }
53
+ } else if (typeof value === "number") {
54
+ dt = DateTime.fromMillis(value);
55
+ } else {
56
+ throw new Error("Unsupported type");
57
+ }
58
+ if (!dt?.isValid) {
59
+ throw new Error("Invalid Date");
60
+ }
61
+ return dt.toFormat(format);
62
+ }
30
63
  function getPrimaryKeys(table) {
31
64
  const { columns, primaryKeys } = getTableMetadata(table);
32
65
  const columnPrimaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
@@ -289,6 +322,275 @@ function formatLimitOffset(limitOrOffset) {
289
322
  function nextVal(sequenceName) {
290
323
  return sql.raw(`NEXTVAL(${sequenceName})`);
291
324
  }
325
+ const CACHE_CONSTANTS = {
326
+ BATCH_SIZE: 25,
327
+ MAX_RETRY_ATTEMPTS: 3,
328
+ INITIAL_RETRY_DELAY: 1e3,
329
+ RETRY_DELAY_MULTIPLIER: 2,
330
+ DEFAULT_ENTITY_QUERY_NAME: "sql",
331
+ DEFAULT_EXPIRATION_NAME: "expiration",
332
+ DEFAULT_DATA_NAME: "data",
333
+ HASH_LENGTH: 32
334
+ };
335
+ function getCurrentTime() {
336
+ const dt = DateTime.now();
337
+ return Math.floor(dt.toSeconds());
338
+ }
339
+ function nowPlusSeconds(secondsToAdd) {
340
+ const dt = DateTime.now().plus({ seconds: secondsToAdd });
341
+ return Math.floor(dt.toSeconds());
342
+ }
343
+ function hashKey(query) {
344
+ const h = crypto.createHash("sha256");
345
+ h.update(query.sql.toLowerCase());
346
+ h.update(JSON.stringify(query.params));
347
+ return "CachedQuery_" + h.digest("hex").slice(0, CACHE_CONSTANTS.HASH_LENGTH);
348
+ }
349
+ async function deleteCacheEntriesInBatches(results, cacheEntityName) {
350
+ for (let i = 0; i < results.length; i += CACHE_CONSTANTS.BATCH_SIZE) {
351
+ const batch = results.slice(i, i + CACHE_CONSTANTS.BATCH_SIZE);
352
+ let transactionBuilder = kvs.transact();
353
+ batch.forEach((result) => {
354
+ transactionBuilder = transactionBuilder.delete(result.key, { entityName: cacheEntityName });
355
+ });
356
+ await transactionBuilder.execute();
357
+ }
358
+ }
359
+ async function clearCursorCache(tables, cursor, options) {
360
+ const cacheEntityName = options.cacheEntityName;
361
+ if (!cacheEntityName) {
362
+ throw new Error("cacheEntityName is not configured");
363
+ }
364
+ const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
365
+ let filters = new Filter();
366
+ for (const table of tables) {
367
+ const wrapIfNeeded = options.cacheWrapTable ? `\`${table}\`` : table;
368
+ filters.or(entityQueryName, FilterConditions.contains(wrapIfNeeded?.toLowerCase()));
369
+ }
370
+ let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityQueryName).filters(filters);
371
+ if (cursor) {
372
+ entityQueryBuilder = entityQueryBuilder.cursor(cursor);
373
+ }
374
+ const listResult = await entityQueryBuilder.limit(100).getMany();
375
+ if (options.logRawSqlQuery) {
376
+ console.warn(`clear cache Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
377
+ }
378
+ await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
379
+ if (listResult.nextCursor) {
380
+ return listResult.results.length + await clearCursorCache(tables, listResult.nextCursor, options);
381
+ } else {
382
+ return listResult.results.length;
383
+ }
384
+ }
385
+ async function clearExpirationCursorCache(cursor, options) {
386
+ const cacheEntityName = options.cacheEntityName;
387
+ if (!cacheEntityName) {
388
+ throw new Error("cacheEntityName is not configured");
389
+ }
390
+ const entityExpirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
391
+ let entityQueryBuilder = kvs.entity(cacheEntityName).query().index(entityExpirationName).where(WhereConditions.lessThan(Math.floor(DateTime.now().toSeconds())));
392
+ if (cursor) {
393
+ entityQueryBuilder = entityQueryBuilder.cursor(cursor);
394
+ }
395
+ const listResult = await entityQueryBuilder.limit(100).getMany();
396
+ if (options.logRawSqlQuery) {
397
+ console.warn(`clear expired Records: ${JSON.stringify(listResult.results.map((r) => r.key))}`);
398
+ }
399
+ await deleteCacheEntriesInBatches(listResult.results, cacheEntityName);
400
+ if (listResult.nextCursor) {
401
+ return listResult.results.length + await clearExpirationCursorCache(listResult.nextCursor, options);
402
+ } else {
403
+ return listResult.results.length;
404
+ }
405
+ }
406
+ async function executeWithRetry(operation, operationName) {
407
+ let attempt = 0;
408
+ let delay = CACHE_CONSTANTS.INITIAL_RETRY_DELAY;
409
+ while (attempt < CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
410
+ try {
411
+ return await operation();
412
+ } catch (err) {
413
+ console.warn(`Error during ${operationName}: ${err.message}, retry ${attempt}`, err);
414
+ attempt++;
415
+ if (attempt >= CACHE_CONSTANTS.MAX_RETRY_ATTEMPTS) {
416
+ console.error(`Error during ${operationName}: ${err.message}`, err);
417
+ throw err;
418
+ }
419
+ await new Promise((resolve) => setTimeout(resolve, delay));
420
+ delay *= CACHE_CONSTANTS.RETRY_DELAY_MULTIPLIER;
421
+ }
422
+ }
423
+ throw new Error(`Maximum retry attempts exceeded for ${operationName}`);
424
+ }
425
+ async function clearCache(schema, options) {
426
+ const tableName = getTableName(schema);
427
+ if (cacheApplicationContext.getStore()) {
428
+ cacheApplicationContext.getStore()?.tables.add(tableName);
429
+ } else {
430
+ await clearTablesCache([tableName], options);
431
+ }
432
+ }
433
+ async function clearTablesCache(tables, options) {
434
+ if (!options.cacheEntityName) {
435
+ throw new Error("cacheEntityName is not configured");
436
+ }
437
+ const startTime = DateTime.now();
438
+ let totalRecords = 0;
439
+ try {
440
+ totalRecords = await executeWithRetry(
441
+ () => clearCursorCache(tables, "", options),
442
+ "clearing cache"
443
+ );
444
+ } finally {
445
+ if (options.logRawSqlQuery) {
446
+ const duration = DateTime.now().toSeconds() - startTime.toSeconds();
447
+ console.info(`Cleared ${totalRecords} cache records in ${duration} seconds`);
448
+ }
449
+ }
450
+ }
451
+ async function clearExpiredCache(options) {
452
+ if (!options.cacheEntityName) {
453
+ throw new Error("cacheEntityName is not configured");
454
+ }
455
+ const startTime = DateTime.now();
456
+ let totalRecords = 0;
457
+ try {
458
+ totalRecords = await executeWithRetry(
459
+ () => clearExpirationCursorCache("", options),
460
+ "clearing expired cache"
461
+ );
462
+ } finally {
463
+ const duration = DateTime.now().toSeconds() - startTime.toSeconds();
464
+ console.info(`Cleared ${totalRecords} expired cache records in ${duration} seconds`);
465
+ }
466
+ }
467
+ async function getFromCache(query, options) {
468
+ if (!options.cacheEntityName) {
469
+ throw new Error("cacheEntityName is not configured");
470
+ }
471
+ const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
472
+ const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
473
+ const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
474
+ const sqlQuery = query.toSQL();
475
+ const key = hashKey(sqlQuery);
476
+ if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
477
+ if (options.logRawSqlQuery) {
478
+ console.warn(`Context contains value to clear. Skip getting from cache`);
479
+ }
480
+ return void 0;
481
+ }
482
+ try {
483
+ const cacheResult = await kvs.entity(options.cacheEntityName).get(key);
484
+ if (cacheResult && cacheResult[expirationName] >= getCurrentTime() && sqlQuery.sql.toLowerCase() === cacheResult[entityQueryName]) {
485
+ if (options.logRawSqlQuery) {
486
+ console.warn(`Get value from cache, cacheKey: ${key}`);
487
+ }
488
+ const results = cacheResult[dataName];
489
+ return JSON.parse(results);
490
+ }
491
+ } catch (error) {
492
+ console.error(`Error getting from cache: ${error.message}`, error);
493
+ }
494
+ return void 0;
495
+ }
496
+ async function setCacheResult(query, options, results, cacheTtl) {
497
+ if (!options.cacheEntityName) {
498
+ throw new Error("cacheEntityName is not configured");
499
+ }
500
+ try {
501
+ const entityQueryName = options.cacheEntityQueryName ?? CACHE_CONSTANTS.DEFAULT_ENTITY_QUERY_NAME;
502
+ const expirationName = options.cacheEntityExpirationName ?? CACHE_CONSTANTS.DEFAULT_EXPIRATION_NAME;
503
+ const dataName = options.cacheEntityDataName ?? CACHE_CONSTANTS.DEFAULT_DATA_NAME;
504
+ const sqlQuery = query.toSQL();
505
+ if (await isTableContainsTableInCacheContext(sqlQuery.sql, options)) {
506
+ if (options.logRawSqlQuery) {
507
+ console.warn(`Context contains value to clear. Skip setting from cache`);
508
+ }
509
+ return;
510
+ }
511
+ const key = hashKey(sqlQuery);
512
+ await kvs.transact().set(
513
+ key,
514
+ {
515
+ [entityQueryName]: sqlQuery.sql.toLowerCase(),
516
+ [expirationName]: nowPlusSeconds(cacheTtl),
517
+ [dataName]: JSON.stringify(results)
518
+ },
519
+ { entityName: options.cacheEntityName }
520
+ ).execute();
521
+ if (options.logRawSqlQuery) {
522
+ console.warn(`Store value to cache, cacheKey: ${key}`);
523
+ }
524
+ } catch (error) {
525
+ console.error(`Error setting cache: ${error.message}`, error);
526
+ }
527
+ }
528
+ const cacheApplicationContext = new AsyncLocalStorage();
529
+ const localCacheApplicationContext = new AsyncLocalStorage();
530
+ async function saveTableIfInsideCacheContext(table) {
531
+ const context = cacheApplicationContext.getStore();
532
+ if (context) {
533
+ const tableName = getTableName(table).toLowerCase();
534
+ context.tables.add(tableName);
535
+ }
536
+ }
537
+ async function saveQueryLocalCacheQuery(query, rows) {
538
+ const context = localCacheApplicationContext.getStore();
539
+ if (context) {
540
+ if (!context.cache) {
541
+ context.cache = {};
542
+ }
543
+ const sql2 = query;
544
+ const key = hashKey(sql2.toSQL());
545
+ context.cache[key] = {
546
+ sql: sql2.toSQL().sql.toLowerCase(),
547
+ data: rows
548
+ };
549
+ }
550
+ }
551
+ async function getQueryLocalCacheQuery(query) {
552
+ const context = localCacheApplicationContext.getStore();
553
+ if (context) {
554
+ if (!context.cache) {
555
+ context.cache = {};
556
+ }
557
+ const sql2 = query;
558
+ const key = hashKey(sql2.toSQL());
559
+ if (context.cache[key] && context.cache[key].sql === sql2.toSQL().sql.toLowerCase()) {
560
+ return context.cache[key].data;
561
+ }
562
+ }
563
+ return void 0;
564
+ }
565
+ async function evictLocalCacheQuery(table, options) {
566
+ const context = localCacheApplicationContext.getStore();
567
+ if (context) {
568
+ if (!context.cache) {
569
+ context.cache = {};
570
+ }
571
+ const tableName = getTableName(table);
572
+ const searchString = options.cacheWrapTable ? `\`${tableName}\`` : tableName;
573
+ const keyToEvicts = [];
574
+ Object.keys(context.cache).forEach((key) => {
575
+ if (context.cache[key].sql.includes(searchString)) {
576
+ keyToEvicts.push(key);
577
+ }
578
+ });
579
+ keyToEvicts.forEach((key) => delete context.cache[key]);
580
+ }
581
+ }
582
+ async function isTableContainsTableInCacheContext(sql2, options) {
583
+ const context = cacheApplicationContext.getStore();
584
+ if (!context) {
585
+ return false;
586
+ }
587
+ const tables = Array.from(context.tables);
588
+ const lowerSql = sql2.toLowerCase();
589
+ return tables.some((table) => {
590
+ const tablePattern = options.cacheWrapTable ? `\`${table}\`` : table;
591
+ return lowerSql.includes(tablePattern);
592
+ });
593
+ }
292
594
  class ForgeSQLCrudOperations {
293
595
  forgeOperations;
294
596
  options;
@@ -305,12 +607,17 @@ class ForgeSQLCrudOperations {
305
607
  * Inserts records into the database with optional versioning support.
306
608
  * If a version field exists in the schema, versioning is applied.
307
609
  *
610
+ * This method automatically handles:
611
+ * - Version field initialization for optimistic locking
612
+ * - Batch insertion for multiple records
613
+ * - Duplicate key handling with optional updates
614
+ *
308
615
  * @template T - The type of the table schema
309
- * @param {T} schema - The entity schema
310
- * @param {Partial<InferInsertModel<T>>[]} models - Array of entities to insert
311
- * @param {boolean} [updateIfExists=false] - Whether to update existing records
312
- * @returns {Promise<number>} The number of inserted rows
313
- * @throws {Error} If the insert operation fails
616
+ * @param schema - The entity schema
617
+ * @param models - Array of entities to insert
618
+ * @param updateIfExists - Whether to update existing records (default: false)
619
+ * @returns Promise that resolves to the number of inserted rows
620
+ * @throws Error if the insert operation fails
314
621
  */
315
622
  async insert(schema, models, updateIfExists = false) {
316
623
  if (!models?.length) return 0;
@@ -319,25 +626,32 @@ class ForgeSQLCrudOperations {
319
626
  const preparedModels = models.map(
320
627
  (model) => this.prepareModelWithVersion(model, versionMetadata, columns)
321
628
  );
322
- const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().insert(schema).values(preparedModels);
629
+ const queryBuilder = this.forgeOperations.insert(schema).values(preparedModels);
323
630
  const finalQuery = updateIfExists ? queryBuilder.onDuplicateKeyUpdate({
324
631
  set: Object.fromEntries(
325
632
  Object.keys(preparedModels[0]).map((key) => [key, schema[key]])
326
633
  )
327
634
  }) : queryBuilder;
328
635
  const result = await finalQuery;
636
+ await saveTableIfInsideCacheContext(schema);
329
637
  return result[0].insertId;
330
638
  }
331
639
  /**
332
640
  * Deletes a record by its primary key with optional version check.
333
641
  * If versioning is enabled, ensures the record hasn't been modified since last read.
334
642
  *
643
+ * This method automatically handles:
644
+ * - Single primary key validation
645
+ * - Optimistic locking checks if versioning is enabled
646
+ * - Version field validation before deletion
647
+ *
335
648
  * @template T - The type of the table schema
336
- * @param {unknown} id - The ID of the record to delete
337
- * @param {T} schema - The entity schema
338
- * @returns {Promise<number>} Number of affected rows
339
- * @throws {Error} If the delete operation fails
340
- * @throws {Error} If multiple primary keys are found
649
+ * @param id - The ID of the record to delete
650
+ * @param schema - The entity schema
651
+ * @returns Promise that resolves to the number of affected rows
652
+ * @throws Error if the delete operation fails
653
+ * @throws Error if multiple primary keys are found
654
+ * @throws Error if optimistic locking check fails
341
655
  */
342
656
  async deleteById(id, schema) {
343
657
  const { tableName, columns } = getTableMetadata(schema);
@@ -358,8 +672,12 @@ class ForgeSQLCrudOperations {
358
672
  conditions.push(eq(versionField, oldModel[versionMetadata.fieldName]));
359
673
  }
360
674
  }
361
- const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().delete(schema).where(and(...conditions));
675
+ const queryBuilder = this.forgeOperations.delete(schema).where(and(...conditions));
362
676
  const result = await queryBuilder;
677
+ if (versionMetadata && result[0].affectedRows === 0) {
678
+ throw new Error(`Optimistic locking failed: record with primary key ${id} has been modified`);
679
+ }
680
+ await saveTableIfInsideCacheContext(schema);
363
681
  return result[0].affectedRows;
364
682
  }
365
683
  /**
@@ -369,13 +687,19 @@ class ForgeSQLCrudOperations {
369
687
  * - Checks for concurrent modifications
370
688
  * - Increments the version on successful update
371
689
  *
690
+ * This method automatically handles:
691
+ * - Primary key validation
692
+ * - Version field retrieval and validation
693
+ * - Optimistic locking conflict detection
694
+ * - Version field incrementation
695
+ *
372
696
  * @template T - The type of the table schema
373
- * @param {Partial<InferInsertModel<T>>} entity - The entity with updated values
374
- * @param {T} schema - The entity schema
375
- * @returns {Promise<number>} Number of affected rows
376
- * @throws {Error} If the primary key is not provided
377
- * @throws {Error} If optimistic locking check fails
378
- * @throws {Error} If multiple primary keys are found
697
+ * @param entity - The entity with updated values (must include primary key)
698
+ * @param schema - The entity schema
699
+ * @returns Promise that resolves to the number of affected rows
700
+ * @throws Error if the primary key is not provided
701
+ * @throws Error if optimistic locking check fails
702
+ * @throws Error if multiple primary keys are found
379
703
  */
380
704
  async updateById(entity, schema) {
381
705
  const { tableName, columns } = getTableMetadata(schema);
@@ -405,13 +729,14 @@ class ForgeSQLCrudOperations {
405
729
  conditions.push(eq(versionField, currentVersion));
406
730
  }
407
731
  }
408
- const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().update(schema).set(updateData).where(and(...conditions));
732
+ const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(and(...conditions));
409
733
  const result = await queryBuilder;
410
734
  if (versionMetadata && result[0].affectedRows === 0) {
411
735
  throw new Error(
412
736
  `Optimistic locking failed: record with primary key ${entity[primaryKeyName]} has been modified`
413
737
  );
414
738
  }
739
+ await saveTableIfInsideCacheContext(schema);
415
740
  return result[0].affectedRows;
416
741
  }
417
742
  /**
@@ -430,8 +755,9 @@ class ForgeSQLCrudOperations {
430
755
  if (!where) {
431
756
  throw new Error("WHERE conditions must be provided");
432
757
  }
433
- const queryBuilder = this.forgeOperations.getDrizzleQueryBuilder().update(schema).set(updateData).where(where);
758
+ const queryBuilder = this.forgeOperations.update(schema).set(updateData).where(where);
434
759
  const result = await queryBuilder;
760
+ await saveTableIfInsideCacheContext(schema);
435
761
  return result[0].affectedRows;
436
762
  }
437
763
  // Helper methods
@@ -575,7 +901,7 @@ class ForgeSQLCrudOperations {
575
901
  const [versionFieldName, versionFieldColumn] = versionField;
576
902
  const primaryKeys = this.getPrimaryKeys(schema);
577
903
  const [primaryKeyName, primaryKeyColumn] = primaryKeys[0];
578
- const resultQuery = this.forgeOperations.getDrizzleQueryBuilder().select({
904
+ const resultQuery = this.forgeOperations.select({
579
905
  [primaryKeyName]: primaryKeyColumn,
580
906
  [versionFieldName]: versionFieldColumn
581
907
  }).from(schema).where(eq(primaryKeyColumn, primaryKeyValues[primaryKeyName]));
@@ -720,7 +1046,148 @@ function createForgeDriverProxy(options, logRawSqlQuery) {
720
1046
  return forgeDriver(modifiedQuery, params, method);
721
1047
  };
722
1048
  }
723
- function createAliasedSelectBuilder(db, fields, selectFn) {
1049
+ const NON_CACHE_CLEARING_ERROR_CODES = [
1050
+ "VALIDATION_ERROR",
1051
+ "CONSTRAINT_ERROR"
1052
+ ];
1053
+ const CACHE_CLEARING_ERROR_CODES = [
1054
+ "DEADLOCK",
1055
+ "LOCK_WAIT_TIMEOUT",
1056
+ "CONNECTION_ERROR"
1057
+ ];
1058
+ const NON_CACHE_CLEARING_PATTERNS = [
1059
+ /validation/i,
1060
+ /constraint/i
1061
+ ];
1062
+ const CACHE_CLEARING_PATTERNS = [
1063
+ /timeout/i,
1064
+ /connection/i
1065
+ ];
1066
+ function shouldClearCacheOnError(error) {
1067
+ if (error?.code && NON_CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
1068
+ return false;
1069
+ }
1070
+ if (error?.message && NON_CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) {
1071
+ return false;
1072
+ }
1073
+ if (error?.code && CACHE_CLEARING_ERROR_CODES.includes(error.code)) {
1074
+ return true;
1075
+ }
1076
+ if (error?.message && CACHE_CLEARING_PATTERNS.some((pattern) => pattern.test(error.message))) {
1077
+ return true;
1078
+ }
1079
+ return true;
1080
+ }
1081
+ async function handleSuccessfulExecution(rows, onfulfilled, table, options, isCached) {
1082
+ try {
1083
+ await evictLocalCacheQuery(table, options);
1084
+ await saveTableIfInsideCacheContext(table);
1085
+ if (isCached && !cacheApplicationContext.getStore()) {
1086
+ await clearCache(table, options);
1087
+ }
1088
+ const result = onfulfilled?.(rows);
1089
+ return result;
1090
+ } catch (error) {
1091
+ if (shouldClearCacheOnError(error)) {
1092
+ await evictLocalCacheQuery(table, options);
1093
+ if (isCached) {
1094
+ await clearCache(table, options).catch(() => {
1095
+ console.warn("Ignore cache clear errors");
1096
+ });
1097
+ } else {
1098
+ await saveTableIfInsideCacheContext(table);
1099
+ }
1100
+ }
1101
+ throw error;
1102
+ }
1103
+ }
1104
+ function handleFunctionCall(value, target, args, table, options, isCached) {
1105
+ const result = value.apply(target, args);
1106
+ if (typeof result === "object" && result !== null && "execute" in result) {
1107
+ return wrapCacheEvictBuilder(result, table, options, isCached);
1108
+ }
1109
+ return result;
1110
+ }
1111
+ const wrapCacheEvictBuilder = (rawBuilder, table, options, isCached) => {
1112
+ return new Proxy(rawBuilder, {
1113
+ get(target, prop, receiver) {
1114
+ if (prop === "then") {
1115
+ return (onfulfilled, onrejected) => target.execute().then(
1116
+ (rows) => handleSuccessfulExecution(rows, onfulfilled, table, options, isCached),
1117
+ onrejected
1118
+ );
1119
+ }
1120
+ const value = Reflect.get(target, prop, receiver);
1121
+ if (typeof value === "function") {
1122
+ return (...args) => handleFunctionCall(value, target, args, table, options, isCached);
1123
+ }
1124
+ return value;
1125
+ }
1126
+ });
1127
+ };
1128
+ function insertAndEvictCacheBuilder(db, table, options, isCached) {
1129
+ const builder = db.insert(table);
1130
+ return wrapCacheEvictBuilder(
1131
+ builder,
1132
+ table,
1133
+ options,
1134
+ isCached
1135
+ );
1136
+ }
1137
+ function updateAndEvictCacheBuilder(db, table, options, isCached) {
1138
+ const builder = db.update(table);
1139
+ return wrapCacheEvictBuilder(
1140
+ builder,
1141
+ table,
1142
+ options,
1143
+ isCached
1144
+ );
1145
+ }
1146
+ function deleteAndEvictCacheBuilder(db, table, options, isCached) {
1147
+ const builder = db.delete(table);
1148
+ return wrapCacheEvictBuilder(
1149
+ builder,
1150
+ table,
1151
+ options,
1152
+ isCached
1153
+ );
1154
+ }
1155
+ async function handleCachedQuery(target, options, cacheTtl, selections, aliasMap, onfulfilled, onrejected) {
1156
+ try {
1157
+ const localCached = await getQueryLocalCacheQuery(target);
1158
+ if (localCached) {
1159
+ return onfulfilled?.(localCached);
1160
+ }
1161
+ const cacheResult = await getFromCache(target, options);
1162
+ if (cacheResult) {
1163
+ return onfulfilled?.(cacheResult);
1164
+ }
1165
+ const rows = await target.execute();
1166
+ const transformed = applyFromDriverTransform(rows, selections, aliasMap);
1167
+ await saveQueryLocalCacheQuery(target, transformed);
1168
+ await setCacheResult(target, options, transformed, cacheTtl).catch((cacheError) => {
1169
+ console.warn("Cache set error:", cacheError);
1170
+ });
1171
+ return onfulfilled?.(transformed);
1172
+ } catch (error) {
1173
+ return onrejected?.(error);
1174
+ }
1175
+ }
1176
+ async function handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected) {
1177
+ try {
1178
+ const localCached = await getQueryLocalCacheQuery(target);
1179
+ if (localCached) {
1180
+ return onfulfilled?.(localCached);
1181
+ }
1182
+ const rows = await target.execute();
1183
+ const transformed = applyFromDriverTransform(rows, selections, aliasMap);
1184
+ await saveQueryLocalCacheQuery(target, transformed);
1185
+ return onfulfilled?.(transformed);
1186
+ } catch (error) {
1187
+ return onrejected?.(error);
1188
+ }
1189
+ }
1190
+ function createAliasedSelectBuilder(db, fields, selectFn, useCache, options, cacheTtl) {
724
1191
  const { selections, aliasMap } = mapSelectFieldsWithAlias(fields);
725
1192
  const builder = selectFn(selections);
726
1193
  const wrapBuilder = (rawBuilder) => {
@@ -733,10 +1200,22 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
733
1200
  };
734
1201
  }
735
1202
  if (prop === "then") {
736
- return (onfulfilled, onrejected) => target.execute().then((rows) => {
737
- const transformed = applyFromDriverTransform(rows, selections, aliasMap);
738
- return onfulfilled?.(transformed);
739
- }, onrejected);
1203
+ return (onfulfilled, onrejected) => {
1204
+ if (useCache) {
1205
+ const ttl = cacheTtl ?? options.cacheTTL ?? 120;
1206
+ return handleCachedQuery(
1207
+ target,
1208
+ options,
1209
+ ttl,
1210
+ selections,
1211
+ aliasMap,
1212
+ onfulfilled,
1213
+ onrejected
1214
+ );
1215
+ } else {
1216
+ return handleNonCachedQuery(target, selections, aliasMap, onfulfilled, onrejected);
1217
+ }
1218
+ };
740
1219
  }
741
1220
  const value = Reflect.get(target, prop, receiver);
742
1221
  if (typeof value === "function") {
@@ -754,13 +1233,122 @@ function createAliasedSelectBuilder(db, fields, selectFn) {
754
1233
  };
755
1234
  return wrapBuilder(builder);
756
1235
  }
757
- function patchDbWithSelectAliased(db) {
1236
+ const DEFAULT_OPTIONS = {
1237
+ logRawSqlQuery: false,
1238
+ disableOptimisticLocking: false,
1239
+ cacheTTL: 120,
1240
+ cacheWrapTable: true,
1241
+ cacheEntityQueryName: "sql",
1242
+ cacheEntityExpirationName: "expiration",
1243
+ cacheEntityDataName: "data"
1244
+ };
1245
+ function createRawQueryExecutor(db, options, useGlobalCache = false) {
1246
+ return async function(query, cacheTtl) {
1247
+ let sql2;
1248
+ if (isSQLWrapper(query)) {
1249
+ const sqlWrapper = query;
1250
+ sql2 = sqlWrapper.getSQL().toQuery(db.dialect);
1251
+ } else {
1252
+ sql2 = {
1253
+ sql: query,
1254
+ params: []
1255
+ };
1256
+ }
1257
+ const localCacheResult = await getQueryLocalCacheQuery(sql2);
1258
+ if (localCacheResult) {
1259
+ return localCacheResult;
1260
+ }
1261
+ if (useGlobalCache) {
1262
+ const cacheResult = await getFromCache({ toSQL: () => sql2 }, options);
1263
+ if (cacheResult) {
1264
+ return cacheResult;
1265
+ }
1266
+ }
1267
+ const results = await db.execute(query);
1268
+ await saveQueryLocalCacheQuery(sql2, results);
1269
+ if (useGlobalCache) {
1270
+ await setCacheResult(
1271
+ { toSQL: () => sql2 },
1272
+ options,
1273
+ results,
1274
+ cacheTtl ?? options.cacheTTL ?? 120
1275
+ );
1276
+ }
1277
+ return results;
1278
+ };
1279
+ }
1280
+ function patchDbWithSelectAliased(db, options) {
1281
+ const newOptions = { ...DEFAULT_OPTIONS, ...options };
758
1282
  db.selectAliased = function(fields) {
759
- return createAliasedSelectBuilder(db, fields, (selections) => db.select(selections));
1283
+ return createAliasedSelectBuilder(
1284
+ db,
1285
+ fields,
1286
+ (selections) => db.select(selections),
1287
+ false,
1288
+ newOptions
1289
+ );
1290
+ };
1291
+ db.selectAliasedCacheable = function(fields, cacheTtl) {
1292
+ return createAliasedSelectBuilder(
1293
+ db,
1294
+ fields,
1295
+ (selections) => db.select(selections),
1296
+ true,
1297
+ newOptions,
1298
+ cacheTtl
1299
+ );
760
1300
  };
761
1301
  db.selectAliasedDistinct = function(fields) {
762
- return createAliasedSelectBuilder(db, fields, (selections) => db.selectDistinct(selections));
1302
+ return createAliasedSelectBuilder(
1303
+ db,
1304
+ fields,
1305
+ (selections) => db.selectDistinct(selections),
1306
+ false,
1307
+ newOptions
1308
+ );
1309
+ };
1310
+ db.selectAliasedDistinctCacheable = function(fields, cacheTtl) {
1311
+ return createAliasedSelectBuilder(
1312
+ db,
1313
+ fields,
1314
+ (selections) => db.selectDistinct(selections),
1315
+ true,
1316
+ newOptions,
1317
+ cacheTtl
1318
+ );
1319
+ };
1320
+ db.selectFrom = function(table) {
1321
+ return db.selectAliased(getTableColumns(table)).from(table);
1322
+ };
1323
+ db.selectFromCacheable = function(table, cacheTtl) {
1324
+ return db.selectAliasedCacheable(getTableColumns(table), cacheTtl).from(table);
1325
+ };
1326
+ db.selectDistinctFrom = function(table) {
1327
+ return db.selectAliasedDistinct(getTableColumns(table)).from(table);
1328
+ };
1329
+ db.selectDistinctFromCacheable = function(table, cacheTtl) {
1330
+ return db.selectAliasedDistinctCacheable(getTableColumns(table), cacheTtl).from(table);
1331
+ };
1332
+ db.insertWithCacheContext = function(table) {
1333
+ return insertAndEvictCacheBuilder(db, table, newOptions, false);
1334
+ };
1335
+ db.insertAndEvictCache = function(table) {
1336
+ return insertAndEvictCacheBuilder(db, table, newOptions, true);
1337
+ };
1338
+ db.updateWithCacheContext = function(table) {
1339
+ return updateAndEvictCacheBuilder(db, table, newOptions, false);
1340
+ };
1341
+ db.updateAndEvictCache = function(table) {
1342
+ return updateAndEvictCacheBuilder(db, table, newOptions, true);
763
1343
  };
1344
+ db.deleteWithCacheContext = function(table) {
1345
+ return deleteAndEvictCacheBuilder(db, table, newOptions, false);
1346
+ };
1347
+ db.deleteAndEvictCache = function(table) {
1348
+ return deleteAndEvictCacheBuilder(db, table, newOptions, true);
1349
+ };
1350
+ db.executeQuery = createRawQueryExecutor(db, newOptions, false);
1351
+ db.executeQueryCacheable = createRawQueryExecutor(db, newOptions, true);
764
1352
  return db;
765
1353
  }
766
1354
  class ForgeSQLAnalyseOperation {
@@ -911,14 +1499,14 @@ class ForgeSQLAnalyseOperation {
911
1499
  * @returns {string} The SQL query for cluster statement history
912
1500
  */
913
1501
  buildClusterStatementQuery(tables, from, to) {
914
- const formatDateTime = (date) => DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
1502
+ const formatDateTime2 = (date) => DateTime.fromJSDate(date).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
915
1503
  const tableConditions = tables.map((table) => `TABLE_NAMES LIKE CONCAT(SCHEMA_NAME, '.', '%', '${table}', '%')`).join(" OR ");
916
1504
  const timeConditions = [];
917
1505
  if (from) {
918
- timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime(from)}'`);
1506
+ timeConditions.push(`SUMMARY_BEGIN_TIME >= '${formatDateTime2(from)}'`);
919
1507
  }
920
1508
  if (to) {
921
- timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime(to)}'`);
1509
+ timeConditions.push(`SUMMARY_END_TIME <= '${formatDateTime2(to)}'`);
922
1510
  }
923
1511
  let whereClauses;
924
1512
  if (tableConditions?.length) {
@@ -991,12 +1579,167 @@ class ForgeSQLAnalyseOperation {
991
1579
  return this.analyzeQueriesHistoryRaw(tableNames, fromDate, toDate);
992
1580
  }
993
1581
  }
1582
+ class ForgeSQLCacheOperations {
1583
+ options;
1584
+ forgeOperations;
1585
+ /**
1586
+ * Creates a new instance of ForgeSQLCacheOperations.
1587
+ *
1588
+ * @param options - Configuration options for the ORM
1589
+ * @param forgeOperations - The ForgeSQL operations instance
1590
+ */
1591
+ constructor(options, forgeOperations) {
1592
+ this.options = options;
1593
+ this.forgeOperations = forgeOperations;
1594
+ }
1595
+ /**
1596
+ * Evicts cache for multiple tables using Drizzle table objects.
1597
+ *
1598
+ * @param tables - Array of Drizzle table objects to clear cache for
1599
+ * @returns Promise that resolves when cache eviction is complete
1600
+ * @throws Error if cacheEntityName is not configured
1601
+ */
1602
+ async evictCacheEntities(tables) {
1603
+ if (!this.options.cacheEntityName) {
1604
+ throw new Error("cacheEntityName is not configured");
1605
+ }
1606
+ await this.evictCache(tables.map((t) => getTableName(t)));
1607
+ }
1608
+ /**
1609
+ * Evicts cache for multiple tables by their names.
1610
+ *
1611
+ * @param tables - Array of table names to clear cache for
1612
+ * @returns Promise that resolves when cache eviction is complete
1613
+ * @throws Error if cacheEntityName is not configured
1614
+ */
1615
+ async evictCache(tables) {
1616
+ if (!this.options.cacheEntityName) {
1617
+ throw new Error("cacheEntityName is not configured");
1618
+ }
1619
+ await clearTablesCache(tables, this.options);
1620
+ }
1621
+ /**
1622
+ * Inserts records with optimistic locking/versioning and automatically evicts cache.
1623
+ *
1624
+ * This method uses `modifyWithVersioning().insert()` internally, providing:
1625
+ * - Automatic version field initialization
1626
+ * - Optimistic locking support
1627
+ * - Cache eviction after successful operation
1628
+ *
1629
+ * @param schema - The table schema
1630
+ * @param models - Array of entities to insert
1631
+ * @param updateIfExists - Whether to update existing records
1632
+ * @returns Promise that resolves to the number of inserted rows
1633
+ * @throws Error if cacheEntityName is not configured
1634
+ * @throws Error if optimistic locking check fails
1635
+ */
1636
+ async insert(schema, models, updateIfExists) {
1637
+ this.validateCacheConfiguration();
1638
+ const number = await this.forgeOperations.modifyWithVersioning().insert(schema, models, updateIfExists);
1639
+ await clearCache(schema, this.options);
1640
+ return number;
1641
+ }
1642
+ /**
1643
+ * Deletes a record by ID with optimistic locking/versioning and automatically evicts cache.
1644
+ *
1645
+ * This method uses `modifyWithVersioning().deleteById()` internally, providing:
1646
+ * - Optimistic locking checks before deletion
1647
+ * - Version field validation
1648
+ * - Cache eviction after successful operation
1649
+ *
1650
+ * @param id - The ID of the record to delete
1651
+ * @param schema - The table schema
1652
+ * @returns Promise that resolves to the number of affected rows
1653
+ * @throws Error if cacheEntityName is not configured
1654
+ * @throws Error if optimistic locking check fails
1655
+ */
1656
+ async deleteById(id, schema) {
1657
+ this.validateCacheConfiguration();
1658
+ const number = await this.forgeOperations.modifyWithVersioning().deleteById(id, schema);
1659
+ await clearCache(schema, this.options);
1660
+ return number;
1661
+ }
1662
+ /**
1663
+ * Updates a record by ID with optimistic locking/versioning and automatically evicts cache.
1664
+ *
1665
+ * This method uses `modifyWithVersioning().updateById()` internally, providing:
1666
+ * - Optimistic locking checks before update
1667
+ * - Version field incrementation
1668
+ * - Cache eviction after successful operation
1669
+ *
1670
+ * @param entity - The entity with updated values (must include primary key)
1671
+ * @param schema - The table schema
1672
+ * @returns Promise that resolves to the number of affected rows
1673
+ * @throws Error if cacheEntityName is not configured
1674
+ * @throws Error if optimistic locking check fails
1675
+ */
1676
+ async updateById(entity, schema) {
1677
+ this.validateCacheConfiguration();
1678
+ const number = await this.forgeOperations.modifyWithVersioning().updateById(entity, schema);
1679
+ await clearCache(schema, this.options);
1680
+ return number;
1681
+ }
1682
+ /**
1683
+ * Updates fields based on conditions with optimistic locking/versioning and automatically evicts cache.
1684
+ *
1685
+ * This method uses `modifyWithVersioning().updateFields()` internally, providing:
1686
+ * - Optimistic locking support (if version field is configured)
1687
+ * - Version field validation and incrementation
1688
+ * - Cache eviction after successful operation
1689
+ *
1690
+ * @param updateData - The data to update
1691
+ * @param schema - The table schema
1692
+ * @param where - Optional WHERE conditions
1693
+ * @returns Promise that resolves to the number of affected rows
1694
+ * @throws Error if cacheEntityName is not configured
1695
+ * @throws Error if optimistic locking check fails
1696
+ */
1697
+ async updateFields(updateData, schema, where) {
1698
+ this.validateCacheConfiguration();
1699
+ const number = await this.forgeOperations.modifyWithVersioning().updateFields(updateData, schema, where);
1700
+ await clearCache(schema, this.options);
1701
+ return number;
1702
+ }
1703
+ /**
1704
+ * Executes a query with caching support.
1705
+ * First checks cache, if not found executes query and stores result in cache.
1706
+ *
1707
+ * @param query - The Drizzle query to execute
1708
+ * @param cacheTtl - Optional cache TTL override
1709
+ * @returns Promise that resolves to the query results
1710
+ * @throws Error if cacheEntityName is not configured
1711
+ */
1712
+ async executeQuery(query, cacheTtl) {
1713
+ this.validateCacheConfiguration();
1714
+ const sqlQuery = query;
1715
+ const cacheResult = await getFromCache(sqlQuery, this.options);
1716
+ if (cacheResult) {
1717
+ return cacheResult;
1718
+ }
1719
+ const results = await query;
1720
+ await setCacheResult(sqlQuery, this.options, results, cacheTtl ?? this.options.cacheTTL ?? 60);
1721
+ return results;
1722
+ }
1723
+ /**
1724
+ * Validates that cache configuration is properly set up.
1725
+ *
1726
+ * @throws Error if cacheEntityName is not configured
1727
+ * @private
1728
+ */
1729
+ validateCacheConfiguration() {
1730
+ if (!this.options.cacheEntityName) {
1731
+ throw new Error("cacheEntityName is not configured");
1732
+ }
1733
+ }
1734
+ }
994
1735
  class ForgeSQLORMImpl {
995
1736
  static instance = null;
996
1737
  drizzle;
997
1738
  crudOperations;
998
1739
  fetchOperations;
999
1740
  analyzeOperations;
1741
+ cacheOperations;
1742
+ options;
1000
1743
  /**
1001
1744
  * Private constructor to enforce singleton behavior.
1002
1745
  * @param options - Options for configuring ForgeSQL ORM behavior.
@@ -1005,28 +1748,185 @@ class ForgeSQLORMImpl {
1005
1748
  try {
1006
1749
  const newOptions = options ?? {
1007
1750
  logRawSqlQuery: false,
1008
- disableOptimisticLocking: false
1751
+ disableOptimisticLocking: false,
1752
+ cacheWrapTable: true,
1753
+ cacheTTL: 120,
1754
+ cacheEntityQueryName: "sql",
1755
+ cacheEntityExpirationName: "expiration",
1756
+ cacheEntityDataName: "data"
1009
1757
  };
1758
+ this.options = newOptions;
1010
1759
  if (newOptions.logRawSqlQuery) {
1011
1760
  console.debug("Initializing ForgeSQLORM...");
1012
1761
  }
1013
1762
  const proxiedDriver = createForgeDriverProxy(newOptions.hints, newOptions.logRawSqlQuery);
1014
1763
  this.drizzle = patchDbWithSelectAliased(
1015
- drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery })
1764
+ drizzle(proxiedDriver, { logger: newOptions.logRawSqlQuery }),
1765
+ newOptions
1016
1766
  );
1017
1767
  this.crudOperations = new ForgeSQLCrudOperations(this, newOptions);
1018
1768
  this.fetchOperations = new ForgeSQLSelectOperations(newOptions);
1019
1769
  this.analyzeOperations = new ForgeSQLAnalyseOperation(this);
1770
+ this.cacheOperations = new ForgeSQLCacheOperations(newOptions, this);
1020
1771
  } catch (error) {
1021
1772
  console.error("ForgeSQLORM initialization failed:", error);
1022
1773
  throw error;
1023
1774
  }
1024
1775
  }
1776
+ /**
1777
+ * Executes operations within a cache context that collects cache eviction events.
1778
+ * All clearCache calls within the context are collected and executed in batch at the end.
1779
+ * Queries executed within this context will bypass cache for tables that were marked for clearing.
1780
+ *
1781
+ * This is useful for:
1782
+ * - Batch operations that affect multiple tables
1783
+ * - Transaction-like operations where you want to clear cache only at the end
1784
+ * - Performance optimization by reducing cache clear operations
1785
+ *
1786
+ * @param cacheContext - Function containing operations that may trigger cache evictions
1787
+ * @returns Promise that resolves when all operations and cache clearing are complete
1788
+ *
1789
+ * @example
1790
+ * ```typescript
1791
+ * await forgeSQL.executeWithCacheContext(async () => {
1792
+ * await forgeSQL.modifyWithVersioning().insert(users, userData);
1793
+ * await forgeSQL.modifyWithVersioning().insert(orders, orderData);
1794
+ * // Cache for both users and orders tables will be cleared at the end
1795
+ * });
1796
+ * ```
1797
+ */
1798
+ executeWithCacheContext(cacheContext) {
1799
+ return this.executeWithCacheContextAndReturnValue(cacheContext);
1800
+ }
1801
+ /**
1802
+ * Executes operations within a cache context and returns a value.
1803
+ * All clearCache calls within the context are collected and executed in batch at the end.
1804
+ * Queries executed within this context will bypass cache for tables that were marked for clearing.
1805
+ *
1806
+ * @param cacheContext - Function containing operations that may trigger cache evictions
1807
+ * @returns Promise that resolves to the return value of the cacheContext function
1808
+ *
1809
+ * @example
1810
+ * ```typescript
1811
+ * const result = await forgeSQL.executeWithCacheContextAndReturnValue(async () => {
1812
+ * await forgeSQL.modifyWithVersioning().insert(users, userData);
1813
+ * return await forgeSQL.fetch().executeQueryOnlyOne(selectUserQuery);
1814
+ * });
1815
+ * ```
1816
+ */
1817
+ async executeWithCacheContextAndReturnValue(cacheContext) {
1818
+ return await this.executeWithLocalCacheContextAndReturnValue(
1819
+ async () => await cacheApplicationContext.run(cacheApplicationContext.getStore() ?? { tables: /* @__PURE__ */ new Set() }, async () => {
1820
+ try {
1821
+ return await cacheContext();
1822
+ } finally {
1823
+ await clearTablesCache(
1824
+ Array.from(cacheApplicationContext.getStore()?.tables ?? []),
1825
+ this.options
1826
+ );
1827
+ }
1828
+ })
1829
+ );
1830
+ }
1831
+ /**
1832
+ * Executes operations within a local cache context and returns a value.
1833
+ * This provides in-memory caching for select queries within a single request scope.
1834
+ *
1835
+ * @param cacheContext - Function containing operations that will benefit from local caching
1836
+ * @returns Promise that resolves to the return value of the cacheContext function
1837
+ */
1838
+ async executeWithLocalCacheContextAndReturnValue(cacheContext) {
1839
+ return await localCacheApplicationContext.run(localCacheApplicationContext.getStore() ?? { cache: {} }, async () => {
1840
+ return await cacheContext();
1841
+ });
1842
+ }
1843
+ /**
1844
+ * Executes operations within a local cache context.
1845
+ * This provides in-memory caching for select queries within a single request scope.
1846
+ *
1847
+ * @param cacheContext - Function containing operations that will benefit from local caching
1848
+ * @returns Promise that resolves when all operations are complete
1849
+ */
1850
+ executeWithLocalContext(cacheContext) {
1851
+ return this.executeWithLocalCacheContextAndReturnValue(cacheContext);
1852
+ }
1853
+ /**
1854
+ * Creates an insert query builder.
1855
+ *
1856
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1857
+ * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
1858
+ *
1859
+ * @param table - The table to insert into
1860
+ * @returns Insert query builder (no versioning, no cache management)
1861
+ */
1862
+ insert(table) {
1863
+ return this.drizzle.insertWithCacheContext(table);
1864
+ }
1865
+ /**
1866
+ * Creates an insert query builder that automatically evicts cache after execution.
1867
+ *
1868
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1869
+ * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
1870
+ *
1871
+ * @param table - The table to insert into
1872
+ * @returns Insert query builder with automatic cache eviction (no versioning)
1873
+ */
1874
+ insertAndEvictCache(table) {
1875
+ return this.drizzle.insertAndEvictCache(table);
1876
+ }
1877
+ /**
1878
+ * Creates an update query builder that automatically evicts cache after execution.
1879
+ *
1880
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1881
+ * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
1882
+ *
1883
+ * @param table - The table to update
1884
+ * @returns Update query builder with automatic cache eviction (no versioning)
1885
+ */
1886
+ updateAndEvictCache(table) {
1887
+ return this.drizzle.updateAndEvictCache(table);
1888
+ }
1889
+ /**
1890
+ * Creates an update query builder.
1891
+ *
1892
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1893
+ * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
1894
+ *
1895
+ * @param table - The table to update
1896
+ * @returns Update query builder (no versioning, no cache management)
1897
+ */
1898
+ update(table) {
1899
+ return this.drizzle.updateWithCacheContext(table);
1900
+ }
1901
+ /**
1902
+ * Creates a delete query builder.
1903
+ *
1904
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1905
+ * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
1906
+ *
1907
+ * @param table - The table to delete from
1908
+ * @returns Delete query builder (no versioning, no cache management)
1909
+ */
1910
+ delete(table) {
1911
+ return this.drizzle.deleteWithCacheContext(table);
1912
+ }
1913
+ /**
1914
+ * Creates a delete query builder that automatically evicts cache after execution.
1915
+ *
1916
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
1917
+ * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
1918
+ *
1919
+ * @param table - The table to delete from
1920
+ * @returns Delete query builder with automatic cache eviction (no versioning)
1921
+ */
1922
+ deleteAndEvictCache(table) {
1923
+ return this.drizzle.deleteAndEvictCache(table);
1924
+ }
1025
1925
  /**
1026
1926
  * Create the modify operations instance.
1027
1927
  * @returns modify operations.
1028
1928
  */
1029
- modify() {
1929
+ modifyWithVersioning() {
1030
1930
  return this.crudOperations;
1031
1931
  }
1032
1932
  /**
@@ -1038,13 +1938,6 @@ class ForgeSQLORMImpl {
1038
1938
  ForgeSQLORMImpl.instance ??= new ForgeSQLORMImpl(options);
1039
1939
  return ForgeSQLORMImpl.instance;
1040
1940
  }
1041
- /**
1042
- * Retrieves the CRUD operations instance.
1043
- * @returns CRUD operations.
1044
- */
1045
- crud() {
1046
- return this.modify();
1047
- }
1048
1941
  /**
1049
1942
  * Retrieves the fetch operations instance.
1050
1943
  * @returns Fetch operations.
@@ -1052,9 +1945,26 @@ class ForgeSQLORMImpl {
1052
1945
  fetch() {
1053
1946
  return this.fetchOperations;
1054
1947
  }
1948
+ /**
1949
+ * Provides query analysis capabilities including EXPLAIN ANALYZE and slow query analysis.
1950
+ * @returns {SchemaAnalyzeForgeSql} Interface for analyzing query performance
1951
+ */
1055
1952
  analyze() {
1056
1953
  return this.analyzeOperations;
1057
1954
  }
1955
+ /**
1956
+ * Provides schema-level SQL operations with optimistic locking/versioning and automatic cache eviction.
1957
+ *
1958
+ * This method returns operations that use `modifyWithVersioning()` internally, providing:
1959
+ * - Optimistic locking support
1960
+ * - Automatic version field management
1961
+ * - Cache eviction after successful operations
1962
+ *
1963
+ * @returns {ForgeSQLCacheOperations} Interface for executing versioned SQL operations with cache management
1964
+ */
1965
+ modifyWithVersioningAndEvictCache() {
1966
+ return this.cacheOperations;
1967
+ }
1058
1968
  /**
1059
1969
  * Returns a Drizzle query builder instance.
1060
1970
  *
@@ -1111,12 +2021,365 @@ class ForgeSQLORMImpl {
1111
2021
  }
1112
2022
  return this.drizzle.selectAliasedDistinct(fields);
1113
2023
  }
2024
+ /**
2025
+ * Creates a cacheable select query with unique field aliases to prevent field name collisions in joins.
2026
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
2027
+ *
2028
+ * @template TSelection - The type of the selected fields
2029
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
2030
+ * @param {number} cacheTTL - cache ttl optional default is 60 sec.
2031
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A select query builder with unique field aliases
2032
+ * @throws {Error} If fields parameter is empty
2033
+ * @example
2034
+ * ```typescript
2035
+ * await forgeSQL
2036
+ * .selectCacheable({user: users, order: orders},60)
2037
+ * .from(orders)
2038
+ * .innerJoin(users, eq(orders.userId, users.id));
2039
+ * ```
2040
+ */
2041
+ selectCacheable(fields, cacheTTL) {
2042
+ if (!fields) {
2043
+ throw new Error("fields is empty");
2044
+ }
2045
+ return this.drizzle.selectAliasedCacheable(fields, cacheTTL);
2046
+ }
2047
+ /**
2048
+ * Creates a cacheable distinct select query with unique field aliases to prevent field name collisions in joins.
2049
+ * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
2050
+ *
2051
+ * @template TSelection - The type of the selected fields
2052
+ * @param {TSelection} fields - Object containing the fields to select, with table schemas as values
2053
+ * @param {number} cacheTTL - cache ttl optional default is 60 sec.
2054
+ * @returns {MySqlSelectBuilder<TSelection, MySql2PreparedQueryHKT>} A distinct select query builder with unique field aliases
2055
+ * @throws {Error} If fields parameter is empty
2056
+ * @example
2057
+ * ```typescript
2058
+ * await forgeSQL
2059
+ * .selectDistinctCacheable({user: users, order: orders}, 60)
2060
+ * .from(orders)
2061
+ * .innerJoin(users, eq(orders.userId, users.id));
2062
+ * ```
2063
+ */
2064
+ selectDistinctCacheable(fields, cacheTTL) {
2065
+ if (!fields) {
2066
+ throw new Error("fields is empty");
2067
+ }
2068
+ return this.drizzle.selectAliasedDistinctCacheable(fields, cacheTTL);
2069
+ }
2070
+ /**
2071
+ * Creates a select query builder for all columns from a table with field aliasing support.
2072
+ * This is a convenience method that automatically selects all columns from the specified table.
2073
+ *
2074
+ * @template T - The type of the table
2075
+ * @param table - The table to select from
2076
+ * @returns Select query builder with all table columns and field aliasing support
2077
+ * @example
2078
+ * ```typescript
2079
+ * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
2080
+ * ```
2081
+ */
2082
+ selectFrom(table) {
2083
+ return this.drizzle.selectFrom(table);
2084
+ }
2085
+ /**
2086
+ * Creates a select distinct query builder for all columns from a table with field aliasing support.
2087
+ * This is a convenience method that automatically selects all distinct columns from the specified table.
2088
+ *
2089
+ * @template T - The type of the table
2090
+ * @param table - The table to select from
2091
+ * @returns Select distinct query builder with all table columns and field aliasing support
2092
+ * @example
2093
+ * ```typescript
2094
+ * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
2095
+ * ```
2096
+ */
2097
+ selectDistinctFrom(table) {
2098
+ return this.drizzle.selectDistinctFrom(table);
2099
+ }
2100
+ /**
2101
+ * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
2102
+ * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
2103
+ *
2104
+ * @template T - The type of the table
2105
+ * @param table - The table to select from
2106
+ * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
2107
+ * @returns Select query builder with all table columns, field aliasing, and caching support
2108
+ * @example
2109
+ * ```typescript
2110
+ * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
2111
+ * ```
2112
+ */
2113
+ selectCacheableFrom(table, cacheTTL) {
2114
+ return this.drizzle.selectFromCacheable(table, cacheTTL);
2115
+ }
2116
+ /**
2117
+ * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
2118
+ * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
2119
+ *
2120
+ * @template T - The type of the table
2121
+ * @param table - The table to select from
2122
+ * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
2123
+ * @returns Select distinct query builder with all table columns, field aliasing, and caching support
2124
+ * @example
2125
+ * ```typescript
2126
+ * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
2127
+ * ```
2128
+ */
2129
+ selectDistinctCacheableFrom(table, cacheTTL) {
2130
+ return this.drizzle.selectDistinctFromCacheable(table, cacheTTL);
2131
+ }
2132
+ /**
2133
+ * Executes a raw SQL query with local cache support.
2134
+ * This method provides local caching for raw SQL queries within the current invocation context.
2135
+ * Results are cached locally and will be returned from cache on subsequent identical queries.
2136
+ *
2137
+ * @param query - The SQL query to execute (SQLWrapper or string)
2138
+ * @returns Promise with query results
2139
+ * @example
2140
+ * ```typescript
2141
+ * // Using SQLWrapper
2142
+ * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
2143
+ *
2144
+ * // Using string
2145
+ * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
2146
+ * ```
2147
+ */
2148
+ execute(query) {
2149
+ return this.drizzle.executeQuery(query);
2150
+ }
2151
+ /**
2152
+ * Executes a raw SQL query with both local and global cache support.
2153
+ * This method provides comprehensive caching for raw SQL queries:
2154
+ * - Local cache: Within the current invocation context
2155
+ * - Global cache: Cross-invocation caching using @forge/kvs
2156
+ *
2157
+ * @param query - The SQL query to execute (SQLWrapper or string)
2158
+ * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
2159
+ * @returns Promise with query results
2160
+ * @example
2161
+ * ```typescript
2162
+ * // Using SQLWrapper with custom TTL
2163
+ * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
2164
+ *
2165
+ * // Using string with default TTL
2166
+ * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
2167
+ * ```
2168
+ */
2169
+ executeCacheable(query, cacheTtl) {
2170
+ return this.drizzle.executeQueryCacheable(query, cacheTtl);
2171
+ }
2172
+ /**
2173
+ * Creates a Common Table Expression (CTE) builder for complex queries.
2174
+ * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
2175
+ *
2176
+ * @returns WithBuilder for creating CTEs
2177
+ * @example
2178
+ * ```typescript
2179
+ * const withQuery = forgeSQL.$with('userStats').as(
2180
+ * forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
2181
+ * .from(users)
2182
+ * .groupBy(users.id)
2183
+ * );
2184
+ * ```
2185
+ */
2186
+ get $with() {
2187
+ return this.drizzle.$with;
2188
+ }
2189
+ /**
2190
+ * Creates a query builder that uses Common Table Expressions (CTEs).
2191
+ * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
2192
+ *
2193
+ * @param queries - Array of CTE queries created with $with()
2194
+ * @returns Query builder with CTE support
2195
+ * @example
2196
+ * ```typescript
2197
+ * const withQuery = forgeSQL.$with('userStats').as(
2198
+ * forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
2199
+ * .from(users)
2200
+ * .groupBy(users.id)
2201
+ * );
2202
+ *
2203
+ * const result = await forgeSQL.with(withQuery)
2204
+ * .select({ userId: withQuery.userId, count: withQuery.count })
2205
+ * .from(withQuery);
2206
+ * ```
2207
+ */
2208
+ with(...queries) {
2209
+ return this.drizzle.with(...queries);
2210
+ }
1114
2211
  }
1115
2212
  class ForgeSQLORM {
1116
2213
  ormInstance;
1117
2214
  constructor(options) {
1118
2215
  this.ormInstance = ForgeSQLORMImpl.getInstance(options);
1119
2216
  }
2217
+ selectCacheable(fields, cacheTTL) {
2218
+ return this.ormInstance.selectCacheable(fields, cacheTTL);
2219
+ }
2220
+ selectDistinctCacheable(fields, cacheTTL) {
2221
+ return this.ormInstance.selectDistinctCacheable(fields, cacheTTL);
2222
+ }
2223
+ /**
2224
+ * Creates a select query builder for all columns from a table with field aliasing support.
2225
+ * This is a convenience method that automatically selects all columns from the specified table.
2226
+ *
2227
+ * @template T - The type of the table
2228
+ * @param table - The table to select from
2229
+ * @returns Select query builder with all table columns and field aliasing support
2230
+ * @example
2231
+ * ```typescript
2232
+ * const users = await forgeSQL.selectFrom(userTable).where(eq(userTable.id, 1));
2233
+ * ```
2234
+ */
2235
+ selectFrom(table) {
2236
+ return this.ormInstance.getDrizzleQueryBuilder().selectFrom(table);
2237
+ }
2238
+ /**
2239
+ * Creates a select distinct query builder for all columns from a table with field aliasing support.
2240
+ * This is a convenience method that automatically selects all distinct columns from the specified table.
2241
+ *
2242
+ * @template T - The type of the table
2243
+ * @param table - The table to select from
2244
+ * @returns Select distinct query builder with all table columns and field aliasing support
2245
+ * @example
2246
+ * ```typescript
2247
+ * const uniqueUsers = await forgeSQL.selectDistinctFrom(userTable).where(eq(userTable.status, 'active'));
2248
+ * ```
2249
+ */
2250
+ selectDistinctFrom(table) {
2251
+ return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFrom(table);
2252
+ }
2253
+ /**
2254
+ * Creates a cacheable select query builder for all columns from a table with field aliasing and caching support.
2255
+ * This is a convenience method that automatically selects all columns from the specified table with caching enabled.
2256
+ *
2257
+ * @template T - The type of the table
2258
+ * @param table - The table to select from
2259
+ * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
2260
+ * @returns Select query builder with all table columns, field aliasing, and caching support
2261
+ * @example
2262
+ * ```typescript
2263
+ * const users = await forgeSQL.selectCacheableFrom(userTable, 300).where(eq(userTable.id, 1));
2264
+ * ```
2265
+ */
2266
+ selectCacheableFrom(table, cacheTTL) {
2267
+ return this.ormInstance.getDrizzleQueryBuilder().selectFromCacheable(table, cacheTTL);
2268
+ }
2269
+ /**
2270
+ * Creates a cacheable select distinct query builder for all columns from a table with field aliasing and caching support.
2271
+ * This is a convenience method that automatically selects all distinct columns from the specified table with caching enabled.
2272
+ *
2273
+ * @template T - The type of the table
2274
+ * @param table - The table to select from
2275
+ * @param cacheTTL - Optional cache TTL override (defaults to global cache TTL)
2276
+ * @returns Select distinct query builder with all table columns, field aliasing, and caching support
2277
+ * @example
2278
+ * ```typescript
2279
+ * const uniqueUsers = await forgeSQL.selectDistinctCacheableFrom(userTable, 300).where(eq(userTable.status, 'active'));
2280
+ * ```
2281
+ */
2282
+ selectDistinctCacheableFrom(table, cacheTTL) {
2283
+ return this.ormInstance.getDrizzleQueryBuilder().selectDistinctFromCacheable(table, cacheTTL);
2284
+ }
2285
+ executeWithCacheContext(cacheContext) {
2286
+ return this.ormInstance.executeWithCacheContext(cacheContext);
2287
+ }
2288
+ executeWithCacheContextAndReturnValue(cacheContext) {
2289
+ return this.ormInstance.executeWithCacheContextAndReturnValue(cacheContext);
2290
+ }
2291
+ /**
2292
+ * Executes operations within a local cache context.
2293
+ * This provides in-memory caching for select queries within a single request scope.
2294
+ *
2295
+ * @param cacheContext - Function containing operations that will benefit from local caching
2296
+ * @returns Promise that resolves when all operations are complete
2297
+ */
2298
+ executeWithLocalContext(cacheContext) {
2299
+ return this.ormInstance.executeWithLocalContext(cacheContext);
2300
+ }
2301
+ /**
2302
+ * Executes operations within a local cache context and returns a value.
2303
+ * This provides in-memory caching for select queries within a single request scope.
2304
+ *
2305
+ * @param cacheContext - Function containing operations that will benefit from local caching
2306
+ * @returns Promise that resolves to the return value of the cacheContext function
2307
+ */
2308
+ executeWithLocalCacheContextAndReturnValue(cacheContext) {
2309
+ return this.ormInstance.executeWithLocalCacheContextAndReturnValue(cacheContext);
2310
+ }
2311
+ /**
2312
+ * Creates an insert query builder.
2313
+ *
2314
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2315
+ * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
2316
+ *
2317
+ * @param table - The table to insert into
2318
+ * @returns Insert query builder (no versioning, no cache management)
2319
+ */
2320
+ insert(table) {
2321
+ return this.ormInstance.insert(table);
2322
+ }
2323
+ /**
2324
+ * Creates an insert query builder that automatically evicts cache after execution.
2325
+ *
2326
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2327
+ * For versioned inserts, use `modifyWithVersioning().insert()` or `modifyWithVersioningAndEvictCache().insert()` instead.
2328
+ *
2329
+ * @param table - The table to insert into
2330
+ * @returns Insert query builder with automatic cache eviction (no versioning)
2331
+ */
2332
+ insertAndEvictCache(table) {
2333
+ return this.ormInstance.insertAndEvictCache(table);
2334
+ }
2335
+ /**
2336
+ * Creates an update query builder.
2337
+ *
2338
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2339
+ * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
2340
+ *
2341
+ * @param table - The table to update
2342
+ * @returns Update query builder (no versioning, no cache management)
2343
+ */
2344
+ update(table) {
2345
+ return this.ormInstance.update(table);
2346
+ }
2347
+ /**
2348
+ * Creates an update query builder that automatically evicts cache after execution.
2349
+ *
2350
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2351
+ * For versioned updates, use `modifyWithVersioning().updateById()` or `modifyWithVersioningAndEvictCache().updateById()` instead.
2352
+ *
2353
+ * @param table - The table to update
2354
+ * @returns Update query builder with automatic cache eviction (no versioning)
2355
+ */
2356
+ updateAndEvictCache(table) {
2357
+ return this.ormInstance.updateAndEvictCache(table);
2358
+ }
2359
+ /**
2360
+ * Creates a delete query builder.
2361
+ *
2362
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2363
+ * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
2364
+ *
2365
+ * @param table - The table to delete from
2366
+ * @returns Delete query builder (no versioning, no cache management)
2367
+ */
2368
+ delete(table) {
2369
+ return this.ormInstance.delete(table);
2370
+ }
2371
+ /**
2372
+ * Creates a delete query builder that automatically evicts cache after execution.
2373
+ *
2374
+ * ⚠️ **IMPORTANT**: This method does NOT support optimistic locking/versioning.
2375
+ * For versioned deletes, use `modifyWithVersioning().deleteById()` or `modifyWithVersioningAndEvictCache().deleteById()` instead.
2376
+ *
2377
+ * @param table - The table to delete from
2378
+ * @returns Delete query builder with automatic cache eviction (no versioning)
2379
+ */
2380
+ deleteAndEvictCache(table) {
2381
+ return this.ormInstance.deleteAndEvictCache(table);
2382
+ }
1120
2383
  /**
1121
2384
  * Creates a select query with unique field aliases to prevent field name collisions in joins.
1122
2385
  * This is particularly useful when working with Atlassian Forge SQL, which collapses fields with the same name in joined tables.
@@ -1155,19 +2418,12 @@ class ForgeSQLORM {
1155
2418
  selectDistinct(fields) {
1156
2419
  return this.ormInstance.selectDistinct(fields);
1157
2420
  }
1158
- /**
1159
- * Proxies the `crud` method from `ForgeSQLORMImpl`.
1160
- * @returns CRUD operations.
1161
- */
1162
- crud() {
1163
- return this.ormInstance.modify();
1164
- }
1165
2421
  /**
1166
2422
  * Proxies the `modify` method from `ForgeSQLORMImpl`.
1167
2423
  * @returns Modify operations.
1168
2424
  */
1169
- modify() {
1170
- return this.ormInstance.modify();
2425
+ modifyWithVersioning() {
2426
+ return this.ormInstance.modifyWithVersioning();
1171
2427
  }
1172
2428
  /**
1173
2429
  * Proxies the `fetch` method from `ForgeSQLORMImpl`.
@@ -1183,25 +2439,107 @@ class ForgeSQLORM {
1183
2439
  analyze() {
1184
2440
  return this.ormInstance.analyze();
1185
2441
  }
2442
+ /**
2443
+ * Provides schema-level SQL cacheable operations with type safety.
2444
+ * @returns {ForgeSQLCacheOperations} Interface for executing schema-bound SQL queries
2445
+ */
2446
+ modifyWithVersioningAndEvictCache() {
2447
+ return this.ormInstance.modifyWithVersioningAndEvictCache();
2448
+ }
1186
2449
  /**
1187
2450
  * Returns a Drizzle query builder instance.
1188
2451
  *
1189
- * ⚠️ IMPORTANT: This method should be used ONLY for query building purposes.
1190
- * The returned instance should NOT be used for direct database connections or query execution.
1191
- * All database operations should be performed through Forge SQL's executeRawSQL or executeRawUpdateSQL methods.
1192
- *
1193
2452
  * @returns A Drizzle query builder instance for query construction only.
1194
2453
  */
1195
2454
  getDrizzleQueryBuilder() {
1196
2455
  return this.ormInstance.getDrizzleQueryBuilder();
1197
2456
  }
2457
+ /**
2458
+ * Executes a raw SQL query with local cache support.
2459
+ * This method provides local caching for raw SQL queries within the current invocation context.
2460
+ * Results are cached locally and will be returned from cache on subsequent identical queries.
2461
+ *
2462
+ * @param query - The SQL query to execute (SQLWrapper or string)
2463
+ * @returns Promise with query results
2464
+ * @example
2465
+ * ```typescript
2466
+ * // Using SQLWrapper
2467
+ * const result = await forgeSQL.execute(sql`SELECT * FROM users WHERE id = ${userId}`);
2468
+ *
2469
+ * // Using string
2470
+ * const result = await forgeSQL.execute("SELECT * FROM users WHERE status = 'active'");
2471
+ * ```
2472
+ */
2473
+ execute(query) {
2474
+ return this.ormInstance.getDrizzleQueryBuilder().executeQuery(query);
2475
+ }
2476
+ /**
2477
+ * Executes a raw SQL query with both local and global cache support.
2478
+ * This method provides comprehensive caching for raw SQL queries:
2479
+ * - Local cache: Within the current invocation context
2480
+ * - Global cache: Cross-invocation caching using @forge/kvs
2481
+ *
2482
+ * @param query - The SQL query to execute (SQLWrapper or string)
2483
+ * @param cacheTtl - Optional cache TTL override (defaults to global cache TTL)
2484
+ * @returns Promise with query results
2485
+ * @example
2486
+ * ```typescript
2487
+ * // Using SQLWrapper with custom TTL
2488
+ * const result = await forgeSQL.executeCacheable(sql`SELECT * FROM users WHERE id = ${userId}`, 300);
2489
+ *
2490
+ * // Using string with default TTL
2491
+ * const result = await forgeSQL.executeCacheable("SELECT * FROM users WHERE status = 'active'");
2492
+ * ```
2493
+ */
2494
+ executeCacheable(query, cacheTtl) {
2495
+ return this.ormInstance.getDrizzleQueryBuilder().executeQueryCacheable(query, cacheTtl);
2496
+ }
2497
+ /**
2498
+ * Creates a Common Table Expression (CTE) builder for complex queries.
2499
+ * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
2500
+ *
2501
+ * @returns WithBuilder for creating CTEs
2502
+ * @example
2503
+ * ```typescript
2504
+ * const withQuery = forgeSQL.$with('userStats').as(
2505
+ * forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
2506
+ * .from(users)
2507
+ * .groupBy(users.id)
2508
+ * );
2509
+ * ```
2510
+ */
2511
+ get $with() {
2512
+ return this.ormInstance.getDrizzleQueryBuilder().$with;
2513
+ }
2514
+ /**
2515
+ * Creates a query builder that uses Common Table Expressions (CTEs).
2516
+ * CTEs allow you to define temporary named result sets that exist within the scope of a single query.
2517
+ *
2518
+ * @param queries - Array of CTE queries created with $with()
2519
+ * @returns Query builder with CTE support
2520
+ * @example
2521
+ * ```typescript
2522
+ * const withQuery = forgeSQL.$with('userStats').as(
2523
+ * forgeSQL.select({ userId: users.id, count: sql<number>`count(*)` })
2524
+ * .from(users)
2525
+ * .groupBy(users.id)
2526
+ * );
2527
+ *
2528
+ * const result = await forgeSQL.with(withQuery)
2529
+ * .select({ userId: withQuery.userId, count: withQuery.count })
2530
+ * .from(withQuery);
2531
+ * ```
2532
+ */
2533
+ with(...queries) {
2534
+ return this.ormInstance.getDrizzleQueryBuilder().with(...queries);
2535
+ }
1198
2536
  }
1199
2537
  const forgeDateTimeString = customType({
1200
2538
  dataType() {
1201
2539
  return "datetime";
1202
2540
  },
1203
2541
  toDriver(value) {
1204
- return DateTime.fromJSDate(new Date(value)).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
2542
+ return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
1205
2543
  },
1206
2544
  fromDriver(value) {
1207
2545
  const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
@@ -1213,7 +2551,7 @@ const forgeTimestampString = customType({
1213
2551
  return "timestamp";
1214
2552
  },
1215
2553
  toDriver(value) {
1216
- return DateTime.fromJSDate(value).toFormat("yyyy-LL-dd'T'HH:mm:ss.SSS");
2554
+ return formatDateTime(value, "yyyy-LL-dd' 'HH:mm:ss.SSS");
1217
2555
  },
1218
2556
  fromDriver(value) {
1219
2557
  const format = "yyyy-LL-dd'T'HH:mm:ss.SSS";
@@ -1225,7 +2563,7 @@ const forgeDateString = customType({
1225
2563
  return "date";
1226
2564
  },
1227
2565
  toDriver(value) {
1228
- return DateTime.fromJSDate(value).toFormat("yyyy-LL-dd");
2566
+ return formatDateTime(value, "yyyy-LL-dd");
1229
2567
  },
1230
2568
  fromDriver(value) {
1231
2569
  const format = "yyyy-LL-dd";
@@ -1237,7 +2575,7 @@ const forgeTimeString = customType({
1237
2575
  return "time";
1238
2576
  },
1239
2577
  toDriver(value) {
1240
- return DateTime.fromJSDate(value).toFormat("HH:mm:ss.SSS");
2578
+ return formatDateTime(value, "HH:mm:ss.SSS");
1241
2579
  },
1242
2580
  fromDriver(value) {
1243
2581
  return parseDateTime(value, "HH:mm:ss.SSS");
@@ -1354,6 +2692,45 @@ async function dropTableSchemaMigrations() {
1354
2692
  return getHttpResponse(500, errorMessage);
1355
2693
  }
1356
2694
  }
2695
+ const clearCacheSchedulerTrigger = async (options) => {
2696
+ try {
2697
+ const newOptions = options ?? {
2698
+ logRawSqlQuery: false,
2699
+ disableOptimisticLocking: false,
2700
+ cacheTTL: 120,
2701
+ cacheEntityName: "cache",
2702
+ cacheEntityQueryName: "sql",
2703
+ cacheEntityExpirationName: "expiration",
2704
+ cacheEntityDataName: "data"
2705
+ };
2706
+ if (!newOptions.cacheEntityName) {
2707
+ throw new Error("cacheEntityName is not configured");
2708
+ }
2709
+ await clearExpiredCache(newOptions);
2710
+ return {
2711
+ headers: { "Content-Type": ["application/json"] },
2712
+ statusCode: 200,
2713
+ statusText: "OK",
2714
+ body: JSON.stringify({
2715
+ success: true,
2716
+ message: "Cache cleanup completed successfully",
2717
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2718
+ })
2719
+ };
2720
+ } catch (error) {
2721
+ console.error("Error during cache cleanup: ", JSON.stringify(error));
2722
+ return {
2723
+ headers: { "Content-Type": ["application/json"] },
2724
+ statusCode: 500,
2725
+ statusText: "Internal Server Error",
2726
+ body: JSON.stringify({
2727
+ success: false,
2728
+ error: error instanceof Error ? error.message : "Unknown error during cache cleanup",
2729
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2730
+ })
2731
+ };
2732
+ }
2733
+ };
1357
2734
  const getHttpResponse = (statusCode, body) => {
1358
2735
  let statusText = "";
1359
2736
  if (statusCode === 200) {
@@ -1373,6 +2750,7 @@ export {
1373
2750
  ForgeSQLSelectOperations,
1374
2751
  applyFromDriverTransform,
1375
2752
  applySchemaMigrations,
2753
+ clearCacheSchedulerTrigger,
1376
2754
  ForgeSQLORM as default,
1377
2755
  dropSchemaMigrations,
1378
2756
  dropTableSchemaMigrations,
@@ -1383,6 +2761,7 @@ export {
1383
2761
  forgeSystemTables,
1384
2762
  forgeTimeString,
1385
2763
  forgeTimestampString,
2764
+ formatDateTime,
1386
2765
  formatLimitOffset,
1387
2766
  generateDropTableStatements,
1388
2767
  getHttpResponse,