<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Vasiliy Zakharchenko</title>
    <description>The latest articles on Forem by Vasiliy Zakharchenko (@vzakharchenko).</description>
    <link>https://forem.com/vzakharchenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3020066%2Ff1f2452c-99e0-4991-b0cf-abe0dd31ce8b.png</url>
      <title>Forem: Vasiliy Zakharchenko</title>
      <link>https://forem.com/vzakharchenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/vzakharchenko"/>
    <language>en</language>
    <item>
      <title>AI Magic in Atlassian Forge: Local Semantic Search with Forge SQL</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Fri, 10 Apr 2026 16:53:03 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/ai-magic-in-atlassian-forge-local-semantic-search-with-forge-sql-367m</link>
      <guid>https://forem.com/vzakharchenko/ai-magic-in-atlassian-forge-local-semantic-search-with-forge-sql-367m</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;Did you know that &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt; can be used for &lt;strong&gt;semantic search&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Since &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt; is backed by TiDB, and TiDB supports &lt;a href="https://docs.pingcap.com/ai/vector-search-overview/#vector-search-overview" rel="noopener noreferrer"&gt;&lt;strong&gt;Vector Search&lt;/strong&gt;&lt;/a&gt;, it is now possible to store embeddings in the database and query them by semantic similarity. That means your app can search by &lt;strong&gt;meaning&lt;/strong&gt;, not only by exact keyword matches. PingCAP highlights semantic search, recommendation systems, and Retrieval-Augmented Generation (RAG) as key use cases for this capability.&lt;/p&gt;

&lt;p&gt;This is a very practical AI pattern. Traditional search works best when users type the same words that already exist in the stored content. Semantic search works differently: both the documents and the user query are converted into embeddings, and the database returns the closest matches based on vector distance. Because of that, a query can still find the right result even when the wording is completely different.&lt;/p&gt;

&lt;p&gt;This is also one of the core ideas behind &lt;strong&gt;RAG&lt;/strong&gt;. Before an LLM generates an answer, the system first retrieves the most relevant documents and passes them as context. Better retrieval usually means better answers.&lt;/p&gt;

&lt;p&gt;In this article, I will show how to build this pattern inside an &lt;strong&gt;Atlassian Forge&lt;/strong&gt; app while still keeping the architecture aligned with &lt;strong&gt;Runs on Atlassian&lt;/strong&gt;. In &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;this example&lt;/a&gt;, embeddings are generated locally in the &lt;strong&gt;Custom UI frontend&lt;/strong&gt;, stored in &lt;strong&gt;&lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;&lt;/strong&gt; using &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;Forge SQL ORM&lt;/strong&gt;&lt;/a&gt;, and queried directly with vector search. No external AI API is required for the semantic search flow. Runs on Atlassian eligibility depends on meeting platform requirements, including around egress, so this kind of local approach can be especially useful for Forge developers.&lt;/p&gt;

&lt;p&gt;The article has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;How embeddings are generated in Custom UI and processed in the Forge backend&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A short demonstration of the app in action, with a link to the full video walkthrough&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vzhdokim1dg1t0hy2ml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vzhdokim1dg1t0hy2ml.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Building the Local Semantic Search Flow
&lt;/h1&gt;

&lt;p&gt;To build semantic search inside a Forge app, we need a way to generate embeddings locally in the browser, send them to the backend, and then use &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt; to search by vector similarity.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;this example&lt;/a&gt;, the flow consists of four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choosing a lightweight embedding model&lt;/li&gt;
&lt;li&gt;configuring the frontend to run the model locally&lt;/li&gt;
&lt;li&gt;generating vectors from user input&lt;/li&gt;
&lt;li&gt;sending those vectors to the backend and querying &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Choosing the embedding model
&lt;/h2&gt;

&lt;p&gt;The first step was choosing an embedding model that could realistically run inside Forge &lt;strong&gt;Custom UI&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;this example&lt;/a&gt;, I used &lt;a href="https://huggingface.co/Xenova/all-MiniLM-L6-v2" rel="noopener noreferrer"&gt;Xenova/all-MiniLM-L6-v2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I chose it for a very practical reason: it is lightweight and fits much better into the kind of size and runtime constraints that Forge developers usually care about.&lt;/p&gt;

&lt;p&gt;Since the goal of this example is to keep the semantic search flow local, the model needs to run directly in the browser without relying on an external AI API. A smaller model is a much better fit for that approach.&lt;/p&gt;

&lt;p&gt;From &lt;a href="https://huggingface.co/Xenova/all-MiniLM-L6-v2/tree/main" rel="noopener noreferrer"&gt;the full model repository&lt;/a&gt;, I only needed a small set of files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;config.json&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;special_tokens_map.json&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tokenizer.json&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tokenizer_config.json&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;onnx/model_quantized.onnx&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, each file has a specific role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;config.json&lt;/strong&gt; contains the main model configuration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;special_tokens_map.json&lt;/strong&gt; defines special tokens used by the tokenizer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tokenizer.json&lt;/strong&gt; contains the tokenizer itself, including how text is split into tokens&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;tokenizer_config.json&lt;/strong&gt; contains tokenizer settings and metadata&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;onnx/model_quantized.onnx&lt;/strong&gt; is the actual neural network model used for inference in the browser&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most important part here is &lt;strong&gt;model_quantized.onnx&lt;/strong&gt;. This is the file that performs embedding generation. It is a quantized ONNX model, which means it is optimized to be &lt;strong&gt;smaller&lt;/strong&gt; and more practical for &lt;strong&gt;client-side&lt;/strong&gt; execution.&lt;/p&gt;

&lt;p&gt;The tokenizer files are also essential, because the model does not work directly with raw text. First, the input text must be converted into tokens in exactly the same way the model expects. Only after that can the ONNX model generate the embedding vector.&lt;/p&gt;

&lt;p&gt;So in practice, the setup is split into two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;the tokenizer files prepare the text input&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the ONNX model converts that tokenized input into an embedding vector&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is enough to run local embedding generation in the frontend and a good fit for a Forge app that aims to stay simple, portable, and aligned with Runs on Atlassian badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Configuring the frontend to run the model locally
&lt;/h2&gt;

&lt;p&gt;To run semantic search fully inside the Forge app, the frontend first needs to load the embedding model locally in the browser.&lt;/p&gt;

&lt;p&gt;The main dependency for this is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @huggingface/transformers &lt;span class="nt"&gt;-S&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the application needs two groups of files:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;the model files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the ONNX runtime WebAssembly files&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2.1 Adding the model files
&lt;/h3&gt;

&lt;p&gt;The model itself, together with its tokenizer and configuration files, can be placed into the frontend public folder. In my case, I used this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/
   models/
      all-MiniLM-L6-v2/
         config.json
         special_tokens_map.json
         tokenizer.json
         tokenizer_config.json
         onnx/
           model_quantized.onnx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the frontend to load the model directly from the app’s own static assets.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2 Adding the ONNX WebAssembly runtime files
&lt;/h3&gt;

&lt;p&gt;In addition to the model, the browser also needs the ONNX runtime files used to execute the model locally.&lt;/p&gt;

&lt;p&gt;These files can be copied from &lt;strong&gt;node_modules/onnxruntime-web/dist/ into&lt;/strong&gt; public/wasm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ort-wasm-simd-threaded.mjs&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ort-wasm-simd-threaded.wasm&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ort-wasm-simd-threaded.asyncify.mjs&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ort-wasm-simd-threaded.asyncify.wasm&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the final structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public/
   models/
      all-MiniLM-L6-v2/
         onnx/
             model_quantized.onnx
         config.json
         special_tokens_map.json
         tokenizer.json
         tokenizer_config.json
   wasm/
      ort-wasm-simd-threaded.mjs
      ort-wasm-simd-threaded.wasm
      ort-wasm-simd-threaded.asyncify.mjs
      ort-wasm-simd-threaded.asyncify.wasm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the frontend has everything it needs to load the model and run inference locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3 Initializing the model in the frontend
&lt;/h3&gt;

&lt;p&gt;The next step is to initialize the model when the frontend starts. This only needs to happen once. After the first load, the browser can reuse cached assets, which makes the next startup much faster.&lt;/p&gt;

&lt;p&gt;Here is the code I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;reference types="vite/client" /&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FeatureExtractionPipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProgressInfo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@huggingface/transformers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localModelPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`./models/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowLocalModels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowRemoteModels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useBrowserCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;useWasmCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDevMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isDevMode&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/wasm/`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`../wasm/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`all-MiniLM-L6-v2`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VectorBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MiniLLM&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProgressInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VectorBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VectorBuilderImpl&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;VectorBuilder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeatureExtractionPipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FeatureExtractionPipeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;pooling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MiniLLMImpl&lt;/span&gt; &lt;span class="kr"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;MiniLLM&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProgressInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;VectorBuilder&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extractor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;feature-extraction&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;progress_callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;VectorBuilderImpl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extractor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;miniLLM&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MiniLLM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MiniLLMImpl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What this configuration does
&lt;/h4&gt;

&lt;p&gt;There are a few important details here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;env.localModelPath = './models/'&lt;/strong&gt; tells transformers.js where to find the model files inside the frontend assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;env.allowLocalModels = true&lt;/strong&gt; and &lt;strong&gt;env.allowRemoteModels = false&lt;/strong&gt; make sure that the application only uses local model files and does not try to download anything from an external model registry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;env.useWasmCache = true&lt;/strong&gt; allows the WebAssembly runtime files to be cached, which helps reduce repeated loading costs.&lt;/p&gt;

&lt;p&gt;The following line is especially important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDevMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backends&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onnx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasm&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wasmPaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isDevMode&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/wasm/`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`../wasm/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added isDevMode because the WebAssembly path needs to be resolved differently when running through &lt;strong&gt;forge tunnel&lt;/strong&gt;. Without that adjustment, the runtime files might not load correctly in local &lt;strong&gt;development mode&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Tracking model loading progress
&lt;/h4&gt;

&lt;p&gt;The progress callback is used to show model loading progress in the UI, for example through a spinner or progress indicator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;progressInfo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProgressInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful because the model is loaded into the frontend on startup, and that can take a little time on the first run.&lt;/p&gt;

&lt;p&gt;An important detail here is that these files are loaded from the app’s own host, not from an external service. So this is just normal asset loading from the frontend itself, not an external AI API call.&lt;/p&gt;

&lt;h4&gt;
  
  
  Result
&lt;/h4&gt;

&lt;p&gt;After this setup, the frontend is ready to initialize the embedding model locally and generate vectors directly in the browser.&lt;/p&gt;

&lt;p&gt;In the next step, we can use this initialized pipeline to convert document text and search queries into embedding vectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Generating the vector
&lt;/h2&gt;

&lt;p&gt;Once the model is available in the frontend, the application can convert plain text into an embedding vector.&lt;/p&gt;

&lt;p&gt;This happens in two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;when a user adds a document&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;when a user enters a search query&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In both cases, the frontend takes the input text, runs it through the embedding model, and produces a fixed-size numeric vector. That vector is the semantic representation of the text. Instead of relying on exact words, the application can now compare meanings by comparing vectors.&lt;/p&gt;

&lt;p&gt;This is the core idea behind semantic search: text is first transformed into embeddings, and only then used for similarity search.&lt;/p&gt;

&lt;p&gt;In practice, generating the vector is very simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vectorBuilder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;miniLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vectorBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, miniLLM.init(onProgress) initializes the local embedding pipeline, and getVector(text) converts the input text into a numeric vector.&lt;/p&gt;

&lt;p&gt;The same approach is used both for storing documents and for searching. When a document is added, the frontend generates an embedding for the document text before sending it to the backend. When a user performs a search, the frontend generates an embedding for the query text and sends that vector to the search resolver.&lt;/p&gt;

&lt;p&gt;So from this point on, the application no longer works with plain text only. It works with semantic representations of that text, which is what makes similarity search possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Sending the vector to the backend and processing it in Forge SQL
&lt;/h2&gt;

&lt;p&gt;Once the vector is generated in the frontend, it can be sent to a Forge resolver and stored in &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt; together with the original document.&lt;/p&gt;

&lt;p&gt;In my example, the model is defined like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mysqlTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;embedded_documents&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;autoincrement&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;vectorTiDBType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;embedding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dimension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;384&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the migration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MigrationRunner&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@forge/sql/out/migration&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;migrationRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MigrationRunner&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;MigrationRunner&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;migrationRunner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1_MIGRATION0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CREATE TABLE `embedded_documents` ( `id` int AUTO_INCREMENT NOT NULL, `document` text NOT NULL, `title` VARCHAR(255) NOT NULL, `embedding` VECTOR(384) NOT NULL, CONSTRAINT `id` PRIMARY KEY(`id`) )&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part here is the &lt;strong&gt;vector dimension&lt;/strong&gt;. It must exactly match the output dimension of the embedding model. In this case, &lt;strong&gt;all-MiniLM-L6-v2&lt;/strong&gt; produces vectors with dimension &lt;strong&gt;384&lt;/strong&gt;, so both the ORM model and the SQL migration use 384 as well.&lt;/p&gt;

&lt;p&gt;After that, saving a document is straightforward. The frontend sends the document text, title, and generated embedding vector to the resolver, and the backend inserts them into the table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InferInsertModel&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;insertId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the database stores not only the original text, but also its semantic representation as a vector.&lt;/p&gt;

&lt;p&gt;The search flow works in a similar way. The frontend generates an embedding for the user’s query and sends that vector to the backend. Then the backend uses &lt;strong&gt;vecCosineDistance&lt;/strong&gt; to compare the query vector with all stored document vectors and return the closest matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fieldAlias&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;distance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;vecCosineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; as &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;fieldAlias&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embeddedDocuments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;asc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fieldAlias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatLimitOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the generated SQL looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
&lt;span class="nv"&gt;`id`&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;`a_id_id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nv"&gt;`document`&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;`a_document_document`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nv"&gt;`title`&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;`a_title_title`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;VEC_COSINE_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`embedded_documents`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;`embedding`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VEC_FROM_TEXT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;`distance`&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;`embedded_documents`&lt;/span&gt;
&lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;asc&lt;/span&gt;
&lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the key step where semantic search actually happens. Instead of checking whether the query contains the same words as the document, &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt; compares vector similarity and returns the nearest results.&lt;/p&gt;

