OneLake Studio: architecture

What this is

OneLake Studio is a VS Code extension that makes a whole Microsoft Fabric tenant act as one database, queried with SQL.

SELECT * FROM lakehouse.aemo.data.dbo.dim_duid;
SELECT dim_region.Region, AGG("Registered MW") FROM semanticmodel.aemo.nem GROUP BY ALL;
SELECT * FROM 'lakehouse.aemo.data/Files/x.parquet';
SHOW ALL TABLES;

Rules every change keeps

Rule What it means Where it is held
One name per object A table has five parts and a model has three. A file is 'lakehouse.<workspace>.<item>/Files/<path>'. Naming, src/names.js
SQL only A model is read with AGG("measure") in SQL. There is no DAX query. Excel's MDX is a second way in, and is compiled to that same SQL. app/semantic.js, src/xmla/
Read only Nothing is written to Fabric or OneLake. The host takes one read per statement, from a tab or the command line, and DuckDB's own parser checks it. The one exception is Export to Fabric, after an OK in VS Code. assertReads and checkedRead in src/sql.js
Cache first Every answer from OneLake or Fabric is kept on this machine and served from there first. It is trusted until using it fails, then read again once. src/onelake.js, src/store.js
Works offline A feature that reads data works from what this machine holds, with no network. What isn't held says so at once, never on a timeout. Only writing to Fabric needs a connection: Export to Fabric. The exported Fabric app is online by design, and this rule doesn't apply to it. src/connectivity.js, src/http.js, cache_httpfs_set_offline in src/engine-host.js
The host does the work Sign-in, the read-only guard and OneLake's listings, metadata and Files/ bytes are in the host, and DuckDB's reads are in the server it starts. A tab is a view: it holds no token and makes no network request. src/host.js, src/panel.js, src/engine-client.js
A table's Pivot is a SQL starter It builds a first SQL. Its only actions are Edit SQL and Close. app/app.js

Out of scope

Results computed from a semantic model are not guaranteed to match Power BI.

How the parts fit

 Tree            Tabs, Explore           Command line, agent skill     Excel
 (VS Code)       (webview, app/)         (bin/cli.mjs)                 (PivotTable)
      |                 |                        |                         |
      |          one message channel       local pipe or socket      XMLA on 127.0.0.1
      |            (src/host.js)            (src/bridge.js, src/agent.js)  (src/xmla/)
      v                 v                        v                         v
 +-------------------------------------------------------------+
 |  Extension host (src/), one per window                      |
 |  sign-in  |  cache-first OneLake  |  the read-only guard    |
 +-------------------------------------------------------------+
      |                  |  Quack, control pipe
      |                  v
      |     +---------------------------------------------+
      |     |  The server (src/engine-server.js)          |
      |     |  one per account: DuckDB, its caches, state |
      |     +---------------------------------------------+
      v                  v
 Fabric REST API      OneLake
 (semantic models,    (tables, files)
  apps)

The model compiler, dax-sql, is pinned in src/vendored.js. Each tab loads it, and the host loads the same code for the command line. To change the syntax, change dax-sql and move the pin: the extension carries no copy of its own.

What happens when a statement runs

The SQL box and the command line follow these steps. Explore compiles its own view and joins at step 3.

  1. Open the models it names. unopenedModels finds each semanticmodel.<ws>.<model> not open yet. The host opens it from its local copy, reading it from Fabric the first time.
  2. Compile. semantic.statement has dax-sql rewrite a model query as plain SQL over five-part table names. A statement that names no model passes through unchanged.
  3. Check. assertReads in the host refuses more than one statement, and anything that isn't a read. checkedRead then asks DuckDB's parser, in this window's own empty DuckDB, what the statement is, so that misreading SQL's lexing isn't enough to write. PIVOT is the one read the parser can't speak for, and runs with its comments gone. A statement that doesn't parse goes on, for DuckDB's own error and where it is.
  4. Prepare. withTables in src/engine-host.js does three things:
    • A statement that reads the catalog (SHOW ALL TABLES, information_schema) gets the catalog synced from the cache first.
    • withTables makes a view for each five-part name: tableRefs finds the names, resolve (src/sqltables.js) looks them up in the cached listings. The view is delta_scan on a Delta table's folder, and iceberg_scan on an Iceberg table's metadata file.
    • withFileUrls gives DuckDB each file's abfss:// URL in place of its name: fileRefs finds the names, and resolveFile looks them up as resolve does. A name that can't be found fails here.
    • catalogStatement rewrites SHOW statements so they report whole paths.
  5. Run. The statement goes to the server, and its tab's Stop reaches only that tab's statements (The server). A result past maxResultRows (200,000 by default) is counted rather than carried into the tab.

