@databricks/appkit 0.33.0 → 0.34.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.
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.33.0";
3
+ var version = "0.34.1";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -74,6 +74,14 @@ declare class CacheManager {
74
74
  * @returns Promise of the value or null if not found or expired
75
75
  */
76
76
  get<T>(key: string): Promise<T | null>;
77
+ /**
78
+ * Get a cached entry only if it has not expired.
79
+ * Returns null on miss or expired (and deletes the expired entry).
80
+ *
81
+ * Storage implementations return entries unconditionally — expiry handling
82
+ * lives at the CacheManager layer.
83
+ */
84
+ private getValid;
77
85
  /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */
78
86
  private maybeCleanup;
79
87
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;AA4BA;;;;;;;;;;;cAAa,YAAA;EAAA,wBACa,uBAAA;EAAA,iBACP,IAAA;EAAA,eACF,QAAA;EAAA,eACA,WAAA;EAAA,QAEP,OAAA;EAAA,QACA,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,iBAAA;EAAA,QACA,kBAAA;EAAA,QAEA,SAAA;EAAA,QACA,gBAAA;EAAA,QAKD,WAAA,CAAA;EAhBU;;;;;;EAAA,OA6CV,eAAA,CAAA,GAAmB,YAAA;EArClB;;;;;;;EAAA,OAuDK,WAAA,CACX,UAAA,GAAa,OAAA,CAAQ,WAAA,IACpB,OAAA,CAAQ,YAAA;EADY;;;;;;;;;;;;EAAA,eA8BF,MAAA;EA+DnB;;;;;;;;EAJI,YAAA,GAAA,CACJ,GAAA,gCACA,EAAA,QAAU,OAAA,CAAQ,CAAA,GAClB,OAAA,UACA,OAAA;IAAY,GAAA;EAAA,IACX,OAAA,CAAQ,CAAA;EAqKT;;;;;EAjDI,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EAiE7B;EAAA,QAhDE,YAAA;EAgDmB;;;;;;;EAjBrB,GAAA,GAAA,CACJ,GAAA,UACA,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,GAAA;EAAA,IACX,OAAA;EAuDY;;;;;EA1CT,MAAA,CAAO,GAAA,WAAc,OAAA;;EAMrB,KAAA,CAAA,GAAS,OAAA;;;;;;EAUT,GAAA,CAAI,GAAA,WAAc,OAAA;;;;;;;EAmBxB,WAAA,CAAY,KAAA,gCAAqC,OAAA;;EAO3C,KAAA,CAAA,GAAS,OAAA;;;;;EAQT,gBAAA,CAAA,GAAoB,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"mappings":";;;;;;;AA4BA;;;;;;;;;;;cAAa,YAAA;EAAA,wBACa,uBAAA;EAAA,iBACP,IAAA;EAAA,eACF,QAAA;EAAA,eACA,WAAA;EAAA,QAEP,OAAA;EAAA,QACA,MAAA;EAAA,QACA,gBAAA;EAAA,QACA,iBAAA;EAAA,QACA,kBAAA;EAAA,QAEA,SAAA;EAAA,QACA,gBAAA;EAAA,QAKD,WAAA,CAAA;EAhBU;;;;;;EAAA,OA6CV,eAAA,CAAA,GAAmB,YAAA;EArClB;;;;;;;EAAA,OAuDK,WAAA,CACX,UAAA,GAAa,OAAA,CAAQ,WAAA,IACpB,OAAA,CAAQ,YAAA;EADY;;;;;;;;;;;;EAAA,eA8BF,MAAA;EA+DnB;;;;;;;;EAJI,YAAA,GAAA,CACJ,GAAA,gCACA,EAAA,QAAU,OAAA,CAAQ,CAAA,GAClB,OAAA,UACA,OAAA;IAAY,GAAA;EAAA,IACX,OAAA,CAAQ,CAAA;EA+KD;;;;;EA5DJ,GAAA,GAAA,CAAO,GAAA,WAAc,OAAA,CAAQ,CAAA;EAgEhC;;;;;;;EAAA,QA/CW,QAAA;EA4EU;EAAA,QAhEhB,YAAA;EA6EI;;;;;;;EA9CN,GAAA,GAAA,CACJ,GAAA,UACA,KAAA,EAAO,CAAA,EACP,OAAA;IAAY,GAAA;EAAA,IACX,OAAA;;;;;;EAaG,MAAA,CAAO,GAAA,WAAc,OAAA;;EAMrB,KAAA,CAAA,GAAS,OAAA;;;;;;EAUT,GAAA,CAAI,GAAA,WAAc,OAAA;;;;;;;EAaxB,WAAA,CAAY,KAAA,gCAAqC,OAAA;;EAO3C,KAAA,CAAA,GAAS,OAAA;;;;;EAQT,gBAAA,CAAA,GAAoB,OAAA;AAAA"}
@@ -146,7 +146,7 @@ var CacheManager = class CacheManager {
146
146
  "cache.persistent": this.storage.isPersistent()
147
147
  } }, async (span) => {
148
148
  try {
149
- const cached = await this.storage.get(cacheKey);
149
+ const cached = await this.getValid(cacheKey);
150
150
  if (cached !== null) {
151
151
  span.setAttribute("cache.hit", true);
152
152
  span.setStatus({ code: SpanStatusCode.OK });
@@ -221,13 +221,23 @@ var CacheManager = class CacheManager {
221
221
  async get(key) {
222
222
  if (!this.config.enabled) return null;
223
223
  this.maybeCleanup();
224
+ return (await this.getValid(key))?.value ?? null;
225
+ }
226
+ /**
227
+ * Get a cached entry only if it has not expired.
228
+ * Returns null on miss or expired (and deletes the expired entry).
229
+ *
230
+ * Storage implementations return entries unconditionally — expiry handling
231
+ * lives at the CacheManager layer.
232
+ */
233
+ async getValid(key) {
224
234
  const entry = await this.storage.get(key);
225
235
  if (!entry) return null;
226
236
  if (Date.now() > entry.expiry) {
227
237
  await this.storage.delete(key);
228
238
  return null;
229
239
  }
230
- return entry.value;
240
+ return entry;
231
241
  }
232
242
  /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */
233
243
  maybeCleanup() {
@@ -282,13 +292,7 @@ var CacheManager = class CacheManager {
282
292
  */
283
293
  async has(key) {
284
294
  if (!this.config.enabled) return false;
285
- const entry = await this.storage.get(key);
286
- if (!entry) return false;
287
- if (Date.now() > entry.expiry) {
288
- await this.storage.delete(key);
289
- return false;
290
- }
291
- return true;
295
+ return await this.getValid(key) !== null;
292
296
  }
293
297
  /**
294
298
  * Generate a cache key
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n // check if the value is in the cache\n const cached = await this.storage.get<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value as T;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // Preserve AppKit errors and Databricks API errors (with status codes)\n // so route handlers can map them to proper HTTP responses.\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry.value as T;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.storage.get(key);\n if (!entry) return false;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return false;\n }\n return true;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { ApiError, WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { CacheConfig, CacheEntry, CacheStorage } from \"shared\";\nimport { createLakebasePool } from \"../connectors/lakebase\";\nimport { AppKitError, ExecutionError, InitializationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { Counter, TelemetryProvider } from \"../telemetry\";\nimport { SpanStatusCode, TelemetryManager } from \"../telemetry\";\nimport { deepMerge } from \"../utils\";\nimport { cacheDefaults } from \"./defaults\";\nimport { InMemoryStorage, PersistentStorage } from \"./storage\";\n\nconst logger = createLogger(\"cache\");\n\n/**\n * Cache manager class to handle cache operations.\n * Can be used with in-memory storage or persistent storage (Lakebase).\n *\n * The cache is automatically initialized by AppKit. Use `getInstanceSync()` to access\n * the singleton instance after initialization.\n *\n * @internal\n * @example\n * ```typescript\n * const cache = CacheManager.getInstanceSync();\n * const result = await cache.getOrExecute([\"users\", userId], () => fetchUser(userId), userKey);\n * ```\n */\nexport class CacheManager {\n private static readonly MIN_CLEANUP_INTERVAL_MS = 60_000;\n private readonly name: string = \"cache-manager\";\n private static instance: CacheManager | null = null;\n private static initPromise: Promise<CacheManager> | null = null;\n\n private storage: CacheStorage;\n private config: CacheConfig;\n private inFlightRequests: Map<string, Promise<unknown>>;\n private cleanupInProgress: boolean;\n private lastCleanupAttempt: number;\n\n private telemetry: TelemetryProvider;\n private telemetryMetrics: {\n cacheHitCount: Counter;\n cacheMissCount: Counter;\n };\n\n private constructor(storage: CacheStorage, config: CacheConfig) {\n this.storage = storage;\n this.config = config;\n this.inFlightRequests = new Map();\n this.cleanupInProgress = false;\n this.lastCleanupAttempt = 0;\n\n this.telemetry = TelemetryManager.getProvider(\n this.name,\n this.config.telemetry,\n );\n this.telemetryMetrics = {\n cacheHitCount: this.telemetry.getMeter().createCounter(\"cache.hit\", {\n description: \"Total number of cache hits\",\n unit: \"1\",\n }),\n cacheMissCount: this.telemetry.getMeter().createCounter(\"cache.miss\", {\n description: \"Total number of cache misses\",\n unit: \"1\",\n }),\n };\n }\n\n /**\n * Get the singleton instance of the cache manager (sync version).\n *\n * Throws if not initialized - ensure AppKit.create() has completed first.\n * @returns CacheManager instance\n */\n static getInstanceSync(): CacheManager {\n if (!CacheManager.instance) {\n throw InitializationError.notInitialized(\n \"CacheManager\",\n \"Ensure AppKit.create() has completed before accessing the cache\",\n );\n }\n\n return CacheManager.instance;\n }\n\n /**\n * Initialize and get the singleton instance of the cache manager.\n * Called internally by AppKit - prefer `getInstanceSync()` for plugin access.\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n * @internal\n */\n static async getInstance(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n if (CacheManager.instance) {\n return CacheManager.instance;\n }\n\n if (!CacheManager.initPromise) {\n CacheManager.initPromise = CacheManager.create(userConfig).then(\n (instance) => {\n CacheManager.instance = instance;\n return instance;\n },\n );\n }\n\n return CacheManager.initPromise;\n }\n\n /**\n * Create a new cache manager instance\n *\n * Storage selection logic:\n * 1. If `storage` provided and healthy → use provided storage\n * 2. If `storage` provided but unhealthy → fallback to InMemory (or disable if strictPersistence)\n * 3. If no `storage` provided and Lakebase available → use Lakebase\n * 4. If no `storage` provided and Lakebase unavailable → fallback to InMemory (or disable if strictPersistence)\n *\n * @param userConfig - User configuration for the cache manager\n * @returns CacheManager instance\n */\n private static async create(\n userConfig?: Partial<CacheConfig>,\n ): Promise<CacheManager> {\n const config = deepMerge(cacheDefaults, userConfig);\n\n if (config.storage) {\n const isHealthy = await config.storage.healthCheck();\n if (isHealthy) {\n return new CacheManager(config.storage, config);\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n // try to use lakebase storage\n try {\n const workspaceClient = new WorkspaceClient({});\n const pool = createLakebasePool({ workspaceClient });\n const persistentStorage = new PersistentStorage(config, pool);\n\n const isHealthy = await persistentStorage.healthCheck();\n if (isHealthy) {\n await persistentStorage.initialize();\n return new CacheManager(persistentStorage, config);\n }\n\n // Health check failed, close the pool and fallback\n await pool.end();\n } catch {\n // lakebase unavailable, continue with in-memory storage\n }\n\n if (config.strictPersistence) {\n const disabledConfig = { ...config, enabled: false };\n return new CacheManager(\n new InMemoryStorage(disabledConfig),\n disabledConfig,\n );\n }\n\n return new CacheManager(new InMemoryStorage(config), config);\n }\n\n /**\n * Get or execute a function and cache the result\n * @param key - Cache key\n * @param fn - Function to execute\n * @param userKey - User key\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async getOrExecute<T>(\n key: (string | number | object)[],\n fn: () => Promise<T>,\n userKey: string,\n options?: { ttl?: number },\n ): Promise<T> {\n if (!this.config.enabled) return fn();\n\n const cacheKey = this.generateKey(key, userKey);\n\n return this.telemetry.startActiveSpan(\n \"cache.getOrExecute\",\n {\n attributes: {\n \"cache.key\": cacheKey,\n \"cache.enabled\": this.config.enabled,\n \"cache.persistent\": this.storage.isPersistent(),\n },\n },\n async (span) => {\n try {\n const cached = await this.getValid<T>(cacheKey);\n if (cached !== null) {\n span.setAttribute(\"cache.hit\", true);\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n });\n\n return cached.value;\n }\n\n // check if the value is being processed by another request\n const inFlight = this.inFlightRequests.get(cacheKey);\n if (inFlight) {\n span.setAttribute(\"cache.hit\", true);\n span.setAttribute(\"cache.deduplication\", true);\n span.addEvent(\"cache.deduplication_used\", {\n \"cache.key\": cacheKey,\n });\n span.setStatus({ code: SpanStatusCode.OK });\n this.telemetryMetrics.cacheHitCount.add(1, {\n \"cache.key\": cacheKey,\n \"cache.deduplication\": \"true\",\n });\n\n logger.event()?.setExecution({\n cache_hit: true,\n cache_key: cacheKey,\n cache_deduplication: true,\n });\n\n span.end();\n return inFlight as Promise<T>;\n }\n\n // cache miss - execute function\n span.setAttribute(\"cache.hit\", false);\n span.addEvent(\"cache.miss\", { \"cache.key\": cacheKey });\n this.telemetryMetrics.cacheMissCount.add(1, {\n \"cache.key\": cacheKey,\n });\n\n logger.event()?.setExecution({\n cache_hit: false,\n cache_key: cacheKey,\n });\n\n const promise = fn()\n .then(async (result) => {\n await this.set(cacheKey, result, options);\n span.addEvent(\"cache.value_stored\", {\n \"cache.key\": cacheKey,\n \"cache.ttl\": options?.ttl ?? this.config.ttl ?? 3600,\n });\n return result;\n })\n .catch((error) => {\n span.recordException(error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n // Preserve AppKit errors and Databricks API errors (with status codes)\n // so route handlers can map them to proper HTTP responses.\n if (error instanceof AppKitError || error instanceof ApiError) {\n throw error;\n }\n throw ExecutionError.statementFailed(\n error instanceof Error ? error.message : String(error),\n );\n })\n .finally(() => {\n this.inFlightRequests.delete(cacheKey);\n });\n\n this.inFlightRequests.set(cacheKey, promise);\n\n const result = await promise;\n span.setStatus({ code: SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus({ code: SpanStatusCode.ERROR });\n throw error;\n } finally {\n span.end();\n }\n },\n { name: this.name, includePrefix: true },\n );\n }\n\n /**\n * Get a cached value\n * @param key - Cache key\n * @returns Promise of the value or null if not found or expired\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.config.enabled) return null;\n\n // probabilistic cleanup trigger\n this.maybeCleanup();\n\n const entry = await this.getValid<T>(key);\n return entry?.value ?? null;\n }\n\n /**\n * Get a cached entry only if it has not expired.\n * Returns null on miss or expired (and deletes the expired entry).\n *\n * Storage implementations return entries unconditionally — expiry handling\n * lives at the CacheManager layer.\n */\n private async getValid<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = await this.storage.get<T>(key);\n if (!entry) return null;\n\n if (Date.now() > entry.expiry) {\n await this.storage.delete(key);\n return null;\n }\n return entry;\n }\n\n /** Probabilistically trigger cleanup of expired entries (fire-and-forget) */\n private maybeCleanup(): void {\n if (this.cleanupInProgress) return;\n if (!this.storage.isPersistent()) return;\n const now = Date.now();\n if (now - this.lastCleanupAttempt < CacheManager.MIN_CLEANUP_INTERVAL_MS)\n return;\n\n const probability = this.config.cleanupProbability ?? 0.01;\n\n if (Math.random() > probability) return;\n\n this.lastCleanupAttempt = now;\n\n this.cleanupInProgress = true;\n (this.storage as PersistentStorage)\n .cleanupExpired()\n .catch((error) => {\n logger.debug(\"Error cleaning up expired entries: %O\", error);\n })\n .finally(() => {\n this.cleanupInProgress = false;\n });\n }\n\n /**\n * Set a value in the cache\n * @param key - Cache key\n * @param value - Value to set\n * @param options - Options for the cache\n * @returns Promise of the result\n */\n async set<T>(\n key: string,\n value: T,\n options?: { ttl?: number },\n ): Promise<void> {\n if (!this.config.enabled) return;\n\n const ttl = options?.ttl ?? this.config.ttl ?? 3600;\n const expiryTime = Date.now() + ttl * 1000;\n await this.storage.set(key, { value, expiry: expiryTime });\n }\n\n /**\n * Delete a value from the cache\n * @param key - Cache key\n * @returns Promise of the result\n */\n async delete(key: string): Promise<void> {\n if (!this.config.enabled) return;\n await this.storage.delete(key);\n }\n\n /** Clear the cache */\n async clear(): Promise<void> {\n await this.storage.clear();\n this.inFlightRequests.clear();\n }\n\n /**\n * Check if a value exists in the cache\n * @param key - Cache key\n * @returns Promise of true if the value exists, false otherwise\n */\n async has(key: string): Promise<boolean> {\n if (!this.config.enabled) return false;\n\n const entry = await this.getValid(key);\n return entry !== null;\n }\n\n /**\n * Generate a cache key\n * @param parts - Parts of the key\n * @param userKey - User key\n * @returns Cache key\n */\n generateKey(parts: (string | number | object)[], userKey: string): string {\n const allParts = [userKey, ...parts];\n const serialized = JSON.stringify(allParts);\n return createHash(\"sha256\").update(serialized).digest(\"hex\");\n }\n\n /** Close the cache */\n async close(): Promise<void> {\n await this.storage.close();\n }\n\n /**\n * Check if the storage is healthy\n * @returns Promise of true if the storage is healthy, false otherwise\n */\n async isStorageHealthy(): Promise<boolean> {\n return this.storage.healthCheck();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;aAI6E;AAQ7E,MAAM,SAAS,aAAa,QAAQ;;;;;;;;;;;;;;;AAgBpC,IAAa,eAAb,MAAa,aAAa;CACxB,OAAwB,0BAA0B;CAClD,AAAiB,OAAe;CAChC,OAAe,WAAgC;CAC/C,OAAe,cAA4C;CAE3D,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,AAAQ;CACR,AAAQ;CAKR,AAAQ,YAAY,SAAuB,QAAqB;AAC9D,OAAK,UAAU;AACf,OAAK,SAAS;AACd,OAAK,mCAAmB,IAAI,KAAK;AACjC,OAAK,oBAAoB;AACzB,OAAK,qBAAqB;AAE1B,OAAK,YAAY,iBAAiB,YAChC,KAAK,MACL,KAAK,OAAO,UACb;AACD,OAAK,mBAAmB;GACtB,eAAe,KAAK,UAAU,UAAU,CAAC,cAAc,aAAa;IAClE,aAAa;IACb,MAAM;IACP,CAAC;GACF,gBAAgB,KAAK,UAAU,UAAU,CAAC,cAAc,cAAc;IACpE,aAAa;IACb,MAAM;IACP,CAAC;GACH;;;;;;;;CASH,OAAO,kBAAgC;AACrC,MAAI,CAAC,aAAa,SAChB,OAAM,oBAAoB,eACxB,gBACA,kEACD;AAGH,SAAO,aAAa;;;;;;;;;CAUtB,aAAa,YACX,YACuB;AACvB,MAAI,aAAa,SACf,QAAO,aAAa;AAGtB,MAAI,CAAC,aAAa,YAChB,cAAa,cAAc,aAAa,OAAO,WAAW,CAAC,MACxD,aAAa;AACZ,gBAAa,WAAW;AACxB,UAAO;IAEV;AAGH,SAAO,aAAa;;;;;;;;;;;;;;CAetB,aAAqB,OACnB,YACuB;EACvB,MAAM,SAAS,UAAU,eAAe,WAAW;AAEnD,MAAI,OAAO,SAAS;AAElB,OADkB,MAAM,OAAO,QAAQ,aAAa,CAElD,QAAO,IAAI,aAAa,OAAO,SAAS,OAAO;AAGjD,OAAI,OAAO,mBAAmB;IAC5B,MAAM,iBAAiB;KAAE,GAAG;KAAQ,SAAS;KAAO;AACpD,WAAO,IAAI,aACT,IAAI,gBAAgB,eAAe,EACnC,eACD;;AAGH,UAAO,IAAI,aAAa,IAAI,gBAAgB,OAAO,EAAE,OAAO;;AAI9D,MAAI;GAEF,MAAM,OAAO,mBAAmB,EAAE,iBADV,IAAI,gBAAgB,EAAE,CAAC,EACI,CAAC;GACpD,MAAM,oBAAoB,IAAI,kBAAkB,QAAQ,KAAK;AAG7D,OADkB,MAAM,kBAAkB,aAAa,EACxC;AACb,UAAM,kBAAkB,YAAY;AACpC,WAAO,IAAI,aAAa,mBAAmB,OAAO;;AAIpD,SAAM,KAAK,KAAK;UACV;AAIR,MAAI,OAAO,mBAAmB;GAC5B,MAAM,iBAAiB;IAAE,GAAG;IAAQ,SAAS;IAAO;AACpD,UAAO,IAAI,aACT,IAAI,gBAAgB,eAAe,EACnC,eACD;;AAGH,SAAO,IAAI,aAAa,IAAI,gBAAgB,OAAO,EAAE,OAAO;;;;;;;;;;CAW9D,MAAM,aACJ,KACA,IACA,SACA,SACY;AACZ,MAAI,CAAC,KAAK,OAAO,QAAS,QAAO,IAAI;EAErC,MAAM,WAAW,KAAK,YAAY,KAAK,QAAQ;AAE/C,SAAO,KAAK,UAAU,gBACpB,sBACA,EACE,YAAY;GACV,aAAa;GACb,iBAAiB,KAAK,OAAO;GAC7B,oBAAoB,KAAK,QAAQ,cAAc;GAChD,EACF,EACD,OAAO,SAAS;AACd,OAAI;IACF,MAAM,SAAS,MAAM,KAAK,SAAY,SAAS;AAC/C,QAAI,WAAW,MAAM;AACnB,UAAK,aAAa,aAAa,KAAK;AACpC,UAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,UAAK,iBAAiB,cAAc,IAAI,GAAG,EACzC,aAAa,UACd,CAAC;AAEF,YAAO,OAAO,EAAE,aAAa;MAC3B,WAAW;MACX,WAAW;MACZ,CAAC;AAEF,YAAO,OAAO;;IAIhB,MAAM,WAAW,KAAK,iBAAiB,IAAI,SAAS;AACpD,QAAI,UAAU;AACZ,UAAK,aAAa,aAAa,KAAK;AACpC,UAAK,aAAa,uBAAuB,KAAK;AAC9C,UAAK,SAAS,4BAA4B,EACxC,aAAa,UACd,CAAC;AACF,UAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,UAAK,iBAAiB,cAAc,IAAI,GAAG;MACzC,aAAa;MACb,uBAAuB;MACxB,CAAC;AAEF,YAAO,OAAO,EAAE,aAAa;MAC3B,WAAW;MACX,WAAW;MACX,qBAAqB;MACtB,CAAC;AAEF,UAAK,KAAK;AACV,YAAO;;AAIT,SAAK,aAAa,aAAa,MAAM;AACrC,SAAK,SAAS,cAAc,EAAE,aAAa,UAAU,CAAC;AACtD,SAAK,iBAAiB,eAAe,IAAI,GAAG,EAC1C,aAAa,UACd,CAAC;AAEF,WAAO,OAAO,EAAE,aAAa;KAC3B,WAAW;KACX,WAAW;KACZ,CAAC;IAEF,MAAM,UAAU,IAAI,CACjB,KAAK,OAAO,WAAW;AACtB,WAAM,KAAK,IAAI,UAAU,QAAQ,QAAQ;AACzC,UAAK,SAAS,sBAAsB;MAClC,aAAa;MACb,aAAa,SAAS,OAAO,KAAK,OAAO,OAAO;MACjD,CAAC;AACF,YAAO;MACP,CACD,OAAO,UAAU;AAChB,UAAK,gBAAgB,MAAM;AAC3B,UAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;AAG9C,SAAI,iBAAiB,eAAe,iBAAiB,SACnD,OAAM;AAER,WAAM,eAAe,gBACnB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CACvD;MACD,CACD,cAAc;AACb,UAAK,iBAAiB,OAAO,SAAS;MACtC;AAEJ,SAAK,iBAAiB,IAAI,UAAU,QAAQ;IAE5C,MAAM,SAAS,MAAM;AACrB,SAAK,UAAU,EAAE,MAAM,eAAe,IAAI,CAAC;AAC3C,WAAO;YACA,OAAO;AACd,SAAK,gBAAgB,MAAe;AACpC,SAAK,UAAU,EAAE,MAAM,eAAe,OAAO,CAAC;AAC9C,UAAM;aACE;AACR,SAAK,KAAK;;KAGd;GAAE,MAAM,KAAK;GAAM,eAAe;GAAM,CACzC;;;;;;;CAQH,MAAM,IAAO,KAAgC;AAC3C,MAAI,CAAC,KAAK,OAAO,QAAS,QAAO;AAGjC,OAAK,cAAc;AAGnB,UADc,MAAM,KAAK,SAAY,IAAI,GAC3B,SAAS;;;;;;;;;CAUzB,MAAc,SAAY,KAA4C;EACpE,MAAM,QAAQ,MAAM,KAAK,QAAQ,IAAO,IAAI;AAC5C,MAAI,CAAC,MAAO,QAAO;AAEnB,MAAI,KAAK,KAAK,GAAG,MAAM,QAAQ;AAC7B,SAAM,KAAK,QAAQ,OAAO,IAAI;AAC9B,UAAO;;AAET,SAAO;;;CAIT,AAAQ,eAAqB;AAC3B,MAAI,KAAK,kBAAmB;AAC5B,MAAI,CAAC,KAAK,QAAQ,cAAc,CAAE;EAClC,MAAM,MAAM,KAAK,KAAK;AACtB,MAAI,MAAM,KAAK,qBAAqB,aAAa,wBAC/C;EAEF,MAAM,cAAc,KAAK,OAAO,sBAAsB;AAEtD,MAAI,KAAK,QAAQ,GAAG,YAAa;AAEjC,OAAK,qBAAqB;AAE1B,OAAK,oBAAoB;AACzB,EAAC,KAAK,QACH,gBAAgB,CAChB,OAAO,UAAU;AAChB,UAAO,MAAM,yCAAyC,MAAM;IAC5D,CACD,cAAc;AACb,QAAK,oBAAoB;IACzB;;;;;;;;;CAUN,MAAM,IACJ,KACA,OACA,SACe;AACf,MAAI,CAAC,KAAK,OAAO,QAAS;EAE1B,MAAM,MAAM,SAAS,OAAO,KAAK,OAAO,OAAO;EAC/C,MAAM,aAAa,KAAK,KAAK,GAAG,MAAM;AACtC,QAAM,KAAK,QAAQ,IAAI,KAAK;GAAE;GAAO,QAAQ;GAAY,CAAC;;;;;;;CAQ5D,MAAM,OAAO,KAA4B;AACvC,MAAI,CAAC,KAAK,OAAO,QAAS;AAC1B,QAAM,KAAK,QAAQ,OAAO,IAAI;;;CAIhC,MAAM,QAAuB;AAC3B,QAAM,KAAK,QAAQ,OAAO;AAC1B,OAAK,iBAAiB,OAAO;;;;;;;CAQ/B,MAAM,IAAI,KAA+B;AACvC,MAAI,CAAC,KAAK,OAAO,QAAS,QAAO;AAGjC,SADc,MAAM,KAAK,SAAS,IAAI,KACrB;;;;;;;;CASnB,YAAY,OAAqC,SAAyB;EACxE,MAAM,WAAW,CAAC,SAAS,GAAG,MAAM;EACpC,MAAM,aAAa,KAAK,UAAU,SAAS;AAC3C,SAAO,WAAW,SAAS,CAAC,OAAO,WAAW,CAAC,OAAO,MAAM;;;CAI9D,MAAM,QAAuB;AAC3B,QAAM,KAAK,QAAQ,OAAO;;;;;;CAO5B,MAAM,mBAAqC;AACzC,SAAO,KAAK,QAAQ,aAAa"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isSQLTypeMarker, sql } from "./shared/src/sql/helpers.js";
2
+ import "./shared/src/index.js";
2
3
  import { RequestedClaimsPermissionSet, createLakebasePool, generateDatabaseCredential, getLakebaseOrmConfig, getLakebasePgConfig, getUsernameWithApiLookup, getWorkspaceClient } from "./connectors/lakebase/index.js";
3
4
  import { AppKitError } from "./errors/base.js";
4
5
  import { AuthenticationError } from "./errors/authentication.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * Core library for building Databricks applications with type-safe SQL queries,\n * plugin architecture, and React integration.\n */\n\n// Types from shared\nexport type {\n BasePluginConfig,\n CacheConfig,\n IAppRouter,\n PluginData,\n StreamExecutionSettings,\n} from \"shared\";\nexport { isSQLTypeMarker, sql } from \"shared\";\nexport { CacheManager } from \"./cache\";\nexport type { JobsConnectorConfig } from \"./connectors/jobs\";\nexport type {\n DatabaseCredential,\n GenerateDatabaseCredentialRequest,\n LakebasePoolConfig,\n RequestedClaims,\n RequestedResource,\n} from \"./connectors/lakebase\";\n// Lakebase Autoscaling connector\nexport {\n createLakebasePool,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n RequestedClaimsPermissionSet,\n} from \"./connectors/lakebase\";\nexport { getExecutionContext } from \"./context\";\nexport { createApp } from \"./core\";\n// Errors\nexport {\n AppKitError,\n AuthenticationError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n InitializationError,\n ServerError,\n TunnelError,\n ValidationError,\n} from \"./errors\";\n// Plugin authoring\nexport {\n type ExecutionResult,\n Plugin,\n type ToPlugin,\n toPlugin,\n} from \"./plugin\";\n// Files plugin types (for custom policy authoring)\nexport type {\n FileAction,\n FilePolicy,\n FilePolicyUser,\n FileResource,\n} from \"./plugins/files/policy\";\nexport {\n PolicyDeniedError,\n READ_ACTIONS,\n WRITE_ACTIONS,\n} from \"./plugins/files/policy\";\nexport * from \"./plugins/ga-exports.generated\";\nexport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobsExport,\n} from \"./plugins/jobs\";\nexport type {\n EndpointConfig,\n ServingEndpointEntry,\n ServingEndpointRegistry,\n ServingFactory,\n} from \"./plugins/serving/types\";\n// Registry types and utilities for plugin manifests\nexport type {\n ConfigSchema,\n PluginManifest,\n ResourceEntry,\n ResourceFieldEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./registry\";\nexport {\n getPluginManifest,\n getResourceRequirements,\n ResourceRegistry,\n ResourceType,\n} from \"./registry\";\n// Telemetry (for advanced custom telemetry)\nexport {\n type Counter,\n type Histogram,\n type ITelemetry,\n SeverityNumber,\n type Span,\n SpanStatusCode,\n type TelemetryConfig,\n} from \"./telemetry\";\nexport {\n extractServingEndpoints,\n findServerFile,\n} from \"./type-generator/serving/server-file-extractor\";\nexport { appKitServingTypesPlugin } from \"./type-generator/serving/vite-plugin\";\n// Vite plugin and type generation\nexport { appKitTypesPlugin } from \"./type-generator/vite-plugin\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCgD;aAa9B"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n *\n * Core library for building Databricks applications with type-safe SQL queries,\n * plugin architecture, and React integration.\n */\n\n// Types from shared\nexport type {\n BasePluginConfig,\n CacheConfig,\n IAppRouter,\n PluginData,\n StreamExecutionSettings,\n} from \"shared\";\nexport { isSQLTypeMarker, sql } from \"shared\";\nexport { CacheManager } from \"./cache\";\nexport type { JobsConnectorConfig } from \"./connectors/jobs\";\nexport type {\n DatabaseCredential,\n GenerateDatabaseCredentialRequest,\n LakebasePoolConfig,\n RequestedClaims,\n RequestedResource,\n} from \"./connectors/lakebase\";\n// Lakebase Autoscaling connector\nexport {\n createLakebasePool,\n generateDatabaseCredential,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n getWorkspaceClient,\n RequestedClaimsPermissionSet,\n} from \"./connectors/lakebase\";\nexport { getExecutionContext } from \"./context\";\nexport { createApp } from \"./core\";\n// Errors\nexport {\n AppKitError,\n AuthenticationError,\n ConfigurationError,\n ConnectionError,\n ExecutionError,\n InitializationError,\n ServerError,\n TunnelError,\n ValidationError,\n} from \"./errors\";\n// Plugin authoring\nexport {\n type ExecutionResult,\n Plugin,\n type ToPlugin,\n toPlugin,\n} from \"./plugin\";\n// Files plugin types (for custom policy authoring)\nexport type {\n FileAction,\n FilePolicy,\n FilePolicyUser,\n FileResource,\n} from \"./plugins/files/policy\";\nexport {\n PolicyDeniedError,\n READ_ACTIONS,\n WRITE_ACTIONS,\n} from \"./plugins/files/policy\";\nexport * from \"./plugins/ga-exports.generated\";\nexport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobsExport,\n} from \"./plugins/jobs\";\nexport type {\n EndpointConfig,\n ServingEndpointEntry,\n ServingEndpointRegistry,\n ServingFactory,\n} from \"./plugins/serving/types\";\n// Registry types and utilities for plugin manifests\nexport type {\n ConfigSchema,\n PluginManifest,\n ResourceEntry,\n ResourceFieldEntry,\n ResourcePermission,\n ResourceRequirement,\n ValidationResult,\n} from \"./registry\";\nexport {\n getPluginManifest,\n getResourceRequirements,\n ResourceRegistry,\n ResourceType,\n} from \"./registry\";\n// Telemetry (for advanced custom telemetry)\nexport {\n type Counter,\n type Histogram,\n type ITelemetry,\n SeverityNumber,\n type Span,\n SpanStatusCode,\n type TelemetryConfig,\n} from \"./telemetry\";\nexport {\n extractServingEndpoints,\n findServerFile,\n} from \"./type-generator/serving/server-file-extractor\";\nexport { appKitServingTypesPlugin } from \"./type-generator/serving/vite-plugin\";\n// Vite plugin and type generation\nexport { appKitTypesPlugin } from \"./type-generator/vite-plugin\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCgD;aAa9B"}
@@ -1,4 +1,5 @@
1
1
  import { isSQLTypeMarker, sql } from "../../shared/src/sql/helpers.js";
2
+ import "../../shared/src/index.js";
2
3
  import { ValidationError } from "../../errors/validation.js";
3
4
  import { init_errors } from "../../errors/index.js";
4
5
  import { getWorkspaceId } from "../../context/execution-context.js";
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;cAG+C;aACA;AAI/C,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
1
+ {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;;cAG+C;aACA;AAI/C,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
@@ -10,10 +10,13 @@ var manifest_default = {
10
10
  "resourceKey": "genie-space",
11
11
  "description": "Genie Space for AI-powered data queries. Space IDs configured via plugin config.",
12
12
  "permission": "CAN_RUN",
13
- "fields": { "id": {
14
- "env": "DATABRICKS_GENIE_SPACE_ID",
15
- "description": "Default Genie Space ID"
16
- } }
13
+ "fields": {
14
+ "id": {
15
+ "env": "DATABRICKS_GENIE_SPACE_ID",
16
+ "description": "Default Genie Space ID"
17
+ },
18
+ "name": { "description": "Genie Space display name" }
19
+ }
17
20
  }],
18
21
  "optional": []
19
22
  },
@@ -0,0 +1,4 @@
1
+ import { isSQLTypeMarker, sql } from "./sql/helpers.js";
2
+ import "./sql/index.js";
3
+
4
+ export { };
@@ -45,22 +45,127 @@ declare const sql: {
45
45
  */
46
46
  timestamp(value: Date | string | number): SQLTimestampMarker;
47
47
  /**
48
- * Creates a NUMERIC type parameter
49
- * Accepts numbers or numeric strings
48
+ * Creates a numeric type parameter. The wire SQL type is inferred from the
49
+ * value so the parameter binds correctly in any context, including `LIMIT`
50
+ * and `OFFSET`:
51
+ *
52
+ * - JS integer in `[-2^31, 2^31 - 1]` → `INT`
53
+ * - JS integer outside `INT` but within `Number.MAX_SAFE_INTEGER` → `BIGINT`
54
+ * - JS non-integer (`3.14`) → `DOUBLE`
55
+ * - integer-shaped string in `INT` range → `INT` (common HTTP-input case)
56
+ * - integer-shaped string outside `INT` but within `BIGINT` → `BIGINT`
57
+ * - decimal-shaped string (`"123.45"`) → `NUMERIC` (preserves precision)
58
+ *
59
+ * Why default to `INT`? Spark's `LIMIT` and `OFFSET` operators require
60
+ * `IntegerType` specifically — `BIGINT` (`LongType`) is rejected with
61
+ * `INVALID_LIMIT_LIKE_EXPRESSION.DATA_TYPE`. Catalyst auto-widens `INT`
62
+ * to `BIGINT` / `DECIMAL` / `DOUBLE` for wider columns, so `INT` is a
63
+ * strictly better default than `BIGINT`.
64
+ *
65
+ * Throws on `NaN`, `Infinity`, JS integers outside `Number.MAX_SAFE_INTEGER`,
66
+ * integer-shaped strings outside the `BIGINT` range, or non-numeric strings.
67
+ * Reach for `sql.int()`, `sql.bigint()`, `sql.float()`, `sql.double()`, or
68
+ * `sql.numeric()` to override the inferred type.
69
+ *
50
70
  * @param value - Number or numeric string
51
- * @returns Marker object for NUMERIC type parameter
71
+ * @returns Marker for a numeric SQL parameter
52
72
  * @example
53
73
  * ```typescript
54
- * const params = { userId: sql.number(123) };
55
- * params = { userId: "123" }
74
+ * sql.number(123); // { __sql_type: "INT", value: "123" }
75
+ * sql.number(3_000_000_000); // { __sql_type: "BIGINT", value: "3000000000" }
76
+ * sql.number(0.5); // { __sql_type: "DOUBLE", value: "0.5" }
77
+ * sql.number("10"); // { __sql_type: "INT", value: "10" }
78
+ * sql.number("123.45"); // { __sql_type: "NUMERIC", value: "123.45" }
56
79
  * ```
80
+ */
81
+ number(value: number | string): SQLNumberMarker;
82
+ /**
83
+ * Creates an `INT` (32-bit signed integer) parameter. Use when the column
84
+ * or context requires `INT` specifically (e.g. legacy schemas, or to make
85
+ * the wire type explicit).
86
+ *
87
+ * Rejects non-integers, values outside `Number.MAX_SAFE_INTEGER` (for
88
+ * number inputs), and values outside the signed 32-bit range
89
+ * `[-2^31, 2^31 - 1]`.
90
+ *
91
+ * @param value - Integer number or integer-shaped string
92
+ * @returns Marker pinned to `INT`
57
93
  * @example
58
94
  * ```typescript
59
- * const params = { userId: sql.number("123") };
60
- * params = { userId: "123" }
95
+ * sql.int(42); // { __sql_type: "INT", value: "42" }
96
+ * sql.int("42"); // { __sql_type: "INT", value: "42" }
61
97
  * ```
62
98
  */
63
- number(value: number | string): SQLNumberMarker;
99
+ int(value: number | string): SQLNumberMarker & {
100
+ __sql_type: "INT";
101
+ };
102
+ /**
103
+ * Creates a `BIGINT` (64-bit signed integer) parameter. Accepts JS
104
+ * `bigint` so callers can round-trip values outside `Number.MAX_SAFE_INTEGER`
105
+ * without precision loss; for `number` inputs, requires
106
+ * `Number.isSafeInteger(value)`.
107
+ *
108
+ * Rejects values outside the signed 64-bit range `[-2^63, 2^63 - 1]`.
109
+ *
110
+ * @param value - Integer number, bigint, or integer-shaped string
111
+ * @returns Marker pinned to `BIGINT`
112
+ * @example
113
+ * ```typescript
114
+ * sql.bigint(42); // { __sql_type: "BIGINT", value: "42" }
115
+ * sql.bigint(9007199254740993n); // { __sql_type: "BIGINT", value: "9007199254740993" }
116
+ * sql.bigint("9007199254740993"); // { __sql_type: "BIGINT", value: "9007199254740993" }
117
+ * ```
118
+ */
119
+ bigint(value: number | bigint | string): SQLNumberMarker & {
120
+ __sql_type: "BIGINT";
121
+ };
122
+ /**
123
+ * Creates a `FLOAT` (single-precision, 32-bit) parameter. Note that JS
124
+ * numbers are 64-bit doubles, so values may be rounded to fit FLOAT
125
+ * precision at bind time.
126
+ *
127
+ * @param value - Number or numeric string
128
+ * @returns Marker pinned to `FLOAT`
129
+ * @example
130
+ * ```typescript
131
+ * sql.float(3.14); // { __sql_type: "FLOAT", value: "3.14" }
132
+ * ```
133
+ */
134
+ float(value: number | string): SQLNumberMarker & {
135
+ __sql_type: "FLOAT";
136
+ };
137
+ /**
138
+ * Creates a `DOUBLE` (double-precision, 64-bit) parameter. Same precision
139
+ * as a JS `number`, so `sql.double(value)` is exact for any JS number.
140
+ *
141
+ * @param value - Number or numeric string
142
+ * @returns Marker pinned to `DOUBLE`
143
+ * @example
144
+ * ```typescript
145
+ * sql.double(3.14); // { __sql_type: "DOUBLE", value: "3.14" }
146
+ * ```
147
+ */
148
+ double(value: number | string): SQLNumberMarker & {
149
+ __sql_type: "DOUBLE";
150
+ };
151
+ /**
152
+ * Creates a `NUMERIC` (fixed-point DECIMAL) parameter. Use when you need
153
+ * exact decimal arithmetic (currency, percentages) — pass values as
154
+ * strings to avoid JS-number precision loss.
155
+ *
156
+ * Note: passing a JS `number` is accepted but lossy for many values
157
+ * (e.g. `0.1 + 0.2` → `"0.30000000000000004"`). Prefer strings.
158
+ *
159
+ * @param value - Number or numeric string (strings preferred for precision)
160
+ * @returns Marker pinned to `NUMERIC`
161
+ * @example
162
+ * ```typescript
163
+ * sql.numeric("12345.6789"); // { __sql_type: "NUMERIC", value: "12345.6789" }
164
+ * ```
165
+ */
166
+ numeric(value: number | string): SQLNumberMarker & {
167
+ __sql_type: "NUMERIC";
168
+ };
64
169
  /**
65
170
  * Creates a STRING type parameter
66
171
  * Accepts strings, numbers, or booleans
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","names":[],"sources":["../../../../../shared/src/sql/helpers.ts"],"mappings":";;;;;AAaA;cAAa,GAAA;;;;;;;;;;;;;;;;;cAiBC,IAAA,YAAgB,aAAA;;;;;;;;;;;;;;;;;;;;;AAyT9B;mBAtQmB,IAAA,qBAAyB,kBAAA;;;;;;;;;;;;;;;;;kCA6CV,eAAA;;;;;;;;;;;;;;;;;;;;;;4CAkDU,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAyDC,gBAAA;;;;;;;;;;;;;;;;;;;;;;gBA8D7B,UAAA,GAAa,WAAA,YAAuB,eAAA;AAAA;;;;;;;;;;;;;;;iBAgDpC,eAAA,CAAgB,KAAA,QAAa,KAAA,IAAS,aAAA"}
1
+ {"version":3,"file":"helpers.d.ts","names":[],"sources":["../../../../../shared/src/sql/helpers.ts"],"mappings":";;;;;AAqGA;cAAa,GAAA;;;;;;;;;;;;;;;;;cAiBC,IAAA,YAAgB,aAAA;EAmbqC;;;;;;;;;;;;;;;;;;;;;mBAhYhD,IAAA,qBAAyB,kBAAA;EAqMX;;;;;;;;;;;;;;;;;;;;;;;AA2OjC;;;;;;;;;;;kCAjXkC,eAAA;;;;;;;;;;;;;;;;;;+BAkEH,eAAA;IAAoB,UAAA;EAAA;;;;;;;;;;;;;;;;;;2CAgC9C,eAAA;IAAoB,UAAA;EAAA;;;;;;;;;;;;;iCAoCQ,eAAA;IAAoB,UAAA;EAAA;;;;;;;;;;;;kCAkBnB,eAAA;IAAoB,UAAA;EAAA;;;;;;;;;;;;;;;;mCAsBnB,eAAA;IAAoB,UAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;4CA4BX,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6CAyDC,gBAAA;;;;;;;;;;;;;;;;;;;;;;gBA8D7B,UAAA,GAAa,WAAA,YAAuB,eAAA;AAAA;;;;;;;;;;;;;;;iBAgDpC,eAAA,CAAgB,KAAA,QAAa,KAAA,IAAS,aAAA"}
@@ -1,4 +1,43 @@
1
1
  //#region ../shared/src/sql/helpers.ts
2
+ const NUMERIC_LITERAL_RE = /^-?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
3
+ const INTEGER_LITERAL_RE = /^-?\d+$/;
4
+ const INT_MIN = -(2n ** 31n);
5
+ const INT_MAX = 2n ** 31n - 1n;
6
+ const BIGINT_MIN = -(2n ** 63n);
7
+ const BIGINT_MAX = 2n ** 63n - 1n;
8
+ function ensureFiniteNumber(value, fnName) {
9
+ if (!Number.isFinite(value)) throw new Error(`${fnName}() expects a finite number, got: ${value}`);
10
+ }
11
+ function ensureSafeInteger(value, fnName) {
12
+ if (!Number.isSafeInteger(value)) throw new Error(`${fnName}() received an integer outside Number.MAX_SAFE_INTEGER (${value}); JS numbers cannot represent it exactly. Pass a bigint (sql.bigint(BigInt("..."))) or an integer-shaped string instead.`);
13
+ }
14
+ function ensureInBigIntRange(parsed, min, max, typeName, fnName, hint) {
15
+ if (parsed < min || parsed > max) throw new Error(`${fnName}() value ${parsed} is outside ${typeName} range [${min}, ${max}]. ${hint}`);
16
+ }
17
+ function coerceNumericLike(value, fnName) {
18
+ if (typeof value === "number") {
19
+ ensureFiniteNumber(value, fnName);
20
+ return value.toString();
21
+ }
22
+ if (typeof value === "string") {
23
+ if (!NUMERIC_LITERAL_RE.test(value)) throw new Error(`${fnName}() expects number or numeric string, got: ${value === "" ? "empty string" : value}`);
24
+ return value;
25
+ }
26
+ throw new Error(`${fnName}() expects number or numeric string, got: ${typeof value}`);
27
+ }
28
+ function coerceIntegerLike(value, fnName) {
29
+ if (typeof value === "number") {
30
+ ensureFiniteNumber(value, fnName);
31
+ if (!Number.isInteger(value)) throw new Error(`${fnName}() expects an integer, got non-integer number: ${value}`);
32
+ ensureSafeInteger(value, fnName);
33
+ return BigInt(value).toString();
34
+ }
35
+ if (typeof value === "string") {
36
+ if (!INTEGER_LITERAL_RE.test(value)) throw new Error(`${fnName}() expects integer number or integer-shaped string, got: ${value === "" ? "empty string" : value}`);
37
+ return value;
38
+ }
39
+ throw new Error(`${fnName}() expects integer number or integer-shaped string, got: ${typeof value}`);
40
+ }
2
41
  /**
3
42
  * SQL helper namespace
4
43
  */
@@ -29,15 +68,85 @@ const sql = {
29
68
  };
30
69
  },
31
70
  number(value) {
32
- let numValue = "";
33
- if (typeof value === "number") numValue = value.toString();
34
- else if (typeof value === "string") {
35
- if (value === "" || Number.isNaN(Number(value))) throw new Error(`sql.number() expects number or numeric string, got: ${value === "" ? "empty string" : value}`);
36
- numValue = value;
37
- } else throw new Error(`sql.number() expects number or numeric string, got: ${typeof value}`);
71
+ if (typeof value === "number") {
72
+ ensureFiniteNumber(value, "sql.number");
73
+ if (Number.isInteger(value)) {
74
+ ensureSafeInteger(value, "sql.number");
75
+ const asBigInt = BigInt(value);
76
+ if (asBigInt >= INT_MIN && asBigInt <= INT_MAX) return {
77
+ __sql_type: "INT",
78
+ value: asBigInt.toString()
79
+ };
80
+ return {
81
+ __sql_type: "BIGINT",
82
+ value: asBigInt.toString()
83
+ };
84
+ }
85
+ return {
86
+ __sql_type: "DOUBLE",
87
+ value: value.toString()
88
+ };
89
+ }
90
+ if (typeof value === "string") {
91
+ if (!NUMERIC_LITERAL_RE.test(value)) throw new Error(`sql.number() expects number or numeric string, got: ${value === "" ? "empty string" : value}`);
92
+ if (INTEGER_LITERAL_RE.test(value)) {
93
+ const parsed = BigInt(value);
94
+ ensureInBigIntRange(parsed, BIGINT_MIN, BIGINT_MAX, "BIGINT (64-bit signed)", "sql.number", "Use sql.numeric() with a string for arbitrary-precision integers.");
95
+ if (parsed >= INT_MIN && parsed <= INT_MAX) return {
96
+ __sql_type: "INT",
97
+ value
98
+ };
99
+ return {
100
+ __sql_type: "BIGINT",
101
+ value
102
+ };
103
+ }
104
+ return {
105
+ __sql_type: "NUMERIC",
106
+ value
107
+ };
108
+ }
109
+ throw new Error(`sql.number() expects number or numeric string, got: ${typeof value}`);
110
+ },
111
+ int(value) {
112
+ const stringValue = coerceIntegerLike(value, "sql.int");
113
+ ensureInBigIntRange(BigInt(stringValue), INT_MIN, INT_MAX, "INT (32-bit signed)", "sql.int", "Use sql.bigint() for 64-bit values.");
114
+ return {
115
+ __sql_type: "INT",
116
+ value: stringValue
117
+ };
118
+ },
119
+ bigint(value) {
120
+ if (typeof value === "bigint") {
121
+ ensureInBigIntRange(value, BIGINT_MIN, BIGINT_MAX, "BIGINT (64-bit signed)", "sql.bigint", "Use sql.numeric() with a string for arbitrary-precision integers.");
122
+ return {
123
+ __sql_type: "BIGINT",
124
+ value: value.toString()
125
+ };
126
+ }
127
+ const stringValue = coerceIntegerLike(value, "sql.bigint");
128
+ ensureInBigIntRange(BigInt(stringValue), BIGINT_MIN, BIGINT_MAX, "BIGINT (64-bit signed)", "sql.bigint", "Use sql.numeric() with a string for arbitrary-precision integers.");
129
+ return {
130
+ __sql_type: "BIGINT",
131
+ value: stringValue
132
+ };
133
+ },
134
+ float(value) {
135
+ return {
136
+ __sql_type: "FLOAT",
137
+ value: coerceNumericLike(value, "sql.float")
138
+ };
139
+ },
140
+ double(value) {
141
+ return {
142
+ __sql_type: "DOUBLE",
143
+ value: coerceNumericLike(value, "sql.double")
144
+ };
145
+ },
146
+ numeric(value) {
38
147
  return {
39
148
  __sql_type: "NUMERIC",
40
- value: numValue
149
+ value: coerceNumericLike(value, "sql.numeric")
41
150
  };
42
151
  },
43
152
  string(value) {