&lt;p&gt;So the backend responsibility is very simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;store document embeddings&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;receive the query embedding&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;calculate vector distance in Forge SQL&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;return the nearest documents sorted by similarity&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, the full semantic search pipeline is complete: the frontend generates embeddings locally, the backend stores them in &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;, and search works by vector similarity instead of exact keyword matching - while still preserving &lt;strong&gt;Runs on Atlassian eligibility&lt;/strong&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  A short demonstration of the app in action
&lt;/h1&gt;

&lt;p&gt;After the technical setup is complete, the easiest way to understand the value of semantic search is to see it working on a small set of example documents.&lt;/p&gt;

&lt;p&gt;For this demo, I added five documents to the application. Each document has a title and a longer text description. When a document is submitted, the frontend generates its embedding locally and the backend stores both the original text and the vector in Forge SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding sample documents
&lt;/h2&gt;

&lt;p&gt;To populate the demo dataset, I added the following documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Title: Dogs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Document Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Unwavering Bond: A Comprehensive Look at Domestic Dogs

Domestic dogs, scientifically known as *Canis lupus familiaris*, have shared a unique evolutionary journey with humans for over fifteen thousand years. Originally descended from ancient wolves, these resilient mammals have transitioned from wild predators to beloved family members, earning their reputation as "man's best friend." Their primary role has shifted significantly through history; while they were once valued strictly for their hunting prowess and guarding abilities, modern canines are now primarily cherished for their companionship and emotional support.

Physically, dogs exhibit an incredible diversity in size, coat texture, and temperament. From the tiny Chihuahua to the massive Great Dane, every breed possesses specific traits developed through centuries of selective breeding. Beyond their physical attributes, dogs are highly intelligent social animals capable of understanding human emotions and complex commands. They communicate through a sophisticated range of vocalizations, including barks and whines, alongside subtle body language like tail wagging or ear positioning.

Furthermore, the working capabilities of dogs remain vital to society today. Specialized service animals assist individuals with visual impairments, while brave search-and-rescue teams navigate treacherous terrain to save lives. Their acute sense of smell, which is thousands of times more sensitive than a human's, allows them to detect specific scents with remarkable precision. Whether they are performing a high-stakes job or simply waiting patiently for their owner to return home, dogs continue to demonstrate an unparalleled level of loyalty, devotion, and unconditional love that enriches human lives across every culture.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p4b626enosdh3nvszbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0p4b626enosdh3nvszbz.png" alt=" " width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Title: Tree
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Document Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Silent Giants: Understanding the Life of Trees

Trees are the fundamental pillars of our planet's terrestrial ecosystems, serving as complex biological organisms that sustain life on Earth. As perennial plants with an elongated stem or trunk, they are uniquely characterized by their woody structure and extensive root systems. Through the remarkable process of photosynthesis, trees convert sunlight, water, and carbon dioxide into life-sustaining oxygen and glucose. This chemical transformation not only supports the tree's own growth but also regulates the global atmospheric balance, making forests the "lungs of our planet."

The internal anatomy of a tree is a marvel of natural engineering. Beneath the protective outer bark lies the cambium layer, which facilitates the growth of new cells, and the xylem, a sophisticated vascular system that transports nutrients from the earth to the highest leaves. Throughout the seasons, deciduous trees undergo dramatic transformations, shedding their foliage in autumn to conserve energy before the harsh winter months. In contrast, evergreens maintain their needles year-round, showcasing the diverse evolutionary strategies plants use to survive in varying climates.

Beyond their biological functions, trees provide critical habitats for countless species of insects, birds, and fungi. They stabilize the soil against erosion, offer cooling shade during intense heat, and contribute to the water cycle by releasing moisture through transpiration. For humanity, trees have been an essential resource for millennia, providing timber for construction, fruit for sustenance, and a profound sense of tranquility. Protecting these ancient, towering organisms is vital for maintaining biodiversity and ensuring the environmental health of future generations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhv41mo7ciqs2dtbrfacd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhv41mo7ciqs2dtbrfacd.png" alt=" " width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Title: Fish
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Document Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Aquatic Realm: Exploring the World of Fish

Fish represent a diverse group of craniate organisms that have mastered life in the world's oceans, rivers, and lakes for over five hundred million years. As cold-blooded vertebrates, they are perfectly adapted to their underwater environments, utilizing specialized organs called gills to extract life-sustaining oxygen directly from the water. Unlike land-dwelling mammals, fish possess streamlined bodies covered in protective scales and use various fins for propulsion, stability, and precise maneuvering through dense aquatic currents.

The biological variety among fish is staggering, ranging from the tiny, colorful inhabitants of tropical coral reefs to the colossal whale sharks that roam the open sea. Many species have evolved incredible sensory capabilities, such as the lateral line system, which detects minute vibrations and pressure changes in the surrounding water. This "sixth sense" allows them to navigate in complete darkness, avoid predators, and hunt with remarkable accuracy. Additionally, some fish exhibit complex social behaviors, forming massive schools that move in perfect unison to confuse attackers or increase foraging efficiency.

Reproduction and survival strategies in the aquatic world are equally fascinating. While some fish lay thousands of delicate eggs in hidden nests, others, like certain sharks, give birth to fully formed live young. Their role in the global food web is indispensable, as they serve as a primary protein source for billions of humans and countless other predators. From the deepest abyssal trenches to the shallowest mountain streams, fish continue to thrive as a testament to evolutionary resilience, playing a vital role in maintaining the delicate ecological balance of our blue planet's hydrosphere.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp83dd5xofisrke0q25h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpp83dd5xofisrke0q25h.png" alt=" " width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Title: Cat
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Document Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Enigmatic Grace: Understanding the Domestic Cat

The domestic cat, or *Felis catus*, is a small carnivorous mammal celebrated for its agility, independent spirit, and mysterious demeanor. Having lived alongside humans for nearly ten thousand years, cats were originally revered in ancient societies—most notably in Egypt—for their ability to protect grain stores from rodents. Unlike dogs, which were bred for cooperation, cats have largely retained their solitary hunting instincts, making them fascinatingly self-sufficient companions in modern households.

Physically, cats are marvels of biological engineering. Their skeletons are incredibly flexible, allowing them to squeeze through tight spaces and always land on their feet thanks to a highly developed righting reflex. They possess extraordinary sensory perceptions; their night vision is far superior to that of humans, and their retractable claws allow for silent stalking and efficient climbing. A cat’s communication is equally nuanced, ranging from the gentle vibration of a purr, which often signals contentment or self-healing, to the sharp hiss used for territorial defense.

Behaviorally, cats are known for their fastidious grooming habits and complex social signals. While they are often labeled as aloof, many cats form deep emotional bonds with their owners, expressing affection through "kneading" or gentle head-butts. Their predatory prowess remains intact, even in indoor environments, where they often treat toys as "prey" to satisfy their instinctive need to hunt. As one of the world's most popular pets, cats continue to captivate us with their blend of wild heritage and domestic charm, offering a quiet, observant presence that has inspired artists and thinkers for millennia.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnj7jaqcyufqex2a0ue5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnj7jaqcyufqex2a0ue5m.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Title: Mice
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Document Text:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Smallest Survivors: The World of Mice

Mice are small rodents belonging to the family Muridae, known for their incredible adaptability and presence in nearly every corner of the globe. Characterized by their pointed snouts, large rounded ears, and long, thin tails, these tiny mammals have successfully thrived alongside human civilizations for thousands of years. While often viewed as mere pests in granaries, mice are highly complex creatures with sophisticated social structures and remarkable survival instincts that allow them to inhabit diverse environments ranging from dense forests to urban households.

Biologically, mice are built for stealth and speed. Their whiskers, or vibrissae, are highly sensitive tactile organs that allow them to navigate in total darkness by sensing air currents and physical obstacles. They possess an extraordinary reproductive rate, a necessary evolutionary strategy to counter their role as a primary food source for numerous predators, including owls, snakes, and felines. Despite their small stature, mice are surprisingly intelligent; they exhibit problem-solving abilities and can communicate with one another using ultrasonic vocalizations that are completely inaudible to the human ear.

In the realm of science and history, the mouse has played an indispensable role. Due to their genetic similarity to humans, mice are the most commonly studied model organisms in medical research, contributing to countless breakthroughs in genetics and pharmacology. Whether they are scurrying through a field or living in a controlled laboratory setting, mice demonstrate a level of resilience and biological efficiency that far outweighs their size. Their ability to find food in the most difficult conditions and their cautious, nocturnal nature continue to make them one of the most successful mammalian species on Earth.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw62x3tamya921lz54k83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw62x3tamya921lz54k83.png" alt=" " width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying semantic search queries
&lt;/h2&gt;

&lt;p&gt;Once the dataset is ready, the next step is to test how the application behaves with natural-language queries.&lt;/p&gt;

&lt;p&gt;The interesting part here is that the queries do not need to contain the exact title of the document. In fact, they work best when they describe the concept in a more human way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I am looking for information about large organisms that live for hundreds of years, have a woody trunk, and use their leaves to turn sunlight into energy while providing shade and stabilized soil for the ecosystem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fka1ovjhsywia4ut4q3yi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fka1ovjhsywia4ut4q3yi.png" alt=" " width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Result: Tree (55.66%)
&lt;/h4&gt;

&lt;p&gt;This query contains a lot of extra words such as “I am looking for information about”, but the important semantic signals are still there: &lt;em&gt;woody trunk&lt;/em&gt;, &lt;em&gt;sunlight into energy&lt;/em&gt;, &lt;em&gt;shade&lt;/em&gt;, and &lt;em&gt;stabilized soil&lt;/em&gt;. A keyword search might be less reliable here, but semantic search can still understand the meaning and rank &lt;strong&gt;Tree&lt;/strong&gt; as the closest match.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I am looking for information about small domestic predators that were respected in ancient history, are very independent, have excellent night vision, and can land on their feet when they jump from high places.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnwvu95wmnj7lo1ltsqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjnwvu95wmnj7lo1ltsqh.png" alt=" " width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Result: Cat (39.90%)
&lt;/h4&gt;

&lt;p&gt;This example is useful because the query never says the word &lt;em&gt;cat&lt;/em&gt;. Instead, it describes distinctive traits: ancient history, independence, night vision, and landing on their feet. A traditional search for the exact word would fail here, but semantic search can still connect the description to the &lt;strong&gt;Cat&lt;/strong&gt; document.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 3:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tell me about tiny mammals that are often found in houses or fields, which scientists use in laboratories to study genetics and develop new medicines because they breed very fast and are biologically similar to humans.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fenxr9h7i3ifimt5u8rfp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fenxr9h7i3ifimt5u8rfp.png" alt=" " width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Result: Mice
&lt;/h4&gt;

&lt;p&gt;This is a good example of a long user-style query. The user might not remember the exact word &lt;em&gt;mice&lt;/em&gt;, but they remember the context: small mammals, homes and fields, laboratory research, genetics, and fast reproduction. That is exactly the kind of scenario where semantic search becomes much more useful than plain keyword matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this demo shows
&lt;/h2&gt;

&lt;p&gt;This small demo shows the practical difference between keyword search and semantic search.&lt;/p&gt;

&lt;p&gt;The application is not matching documents only by title or exact words. Instead, it compares the semantic meaning of the query vector against the stored document vectors and returns the nearest results. That is why the search can still work even when the user describes the idea indirectly or uses very different wording.&lt;/p&gt;

&lt;p&gt;If you want to see the complete flow in action, including model loading, document creation, and semantic search in the UI, you can watch the full video walkthrough here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/OtVV2oLS6Eo"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;This example&lt;/a&gt; shows that semantic search can be implemented directly in Atlassian Forge by combining local embeddings in Custom UI with vector search in &lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That already gives you a useful AI-powered retrieval flow: the app searches by meaning, not just by exact words, while still preserving &lt;strong&gt;Runs on Atlassian&lt;/strong&gt; eligibility.&lt;/p&gt;