Sign-in

No app registration is needed. VS Code's Microsoft account provider is a first-party application, so getSession returns the OneLake token directly. The Fabric API needs a second token, in its own audience. Expanding Semantic models doesn't ask for it: the tree shows a sign-in node, and VS Code asks when that is clicked.

The token lives only in the extension host and, as a DuckDB storage secret, in the server. When VS Code hands back a new one, the secret is reissued.

Sign Out is out of OneLake Studio, not out of VS Code: an extension cannot remove an account, so it stops asking for it until you sign in again. To remove the account too, use VS Code's Accounts menu. The decision outlives the window: the next VS Code start asks for no session, sends nothing to OneLake, and prompts for nothing, until Sign in to OneLake or Switch Microsoft Account is chosen.

The title says where this window reads from (src/title.js): the sidebar's header and the title of a tab with a SQL box read OneLake Studio and then offline or the tenant in use. The tenant is the onelakeStudio.tenantId setting when it is set, since a guest's own domain would name the wrong tenant, and otherwise the account's domain. Refresh and Open are in the view's "..." menu so it fits. A tab opened on a table, file, semantic model or saved query is named after it instead, as a file's tab is in an editor.

Offline

Offline is a requirement for every feature of the extension that reads data, so a new one has to work here too. The exported Fabric app is online by design.

Nothing turns this on. The extension is offline when there is no internet or no session, says which in the status bar, and comes back by itself (src/connectivity.js).

Works offline, if cached Needs OneLake
The tree, SHOW ALL TABLES, information_schema, Go To A column or a file never read
A table's Stats and History, and first rows already read A wildcard over files
Semantic models: opening them, Explore's Model page, SHOW SEMANTIC … A table whose metadata was never read
Table rows, a model's queries and its Pivot, for what was read Export to Fabric
Files/ objects read before

How rows are read offline: cache_httpfs keeps a file's bytes on disk, but opening a file asks OneLake for its size and version first. So what an open answers is kept too, as one more cache entry for the file, and offline (cache_httpfs_set_offline) an open builds its handle from that with no request. A block it holds reads as usual. A block it doesn't is an error, and the engine says that part isn't cached. Bytes with no such entry need one open online to be readable offline, and aren't downloaded again.

With the disk cache off, there is nothing to read, and a statement that reads OneLake is refused at once.

Losing the internet during a read: the read fails on DuckDB's own timeout, the engine checks the internet once, and if it has gone the statement runs again from what is kept.

The engine

The engine is DuckDB 2.0 running natively in a process of its own (The server, src/engine-host.js), not a WebAssembly build in the page.

The server

DuckDB runs in a process of its own (src/engine-server.js), and every tab of every window is a client of it over DuckDB's Quack protocol (src/engine-client.js).

Outside those files there is one cache folder, file-cache-v1: what DuckDB reads and, under <account>\host\, the bytes of a Files/ object the host reads, and a semantic model's .yaml copy under models\. Saved queries are yours, under ~/OneLakeStudio/queries.

