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