&lt;p&gt;It is also a natural foundation for &lt;strong&gt;RAG&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgbebjijcuhxgjsu1s33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzgbebjijcuhxgjsu1s33.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the top matching documents returned by &lt;strong&gt;&lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;&lt;/strong&gt; are passed into the &lt;a href="https://developer.atlassian.com/platform/forge/runtime-reference/forge-llms-api/" rel="noopener noreferrer"&gt;&lt;strong&gt;Forge LLM API&lt;/strong&gt;&lt;/a&gt; as context, the app can move from semantic search to full &lt;strong&gt;Retrieval-Augmented Generation&lt;/strong&gt;. The retrieval step stays in &lt;strong&gt;&lt;a href="https://developer.atlassian.com/platform/forge/storage-reference/sql/" rel="noopener noreferrer"&gt;Forge SQL&lt;/a&gt;&lt;/strong&gt;, and the generation step can be handled by Atlassian-hosted LLMs.&lt;/p&gt;

&lt;p&gt;So &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;this example&lt;/a&gt; is not only a semantic search demo. It is also a practical starting point for building a fully &lt;strong&gt;Forge-native RAG application&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to explore the full source code, you can find the example application here: &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-ai" rel="noopener noreferrer"&gt;Example application on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>atlassian</category>
      <category>webassembly</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Virtual Materialized Views for Atlassian Forge SQL: A Two-Level Caching Approach in forge-sql-orm</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Mon, 16 Mar 2026 16:29:16 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/virtual-materialized-views-for-atlassian-forge-sql-a-two-level-caching-approach-in-forge-sql-orm-2p15</link>
      <guid>https://forem.com/vzakharchenko/virtual-materialized-views-for-atlassian-forge-sql-a-two-level-caching-approach-in-forge-sql-orm-2p15</guid>
      <description>&lt;p&gt;If you build non-trivial apps on Atlassian Forge SQL, sooner or later you run into the same limitation: &lt;a href="https://docs.pingcap.com/tidb/stable/views/#limitations" rel="noopener noreferrer"&gt;&lt;strong&gt;TiDB in Forge does not support materialized views.&lt;/strong&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4i7bv5c1237b6zzhuj6p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4i7bv5c1237b6zzhuj6p.png" alt=" " width="800" height="175"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This becomes painful as soon as your application needs read-heavy features built on complex joins, aggregations, or denormalized relational data.&lt;/p&gt;

&lt;p&gt;In practice, this usually leaves you with two unattractive options: recompute expensive queries on every resolver call and risk hitting &lt;strong&gt;strict Forge SQL limits&lt;/strong&gt; (like the 5s connection timeout or 16MB memory usage), or manually maintain precomputed tables and synchronization logic, which quickly turns into brittle application code.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt;, I’ve implemented a &lt;strong&gt;two-level caching approach&lt;/strong&gt; that acts as a &lt;strong&gt;virtual materialized view&lt;/strong&gt; at the application layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The first level&lt;/strong&gt; is an invocation-scoped in-memory cache that eliminates duplicate queries during a single resolver execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The second level&lt;/strong&gt; is a persistent cross-invocation cache backed by &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs, allowing expensive results to be reused across requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The library provides cache-aware write operations and automatic invalidation of affected queries after data changes. This means you can effectively “materialize” the results of expensive read queries and keep your relational features responsive without building a manual synchronization subsystem.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show how this pattern works, why a single invocation cache layer is usually not enough for Forge apps, and how this “virtual materialized“ view approach helps reduce repeated expensive SQL execution and keeps cacheable read paths fast within Forge platform limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “Why”: Understanding the 5-Second Timeout and the 16MB Memory Limit
&lt;/h2&gt;

&lt;p&gt;When you start building simple apps on &lt;strong&gt;Forge SQL&lt;/strong&gt;, TiDB feels surprisingly capable. Basic queries are fast, relational modeling is straightforward, and everything works well on small datasets. But as the application grows, the query layer changes too. Instead of simple lookups, you start building read-heavy features based on joins, aggregations, sorting, and denormalized relational views.&lt;/p&gt;

&lt;p&gt;That is usually the point where you hit the real platform boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 5-Second Timeout
&lt;/h3&gt;

&lt;p&gt;Forge SQL enforces a strict &lt;strong&gt;5-second connection timeout&lt;/strong&gt; for SELECT queries. Even if the Forge function itself still has time left to run (up to 25s), the database query can be terminated before the resolver finishes.&lt;/p&gt;

&lt;p&gt;This creates a common and dangerous pattern: a query that performs well on a small development dataset can become unreliable or fail completely on a large production tenant. Managing this is difficult for two main reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small datasets hide the problem:&lt;/strong&gt; A query that looks acceptable during development can cross the timeout boundary once the tables contain hundreds of thousands or millions of rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant size is unpredictable:&lt;/strong&gt; Forge apps run in a multi-tenant environment where different customers have radically different data volumes. You cannot reliably predict query behavior from your local setup alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The 16MB Memory Limit
&lt;/h3&gt;

&lt;p&gt;Execution time is not the only constraint. Forge SQL also enforces a strict &lt;strong&gt;per-query memory limit of 16MB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This means a query can still fail even when it is not particularly slow. Complex joins or memory-heavy execution strategies, such as &lt;strong&gt;HashJoin&lt;/strong&gt;, may exceed the memory budget and get cancelled with an “Exceeding the allowed memory limit” error. In practice, this often shows up only on enterprise-scale tenants, making these failures especially painful to reproduce locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability, Optimization, and the Caching Alternative
&lt;/h3&gt;

&lt;p&gt;Once you hit these limits, you need a clear strategy.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; You need to know which queries are slow or failing. I explored how to detect and surface these cases in my work on &lt;strong&gt;&lt;a href="https://community.developer.atlassian.com/t/practical-sql-observability-for-forge-apps-with-forge-sql-orm/97237" rel="noopener noreferrer"&gt;Practical SQL Observability for Forge Apps&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimization:&lt;/strong&gt; Some queries can be improved with better indexing or rewriting logic using EXPLAIN ANALYZE. I covered this in detail in &lt;strong&gt;&lt;a href="https://www.atlassian.com/blog/developer/optimizing-forge-sql-on-a-600k-database-with-tidb-explain" rel="noopener noreferrer"&gt;Optimizing Forge SQL on a 600K database with TiDB EXPLAIN&lt;/a&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Caching Alternative:&lt;/strong&gt; However, optimization is not always enough. Some queries are expensive by nature due to business requirements. As discussed in my &lt;strong&gt;AtlasCamp 2026&lt;/strong&gt; session, &lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=EL-kbJgk12o" rel="noopener noreferrer"&gt;Making Forge SQL observable&lt;/a&gt;&lt;/strong&gt;, caching becomes an architectural necessity.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The “Virtual Materialized View” Approach
&lt;/h2&gt;

&lt;p&gt;The most practical way to bypass these limits is to mimic the behavior of a &lt;strong&gt;Materialized View&lt;/strong&gt; at the application layer. Since TiDB in Forge does not support native materialized views, forge-sql-orm implements a &lt;strong&gt;Virtual Materialized View pattern&lt;/strong&gt; using two-level caching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Materialized Read Results:&lt;/strong&gt; Expensive query results are stored in &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs, creating a persistent snapshot that survives across invocations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent Integration:&lt;/strong&gt; Methods like selectCacheable() and executeCacheable() allow caching to happen behind the scenes, with cache keys derived from the SQL and parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Invalidation:&lt;/strong&gt; The ORM tracks &lt;strong&gt;affected tables&lt;/strong&gt; and automatically invalidates cached entries after INSERT, UPDATE, or DELETE operations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch-based Eviction:&lt;/strong&gt; To stay within Forge platform limits, invalidation is performed in controlled batches (up to 25 per transaction), maintaining consistency without manual synchronization code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a practical alternative to native materialized views: expensive relational queries are no longer recomputed on every request, keeping your application responsive within strict platform limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: The Two-Level Caching Architecture
&lt;/h2&gt;

&lt;p&gt;To improve performance while staying within Forge platform limits, &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt; uses a hybrid two-level caching strategy. The idea is simple: combine the low-latency benefits of in-memory caching inside a single invocation with the cross-invocation persistence of the Forge Key-Value Store (KVS).&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 1: Local Invocation Cache
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftsd4byef6bochl7rajs6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftsd4byef6bochl7rajs6.png" alt=" " width="800" height="1314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first cache layer lives entirely in memory and is scoped to a single resolver invocation. It is implemented using Node.js &lt;strong&gt;AsyncLocalStorage&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This layer solves a common problem in Forge applications: the same data may be requested multiple times during one execution path. A resolver might call helper functions, perform permission checks, or fetch related entities - all of which can trigger repeated reads for the same query.&lt;/p&gt;

&lt;p&gt;With L1 cache, the database is queried only once for a given cacheable read during that invocation. Any subsequent access returns the result directly from memory in &lt;strong&gt;&amp;lt;1ms&lt;/strong&gt;, avoiding additional SQL execution and unnecessary work. Because it is invocation-scoped, it is automatically discarded when the resolver finishes, ensuring no state is carried across unrelated requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Level 2: Persistent Cross-Invocation Cache
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfcf8pxa08l1tirb2nsc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxfcf8pxa08l1tirb2nsc.png" alt=" " width="800" height="1148"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second layer extends caching beyond a single invocation by storing results in &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Unlike in-memory state, KVS-backed entries survive across cold starts and new lambda initializations until their TTL expires or they are explicitly invalidated. This makes L2 caching ideal for expensive read results that are stable enough to be reused across requests. Instead of recalculating a heavy join for every user, the ORM reuses the stored result.&lt;/p&gt;

&lt;p&gt;To make this deterministic, the ORM generates a unique cache key from a hash of the SQL query text and its parameters. KVS is significantly faster than Forge SQL; while a SQL query might take 500ms (including latency), KVS typically returns results in &lt;strong&gt;40–50ms&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Architecture Fits Forge Well
&lt;/h3&gt;

&lt;p&gt;This two-level design addresses two distinct inefficiencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduces repeated SQL work within an invocation:&lt;/strong&gt; It stops duplicate reads triggered by nested service calls or helper functions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduces repeated computation across invocations:&lt;/strong&gt; Since local process memory is not persistent in serverless environments, &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs provides the necessary persistence across execution boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture creates a practical &lt;strong&gt;optimization path&lt;/strong&gt;. While KVS doesn’t “solve” a &lt;em&gt;5-second SQL timeout&lt;/em&gt; for a query that is inherently too slow to run even once, it allows you to &lt;strong&gt;decompose complex queries&lt;/strong&gt; into stable, reusable parts. By caching these intermediate or final results, you significantly reduce pressure on Forge SQL limits.&lt;/p&gt;

&lt;p&gt;Finally, reusing cached results also reduces repeated processing work in the application layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing the Virtual Materialized View Pattern
&lt;/h2&gt;

&lt;p&gt;To make the two-level caching model practical, &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt; provides a set of cache-aware query methods that let you treat expensive relational reads as reusable, cacheable views. Instead of manually working with &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs, you use an API that stays close to regular Drizzle ORM query building while adding transparent caching support for joins, distinct queries, and raw SQL.&lt;/p&gt;

&lt;p&gt;The library includes several cache-aware methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;selectCacheable()&lt;/strong&gt; - the main method for custom selections, joins, and derived read models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;selectCacheableFrom()&lt;/strong&gt; - shorthand for selecting and caching all columns with automatic field aliasing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;selectDistinctCacheable()&lt;/strong&gt; and &lt;strong&gt;selectDistinctCacheableFrom()&lt;/strong&gt; - variants for queries that require distinct results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;executeCacheable()&lt;/strong&gt; - caching support for raw SQL queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Core Method: selectCacheable()
&lt;/h3&gt;

&lt;p&gt;The most flexible entry point is selectCacheable(). It allows you to define exactly which columns should be included in the cached result and how the underlying relational query should be composed.&lt;/p&gt;