Logs (src/logs.js), a node in the tree, with one tab per window. The record is DuckDB's own: its Quack log has a row per statement with the time to its first rows and any error, for every tab, window and the command line. A large result's later rows aren't in that time, since DuckDB doesn't log them. It is kept in memory (file storage can't be read back on Windows, since DuckDB locks its own file), and the server moves it to the log file every two seconds (src/enginelog.js), up to onelakeStudio.logMaxGB, the oldest going first. It holds your SQL and its errors in plain text, and never the OneLake token. A row reopens its SQL in a new tab, not run. SELECT * FROM duckdb_logs answers in a SQL box for what is still in memory.

Performance (src/performance.js), a node in the tree, with one tab per window. Live charts of memory, cores in use, queries per second and the disk cache, while the tab is on screen. Nothing is stored. It has no SQL box, no Stop and no Restart.

The cache

What Where it is kept When it is read again
Listings (workspaces, items, tables, Files/, semantic models), table metadata, first rows The catalog file, src/catalogfile.js Served at once, refreshed in the background past its window
The name behind a Direct Lake model's item GUIDs The catalog file Only when using it fails
The bytes of a Files/ object the host reads (an image, a .bim, a SQLite copy, a saved file) The cache folder, under the etag its cached listing gave Only when the file was rewritten
What DuckDB reads: table data and Files/ objects DuckDB's disk cache (cache_httpfs) Only when the file was rewritten: a kept copy is checked against the file's version tag and modification time
That a table's rows were read, per snapshot The catalog file Never: a new snapshot is a new entry
A semantic model's definition The cache folder, models/<account>/<workspace>/<model>.yaml On Download Again

The explorer

The tree (src/tree.js) lists workspaces, then lakehouses, warehouses, SQL and mirrored databases, their tables and files, semantic models and apps. Queries, Logs and Performance come after them, and stay there when a listing fails or finds nothing. Workspaces can be pinned, and the tree can be filtered to one type.

A table's icon says whether it is cached, as a model's does: a cloud until a statement has read its rows at its current snapshot, a check after (cached.rows in src/onelake.js). What is kept is the part of each file that was read, so a check doesn't promise every column, unless its tooltip says it was read whole. A new snapshot goes back to the cloud. A table nothing was ever written to has a check, since it reads no file. A file's icon says the same, per version of the file (cached.file): a statement or a tab read it, or the host kept it whole, and a file rewritten in OneLake goes back to the cloud. A wildcard marks no one file. A folder under Tables/ with neither a Delta log nor Iceberg metadata has a warning and says not a valid table. That is kept like any table's metadata, so it shows offline, and it is asked again when the table is opened or its Tables node is, so a folder that becomes a table stops saying so.

Clicking a table opens it in a new tab: Data, Stats, History and Pivot. The SQL box gets SELECT * FROM <its name> LIMIT 100 to start from, not run. A file does the same with what its preview runs.

Clicking a file opens it in a new tab: Parquet, CSV, JSON, Excel, DuckDB, SQLite, and files inside a zip.

Each workspace's Apps node lists its Fabric apps (AppBackend items), however they were made.

The SQL box and results

SQL is written in a box in each tab (app/app.js), not in VS Code's editor. A query never opens a .sql file, because opening one starts every SQL extension installed, on the extension host every extension shares, and a click on Run then waits behind them with nothing on screen. A .sql file you open yourself is left to those extensions. The box reacts by itself: a click says Running… at once and counts, though the answer still comes through the host.

Results show in the tab:

Semantic models

A Power BI semantic model is a Fabric item. It is not in OneLake: its list and its definition come from the Fabric REST API (src/fabric.js). In SQL it is a semantic view, named semanticmodel.<workspace>.<model> and listed by SHOW ALL TABLES with the rest of the catalog. Every workspace has one model of a name.

Opening one

Reading one. A Direct Lake table is read from the lakehouse its model names, whichever item is selected, and only when a query actually reads it. A model's query can be a subquery or CTE inside SQL of your own.

SELECT dim_region.Region, AGG("Registered MW") AS mw FROM semanticmodel.aemo.nem GROUP BY ALL;

Describing one. These statements are dax-sql's (createViews()). Only those that read are taken.

SHOW SEMANTIC VIEWS;
SHOW SEMANTIC DIMENSIONS IN semanticmodel.ws.sales [FOR METRIC m];
SHOW SEMANTIC METRICS IN semanticmodel.ws.sales;
DESCRIBE semanticmodel.ws.sales;
SELECT * FROM (SHOW SEMANTIC METRICS IN semanticmodel.ws.sales) WHERE ...;

A model file is a file. A .bim under Files/, or an Apache Ossie .yaml or .yml whose top level has version: and datasets:, opens in the model view in place of the SQL box. dax-sql's formats/ossie.js turns Ossie into the .bim it stands for, and from there the two are one. These files can be stored anywhere and have no model name. A model created from one, for Fabric or only for a Fabric app, must be named semanticmodel.<workspace>.<model>, and it overwrites whatever we have under that name. See Naming.

Explore

A semantic model opens as Explore (app/explore/), on two pages:

view.js is host-neutral: the host passes in the compiler, how to run SQL, and where ECharts comes from. A tab runs it on the native engine. The Fabric app runs the same files on DuckDB-WASM.

Export to Fabric

The one write. Explore's Export to Fabric button, or the command line's export, publishes that model's Explore, nothing else, as a Fabric app (item type AppBackend, named <model>-explore). VS Code and the extension are all it needs: no CLI, no Node, no GitHub.

What it writes

What it needs

How it behaves

The template (fabric_app/)

src/exportapp.js writes the local copy from fabric_app/, Studio's app/explore/ and dax-sql. Its dist/ is site/ stamped with the build, plus rayfin.config.json, zipped in-process.

Publishing (src/fabricapp.js) is the REST sequence that rayfin up runs, in the same order, with the user's Fabric-audience token:

  1. Find the item, or create it.
  2. Wait for its backend, then fetch the publishable key.
  3. Apply the runtime settings.
  4. Set the CATALOG_URL and MODEL_URL secrets.
  5. Upload the site.
  6. Deploy the function and poll it until it is done.
  7. Add the hosting URL to the sign-in redirects.

The function's deploy package (an esbuild bundle plus its metadata) is built once, at package time, by scripts/function-bundle.mjs with Rayfin's own packager, from fabric_app/rayfin. It ships in the vsix under fabric-function/.

The desktop app

The app is the extension. Its Node process (src/desktop/main.js) runs src/extension.js as it is, bundled with vscode pointing at src/desktop/vscode.js: the part of VS Code's API the extension uses, and no more. So every command, menu, tab and message is the same code in both, and a change to the extension is a change to the app.

The command line and the agent skill

bin/cli.mjs is a doorbell first. It hands its arguments to the running extension over a local pipe or socket (src/bridge.js). The extension answers (src/agent.js) with its own sign-in, cache, engine and compiler.

With VS Code closed, on Windows with Node 22.5 or later, the same answer comes from src/standalone.js, which is src/core.js with no editor around it:

serve <model.yaml|.bim> is always that, as it stays running: Excel's endpoint (Open in Excel) on a model file, with no VS Code. The model takes the name semanticmodel.<workspace>.<model>, from where its tables are and the file's name, or --workspace and --name, as Naming says of a model made from a file. Excel's PivotTables are its only client. --open opens a new workbook on it.

The skill is claude-skill/SKILL.md. Install Agent Skill writes it to ~/.claude/skills/onelake/, where Claude Code and GitHub Copilot in VS Code (agent mode) look for personal skills, and it follows the extension through updates.

node bin/cli.mjs --help is the full reference for commands and options. An agent should branch on:

Exit code Meaning
0 Answered. With --json, complete and truncated say how much.
1 Failed.
2 Wrong call.
3 Not signed in, or export with VS Code closed.
4 Not cached. --fresh asks OneLake or Fabric now, unless offline, when it fails the same way.
5 You declined an export.
6 Timed out.

Keep offline

Keep offline, a switch beside a semantic model's tabs, keeps every table the model reads on this machine and current, so it answers from disk and with no connection (src/modeldata.js). Each table is read whole through the engine, as the idle job does it (keepTable): every column of every file goes through the disk cache, and no row comes back.

Open in Excel

Open in Excel, a button on a semantic model's Pivot, opens a new workbook with a PivotTable live on the model, on Windows and the Excel desktop app. Any model that has a Pivot has it, a local copy or a file in a lakehouse's Files/: the model is found by its SQL name, through its kept view. It opens with no fields placed, whatever the Pivot's shelves hold. Excel's provider speaks XMLA over HTTP, so src/xmla/ answers it from the window:

Excel (MSOLAP) --SOAP over http://127.0.0.1--> extension host
  Discover -> rowsets built from the model's TMSL
  Execute  -> MDX -> model SELECTs -> dax-sql -> the guard -> the server -> a cell set

Commands

Command What it does
Open OneLake Studio Brings the last tab forward, or opens one, signing in if needed. In the Command Palette only: the tree opens a tab with New Query.
Sign in to OneLake Signs in and fills the explorer, without opening a tab.
Sign in to the Fabric API Asks for the second token, for semantic models and apps.
Switch Microsoft Account Asks which account to use, closes every tab and opens a new one, so the previous account's tables do not survive the switch.
Sign Out Signs out of OneLake Studio, closes every tab and stops the server. What is cached stays on show. In the explorer's "..." menu.
Settings Opens VS Code's Settings page, filtered to this extension. In the explorer's "..." menu.
Logs, Performance Nodes in the tree. See The server.
Refresh Re-reads a workspace, item or folder from OneLake in the foreground.
Filter by Type The filter button on the explorer. Shows one type, and only the workspaces that hold it, which reads every workspace's listing, cache-first. The explorer's header names the filter. Not remembered.
Pin to Favorites / Unpin from Favorites Moves a workspace in or out of the tree's favorites group.
Show Read Log Every OneLake request the host made, with sizes and timings. DuckDB's own reads aren't in it.
Clear Cached OneLake Data Empties the on-disk cache and reports what it freed. In the explorer's "..." menu.
Install Agent Skill (Claude Code, GitHub Copilot) Installs the skill above.
New Query, Open Query, Rename Query…, Delete Query, Show Queries Folder Saved queries, under Queries in the tree.
Go to a Table, Column or Measure… (Ctrl+Alt+O) Searches everything the cache holds.
Clear Query Variables Forgets values from SET VARIABLE.
Select Top Rows, Copy SQL Name, Copy Column List A table's right-click. Copy SQL Name is on a file's and a folder's too.
Download Again (from Fabric) Replaces a semantic model's local copy.
Copy App URL An app's button.
Open App, Open Table, Open File, Open Semantic Model A click in the tree. A double click counts as one (src/doubleclick.js).
Copy, Copy with Headers, Copy as CSV, Copy as Markdown, Copy as JSON, Open as CSV Document, Open as JSON Document The grid's right-click.

Settings

Setting Default What it does
onelakeStudio.tenantId empty Directory (tenant) GUID to sign in against. Empty is your home tenant. Set it to reach a tenant you are a guest in.
onelakeStudio.catalogTtlMinutes 5 How old a listing may be before it is refreshed. The last known answer is always served from disk first.
onelakeStudio.tableMetadataTtlHours 24 How old a table's Iceberg metadata may be before it is refreshed. Refresh on its lakehouse re-reads it at once.
onelakeStudio.keepUsedTablesGB 10 How much of the tables statements keep reading is read whole in the background and kept, and may come down in a day. Never over half of diskCacheMaxGB. 0 turns it off, and their metadata is still read every hour.
onelakeStudio.prefetchMetadata on Reads every table's metadata in the background once a lakehouse's Tables node is opened, so the first click is a disk read.
onelakeStudio.diskCacheMaxGB 64 How much of what DuckDB reads from OneLake, tables and Files/, is kept on disk, per account. 0 turns the disk cache off.
onelakeStudio.cacheDirectory empty Where the disk cache is kept, as a full path. Empty is VS Code's storage for this extension. A new folder starts empty.
onelakeStudio.engineMemoryLimit empty Most memory DuckDB may use, e.g. 4GB. Empty is a quarter of the machine's RAM.
onelakeStudio.engineThreads 0 How many threads DuckDB may use. 0 is every core.
onelakeStudio.engineSpillMaxGB 0 How much a query bigger than memory may spill to the temp folder. 0 is DuckDB's default.
onelakeStudio.logMaxGB 1 How much of what the engine ran is kept for Logs, per account. 0 keeps none.
onelakeStudio.selectTopRows 1000 How many rows Select Top Rows asks for.
onelakeStudio.maxResultRows 200,000 The most rows of one result a tab and the command line keep, from 1,000 to 1,000,000. Past it, rows are only counted.
onelakeStudio.tableOpensOn stats What clicking a table shows first: stats or data.
onelakeStudio.gridThousandsSeparator off Shows numbers in the grid with thousands separators. A copy still gets the raw value.

The engine locks its configuration after it boots. Disk cache size, cache folder, memory, threads, spill limit, log limit and the result row limit therefore apply when it restarts, which closes every tab of every window. The rest apply at once.

Limitations

Where the code is

Folder or file What it holds
src/ The extension host and the server. Ship bundled, with @duckdb/node-api, as dist/extension.js and dist/engine-server.js.
src/extension.js What is VS Code's: activation, commands, the tree's view, dialogs and the status bar. It hands the rest in.
src/core.js What a window builds that isn't about how it looks: the store, the catalog, the command line, Excel, and what happens when the account moves. Nothing it loads needs VS Code.
src/signin.js, src/settings.js, src/readlog.js When a session is asked for, and its tokens. What the settings mean. What a window says about its reads.
src/engines.js The engine this window reads with, the background engine, and the catalog files.
src/queries.js, src/models.js Saved queries on disk. A model's local copy, and what the cache can name.
src/exportflow.js Export to Fabric, from its first question to the published app.
src/skill.js, src/diskuse.js The agent skill's file. How much a folder holds.
src/names.js What items hold tables, and how a table or model is named.
src/sql.js The read-only guard, tableRefs, and SHOW rewriting.
src/engine-host.js The native DuckDB engine: what the server boots, and how a window prepares and runs a statement.
src/catalog.js, src/onelake.js, src/store.js OneLake requests, cache first, and the store.
src/sqltables.js A table's SQL name found in OneLake, and a model's GUIDs named.
src/connectivity.js Whether this window is offline, and why.
src/http.js How the host asks OneLake and the Fabric API for anything.
src/deltalog.js A Delta table's own log, for Stats and History.
src/deltamirror.js A Delta table's log kept in the cache folder, for delta_scan to run on.
src/fabric.js A workspace's semantic models and apps, and a model's definition.
src/sqlendpoint.js A Direct Lake model over a SQL endpoint, pointed at the item behind it.
src/semviews.js The models opened, per account.
src/tree.js, src/doubleclick.js The explorer, and a double click in it counting as one click.
src/title.js What the sidebar's header and a tab's title say.
app/querytext.js The SQL box's statements, error positions and variables.
src/panel.js, src/host.js, src/html.js, src/webview.js A tab's webview, its channel, and its page.
src/performance.js, src/logs.js The Performance and Logs tabs.
src/catalogfile.js, src/catalogtables.js, src/syncworker.js What is remembered, in SQLite, the catalog's tables there, and what keeps them current and looks after tables in use.
src/engine-server.js, src/engine-client.js, src/enginelog.js, src/logfile.js The server: its process, a window's side of it, and its log.
src/agent.js, src/bridge.js, bin/cli.mjs The command line.
src/desktop/, desktop/ The desktop app: VS Code's API for the extension, its server, its window and its Tauri shell.
src/standalone.js, src/msalsession.js, src/tokenstore.js The command line with VS Code closed: core.js with no editor, its sign-in, and its token cache.
src/diaglog.js A window's own log.
src/exportapp.js, src/fabricapp.js, src/onelakewrite.js Export to Fabric.
src/vendored.js The one list of pinned third-party code the webview runs, dax-sql included.
app/ A tab's page: index.html, app.js, data.js, grid.js, chart.js, docview.js, dbview.js, paths.js, semantic.js, cdn.js, host.js, vscode-api.js.
app/explore/ Explore, shared with the Fabric app.
fabric_app/ The template for an exported app.
claude-skill/ The agent skill.
test/ js/: the node --test suite and browser runners. Its cli*.test.mjs run bin/cli.mjs as a process. sql/: SQL statements and their answers (sqllogictest), run through the agent's sql. e2e/: the vsix in a real VS Code server and headless Chrome, signed out, and signed in with no internet.
scripts/ Packaging: bundle, stage-webview, vendor, function-bundle, fetch-extensions, check-binding, fetch-binding, fetch-retry, make-icon. The website: build-website, and website-shots, which takes its screenshots in the installed app.
media/ Icons.
docs/ This file, the naming contract, the catalog specification and the changelog, which the website renders as pages of its own; an index and a screenshot for GitHub.
website/ The public website: static HTML and its screenshots, built with the docs by scripts/build-website.mjs and deployed by .github/workflows/pages.yml.

dist/, webview/, vendor/, duckdb-extensions/, fabric-function/ and blog/ are ignored by git. The first five are build output or fetched at package time, so do not edit them.