&lt;p&gt;In practice, this method handles the full read lifecycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the invocation-scoped memory cache (L1).&lt;/li&gt;
&lt;li&gt;Check the persistent KVS cache (L2).&lt;/li&gt;
&lt;li&gt;Fall back to Forge SQL if no cached result exists.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the following example, a joined query over users and orders is treated as a virtual materialized view. To make the behavior easier to observe, the query includes SLEEP(0.5) to simulate an expensive read path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SQL_CACHE_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectCacheable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`SLEEP(0.5)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;leftJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Happens at Runtime
&lt;/h3&gt;

&lt;p&gt;When this cacheable query is executed, the ORM performs a deterministic sequence of steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache key generation&lt;/strong&gt;: A unique key is derived from the generated SQL and its bound parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L1 cache lookup&lt;/strong&gt;: If the same query was already executed during the current resolver invocation, the result is returned directly from memory in &lt;strong&gt;&amp;lt;1ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L2 cache lookup&lt;/strong&gt;: If no in-memory entry exists, the ORM checks &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs for a persisted result. KVS typically responds in &lt;strong&gt;40–50ms&lt;/strong&gt;, which is much faster than a full SQL execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database execution on cache miss&lt;/strong&gt;: If both layers miss, the ORM executes the SQL, stores the result in the cache layers, and reuses it for subsequent reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Using Virtual Materialized Views in Subsequent Queries
&lt;/h3&gt;

&lt;p&gt;Once you have "materialized" your complex query results into a cached array, you can use that data to drive further relational queries with high efficiency. A common pattern is to use the IDs or keys from your materialized view to filter a primary table, essentially performing a "join" between a cached snapshot and live SQL data.&lt;/p&gt;

&lt;p&gt;Because the materialized result is already in memory (L1) or quickly retrieved from KVS (L2), you can use it to build highly targeted &lt;code&gt;WHERE ... IN (...)&lt;/code&gt; clauses. This avoids re-executing the original complex logic while ensuring you only fetch the specific records relevant to your materialized view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Filtering by Materialized IDs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this example, we take the results of our joined &lt;code&gt;SQL_CACHE_QUERY&lt;/code&gt; and use the resulting user IDs to fetch full profiles from the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Fetch the materialized view results (from cache or SQL fallback)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;materializedViewResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;SQL_CACHE_QUERY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Use the cached results to drive a targeted SQL query&lt;/span&gt;
&lt;span class="c1"&gt;// This is significantly faster than re-joining inside the database&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detailedUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;inArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="nx"&gt;materializedViewResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Hard Part: Cache Invalidation and Consistency
&lt;/h2&gt;

&lt;p&gt;A Virtual Materialized View is only as good as its invalidation strategy. In Forge, the real challenge is not storing expensive query results, but keeping them consistent after the underlying relational data changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt; addresses this by automating the synchronization between Forge SQL tables and cached entries stored in &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/kvs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Invalidation
&lt;/h3&gt;

&lt;p&gt;The library tracks table-level dependencies for cacheable queries. When data is modified through the ORM, any cached query associated with the affected tables is invalidated automatically.&lt;/p&gt;

&lt;p&gt;This applies to write operations such as INSERT, UPDATE, and DELETE, but only when they are executed through the cache-aware ORM APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immediate Eviction for Single Writes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rwcu70hed48ruula9wv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9rwcu70hed48ruula9wv.png" alt=" " width="737" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For standalone write operations, the library provides methods that execute the SQL statement and immediately evict related cache entries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;insertAndEvictCache()&lt;/li&gt;
&lt;li&gt;updateAndEvictCache()&lt;/li&gt;
&lt;li&gt;deleteAndEvictCache()&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These methods are essential when a resolver performs a single modification and consistency must be preserved immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consolidated Eviction with Cache Context
&lt;/h3&gt;

&lt;p&gt;When a resolver modifies multiple tables in one logical flow, evicting cache entries after each individual write is inefficient.&lt;/p&gt;

&lt;p&gt;To solve this, &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt; provides executeWithCacheContext(). Instead of triggering eviction immediately for every operation, this wrapper collects all affected tables during execution and performs &lt;strong&gt;one consolidated invalidation step&lt;/strong&gt; at the end.&lt;/p&gt;

&lt;p&gt;This makes multi-step write flows both faster and cheaper, which is critical in the Forge environment where execution time and KVS operations are metered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Batched Invalidation Across Multiple Tables
&lt;/h3&gt;

&lt;p&gt;In the resolver below, both &lt;em&gt;demo_users&lt;/em&gt; and &lt;em&gt;demo_orders&lt;/em&gt; may be modified inside the same cache context. Instead of purging related cache entries multiple times, the ORM aggregates the affected tables and performs one batch invalidation pass after the block completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;insertUserOrOrder&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeWithCacheContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userExists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Registers demoUsers for consolidated eviction&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;insertId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newOrder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Registers demoOrders for consolidated eviction&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// After the block finishes, the ORM invalidates cache entries&lt;/span&gt;
    &lt;span class="c1"&gt;// associated with the affected tables in one consolidated pass.&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optional Scheduled Cleanup
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1dhsrllplqtyhl5depab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1dhsrllplqtyhl5depab.png" alt=" " width="745" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As cache usage grows, invalidation-related operations can become more expensive, especially if many expired cache entries remain in storage.&lt;/p&gt;

&lt;p&gt;Although Forge KVS supports TTL, expired entries are not removed immediately; physical cleanup is asynchronous and can take up to 48 hours. Over time, this can increase the amount of stale metadata the invalidation logic needs to process.&lt;/p&gt;

&lt;p&gt;To reduce this overhead, &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt; provides an optional &lt;strong&gt;scheduled cleanup trigger&lt;/strong&gt;. It periodically removes expired cache entries using an expiration index, keeping the cache footprint small and eviction performance predictable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;clearCacheSchedulerTrigger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;forge&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;orm&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clearCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;clearCacheSchedulerTrigger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
     &lt;span class="na"&gt;cacheEntityName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# manifest.yml&lt;/span&gt;
  &lt;span class="na"&gt;scheduledTrigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clear-cache-trigger&lt;/span&gt;
      &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clearCache&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fiveMinute&lt;/span&gt; &lt;span class="c1"&gt;# Proactive cleanup every 5 minutes&lt;/span&gt;
  &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;clearCache&lt;/span&gt;
       &lt;span class="s"&gt;handler&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.clearCache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scheduler is particularly useful in high-traffic apps with heavy cache churn, preventing the accumulation of expired entries from slowing down your INSERT and UPDATE operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Benchmarks and Conclusion
&lt;/h2&gt;

&lt;p&gt;To see the practical effect of the virtual materialized view pattern, let’s compare the execution of the same joined query in two modes: direct SQL execution and cached execution through &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this example, the query joins demo_users and demo_orders. To make the difference easier to observe, the SQL includes an artificial SLEEP(0.5) delay that simulates an expensive read path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SQL_CACHE_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectCacheable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`SLEEP(0.5)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;leftJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;demoOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;demoUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scenario A: Direct SQL Execution
&lt;/h3&gt;

&lt;p&gt;Without caching, every request forces Forge SQL to execute the full joined query again. That means the database must repeat the join work and consume execution resources for every invocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Execution Plan (EXPLAIN):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQL: SELECT … FROM demo_users LEFT JOIN demo_orders … | Time: 4014 ms
Plan:
Projection_6 | task:root | … | \[estRows:8.75, actRows:8\] | execution info:time:4.01s
└─IndexHashJoin_13 | task:root | left outer join, inner:IndexLookUp_10 …
├─TableReader_30(Build) | task:root | data:TableFullScan_29 | \[estRows:7.00, actRows:7\]
│ └─TableFullScan_29 | task:cop\[tikv\] | access object:table:demo_users
└─IndexLookUp_10(Probe) | task:root | \[estRows:8.75, actRows:8\]
├─IndexRangeScan_8(Build) | task:cop\[tikv\] | access object:table:demo_orders, index:user_id
└─TableRowIDScan_9(Probe) | task:cop\[tikv\] | access object:table:demo_orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the benchmark environment, this query completed in &lt;strong&gt;4014 ms&lt;/strong&gt;. From a platform perspective, that is already uncomfortably close to the &lt;strong&gt;5-second SELECT timeout&lt;/strong&gt;. On a larger tenant, this kind of query can easily cross the boundary and fail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario B: L2 Cache Hit
&lt;/h3&gt;

&lt;p&gt;Once the query result has been stored in the persistent KVS-backed cache, subsequent executions no longer need to hit Forge SQL for the same read.&lt;/p&gt;

&lt;p&gt;In the benchmark environment, the cached result was returned in &lt;strong&gt;20 ms&lt;/strong&gt;. The important difference is not only lower latency, but also a different &lt;strong&gt;failure profile&lt;/strong&gt;: the application avoids repeating the expensive SQL execution path entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Benchmark Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Metric&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Direct SQL&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Cache Hit&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Effect&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Response time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4014 ms&lt;/td&gt;
&lt;td&gt;20 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Significantly lower latency&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SQL timeout exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Safer read path&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database execution cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Paid on every call&lt;/td&gt;
&lt;td&gt;Avoided&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Less repeated SQL work&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When This Pattern Makes Sense
&lt;/h3&gt;

&lt;p&gt;The virtual materialized view approach is especially useful for read-heavy features where the underlying data changes less frequently than it is read. Typical examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dashboards and reporting queries.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived read models&lt;/strong&gt; or aggregated relational views.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission-related lookups&lt;/strong&gt; and complex configurations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also serves as a late-stage optimization strategy. When a query remains expensive even after better indexes or query rewrites, caching the materialized result at the application layer becomes a practical alternative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Practices
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use this pattern selectively.&lt;/strong&gt; Small, cheap, and highly dynamic reads are often better executed directly in SQL to avoid KVS overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind Platform Limits.&lt;/strong&gt; This pattern works best for filtered, aggregated, or derived results, not for copying large tables into the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency Matters.&lt;/strong&gt; If a resolver modifies multiple tables, wrapping the operation in executeWithCacheContext() helps keep invalidation efficient and predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Forge SQL gives developers a powerful relational model, but platform limits mean some read-heavy workloads need more than query tuning alone. When native materialized views are not available, moving that behavior into the application layer keeps complex features responsive.&lt;/p&gt;

&lt;p&gt;That is the role of the virtual materialized view pattern in &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt;: combining two-level caching, deterministic cache keys, and automated invalidation to make expensive relational reads reusable without building a separate synchronization subsystem by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resources &amp;amp; Implementation Examples
&lt;/h3&gt;

&lt;p&gt;If you want to dive deeper into the code or see how this pattern is implemented in production-grade apps, explore the following resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Library&lt;/strong&gt;: &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm" rel="noopener noreferrer"&gt;forge-sql-orm on GitHub&lt;/a&gt; - the complete source code, documentation, and advanced features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official Cache Example&lt;/strong&gt;: &lt;a href="https://github.com/forge-sql-orm/forge-sql-orm/tree/master/examples/forge-sql-orm-example-cache" rel="noopener noreferrer"&gt;Advanced Caching Capabilities Example&lt;/a&gt;- a dedicated example showcasing performance monitoring and tiered caching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-World Production App&lt;/strong&gt;: &lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira" rel="noopener noreferrer"&gt;Forge Secure Notes for Jira&lt;/a&gt; - a full implementation of an AI-powered security app using the Virtual Materialized View pattern for secure analytics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atlassian Marketplace&lt;/strong&gt;: &lt;a href="https://marketplace.atlassian.com/apps/1267328525/secure-notes-for-jira" rel="noopener noreferrer"&gt;Secure Notes for Jira&lt;/a&gt; - see the final product in action on the marketplace.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>atlassian</category>
      <category>tidb</category>
      <category>forge</category>
      <category>database</category>
    </item>
    <item>
      <title>Practical SQL Observability for Forge Apps with forge-sql-orm</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Tue, 02 Dec 2025 20:01:44 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/practical-sql-observability-for-forge-apps-with-forge-sql-orm-473d</link>
      <guid>https://forem.com/vzakharchenko/practical-sql-observability-for-forge-apps-with-forge-sql-orm-473d</guid>
      <description>&lt;p&gt;Over the past month, I introduced a new observability layer inside my library &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;strong&gt;forge-sql-orm&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
The goal was simple: make Forge SQL &lt;em&gt;measurable&lt;/em&gt; and &lt;em&gt;transparent&lt;/em&gt; — without breaking &lt;strong&gt;Runs on Atlassian&lt;/strong&gt; compliance and without accessing &lt;strong&gt;any customer data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As part of my &lt;strong&gt;Codegeist&lt;/strong&gt; project, I integrated this layer into a real Forge app and connected it to PostHog.&lt;br&gt;
This instantly gave me end-to-end visibility into resolver performance across environments, tenants, and app versions - all while staying fully within the platform boundary.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Observability Is Especially Hard in Forge SQL
&lt;/h2&gt;

&lt;p&gt;Forge SQL is multitenant — the physical database is shared across many customers, and each tenant gets its own logical slice of data.&lt;/p&gt;

&lt;p&gt;In practice, this creates two major challenges:&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;1. Tenants can have radically different dataset sizes&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A resolver that runs in &lt;strong&gt;50–100 ms&lt;/strong&gt; for one customer may take &lt;strong&gt;hundreds of milliseconds&lt;/strong&gt;, or even &lt;strong&gt;seconds&lt;/strong&gt;, for another.&lt;/p&gt;

&lt;p&gt;And you have no way to know this ahead of time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you cannot see the tenant’s actual table sizes&lt;/li&gt;
&lt;li&gt;you cannot log into the underlying database&lt;/li&gt;
&lt;li&gt;you cannot estimate index selectivity per tenant&lt;/li&gt;
&lt;li&gt;you can receive slow-query entries from TiDB, but only &lt;em&gt;after&lt;/em&gt; a tenant has already experienced degraded performance&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;2. Platform-level analytics are available - but not enough&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Forge SQL exposes some low-level database metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow-query logs&lt;/li&gt;
&lt;li&gt;cluster statistics&lt;/li&gt;
&lt;li&gt;execution summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, these analytics are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;not tied to specific resolvers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;not correlated with app versions or environments&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;not connected to payload size or resolver logic&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;not continuous&lt;/strong&gt; (visible only when TiDB marks a query as “slow”)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;not designed to show trends, regressions, or per-tenant behavior&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They help diagnose extreme cases, but they’re not sufficient for understanding how your &lt;strong&gt;application&lt;/strong&gt; performs in real-world multi-tenant conditions.&lt;/p&gt;

&lt;p&gt;You only see what your resolver sees — nothing more.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;Why That Becomes a Real Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As schemas grow and joins become more complex, behavior becomes unpredictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A query with a perfect execution plan can still be slow for a large tenant.&lt;/li&gt;
&lt;li&gt;Pagination with large OFFSET becomes inconsistent between customers.&lt;/li&gt;
&lt;li&gt;A new join may be harmless in dev but catastrophic for a tenant with millions of rows.&lt;/li&gt;
&lt;li&gt;Regression detection is impossible without telemetry — you cannot see if performance worsened after a release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because all real data lives inside Atlassian infrastructure, the app developer has almost no visibility into how SQL behaves &lt;em&gt;“out in the wild.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is exactly the gap that the new observability layer is designed to fill.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;What the New Observability Layer Provides&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The layer automatically captures performance characteristics for every resolver invocation.&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Aggregated total DB execution time&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;All SQL statements executed by the resolver contribute to a single aggregated DB time metric.&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Tiered logging thresholds&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug&lt;/strong&gt; when total DB time &amp;gt; &lt;strong&gt;1000 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warn&lt;/strong&gt; when total DB time &amp;gt; &lt;strong&gt;2000 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thresholds can be tuned per resolver.&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Automatic SQL plan dump&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When total DB time exceeds &lt;strong&gt;2000 ms&lt;/strong&gt;, the ORM prints full execution plans for &lt;strong&gt;all queries executed inside the resolver&lt;/strong&gt; directly into Forge logs.&lt;/p&gt;

&lt;p&gt;This helps diagnose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unexpected &lt;code&gt;TableFullScan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;heavy &lt;code&gt;IndexJoin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;missing indexes&lt;/li&gt;
&lt;li&gt;window functions with large memory needs&lt;/li&gt;
&lt;li&gt;skewed statistics for a large tenant&lt;/li&gt;
&lt;li&gt;inefficient pagination&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Performance telemetry sent to allowed analytics tools (e.g., PostHog)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Only anonymized metadata is sent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cloudId&lt;/li&gt;
&lt;li&gt;environment&lt;/li&gt;
&lt;li&gt;resolverName&lt;/li&gt;
&lt;li&gt;appVersion&lt;/li&gt;
&lt;li&gt;totalDbExecutionTime&lt;/li&gt;
&lt;li&gt;totalResponseSize&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This enables weekly insights, multi-tenant comparisons, and regression detection — fully compliant and &lt;strong&gt;PII-free&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Automatic timeout and out-of-memory diagnostics&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The observability layer also detects and analyzes failures such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;“Your query has been cancelled due to exceeding the allowed memory limit for a single SQL query.”&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;“The provided query took more than 5000 milliseconds to execute.”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When these errors occur, the ORM retrieves and &lt;strong&gt;logs execution plans&lt;/strong&gt; for the failed queries — making it possible to understand the issue even without access to tenant data.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;Configurable by Design&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One important aspect of this observability layer is that it is &lt;strong&gt;not global&lt;/strong&gt;.&lt;br&gt;
It is &lt;strong&gt;resolver-level&lt;/strong&gt;, meaning every resolver can define its own behavior independently.&lt;/p&gt;

&lt;p&gt;Each resolver can configure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom thresholds&lt;/li&gt;
&lt;li&gt;warning levels&lt;/li&gt;
&lt;li&gt;plan-dump behavior&lt;/li&gt;
&lt;li&gt;analytics logic&lt;/li&gt;
&lt;li&gt;additional metadata&lt;/li&gt;
&lt;li&gt;sampling rules&lt;/li&gt;
&lt;li&gt;environment-specific overrides&lt;/li&gt;
&lt;li&gt;or disable observability entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This flexibility is powered by a simple callback structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;executeWithMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// resolver logic&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totaldbTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalResponseSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;printPlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// your custom logic here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the logic lives at the resolver level, the system is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;easy to tune&lt;/li&gt;
&lt;li&gt;lightweight&lt;/li&gt;
&lt;li&gt;platform-safe&lt;/li&gt;
&lt;li&gt;precise and predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each resolver gets exactly the level of observability it needs — no more, no less.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Even If Analytics Are Disabled — Observability Still Works&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Some customers disable analytics or block outbound requests entirely.&lt;/p&gt;

&lt;p&gt;In this case, no telemetry is sent to PostHog (or any analytics tool), but the observability layer &lt;strong&gt;still provides full visibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All essential diagnostic information remains available directly in the logs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;totalDbExecutionTime&lt;/li&gt;
&lt;li&gt;totalResponseSize&lt;/li&gt;
&lt;li&gt;per-query execution details&lt;/li&gt;
&lt;li&gt;full SQL plans&lt;/li&gt;
&lt;li&gt;timeout and OOM diagnostics&lt;/li&gt;
&lt;li&gt;resolver-level warnings and thresholds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means that even if analytics events never leave the customer’s infrastructure, the customer can still provide logs that fully explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;which resolver was slow&lt;/li&gt;
&lt;li&gt;which queries were executed&lt;/li&gt;
&lt;li&gt;what each plan looked like&lt;/li&gt;
&lt;li&gt;why the slowdown happened (bad plan, full scan, index join, statistics skew, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Observability does not depend on outbound analytics — telemetry is optional, but diagnostics are always available.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;100% inside the Forge boundary&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No external storage&lt;/li&gt;
&lt;li&gt;No outbound data beyond anonymized telemetry&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No PII&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;No customer content&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Runs on Atlassian — by design.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtefpdzd2i2q9ldc2skl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtefpdzd2i2q9ldc2skl.png" alt=" " width="800" height="45"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Integration Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;1. &lt;code&gt;manifest.yml&lt;/code&gt; (permissions)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;small&gt;&lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira/blob/c3be462ae8c939eae6f9ee150a8273822834f5cf/manifest.yml#L439" rel="noopener noreferrer"&gt;source&lt;/a&gt;&lt;/small&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.posthog.com"&lt;/span&gt;
          &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics&lt;/span&gt;
          &lt;span class="na"&gt;inScopeEUD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;2. Wrapping a resolver&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;small&gt;&lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira/blob/master/src/core/resolver/ActualResolver.ts" rel="noopener noreferrer"&gt;source&lt;/a&gt;&lt;/small&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Resolver&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolverName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test Resolver&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeWithMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;// resolver logic&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalResponseSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;printQueriesWithPlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ANALYTIC_SERVICE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendAnalytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sql_resolver_performance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalResponseSize&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s2"&gt;`Resolver &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has high database execution time: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;printQueriesWithPlan&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="s2"&gt;`Resolver &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has elevated database execution time: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;ms`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  &lt;strong&gt;3. Sending analytics to PostHog&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;small&gt;&lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira/blob/master/src/core/services/AnalyticService.ts" rel="noopener noreferrer"&gt;source&lt;/a&gt;&lt;/small&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAppContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;envName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environmentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;envId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environmentAri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environmentId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parsedVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appVersion&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;totalResponseSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalResponseSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;eventVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://eu.i.posthog.com/capture/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANALYTICS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;distinct_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is enough to build dashboards that reflect real-world performance across tenants.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;4. PostHog query for weekly resolver performance&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;envId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalDbExecutionTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avgTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;envName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;resolverName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsedVersion&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_seen_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'sql_resolver_performance'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eventVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;envId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;envName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolverName&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="n"&gt;avgTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  &lt;strong&gt;Putting It to the Test&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To verify the full pipeline, I intentionally left a performance bottleneck in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;SLEEP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pushed the total DB time above the 2000 ms threshold.&lt;/p&gt;

&lt;p&gt;Here’s what happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In PostHog, I immediately saw a spike:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmikm0a2m3ihxbiwbsnb1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmikm0a2m3ihxbiwbsnb1.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I opened the Forge logs and found the detailed plans:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87ti4eqzalsz2euddgnx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F87ti4eqzalsz2euddgnx.png" alt=" " width="800" height="86"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The execution plan pointed directly to the problematic place in code:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr2lgwt4nllybn9710bs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqr2lgwt4nllybn9710bs.png" alt=" " width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After removing the artificial delay, latency dropped from &lt;strong&gt;~2200 ms → ~150 ms&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;From Black Box to Glass Box&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before, Forge SQL was essentially a black box.&lt;br&gt;
Now, with observability integrated directly into &lt;strong&gt;forge-sql-orm&lt;/strong&gt;, it becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;diagnosable&lt;/li&gt;
&lt;li&gt;measurable&lt;/li&gt;
&lt;li&gt;predictable&lt;/li&gt;
&lt;li&gt;transparent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This observability layer is lightweight, compliant, and extremely helpful when building applications with complex schemas, heavy joins, and tenant-specific performance patterns.&lt;/p&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;Try It Yourself&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Codegeist project:&lt;br&gt;
👉 &lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira" rel="noopener noreferrer"&gt;https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;forge-sql-orm&lt;/code&gt; repository:&lt;br&gt;
👉 &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;https://github.com/vzakharchenko/forge-sql-orm&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Updates:
&lt;/h2&gt;
&lt;h2&gt;
  
  
  1. New deterministic default mode (TopSlowest)
&lt;/h2&gt;

&lt;p&gt;The default behavior no longer depends on &lt;code&gt;CLUSTER_STATEMENTS_SUMMARY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;forge-sql-orm now logs the &lt;strong&gt;exact SQL digests&lt;/strong&gt; executed inside the resolver, giving deterministic diagnostics even for long-running logic.&lt;/p&gt;

&lt;p&gt;By default it prints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;single slowest query&lt;/strong&gt;, and&lt;/li&gt;
&lt;li&gt;optionally that query’s &lt;strong&gt;execution plan&lt;/strong&gt; (&lt;code&gt;showSlowestPlans: true&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configurable like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;topQueries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// how many slowest queries to analyze&lt;/span&gt;
  &lt;span class="nx"&gt;showSlowestPlans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// re-executes them with EXPLAIN ANALYZE&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;showSlowestPlans&lt;/code&gt; is enabled — the library re-executes these queries with &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;.&lt;br&gt;
If disabled — it prints only SQL + timing.&lt;/p&gt;

&lt;p&gt;plan enabled:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmns424eeh1y0yfsjhmy7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmns424eeh1y0yfsjhmy7.png" alt=" " width="800" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;plan disabled:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52axr1bujvc5hkom5wa1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F52axr1bujvc5hkom5wa1.png" alt=" " width="800" height="30"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  2. SummaryTable mode (optional)
&lt;/h2&gt;

&lt;p&gt;SummaryTable mode still exists, but now works as an &lt;strong&gt;advanced diagnostic option&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It uses a &lt;strong&gt;short memory window&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;summaryTableWindowTime: 15000 // 15s default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If resolver execution exceeds this window, forge-sql-orm automatically falls back to &lt;strong&gt;TopSlowest&lt;/strong&gt;, avoiding stale diagnostics.&lt;/p&gt;

&lt;p&gt;This keeps SummaryTable useful for fresh metadata, but avoids relying on it for long workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Updated API
&lt;/h2&gt;

&lt;p&gt;Here is the updated API with all configuration options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;executeWithMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// resolver logic&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalDbTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalResponseSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;printPlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// your custom logic:&lt;/span&gt;
    &lt;span class="c1"&gt;// analytics, thresholds, alerts, logging, etc.&lt;/span&gt;
    &lt;span class="c1"&gt;// e.g.: if (totalDbTime &amp;gt; 1000) await printPlan();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;QueryPlanMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// "TopSlowest" | "SummaryTable" (default: TopSlowest)&lt;/span&gt;
    &lt;span class="nl"&gt;summaryTableWindowTime&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ms window for SummaryTable (default: 15000)&lt;/span&gt;
    &lt;span class="nl"&gt;topQueries&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// number of slowest queries to print (default: 1)&lt;/span&gt;
    &lt;span class="nl"&gt;showSlowestPlans&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// print EXPLAIN ANALYZE in TopSlowest mode (default: true)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything is opt-in and Forge-safe by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Timeout &amp;amp; OOM post-mortem diagnostics
&lt;/h2&gt;

&lt;p&gt;For catastrophic SQL failures, the library performs an immediate &lt;strong&gt;post-mortem lookup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Right after a Timeout or OOM, TiDB’s metadata is still in memory — so forge-sql-orm extracts the &lt;strong&gt;actual plan of the failing query&lt;/strong&gt; before eviction can occur.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case A: Timeout
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;“The provided query took more than 5000 milliseconds to execute…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88h8bv7ia6r8jdbf4huc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88h8bv7ia6r8jdbf4huc.png" alt=" " width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;forge-sql-orm automatically logs the execution plan of the failing query:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzhzwau091ud0xcf2f6v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhzhzwau091ud0xcf2f6v.png" alt=" " width="800" height="122"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Case B: Out of Memory (OOM)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;“Your query has been cancelled due to exceeding the allowed memory limit…”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4azndwzc0ane40e98em5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4azndwzc0ane40e98em5.png" alt=" " width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The library captures the memory-heavy plan that triggered the crash:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk381gaeff4heio84pwl3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk381gaeff4heio84pwl3.png" alt=" " width="800" height="107"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No re-execution required&lt;/strong&gt; - avoids triggering the same timeout or OOM again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plans come from the real execution&lt;/strong&gt; - captured with actual data distribution and bind parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tenant data is exposed&lt;/strong&gt; - metadata only, fully compliant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs entirely inside the Forge boundary&lt;/strong&gt; - no privileged access or special APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works reliably even for complex, deeply nested SQL workloads&lt;/strong&gt; - joins, pagination chains, window functions, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives developers a safe way to understand severe failures without privileged access.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why developer-side observability matters
&lt;/h2&gt;

&lt;p&gt;This configurability — implemented &lt;strong&gt;on the application side&lt;/strong&gt; — lets developers enable observability exactly where it’s needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resolver/long function/scheduler-level instrumentation&lt;/li&gt;
&lt;li&gt;custom thresholds&lt;/li&gt;
&lt;li&gt;selective plan printing&lt;/li&gt;
&lt;li&gt;sampling&lt;/li&gt;
&lt;li&gt;environment rules&lt;/li&gt;
&lt;li&gt;optional analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You don’t need my library to do any of this.&lt;/strong&gt;&lt;br&gt;
Developers can implement the same pattern manually — forge-sql-orm simply makes it easier and more consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developer-side observability naturally complements platform-level observability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Together, they enable building Forge apps with &lt;strong&gt;deep SQL execution paths&lt;/strong&gt;: complex joins, multi-stage pagination, window functions, large OFFSET workflows — while still maintaining transparency and safety.&lt;/p&gt;

</description>
      <category>sql</category>
      <category>monitoring</category>
      <category>performance</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Natural-Language SQL on Atlassian Forge: A Secure Pattern with Rovo (LLM) + Forge SQL (TiDB)</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Mon, 17 Nov 2025 07:00:00 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/atlassian-rovollm-forge-sql-a-secure-pattern-for-natural-language-analytics-in-forge-apps-2ap4</link>
      <guid>https://forem.com/vzakharchenko/atlassian-rovollm-forge-sql-a-secure-pattern-for-natural-language-analytics-in-forge-apps-2ap4</guid>
      <description>&lt;p&gt;While working on a Forge app that stores structured metadata in Forge SQL, I explored how Rovo could be used not only for documentation/explanations but also for natural-language analytics. &lt;br&gt;
(For context: this comes from the Codegeist 2025 project &lt;a href="https://github.com/vzakharchenko/Forge-Secure-Notes-for-Jira" rel="noopener noreferrer"&gt;Forge Secure Notes for Jira&lt;/a&gt; — built with &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;forge-sql-orm&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Rovo can generate SQL from natural language with high accuracy, but executing LLM-generated SQL requires a carefully controlled environment.&lt;/p&gt;

&lt;p&gt;This post describes a reproducible pattern for using Rovo with Forge SQL safely and predictably - with strict query validation, enforced single-table scope, row-level security, metadata checks, and EXPLAIN-based join detection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem: Safe Execution of LLM-Generated SQL
&lt;/h2&gt;

&lt;p&gt;Executing arbitrary SQL from an AI model introduces risks:&lt;br&gt;
• ensuring the query is strictly SELECT&lt;br&gt;
• ensuring it only targets the intended table&lt;br&gt;
• preventing hidden joins or subqueries&lt;br&gt;
• preventing column spoofing&lt;br&gt;
• enforcing row-level security&lt;br&gt;
• ensuring context variables cannot be manipulated&lt;/p&gt;

&lt;p&gt;Rovo must never be trusted to enforce constraints.&lt;/p&gt;

&lt;p&gt;All protections belong in the backend executor.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Solution: The “Guide + Guard” Pattern&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Two independent layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Guide - the Rovo prompt in &lt;em&gt;manifest.yml&lt;/em&gt;&lt;br&gt;
Defines strict boundaries and shapes expected SQL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Guard - backend validator in &lt;em&gt;RovoService.ts&lt;/em&gt;&lt;br&gt;
Enforces safety regardless of the generated SQL.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Guide improves accuracy.&lt;br&gt;
The Guard guarantees security.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Guide Layer (manifest.yml)
&lt;/h3&gt;

&lt;p&gt;On the “guide” side, the rovo:agent prompt inside manifest.yml defines strict boundaries for what Rovo is allowed to generate.&lt;br&gt;
This ensures the SQL surface is small, predictable, and secure.&lt;br&gt;
The pattern follows one core rule:&lt;br&gt;
&lt;strong&gt;One Rovo Action → One table → No joins → Single SELECT statement&lt;/strong&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 1.  Hard constraints enforced by the agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The prompt defines non-negotiable SQL restrictions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;only&lt;/strong&gt; read-only SELECT statements&lt;/li&gt;
&lt;li&gt;Query &lt;strong&gt;only&lt;/strong&gt; the designated table (security_notes in this case)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; generate any join:

&lt;ul&gt;
&lt;li&gt;no INNER / LEFT / RIGHT / FULL / CROSS JOINs&lt;/li&gt;
&lt;li&gt;no implicit joins&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never&lt;/strong&gt; reference other tables:

&lt;ul&gt;
&lt;li&gt;not in FROM&lt;/li&gt;
&lt;li&gt;not in JOINs&lt;/li&gt;
&lt;li&gt;not in subqueries&lt;/li&gt;
&lt;li&gt;not in CTEs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;No DML or DDL:

&lt;ul&gt;
&lt;li&gt;no INSERT, UPDATE, DELETE&lt;/li&gt;
&lt;li&gt;no ALTER, DROP, CREATE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;All execution must go through the backend action&lt;/li&gt;
&lt;li&gt;Permissions and row-level filtering are enforced server-side, not by Rovo itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These restrictions ensure that all generated SQL falls inside a safe, enforceable envelope.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 2.  Mandatory security columns&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every query must include &lt;strong&gt;exactly as raw table fields&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* created_by
* target_user_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These fields allow the backend to apply strict row-level security and to validate the column origins.&lt;br&gt;
The agent is forbidden from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aliasing&lt;/li&gt;
&lt;li&gt;renaming&lt;/li&gt;
&lt;li&gt;duplicating&lt;/li&gt;
&lt;li&gt;wrapping in expressions&lt;/li&gt;
&lt;li&gt;generating constants&lt;/li&gt;
&lt;li&gt;pulling them from derived tables or subqueries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend enforces this via metadata (orgTable) returned by Forge SQL.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 3.  Context variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent may insert these placeholders, which the backend later replaces with authenticated Forge context values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;:currentUserId&lt;/li&gt;
&lt;li&gt;:currentIssueKey&lt;/li&gt;
&lt;li&gt;:currentProjectKey&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt also defines natural-language mappings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“my notes” → (created_by = :currentUserId OR target_user_id = :currentUserId)&lt;/li&gt;
&lt;li&gt;“notes I created” → created_by = :currentUserId&lt;/li&gt;
&lt;li&gt;“this issue” → issue_key = :currentIssueKey&lt;/li&gt;
&lt;li&gt;“last week” → created_at &amp;gt;= DATE_SUB(NOW(), INTERVAL 1 WEEK)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives Rovo deterministic, controlled semantics.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Step 4.  Tools / actions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the end of every interaction, Rovo must:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build a valid SELECT query&lt;/strong&gt; following all rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute it strictly via the action:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ "sql": "&amp;lt;your select query&amp;gt;" }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;After receiving the backend result, the agent must:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;summarize the results clearly&lt;/li&gt;
&lt;li&gt;highlight useful counts or trends&lt;/li&gt;
&lt;li&gt;optionally present a small table of values&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives users friendly natural-language insights while preserving backend-enforced safety.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;2. Guard Layer (RovoService.ts)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This function rewrites and validates SQL generated by Rovo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1. SQL normalization&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All queries are normalized:&lt;/p&gt;

&lt;p&gt;• newlines removed&lt;br&gt;
• tabs and multiple spaces collapsed&lt;br&gt;
• trailing semicolon removed&lt;br&gt;
• user URI prefixes removed&lt;/p&gt;

&lt;p&gt;This prevents multiline or formatting-based evasion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;sql&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[\\&lt;/span&gt;&lt;span class="sr"&gt;n&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;r&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;t&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;s+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="sr"&gt;;&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="sr"&gt;s&lt;/span&gt;&lt;span class="se"&gt;\*&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;“”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2. Static analysis: single SELECT via AST&lt;/strong&gt;&lt;br&gt;
Before touching the forge sql, the query is parsed into an AST using node-sql-parser.&lt;br&gt;
This guarantees that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the query is a &lt;strong&gt;single&lt;/strong&gt; statement&lt;/li&gt;
&lt;li&gt;the statement type is strictly &lt;strong&gt;SELECT&lt;/strong&gt; (no INSERT/UPDATE/DELETE/DDL)&lt;/li&gt;
&lt;li&gt;multiple statements or mixed types are immediately rejected
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedQuery&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`SQL parsing error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid SQL syntax&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. `&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please check your query syntax.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AST may be a single node or an array of statements&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only a single SELECT query is allowed. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Multiple statements or non-SELECT statements are not permitted.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;ast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ast&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only SELECT queries are allowed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Step 3. AST-based table scope + + EXPLAIN-based join detection&lt;/strong&gt;&lt;br&gt;
Two protections work together:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.1. The query must reference only the allowed table&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const tablesInQuery = this.extractTables(ast);      // walks FROM, JOIN, subqueries
const uniqueTables = [...new Set(tablesInQuery)];
const invalidTables = uniqueTables.filter(
  (table) =&amp;gt; table !== "SECURITY_NOTES"
);

if (invalidTables.length &amp;gt; 0) {
  throw new Error(
    "Security violation: query references table(s) other than 'security_notes': " +
      invalidTables.join(", ") +
      ". Only queries against the security_notes table are allowed. " +
      "JOINs, subqueries, or references to other tables are not permitted."
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the check is AST-based, it also catches:&lt;br&gt;
    • tables used inside scalar subqueries&lt;br&gt;
    • tables referenced in nested expressions&lt;br&gt;
    • tables hidden behind aliases&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3.2. EXPLAIN plan must not contain join operators&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This prevents hidden joins, optimizer-rewritten joins, or subquery-based joins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;explainRows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;FORGE_SQL_ORM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;explainRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasJoin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;explainRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operatorInfo&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="err"&gt;“”&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;JOIN&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
       &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;HASH&lt;/span&gt; &lt;span class="nx"&gt;JOIN&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
       &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;NESTED&lt;/span&gt; &lt;span class="nx"&gt;LOOP&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
       &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;CARTESIAN&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasJoin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="nx"&gt;JOIN&lt;/span&gt; &lt;span class="nx"&gt;operations&lt;/span&gt; &lt;span class="nx"&gt;are&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="nx"&gt;Rovo&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3.3. EXPLAIN plan must not contain window functions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To keep the model simple and safe, we completely block any window functions at the EXPLAIN level.&lt;/p&gt;

&lt;p&gt;Window functions like COUNT(*) OVER(...) can leak information about the total number of rows that exist beyond the current user’s slice of data.&lt;br&gt;
This ensures the query executes strictly against a single table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;explainRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operatorInfo&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;WINDOW&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; OVER(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; OVER()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Window functions (for example COUNT(*) OVER(...)) are not allowed in Rovo SQL for this app. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Please rephrase your question so that it uses regular aggregates instead of window functions.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3.4. EXPLAIN plan must not reference other tables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To guarantee that this Rovo action truly works &lt;strong&gt;only&lt;/strong&gt; on security_notes, we validate the execution plan itself.&lt;/p&gt;

&lt;p&gt;We scan the accessObject column from EXPLAIN and reject any plan that touches a table other than security_notes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Detect any table access other than `security_notes`
const tablesInPlan = explainRows.filter(
  (row) =&amp;gt;
    row.accessObject?.startsWith("table:") &amp;amp;&amp;amp;
    row.accessObject !== "table:security_notes",
);

if (tablesInPlan.length &amp;gt; 0) {
  throw new Error(
    "Security violation: query plan references tables other than 'security_notes'. " +
      "This Rovo action only allows analytics over the security_notes table. " +
      "Please remove JOINs, subqueries, or references to other tables and try again.",
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures the query executes strictly against a single table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4. Replace context variables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Placeholders are replaced with authenticated values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;:currentUserId
:currentIssueKey
:currentProjectKey
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prevents Rovo from injecting or modifying identity context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5. Row-Level Security (non-admin users)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Non-admin queries are wrapped with enforced filtering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT * FROM (&amp;lt;normalized_rovo_query&amp;gt;) AS t
       WHERE t.created_by = ‘currentUserId’ OR t.target_user_id = ‘currentUserId’;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This guarantees users can only see:&lt;br&gt;
• notes they created&lt;br&gt;
• notes shared with them&lt;br&gt;
even if the model produces an incorrect or unsafe WHERE clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6. Post-execution metadata validation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forge SQL exposes orgTable metadata for each column.&lt;br&gt;
The executor validates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created_by.orgTable = “security_notes”
target_user_id.orgTable = “security_notes”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the columns originate from:&lt;br&gt;
• constants&lt;br&gt;
• expressions&lt;br&gt;
• subqueries&lt;br&gt;
• other tables&lt;/p&gt;

&lt;p&gt;the query is rejected.&lt;br&gt;
Metadata validation prevents column spoofing and ensures RLS correctness.&lt;/p&gt;

&lt;p&gt;Additionally, we validate that &lt;strong&gt;every column that has an orgTable defined&lt;/strong&gt; must also come from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;orgTable === "security_notes"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides a third layer of protection by ensuring that no fields in the result set were sourced from other tables via joins or nested queries — even if the SQL text tried to hide it.&lt;/p&gt;

&lt;p&gt;Metadata validation fully closes the loop and ensures the backend executes exactly what was intended:&lt;br&gt;
a safe, single-table query over security_notes(&lt;strong&gt;Computed fields remain allowed&lt;/strong&gt;) with correct RLS boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7. Most Important, platform guarantees that strengthen the pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addition to app-level validation, Forge SQL provides two important safety properties:&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Single-statement execution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forge SQL only allows &lt;em&gt;one&lt;/em&gt; SQL statement per call.&lt;/p&gt;

&lt;p&gt;No multi-statements, no chaining — making traditional SQL injection impossible.&lt;/p&gt;

&lt;p&gt;• &lt;strong&gt;Tenant isolation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Forge SQL’s architecture ensures strict separation between tenants.&lt;/p&gt;

&lt;p&gt;Even incorrectly formed queries cannot access data outside the current tenant’s schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architectural Note: One Table per Rovo Action
&lt;/h2&gt;

&lt;p&gt;The security guarantees in this pattern rely on the fact that each Rovo Action operates on &lt;strong&gt;exactly one table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This constraint keeps the execution predictable:&lt;/p&gt;

&lt;p&gt;• ensures stable SQL structure&lt;br&gt;
• simplifies EXPLAIN-based join detection&lt;br&gt;
• makes metadata validation (orgTable) reliable&lt;br&gt;
• ensures enforceable row-level security&lt;br&gt;
• eliminates ambiguity in column origin&lt;br&gt;
If analytics require data from multiple datasets, there are several possible approaches that remain compatible with the “one table per action” pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Use a consolidated analytics table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A process (scheduled trigger, background sync, or nightly aggregation) can populate a dedicated table containing the combined fields needed for analytics.&lt;br&gt;
Rovo then queries that single table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — Use a separate rovo:action per table&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the application exposes multiple datasets, each dataset can have its own Rovo Action with its own validation logic — each still limited to one table.&lt;br&gt;
This keeps the security properties of the pattern intact.&lt;br&gt;
Both approaches preserve the core property:&lt;br&gt;
&lt;strong&gt;each Rovo Action should operate on one table to ensure strict, deterministic validation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a structured Guide in the manifest and a strict Guard in the executor:&lt;br&gt;
• natural-language analytics becomes possible&lt;br&gt;
• all LLM-generated SQL is sandboxed&lt;br&gt;
• row-level security is always enforced&lt;br&gt;
• joins and cross-table access are safely blocked&lt;br&gt;
• SQL injection is prevented by design&lt;br&gt;
• users get a powerful analytics interface without compromising security&lt;/p&gt;

&lt;p&gt;This pattern provides a safe execution envelope for Rovo-driven SQL in Forge apps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzf3y648owyg6ebbt6k0m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzf3y648owyg6ebbt6k0m.gif" alt=" " width="468" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full Source Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rovo Prompt &lt;a href="https://raw.githubusercontent.com/vzakharchenko/Forge-Secure-Notes-for-Jira/refs/heads/master/manifest.yml" rel="noopener noreferrer"&gt;manifest.yml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Secure Executor &lt;a href="https://raw.githubusercontent.com/vzakharchenko/Forge-Secure-Notes-for-Jira/refs/heads/master/src/core/services/RovoService.ts" rel="noopener noreferrer"&gt;RovoService.ts&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>atlassian</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Integrating External Services in Atlassian Forge — Without Losing the “Runs on Atlassian” Eligibility</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Sun, 12 Oct 2025 09:59:13 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/integrating-external-services-in-atlassian-forge-without-losing-the-runs-on-atlassian-5d73</link>
      <guid>https://forem.com/vzakharchenko/integrating-external-services-in-atlassian-forge-without-losing-the-runs-on-atlassian-5d73</guid>
      <description>&lt;p&gt;When building Forge apps, sooner or later you’ll want to integrate with an &lt;strong&gt;external system&lt;/strong&gt; — maybe for monitoring, data synchronization, or automation.&lt;br&gt;
But the moment you call an external API using fetch(), your app loses the &lt;strong&gt;Runs on Atlassian&lt;/strong&gt; badge.&lt;/p&gt;

&lt;p&gt;This article shows how to design such an integration without any egress — meaning the Forge app never sends data outside Atlassian Cloud — and still communicates securely with an external backend.&lt;/p&gt;

&lt;p&gt;The full demo project is available on GitHub:&lt;br&gt;
&lt;a href="https://github.com/vzakharchenko/Forge-Health-Monitor" rel="noopener noreferrer"&gt;Forge Health Monitor&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The solution is based on two key capabilities that Atlassian Forge provides.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Safe navigation to external resources via router.navigate
&lt;/h3&gt;

&lt;p&gt;Forge allows &lt;a href="https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/router/#navigate" rel="noopener noreferrer"&gt;navigation to external pages&lt;/a&gt; using the &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/bridge&lt;/strong&gt; API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@forge/bridge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registrationUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this happens, Forge automatically shows a security confirmation dialog, warning the user that they’re leaving the Atlassian environment.&lt;br&gt;
Importantly, this does not require any permissions in your manifest.yml.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jqz24j1u07m2nwlsee0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jqz24j1u07m2nwlsee0.png" alt=" " width="604" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key idea is that during this redirect, the app passes non-sensitive contextual data to the external backend, such as:&lt;br&gt;
• &lt;strong&gt;cloudId&lt;/strong&gt; — the unique tenant identifier (used to separate data by tenant on the external backend)&lt;br&gt;
• &lt;strong&gt;accountId&lt;/strong&gt; — the Atlassian account of the user (may be shared across multiple tenants)&lt;br&gt;
• &lt;strong&gt;triggerUrl&lt;/strong&gt; — a static web trigger endpoint unique to each tenant (&lt;strong&gt;unauthenticated by default&lt;/strong&gt;, so custom authorization logic must be implemented)&lt;br&gt;
• &lt;strong&gt;callbackUrl&lt;/strong&gt; — the Forge page to return &lt;/p&gt;

&lt;p&gt;This pattern can also be extended to support OAuth authorization flows (Google, Microsoft, or Keycloak).&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Static Web Triggers for inbound communication
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.atlassian.com/platform/forge/manifest-reference/modules/web-trigger/#static-web-trigger" rel="noopener noreferrer"&gt;Static Web Triggers&lt;/a&gt; are the second key mechanism.&lt;br&gt;
They allow external services to send HTTP requests into Forge — and doing so does not violate the Runs on Atlassian requirement, because the call direction is inbound, not outbound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web triggers don’t provide built-in authentication, so you must implement it yourself&lt;/strong&gt;.&lt;br&gt;
In this example, authentication is done using Ed25519-signed requests — it’s a minimal working approach, though it could be further improved.&lt;/p&gt;
&lt;h2&gt;
  
  
  What &lt;a href="https://github.com/vzakharchenko/Forge-Health-Monitor" rel="noopener noreferrer"&gt;Forge Health Monitor&lt;/a&gt; Application Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Forge Health Monitor&lt;/strong&gt; is a monitoring solution that allows you to track the health status of external services directly from your Jira instance. Here's what it does:&lt;/p&gt;
&lt;h3&gt;
  
  
  Core Functionality:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monitors External Services&lt;/strong&gt; - Continuously checks if your external APIs, websites, or services are running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time Status Display&lt;/strong&gt; - Shows current health status (ALIVE/DOWN) for all monitored services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-friendly Interface&lt;/strong&gt; - Simple Jira global page where you can view all your service statuses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Updates&lt;/strong&gt; - Refreshes status every 30 seconds without manual intervention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how it works conceptually:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1p1cmbzxi40s49seczkv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1p1cmbzxi40s49seczkv.png" alt=" " width="800" height="873"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How to Run the Example
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clone the repository&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/vzakharchenko/Forge-Health-Monitor
cd Forge-Health-Monitor
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Install dependencies&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Register the app in your Atlassian Developer Console&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge register
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Start an ngrok tunnel on port 8080 — this will expose your custom backend:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ngrok http 8080
# Note the HTTPS URL (e.g., https://abc123.ngrok.app)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nzxankb9gug68dr4um3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7nzxankb9gug68dr4um3.png" alt=" " width="800" height="68"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Set the ngrok URL as a Forge environment variable&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge variables set BACKEND_URL https://abc123.ngrok.app
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Deploy the Forge app&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge deploy
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Install it into your Jira instance&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;forge install
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Start the custom backend&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd ./customBackend
npm install
npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj06hulru7syi7sqqpnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmj06hulru7syi7sqqpnj.png" alt=" " width="774" height="246"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;The app and backend are now running — open your Forge app in Jira and test the flow!&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;h2&gt;
  
  
  Exploring the App
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open your Jira instance and launch the Forge app&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjilfi4yxpx117z5dekd6.png" alt=" " width="638" height="326"&gt;
&lt;/li&gt;
&lt;li&gt;Click “Add Service”&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnr3mo8tyh5we2ee4ptug.png" alt=" " width="800" height="143"&gt;
&lt;/li&gt;
&lt;li&gt;Confirm navigation by clicking “Continue” in the system dialog&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F10vcboqm1mrj2ffkdgjs.png" alt=" " width="604" height="202"&gt;
&lt;/li&gt;
&lt;li&gt;Enter any publicly available health check URL that returns HTTP 200 (no authentication required).
Only http and https URLs are accepted in this demo for simplicity.&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwdb00qajqsafb9g379d.png" alt=" " width="800" height="406"&gt;
&lt;/li&gt;
&lt;li&gt;Click Continue, and you’ll return to the Forge app.
You’ll see the new service in the list — the backend will now periodically (every 5 minutes) check the service’s availability.&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F27yzde74ghfum0r9uitf.png" alt=" " width="800" height="353"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;With this setup, we successfully integrated an external service without violating Runs on Atlassian.&lt;br&gt;
All outbound requests are performed by the external backend, while Forge remains completely isolated and only receives signed updates through web triggers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update (confirmed by Atlassian Staff)
&lt;/h2&gt;

&lt;p&gt;Atlassian Staff member &lt;strong&gt;rmassaioli&lt;/strong&gt; confirmed that this inbound-only integration pattern —&lt;br&gt;
using &lt;strong&gt;route.navigate&lt;/strong&gt; and &lt;strong&gt;static web triggers&lt;/strong&gt; — is exactly what those features were designed for and fully compatible with the &lt;strong&gt;Runs on Atlassian&lt;/strong&gt; model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpt5a9407d3wa32a91a5f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpt5a9407d3wa32a91a5f.png" alt=" " width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This means the approach isn’t a workaround — it’s an intended architectural path for secure external integrations that stay fully within Atlassian’s trusted boundary.&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>forge</category>
      <category>integration</category>
      <category>development</category>
    </item>
    <item>
      <title>Pagination with COUNT(*) OVER() in Atlassian Forge SQL: How to avoid double queries — and stay secure in TypeScript</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Sun, 06 Apr 2025 11:21:41 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/pagination-with-count-over-in-atlassian-forge-sql-how-to-avoid-double-queries-and-stay-3o8e</link>
      <guid>https://forem.com/vzakharchenko/pagination-with-count-over-in-atlassian-forge-sql-how-to-avoid-double-queries-and-stay-3o8e</guid>
      <description>&lt;p&gt;Are you working with &lt;code&gt;@forge/sql&lt;/code&gt; and struggling with pagination + total count?&lt;br&gt;&lt;br&gt;
Here's a more efficient and safer way to do it using &lt;strong&gt;window functions&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  💡 The problem
&lt;/h2&gt;

&lt;p&gt;In typical pagination, we often do &lt;em&gt;two queries&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if your SQL engine supports &lt;strong&gt;window functions&lt;/strong&gt; — like &lt;code&gt;TiDB&lt;/code&gt; in Atlassian Forge — you can get everything in &lt;strong&gt;a single query&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;OVER&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_count&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll get a &lt;code&gt;total_count&lt;/code&gt; in each row — and no second query needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Using it with &lt;code&gt;forge-sql-orm&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ForgeSQL&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-sql-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForgeSQL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;totalCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;`COUNT(*) OVER()`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatLimitOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatLimitOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s a safe helper for &lt;code&gt;LIMIT&lt;/code&gt;/&lt;code&gt;OFFSET&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatLimitOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limitOrOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;limitOrOffset&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limitOrOffset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;limitOrOffset must be a valid number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limitOrOffset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ Why this matters — Forge SQL doesn’t support bind params for LIMIT/OFFSET
&lt;/h2&gt;

&lt;p&gt;Libraries like Drizzle normally compile queries like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But Forge SQL (powered by TiDB) doesn’t support parameterized &lt;code&gt;LIMIT&lt;/code&gt; or &lt;code&gt;OFFSET&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
So you have to inline the values — &lt;strong&gt;carefully&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔐 And yes, you can still get SQL injection in &lt;code&gt;.prepare(...)&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Even this Forge-native code is &lt;strong&gt;vulnerable&lt;/strong&gt;, if &lt;code&gt;limit&lt;/code&gt; or &lt;code&gt;offset&lt;/code&gt; come from user input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepare&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM users LIMIT &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; OFFSET &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Just because &lt;code&gt;limit: number&lt;/code&gt; in TypeScript doesn’t mean it’s actually a number at runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;✅ Use runtime checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeLimitOffset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid limit or offset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ✅ One query, safer code, better performance
&lt;/h2&gt;

&lt;p&gt;This pattern works in Atlassian Forge SQL, MySQL, TiDB, PlanetScale, and more.&lt;/p&gt;

&lt;p&gt;If you're using &lt;code&gt;@forge/sql&lt;/code&gt;, &lt;code&gt;forge-sql-orm&lt;/code&gt;, or raw Drizzle — this will help keep things clean and fast.&lt;/p&gt;

&lt;p&gt;🧑‍💻 Project: &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;forge-sql-orm&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Questions and feedback welcome!&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>forge</category>
      <category>drizzle</category>
      <category>sql</category>
    </item>
    <item>
      <title>Type-safe ORM for Atlassian Forge SQL: forge-sql-orm with Drizzle support</title>
      <dc:creator>Vasiliy Zakharchenko</dc:creator>
      <pubDate>Sat, 05 Apr 2025 12:41:02 +0000</pubDate>
      <link>https://forem.com/vzakharchenko/type-safe-orm-for-atlassian-forge-sql-forge-sql-orm-with-drizzle-support-18ck</link>
      <guid>https://forem.com/vzakharchenko/type-safe-orm-for-atlassian-forge-sql-forge-sql-orm-with-drizzle-support-18ck</guid>
      <description>&lt;h2&gt;
  
  
  💡 &lt;code&gt;forge-sql-orm&lt;/code&gt;: Type-safe ORM for &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/sql based on Drizzle
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Are you working with &lt;code&gt;@forge/sql&lt;/code&gt; and tired of writing raw SQL with manual typings?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;forge-sql-orm&lt;/code&gt; brings a structured, type-safe, and developer-friendly experience using Drizzle ORM — fully compatible with Forge’s infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;I'd like to introduce &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;&lt;code&gt;forge-sql-orm&lt;/code&gt;&lt;/a&gt; — a type-safe ORM designed for working with the Atlassian Forge platform using &lt;code&gt;@forge/sql&lt;/code&gt;, built on top of &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;Drizzle ORM&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔍 Why &lt;code&gt;forge-sql-orm&lt;/code&gt;?
&lt;/h3&gt;

&lt;h4&gt;
  
  
  🗋 Atlassian provides &lt;code&gt;@forge/sql&lt;/code&gt;...
&lt;/h4&gt;

&lt;p&gt;...but you still have to write SQL queries manually and manage typings yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@forge/sql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;City&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepare&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;City&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM cities WHERE name = ?`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New York&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⛔️ You must manually define interfaces, manage aliasing, and ensure column matching for joins.&lt;/p&gt;

&lt;p&gt;⚠️ Also, &lt;code&gt;prepare&amp;lt;City&amp;gt;()&lt;/code&gt; &lt;strong&gt;does not guarantee&lt;/strong&gt; that the returned SQL data actually matches the &lt;code&gt;City&lt;/code&gt; interface — it's just a TypeScript hint, not runtime-validated.&lt;/p&gt;

&lt;p&gt;📝 While this works fine for simple queries, it becomes problematic when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fields are &lt;strong&gt;selected dynamically&lt;/strong&gt; (e.g., from frontend input),&lt;/li&gt;
&lt;li&gt;The structure of the result changes conditionally,&lt;/li&gt;
&lt;li&gt;You join multiple tables that may share the same column names.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;-- which 'name' is which?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You end up needing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;users_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;company_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then you must manually write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prepare&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;users_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🙄 It's verbose and hard to maintain.&lt;/p&gt;




&lt;h3&gt;
  
  
  ✅ &lt;code&gt;forge-sql-orm&lt;/code&gt; solves this in 2 ways:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Automatically via &lt;code&gt;forgeSQL.select({...})&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ForgeSQL&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-sql-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForgeSQL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;innerJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🌟 Columns are aliased by object keys (&lt;code&gt;user.name&lt;/code&gt;, &lt;code&gt;order.id&lt;/code&gt;) and typed safely.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Manually via &lt;code&gt;selectAliased(...)&lt;/code&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/mysql-proxy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;forgeDriver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patchDbWithSelectAliased&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-sql-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;patchDbWithSelectAliased&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;forgeDriver&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;selectAliased&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;innerJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Ideal for those working directly with Drizzle's low-level API.&lt;/p&gt;




&lt;h3&gt;
  
  
  📐 Schema First Migration Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;forge-sql-orm&lt;/code&gt; and &lt;code&gt;forge-sql-orm-cli&lt;/code&gt; follow a &lt;strong&gt;Schema First&lt;/strong&gt; approach — where your database schema is the single source of truth, rather than relying on manually written model definitions.&lt;/p&gt;

&lt;p&gt;This means you have two paths to get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you already have a working Forge SQL database, you can extract the schema directly from it.&lt;/li&gt;
&lt;li&gt;Or, if you're starting fresh, you can create a brand-new local or shared MySQL database and define your schema manually.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🧠 What does that mean?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;You define your tables using SQL or existing Forge migrations.&lt;/li&gt;
&lt;li&gt;You can extract your current schema from Forge SQL and apply it to a local environment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forge-sql-orm-cli&lt;/code&gt; then generates Drizzle models based on the actual schema — not assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ Bonus: it works with &lt;strong&gt;any MySQL-compatible database&lt;/strong&gt;, not just Forge SQL. That means you can use a shared or local dev DB as your source of truth.&lt;/p&gt;

&lt;p&gt;This is especially useful if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have an existing Forge SQL project with migrations.&lt;/li&gt;
&lt;li&gt;You want to migrate to a type-safe ORM layer.&lt;/li&gt;
&lt;li&gt;You work in a team and want consistent, reliable database structure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🌐 Extracting schema from a Forge app
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fetchSchemaWebTrigger&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-sql-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetchSchemaWebTrigger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;manifest.yml&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;webtrigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetch-schema&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetchSchema&lt;/span&gt;
&lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql&lt;/span&gt;
&lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;fetchSchema&lt;/span&gt;
    &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.fetchSchema&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;foreign_key_checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;foreign_key_checks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧪 You can apply this script to a local database and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge-sql-orm-cli generate:model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  🏗️ Defining a Schema from Scratch
&lt;/h4&gt;

&lt;p&gt;Alternatively, you don't need to start with a Forge SQL database at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can create a brand-new local (or shared) MySQL database,&lt;/li&gt;
&lt;li&gt;Define your schema from scratch or apply an existing SQL script,&lt;/li&gt;
&lt;li&gt;Then use it as your reference schema for generating models and migrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes &lt;code&gt;forge-sql-orm&lt;/code&gt; ideal for greenfield projects or building outside-in from an existing backend architecture.&lt;/p&gt;

&lt;p&gt;To create an initial migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge-sql-orm-cli migrations:create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To generate follow-up migrations after schema changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge-sql-orm-cli migrations:update
npx forge-sql-orm-cli generate:model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🐳 Docker Example for Local Database
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; forge-sql-orm-example-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3366:3306 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; &lt;span class="nv"&gt;seccomp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unconfined &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;always &lt;span class="se"&gt;\&lt;/span&gt;
  mysql

&lt;span class="c"&gt;# Wait 30 seconds, then:&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;forge-sql-orm-example-db &lt;span class="se"&gt;\&lt;/span&gt;
  mysql &lt;span class="nt"&gt;-uroot&lt;/span&gt; &lt;span class="nt"&gt;-padmin&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"create database forgesqlorm"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧬 UUID as Primary Key (with VARBINARY(16))
&lt;/h3&gt;

&lt;p&gt;Although Atlassian’s examples often use &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; for primary keys, &lt;a href="https://docs.pingcap.com/tidb/stable/uuid/" rel="noopener noreferrer"&gt;TiDB documentation&lt;/a&gt; recommends UUIDs.&lt;/p&gt;

&lt;h4&gt;
  
  
  ✅ Why UUID?
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Better support for distributed systems.&lt;/li&gt;
&lt;li&gt;Lower chance of key conflicts.&lt;/li&gt;
&lt;li&gt;More scalable for indexing.&lt;/li&gt;
&lt;li&gt;When using &lt;code&gt;UUID_TO_BIN&lt;/code&gt;, binary UUIDs reduce storage size and improve index performance.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;UUID_TO_BIN(uuid, 1)&lt;/code&gt; sorts UUIDs by timestamp, enhancing insertion order and avoiding random I/O bottlenecks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;VARBINARY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UUID_TO_BIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To implement UUIDs with &lt;code&gt;varbinary(16)&lt;/code&gt; in Drizzle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uuidBinary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;customType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;driverData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Buffer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;dataType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;varbinary(16)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;toDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`UUID_TO_BIN(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;fromDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;uuidStringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mysqlTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuidBinary&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🧠 TiDB also supports &lt;code&gt;UUID_TO_BIN(uuid, 1)&lt;/code&gt; to reorder bits and improve clustering/indexing.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔒 Optimistic Locking Support
&lt;/h3&gt;

&lt;p&gt;To prevent race conditions when multiple users edit the same record, &lt;code&gt;forge-sql-orm&lt;/code&gt; supports optimistic locking using a &lt;code&gt;version&lt;/code&gt; column.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;On &lt;code&gt;crud().insert()&lt;/code&gt;, if a &lt;code&gt;version&lt;/code&gt; field exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If it's a number, it defaults to &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If it's a date/time, it's set to current timestamp.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;On &lt;code&gt;crud().updateById()&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The version is incremented or updated.&lt;/li&gt;
&lt;li&gt;The version is included in the &lt;code&gt;WHERE&lt;/code&gt; clause to ensure no conflicts.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This helps avoid overwriting someone else's changes by accident.&lt;/p&gt;

&lt;p&gt;⚠️ This logic is not available if you use &lt;code&gt;.getDrizzleQueryBuilder().insert(...)&lt;/code&gt; — that API is lower-level and does not handle &lt;code&gt;version&lt;/code&gt; automatically.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧪 Insert Examples
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Single insert&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crud&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]);&lt;/span&gt;

&lt;span class="c1"&gt;// Bulk insert&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crud&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Smith&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vasyl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  📦 Quick Start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;forge-sql-orm @forge/sql drizzle-orm moment &lt;span class="nt"&gt;-S&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;forge-sql-orm-cli &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔹 Generate migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge-sql-orm-cli migrations:create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔹 Generate models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge-sql-orm-cli generate:model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔹 Query example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ForgeSQL&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-sql-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ForgeSQL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderWithUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;forgeSQL&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;innerJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  💬 Feedback
&lt;/h3&gt;

&lt;p&gt;Give it a try and let me know what you think!&lt;/p&gt;

&lt;p&gt;If you're already using &lt;code&gt;@forge/sql&lt;/code&gt; and want to level up your developer experience with type safety and migrations, I’d love to hear your feedback.&lt;/p&gt;

&lt;p&gt;⭐️ Star the repo on GitHub: &lt;a href="https://github.com/vzakharchenko/forge-sql-orm" rel="noopener noreferrer"&gt;vzakharchenko/forge-sql-orm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🧑‍💻 PRs, issues, and questions are welcome — or just drop a comment below!&lt;/p&gt;

</description>
      <category>forge</category>
      <category>atlassian</category>
      <category>drizzle</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
