Skip to content

The console

Every Pondra node serves a console at /. Open the node’s address in a browser and you get the lake’s tables and files on the left, your notebooks, SQL, Python and data files in tabs in the middle, and on the right the details of whatever you pick and your Python variables. There is nothing to install. The page is part of the pondra binary: the code it loads first is under 70 KB compressed, the rest (charts, plans, History, Jobs, Settings, Markdown, data files) loads the first time you use it, and its fonts are 53 KB. It works offline, and it sends requests only to the node that served it.

The console: the Data and Workspace trees on the left; a notebook in a tab, with a SQL cell and its answer in the grid; on the right, a table’s details.

Start a node and open its address:

Terminal window
pondra serve lake # then open http://localhost:8080
  • The shell starts a node too. Its second line is the console’s link, with the shell’s key: The console, as this shell: http://127.0.0.1:37957/#key=…. Open it while the shell runs, and the page works as the shell does (it reads this machine’s files too).
  • pondra serve --lakes data serves each lake in a folder as a database, and its console lists them all. See Many databases.
  • On another machine, start the node with tokens (--admin-token) or make users (see Security). The console asks the first time: a user’s name and password (it then holds a session the node signs, 12 hours by default), or a token; your browser keeps it. Sign in ▾ in the top bar signs in as someone else, or out.
  • The top bar: where the page runs (the database, this node), search (Ctrl+K: tables, files and commands), the three pane switches (left, bottom, right), History (the clock: it opens the right pane at History, and a second click closes it), Jobs (the calendar, the same way), the keys (?), Settings (the gear) and Sign in.
  • The left pane: a Filter box, then Data (databases, schemas, tables, views, columns) and Workspace (the lake’s files). The filter narrows both trees to the names that hold what you type; Esc clears it. Click a group’s name to fold it: Data folds to the top, Workspace to the bottom, with a line between them.
  • The middle: a tab per open file. A dot on a tab (and beside the file in the Workspace) means it has changes not saved; a new file gets it once you type in it. The file’s toolbar shows Save only when there is something to save, and Ctrl+S saves the one in front.
  • Many tabs keep their names and scroll: the wheel over them scrolls them, a thin bar above them shows where you are (drag it), and ⌄ at their right lists them all: a click brings one forward, its ✕ closes it, a right-click gives its tab’s menu, and Ctrl or Shift picks several to close at once. Drag a tab onto another to move it there. A tab comes back where you left it: scrolled to the same place, in the same cell. Right-click a tab to Pin it: a pinned tab stays at the left, always in sight, shows a pin instead of its ✕, and isn’t closed by Close others, Close to the right, Close saved or Close all, which the same menu has. Pinned tabs are pinned again next time.
  • The right pane: Details, Variables, History (what ran: every statement, slow ones with their plans, and the node’s runs) and Jobs (what runs on its own: the schedules). Drag a tab to put it in another place. Details follows the tab in front; a table you pick stays there until you switch tabs. In a window up to 1180 px wide the pane opens over the page when you pick a table or a column, and closes as soon as you go back to your work.
  • Each view’s ⋯ (or a right-click on its name) moves it to the other pane: Workspace can sit as a tab on the right, for instance. Or drag it there: a group by its name onto the right pane’s tabs (it goes beside the tab you drop it on), a tab onto the left pane. Drag a pane’s edge to set its width (a double-click resets it).

The gear at the top opens Settings (Esc, its ✕ or a click outside it closes it). Its sections are at the left, grouped, and Search settings at the top finds a setting in any of them:

  • Appearance: the theme (light the first time, dark, or as the system is); the accent, one of a few or a colour of your own, and the background, for each theme (Default puts the theme’s back: the panes, lines and pop-ups take their shades from it); the font, the console’s own (Geist) or the system’s.
  • Editor and results: a SQL file’s statements (an answer for each, or only the last one’s), where its results show (below or at the right), and how pages of rows and Format work.
  • Layout: Data first or Workspace first on the left, and Reset the layout (the panes, their widths and their tabs, as they were at first).
  • Keys: every key, where it works (? opens Settings here).
  • This node: Python (the one in use, and Choose…) and About (the version, the lake, the cluster, and where the settings are kept).

Sections an extension (or an enterprise build: users, tokens, audit) adds show here too, under their own heading: register.setting below.

The node keeps them on its machine, so every lake and every session you open on that machine has them: in PONDRA_CONFIG_DIR if it is set, else %APPDATA%\pondra on Windows, ~/Library/Application Support/pondra on a Mac, and ~/.config/pondra elsewhere (console.json). A page opened from another machine keeps its own, in its browser.

  • The tabs you had open open again next time, and the address (#notebook=…, #file=…) opens one directly.
  • In a narrow window the side panes become drawers that close when you use the page.

Click a file in the Workspace to open it in a tab (its tab comes forward if it is open):

File Opens as Below it
A notebook (notebooks/<name>, or an .ipynb in any other folder) cells: SQL, Python and Markdown —
.sql an editor with line numbers Results, Messages, Chart and Plan (or at the right)
.py the same editor a console: the file’s runs, and a >>> line
.csv, .tsv, .json, .jsonl, .parquet a table: CSV and JSON edited in place, Parquet read-only Results, Chart and Data profile, as an answer’s
.md, .txt and other text: .yaml, .toml, .ini, .xml, .html, .css, .js, .sh, a README or Makefile the editor (Preview draws a .md file’s Markdown) —

The Workspace’s + (the tab bar’s too) makes a notebook, a SQL or a Python file, a file of any other kind by its name (New file…: notes.md, config.yaml, rows.csv, opened in its editor), or a folder, or puts files from your computer in the lake (Upload files…, or Upload a folder… with everything in it). Files and folders dragged from your computer onto the Workspace go in too, into the folder you drop them on. A new notebook, SQL or Python file is in the Workspace at once, in the folder it will be saved to; it is not in the lake until you save it, and it asks for its path the first time you do.

A file is saved where it is: Ctrl+S replaces it in the lake. If someone else saved it since you opened it, your save is refused and theirs kept; open it again, or save under another name. (Over HTTP, it’s If-Match: see Files.) Every node reads the new file at its next statement.

New folder (in a + menu, or New folder here in a folder’s menu) asks for a name (a/b makes both) and makes the folder. A lake holds files, not folders, so an empty folder is a zero-byte file, <folder>/.folder, put as any file is. The console never lists it, in the Workspace or in search. A folder shows while it holds a file or a marker.

Each row of the Workspace has a ⋯. It shows when you point at the row, focus it, or have it picked, and opens what a right-click opens. A file’s menu opens, renames, downloads and deletes it. An open file’s name in its toolbar renames it too: click it, type, and Enter (Esc puts it back); its kind stays. A tab’s right-click and its ⋯ have Rename… and Copy path. A folder’s menu has New notebook here, New SQL file here, New Python file here, New file here…, New folder here, Upload files here…, Upload a folder here… and Delete folder…, which says how many files go, then deletes them all, the marker too.

Drag a file or a folder onto a folder to move it there (onto a file: into that file’s folder; below the tree: to the top). A folder moves with everything in it, and an open file’s tab follows it. The folder you would drop into lights up as you drag.

A SQL file in a tab: the editor with line numbers, and its results below, in the grid.

Run (Ctrl+Enter) runs what is selected, or else the statement the caret is in. Run file beside it (Ctrl+Shift+Enter) runs every statement of the file, in order. (Settings → Ctrl+Enter in a SQL file swaps the two keys.) Run ▾ has them both, Show execution plan, Format file, Format selection, Create as table or view…, Run as a job and Schedule…. A right-click in the editor starts with Run and Run file, then cut, copy and paste, Comment / Uncomment, and the rest, Save as… too. Create as makes the statement at the caret (or the selection) a table (its rows as they are now), a view (its query, run when read) or a materialized view (kept up to date as its tables change), showing the CREATE … AS it runs; Open in a new tab gives you that SQL to change first. The database’s name in the toolbar says where the file runs. Its ▾, on a server (pondra serve --lakes), picks another database or takes one by name; on a lake, it lists the lakes attached (a click puts the name in the file, for name.table); both copy the name.

  • Each statement gets an answer. They run in order in the tab’s own session, and stop at the first that fails. Results shows the answers with rows (and a failure); what the others did (a DECLARE, a CREATE, an INSERT) is in Messages, which opens by itself when nothing gave rows. Above the results, a number per answer (2 5 rows, 3 failed, numbered as in the file, in one row that scrolls, above Chart, Data profile and Plan too) picks the one shown, and 2 more in Messages says how many did something else. The same numbers stand beside the statements in the editor, tinted once they ran: the one shown has its lines tinted and its number filled. Pointing at a number shows its statement; a click shows its answer and leaves the caret where it is, and a double-click goes to its lines in the file. A ; inside a string, a comment or a $$ body doesn’t end a statement.
  • Each tab is its own session, as each editor tab of a database tool is its own connection: its SQL variables, temporary tables and Python are its own, and another file’s don’t leak into it. A tab’s session ends when you close it. A job’s files, run one after another, share one.
  • Show execution plan (in Run ▾, a right-click, or Ctrl+Shift+E) shows the plan of the statement the caret is in, or of what is selected, without running it, in the Plan tab. Run query profile runs it with EXPLAIN ANALYZE and shows each step’s rows and time. In a file of several statements, Show every statement’s plan gives each its plan, picked by its number (a DECLARE or a SET has none, and says so). The next Run shows rows again.
  • Settings → A SQL file’s statements → The last one’s answer runs the file at once instead and shows only its last statement’s answer.
  • A DECLARE PARAMETER $name in the file is a parameter, with an input above the editor: its type, its default as the value, and what the comment above it (or after it, on its line) says. A value you change is marked, and ↺ puts the default back; Enter in one runs the file. Run sends the values with the statements, and the node binds them (numbers and true/false as such, the rest as text); the browser keeps them for the file. A plain DECLARE $name and $name = … are the file’s own working variables, never in the bar. (A $name used and set nowhere gets an input too, with no default.)
  • Messages lists every statement that ran: its number, its SQL, how long it took, what it did (rows 3), and what it printed, set apart under Printed. A click picks one (its text stays selectable, to copy); a double-click or Enter shows its answer and its lines.
  • Chart draws the rows: bars, lines, areas, points or a pie, by any column, of any of the number columns (the chart picks lines over time, else bars, until you pick). Save keeps it as a PNG picture or an SVG drawing.
  • Plan draws the statement’s plan as a graph: each step (a scan, a filter, a join, an aggregate) above the steps it reads from, with the rows the planner expects from it, and a dashed box where rows move between cores, or between nodes when a query is spread. Text shows EXPLAIN as it is: the logical plan and the physical one, as planned. Query profile runs the statement with EXPLAIN ANALYZE (it does run it) and puts each step’s rows (beside those expected) and time on it, the costliest marked in red, so you see where the time goes. Its download button copies the plan as text, or saves it as text or as a picture (SVG or PNG).
  • Data profile summarizes each column of the answer: its share of NULLs, its distinct values, its least and greatest value, and a histogram or its commonest values, from the rows shown; Profile every row runs it over the whole answer when there are more pages. (Two words, two things: a data profile is about the rows, a query profile about the time the query took.)
  • Copy (at the right of the tabs) copies the rows, or the selection, tab-separated with the headers; its ▾ has the other forms: without the headers, CSV, JSON, a Markdown table, SQL VALUES rows, a SQL list for IN (…), or the columns’ names. Download saves every row, not only the ones shown, from the answer the node keeps (it keeps each for 20 minutes after it was last read; past that, the statement runs again): CSV, TSV, JSON, JSON lines, Parquet or Excel (.xlsx).
  • The results sit below the editor or at its right (the button at the right of their bar); drag the line between them. In a narrow window, the results give way to keep their buttons in reach.
  • Right-click in the editor for Run and Run file, cut, copy, paste, select all, Comment / Uncomment, the Run ▾’s other items (above), Format file and Format selection: the SQL words in capitals and each clause on its own line, strings, names and comments as you wrote them. Shift+Alt+F formats the selection, or with nothing selected the whole file. Anything it can’t format stays as it was.

Run file runs it in the tab’s own Python: what it printed and its answer show in the console below. With something selected, Run (Ctrl+Enter) runs only that, and the right-click has both. Stop interrupts it (its variables stay), as Interrupt in the Python chip’s menu does. Type a line after >>> and press Enter to run it there too (↑ ↓ bring back the ones before).

Format file and Format selection (Shift+Alt+F, a right-click, or Run ▾) format it as ruff does, by the node’s Python: it needs ruff (or black) installed there (pip install ruff). Code that doesn’t parse stays as it was, and the message says where it went wrong.

A file’s Run ▾ (and a notebook’s, in notebooks) runs it on the node, where it goes on without the page:

  • Run as a job saves the file and starts it, with the parameters’ values as they are now. History opens with the job at its top, and a note says when it is done or why it failed.
  • Schedule… makes a task that runs it every so often (15 minutes, 1 day, or a cron line such as cron 0 2 * * * UTC).

Jobs, in the right pane (or the calendar at the top), is what runs on its own. Each schedule is a card: its name and whether its last run worked, how often it runs and when next, a bar for each of its last runs (its height the time it took, red if it failed), and the statement it runs. Click the card for its last runs; Run now starts it at once (a CALL as a job), and its ⋯ opens its file, copies the statement, or drops it. Flows will be a section here too.

Edit (and New schedule) opens a schedule’s editor. Pick how often: Every so many minutes, hours or days, Daily, Weekly (on the days picked) or Monthly (on a day) at a time in a time zone, or Cron for any cron line. The line below shows it as SQL takes it (1 hour, cron 30 6 * * 1,3 UTC) and can be typed too: the picker follows what you type, and the line what you pick. Below them is the statement it runs, in an editor of its own. A schedule the node refuses (a cron line it can’t read) keeps the editor open with why.

History, in the right pane (or the clock at the top), is what ran, newest first. Its switch picks whose:

  • This tab, This page or All: statements as the node remembers them (pondra.history). Each shows its SQL (or a Python cell’s first line), how long it took or how it ended, when, its rows, and how many nodes it ran on. All is every statement you may see, from every door (an admin sees everyone’s, with who ran it), but not the console’s own queries. A statement shows up a second or so after it ends.
  • Runs: the node’s runs: a file, a procedure, or a Python cell’s first line of code, with its id, who ran it and when, and how long it took or its error. A schedule’s runs are tagged with its name (click the tag for Jobs). A running one’s row updates until it ends.

Slow or failed keeps only statements that failed or took longer than PONDRA_SLOW_MS (1 s), and runs that failed. Click a statement to see it whole, who ran it, and from where. A slow one also shows its plan as it ran, with each step’s rows and time (Graph or Text), and where its time went, each node’s part on a timeline. Open in a new file, Put it in the tab in front, Copy it, Run it again and See its plan are its buttons, and a right-click gives the same. Click a run for all of it (a cell’s whole code), to open its file or the code in a new file; right-click to copy its id.

It’s the workspace of Run files as jobs.

A CSV or JSON file opens as a table you can edit, in the grid every answer has (with its Chart and Data profile):

  • Editing: double-click a cell, or select it and type; the value is edited in the cell itself. Enter keeps it and moves down, Tab moves right, Esc leaves the cell as it was. Delete empties the cells selected, Ctrl+X cuts them.
  • Undo: Ctrl+Z takes back the last change and Ctrl+Y (or Ctrl+Shift+Z) does it again, as many steps as you like, before a save and after it (the buttons under the table do the same). A paste, or the rows deleted at once, is one step.
  • Pasting: Ctrl+V pastes cells copied from a spreadsheet or from the grid. One value fills every cell selected; a block starts at the cell selected, and adds the columns and rows it needs when it runs past the table’s edges.
  • Rows and columns: Add row and Add column are under the table; right-click a row’s number to delete it (or the rows selected).

A changed cell is tinted, a new row too, until you save; the toolbar says what changed, with Discard and Save. The lines you didn’t touch are saved exactly as they were. Query with SQL reads the file as a table in a SQL tab. A Parquet file, or a file over 10 MB, opens read-only (the first 10,000 rows): load it into a table to change it. A JSON file that isn’t a list of rows (a settings file, say) opens as text.

Every answer (a cell’s, a SQL file’s) and every data file is shown in the same grid:

  • Types: a coloured mark before each column’s name (T text, # whole numbers, .01 decimals, a calendar or a clock for dates and times, a switch for true/false, braces for JSON-like values). Hover over a header for its full type, least and greatest value, NULLs and distinct values (the card shows above the pointer, so it hides no rows).
  • Selecting: click a cell and its whole row lights up, to follow it across a wide table. Drag or Shift+click for a range, a row number for a row, a header for a column (and its profile in Details), the corner for everything. The arrow keys move (Shift: extend), Ctrl+A takes all. The bar below shows the selection’s sum and average.
  • Columns: drag a header’s right edge to make a column wider or narrower; double-click the edge to fit it to its values.
  • JSON: structs, lists, maps and text that holds JSON are shown in mono, keys, strings, numbers and true/null each in their colour; point at one to see it whole, laid out.
  • Copying: Ctrl+C copies tab-separated, so it pastes into a spreadsheet as cells; Ctrl+Shift+C adds the headers. Right-click for Copy as CSV, JSON, SQL VALUES rows or a SQL list of every value selected, Filter to these values, Sort and Profile.
  • A header’s right-click: copy the column’s name (or the names of the columns selected), as SQL too; copy its values with the header; sort; filter; profile it.
  • Filtering: Filter column… (a right-click) asks how to compare (contains, starts with, does not contain, =, ≠, >, ≥, <, ≤, is NULL, is not NULL) and the value; Filter to these values keeps the rows holding a value selected. Each filter is a pill above the grid (city is Oslo, amount > 50): click one to change it, its ✕ takes it away, + Filter adds one on any column, and Clear all takes them all away. They all apply. A filtered column has a small filter mark by its name: click it to change its filter.
  • Sorting: the small arrow in a header sorts by it (again: the other way).
  • The line under it (a SQL file’s answer and a cell’s alike) says how many rows there are and how long they took (200,000 rows · 28 ms), how many the filters leave, and the selection’s sum and average.
  • Pages: an answer of more than 10,000 rows comes a page at a time. At the right of the line under it, 10,001–20,000 ▾ says which rows show, and ‹ › turn the pages (Alt+Page Down and Alt+Page Up in the grid). Its ▾ goes to a page, the first or the last, and sets the rows a page (100 to 50,000), for this answer and those after it; so does Settings → Editor and results → Rows a page. The node keeps the answer a while (PONDRA_PAGES_MB, 256 MB in all, for 20 minutes after it was last read), so a page is the same rows, in the same order, without running the query again; after that, turning a page runs it again for that page. Sorting and filtering are of the page shown.

Type SQL and press Ctrl+Enter (⌘+Enter on a Mac), or click the cell’s Run. The answer shows below the cell: the rows, each column’s type, how many rows there are, and how long it took. Tab completes a name: a table’s, a column’s (the columns of the tables the cell names come first), a function’s or a SQL word. After a table’s name or its alias and a dot (o.), it lists that table’s columns; after a schema’s (sales.), its tables. When several fit, it lists them; ↑ ↓ and Enter pick one.

SELECT item, count(*) AS sales, sum(amount) AS total
FROM sales
GROUP BY item
ORDER BY total DESC;
  • A cell may hold several statements. They run in order, and the cell shows the last one’s answer (a SQL file shows each one’s).
  • A statement that changes something (INSERT, CREATE TABLE) shows what it did, such as rows 3, and the tables on the left update.
  • Errors come back in plain words, such as No field named nope. Did you mean 'sales.item'?.
  • Show execution plan (the cell’s ⋯, a right-click, or Ctrl+Shift+E) shows the plan of what is selected, or of the cell’s last statement, without running it; Run query profile runs it and shows each step’s rows and time.
  • An answer looks as a SQL file’s does: Results, Chart, Data profile and Plan as tabs above it (a chart of the rows, each column summarized, and the plan as a graph with its Query profile), Copy ▾ and Download ▾ at their right, and the line under it with the rows, the time and the pages. The tab open, and the chart’s settings, are kept with the notebook, so it opens with its chart. Copy ▾ copies the rows shown in any of the forms a SQL file offers, and Download ▾ saves them as CSV, TSV or JSON, or every row of the answer as the node keeps it (in any of the SQL file’s forms).
  • Decimals keep their digits (1.50), and integers too big for JavaScript arrive as text, so what you see is exactly what the lake holds.

A long answer scrolls smoothly however many rows it has: the page draws only the rows in sight. Click a column’s header and the details panel summarizes that column; Data profile summarizes them all.

A SQL cell and a Python cell see each other’s work, in both directions:

  • A SQL cell’s answer in Python. Type a name in the cell’s Result in Python field, beside Live (a Python name: a keyword such as as is refused). Once the cell runs, that name is a frame of its query in the page’s Python: df.to_pandas(), df.to_polars(), df.filter(…). A frame is the query, not a copy, so it reads the lake as it is when used. In the .ipynb file the cell begins %%sql df <<, as Pondra’s Jupyter magic writes it, and a notebook run as a job does the same.
  • Python’s tables in a SQL cell. A SQL cell that names a pandas, Polars or Arrow table, or a frame, that a Python cell made reads it by that name. The table is sent along with the query, as db.sql sends it, and a table in the lake wins over a variable of the same name.
  • Temporary tables and views (CREATE TEMP VIEW) are the page’s too, seen from both kinds.
import pandas as pd
targets = pd.DataFrame({"item": ["tea", "cake"], "target": [3, 1]})
-- a SQL cell on the same page: `targets` is the Python cell's
SELECT s.item, count(*) AS sold, t.target
FROM sales s JOIN targets t USING (item)
GROUP BY s.item, t.target ORDER BY s.item;
item sold target
cake 1 1
tea 2 3
  • Data: tables and views. Each kind has its icon: a table a grid, a view the same grid drawn dashed, a materialized view a grid with a solid header, and a view of files (CREATE EXTERNAL TABLE) a page with a grid on it. Open one to see its columns: each has its type’s coloured mark and its SQL type on the right; a key column has a key.
  • Click a table, a view, a function, a procedure, a schedule or a secret to see its details on the right. Double-click a table or a view to open it in a tab of its own (below). Double-click a column to put its name where you are typing (a click only picks it).
  • A table’s own tab shows its rows in the grid, with its details at the right. WHERE and ORDER BY above them pick the rows it reads (Enter reads them again), and so does how many (100, 1,000 or 10,000). A table’s rows are edited in place as a data file’s are: double-click a cell or type, paste a block (it adds the rows it needs, never a column), Add row, a row number’s right-click to delete rows, Ctrl Z and Ctrl Y. An empty cell is NULL. The toolbar says what changed; click it for the SQL a save runs, in a new SQL tab. Ctrl S (or Save) runs it as one transaction: the rows added, then the cells changed, then the rows deleted, each row found by its _row_id. A value its column can’t take (text in a number) is refused before anything is sent. Discard reads the rows again. A view, a materialized view and a view of files open read only. The tab comes back with the others when the page is opened again.
  • Right-click anything for what can be done with it, each as the SQL it runs (a change that can’t be undone asks first):
    • a table, a view or a materialized view: Open (its tab), Preview (its first 100 rows: in a new cell when a notebook is in front, else in a new SQL tab; or in Python), Details and data profile, Watch it live (a SQL tab of its rows, Live on), Script as…, Insert rows…, Import rows from a file… (a CSV, JSON or Parquet file from your computer, put in the lake’s imports/ folder and read in by column name), Download every row…, As a Kafka topic…, New ▸ (a view of it, a materialized view counting it, a copy of it), Add a column…, Rename…, Refresh, Copy the name (the whole name, lake.schema.table; Copy the short name as SQL here writes it; its column names; as Python), Truncate… and Drop…. Script as shows its SELECT, INSERT, upsert, UPDATE, DELETE, MERGE, loading a file, exporting, CREATE (from the catalog: its columns, key, partitioning, options) and DROP, each in SQL or in Python (db.table("t"), db.sql(…)), to open in a tab or copy;
    • a column: its values counted, its range and NULLs, Add a column…, Rename…, Change its type…, Drop…;
    • a schema: New ▸, a second menu with a table, view, materialized view, external table, function, procedure or schedule in it (a template in a new tab) and a table from a file; List its tables and views (as an answer), Refresh, Copy the name, Copy the table names, Drop…;
    • the lake: New ▸ (a schema, a table, a view, …), Attach a lake or catalog…, List its tables and views, Who has access…, a new database (a server’s), Checkpoint, Refresh, Copy the name, and for an attached one Detach…;
    • Functions, Procedures and Schedules, listed under each schema, as Postgres keeps them (CREATE FUNCTION sales.f … is in sales; a name without a schema is in public), and Secrets under the lake (a secret covers a bucket, not a schema): each one’s definition, use or call, start as a job, schedule, and drop; a secret’s values are never shown, only replaced;
    • Users and roles under the lake: New user… (a name, a password, superuser or not, the roles it is in) and New role… on the group’s menu, the statement shown as you fill it in (the password as '***'). Click one for how it signs in, its roles and members, its grants (each with a × to take it back) and its definition; its menu has Grant…, Its roles… (a click joins or leaves one), Set a password…, New token… (shown once), Limits…, superuser or not, and Drop….
  • Who has access…, on a table’s, a schema’s or the lake’s menu and Who may use it… on a secret’s, lists who may do what with it: granted on it, or through its schema or every table (those show where they come from), and the superusers. Each grant has a × that takes it back at once (REVOKE). Below, pick a user or role, what it may do (SELECT, INSERT, UPDATE, DELETE) and, for reading a table, which columns, and Grant: the GRANT it runs is shown as you pick. A user’s Grant… is the same the other way round: pick what it is on. See Users, roles and grants.
  • Workspace lists the lake’s own files (PUT /files/…, files()) by folder, and your notebooks under notebooks, with the headings of the one in front as its outline. A new file not saved yet is listed too, with the dot. Whoever may read the lake may read its files, from SQL too.

The panel on the right (its button is at the top right) shows what you picked:

Picked It shows
A table rows, columns, key, partitioning, clustering, the formats it is published in, and its size in files; each column with its type, NOT NULL and default
A view its definition, and its columns
Data profile (a table’s or a view’s) each column’s share of NULLs, its distinct values (approximate), its least and greatest value, and a histogram (numbers, dates, times) or its five commonest values. It reads the whole table: the numbers are the table’s, not a sample’s
A column of an answer the same, from the rows the answer holds
A file its size, when it was written, and how SQL reads it
A function, a procedure, a schedule or a secret what it is, its first actions as buttons (the rest in More), its facts (language, arguments, what it returns; how often and when next) and its definition
A user or a role how it signs in (a password, tokens: each with a × to drop it), its roles and members, its limits, its grants (each with a ×) and its definition
Nothing the lake: its tables, views, rows and size, and its cluster

A Python cell runs on the node, next to the data. db is the connection. What the code prints shows under the cell, and its last line, if it is an expression, comes back as rows: a frame, a pandas or Polars DataFrame, an Arrow table, a list of dicts, or one value.

by_region = db.table("sales").group_by("region").agg(pondra.col("amount").sum().alias("total"))
print("regions:", len(by_region.rows()))
by_region.sort("total", descending=True)

Cells share variables, as in a notebook: the next cell sees what this one made.

best = by_region.sort("total", descending=True).rows()[0]
print("best region:", best["region"], best["total"])

The node runs a Python cell as Postgres’s anonymous code block, DO LANGUAGE python. You can send the same block from psql or any client:

DO LANGUAGE python $$
print("tea sold:", db.sql("SELECT count(*) FROM sales WHERE item = 'tea'").item())
$$;
  • The node needs Python: start it with --python auto (the Python that has the pondra package) or --python /path/to/python. pondra.local() does this for you.
  • A Python cell needs an admin token, since it runs code on the node. With no tokens set, the node listens only on 127.0.0.1, and anyone who can reach it is its admin.
  • A notebook is a session, and its Python cells run on one Python worker kept for that session, in one namespace, like a notebook kernel; another notebook (or Python file) has a worker of its own. What one cell makes (variables, imports, functions, frames such as orders = db.table("sales")), the next cell sees. A cell that fails keeps what it did before the failing line. _ holds the last answer, and a cell’s single value comes back as a column named value. One cell runs at a time.
  • The session’s Python ends when you close its tab or the page (or, in psql, when the connection closes), after PONDRA_SESSION_IDLE_SECS (3600) seconds idle, or if it grows past PONDRA_WORKER_MB (2048) MB. The next cell then starts empty, and a notice says so.
  • A DO block sent without a session runs fresh and shares nothing. In psql, a connection is a session. Over HTTP, send the x-pondra-session: <id> header, as the Python and JavaScript clients do: see Functions and procedures.
  • A Python error names its line in the cell: ZeroDivisionError: division by zero, line 2: 1 / 0.
  • Stop (the cell’s Run button while it runs, I I on a cell after Esc, or Interrupt in the Python menu) interrupts the cell on the node: it ends with KeyboardInterrupt, and its variables stay. On Windows, where one process can’t interrupt another, Python restarts instead and the variables go. A cell whose page stopped waiting for it never holds up the next one.

The Python chip in a notebook’s (or Python file’s) toolbar says whether Python is running, and its menu interrupts a cell, restarts Python, shows the variables, and has Choose the Python…. That lists every Python the node finds on its machine (on the PATH, the py launcher’s, conda’s and virtual environments’ among them), each with its version and whether it has pondra and pyarrow, and for one that lacks them the pip install command to run. Pick one, or give a Python’s path. The choice is the node’s from then on, kept on its machine for next time (beside the settings, python.txt).

With --python auto, the node tries the Pythons it finds at once and takes the first that works; one that doesn’t answer within PONDRA_PYTHON_PROBE_SECS (15) seconds is passed over, so a broken install never holds it up. If Python can’t start, the message says why, with what it printed.

A figure shows under the cell as a picture: a matplotlib figure (or seaborn’s) that the cell ends with, or draws with pyplot, and a Pillow image.

import matplotlib.pyplot as plt
by_day = db.sql("SELECT day, sum(amount) AS total FROM sales GROUP BY day ORDER BY day").to_pandas()
fig, ax = plt.subplots(figsize=(7, 2.5))
ax.plot(by_day["day"], by_day["total"])
fig

The node needs matplotlib (or Pillow) in its Python for this: pip install matplotlib.

The Variables tab of the panel on the right lists what this page’s Python holds: each name, its type, its size (a frame’s rows and columns, a list’s length) and a short look at it (a frame’s columns). Tab in a Python cell completes these names, and inside the string given to db.sql("…") or db.table("…") it completes SQL’s: the lake’s tables, their columns, functions and SQL’s words, as in a SQL cell.

The Python chip says whether a cell is running. Restart Python in its menu (or 0 0 on a cell after Esc) ends it: its variables go, and the next cell starts afresh (temporary tables stay, since they are SQL’s).

Markdown cells hold text, drawn as GitHub draws Markdown: headings (# to ######, or underlined), bold, italics, strikethrough, code, links ([text](url), <url>, a bare https://…, or [text][name] with [name]: url below), pictures (![what it shows](path)), lists (nested, numbered, and of tasks: - [ ], - [x]), quotes, tables (with :--: to align their columns), rules (---) and code blocks (SQL and Python ones highlighted). A few HTML tags work as they are (<br>, <kbd>, <sub>, <sup>, <u>, <mark>); any other HTML shows as text.

A link or a picture without a scheme is a file of the lake’s (data/chart.png, queries/daily.sql): a link opens it in a tab, a picture is read from the lake. Press Shift+Enter to draw the cell, and double-click it to edit. Its headings make the notebook’s outline, under it in the Workspace: click one to go to its cell. A .md file’s Preview draws it the same way, its paths read from its folder.

Point at the space between two cells (or above the first) for + SQL, + Python and + Markdown: each adds a cell there. A and B add one above or below the cell picked, and so do its ⋯’s Add a cell above and Add a cell below.

The kind at a cell’s top left (SQL ▾) makes it SQL, Python or Markdown, and back again. The ⋯ at its top right (it shows when you point at the cell) runs the cells above it, or this one and those below; hides its output (O: its code stays, and a click on the output brings it back); clears its output; and deletes it. A right-click in its code has cut, copy, paste, comment, Run cell, Run the cells above, Run this and the cells below, Format cell and Format selection (Shift+Alt+F: SQL here, Python by the node’s ruff or black, as in files), a SQL cell’s Create as table or view…, and Make it Python (or Make it SQL): the same query either way, SELECT … becoming db.sql("""SELECT …""") and back, so any cell moves between the two. The notebook’s own ⋯ (in its toolbar) shows its Versions…, downloads it as .ipynb and clears every output. Ctrl+K finds every command: a new notebook, file or folder, Upload files…, restarting or choosing Python, the token, the keys.

The Live switch keeps an answer current: a SQL cell’s, or a SQL file’s (at the right of its results bar, as a database tool’s grid has it). The node sends the answer again each time a commit changes a table the query reads, and the console changes the rows in place, lighting the ones that changed a moment; a SQL file’s bar says when it last changed. Nothing runs while nothing changes. Turn the switch off and the query stops. Watch it live in a table’s menu (the Data tree) opens a SQL tab of its own with its first rows, Live on.

SELECT region, sum(amount) AS total FROM sales GROUP BY region ORDER BY region;

With Live on, an INSERT into sales from anywhere (another cell, psql, a Kafka producer, a program appending over HTTP) shows in the cell within a fraction of a second. The console uses /live, which any HTTP client can use too: every live cell of the page shares one connection to the node, so however many are on, the page’s other requests (a Python cell, a query) never wait for them.

Ctrl+S saves the notebook in the lake, under the name in its toolbar:

  • It is one file, files/notebooks/<name>.ipynb, or <folder>/<name>.ipynb in any other folder (New notebook here in a folder’s menu, or an .ipynb uploaded there). Ctrl+S replaces it in place, refused if someone saved it since you opened it, as any file is.
  • Every file keeps its versions, notebooks or not: each save, with who made it and when. Versions… in a file’s ⋯ (or in the Workspace’s menu, or in Details) lists them; pick one to see what changed since, line by line, and Restore this version to make it the file again. A deleted file keeps its versions too.
  • A notebook saved before versions, as notebooks/<name>/<time>.ipynb, still opens; its next save makes it notebooks/<name>.ipynb, and its earlier saves are among its versions.
  • Any notebook can be a job or a schedule (Run all ▾), and a run records the version it ran.
  • Everyone who can read the lake can open them. They are files like any other, so you can list them in SQL:
SELECT path, size, written FROM files('notebooks/') ORDER BY written DESC;

A notebook is a Jupyter notebook (.ipynb, format 4.5). SQL cells are saved as %%sql cells, Python cells as code cells, and Markdown as Markdown. Each answer is kept with its cell (the first 100 rows) and shown, marked saved, until you run the cell again; a cell’s chart (or plan) open is kept in its metadata (pondra.view, pondra.chart).

  • Download saves the notebook to your computer. Jupyter, VS Code and GitHub open it as it is. In Jupyter, %load_ext pondra makes the %%sql cells run (see Notebooks).
  • Upload files… puts any files from your computer in the lake. An .ipynb (from Jupyter or anywhere else) opens as a notebook instead, and Ctrl+S then keeps it in the lake. Upload files here… in a folder’s menu puts them in that folder, an .ipynb too, as does dragging them onto the folder.

The console follows Jupyter’s keys. In a cell:

Keys What they do
Ctrl+Enter Run the cell (a right-click has Run selection for a part of it)
Shift+Enter Run it, then go to the next cell (a new one at the end)
Alt+Enter Run it, then add a cell below
Ctrl+Shift+Enter Run every cell, in order (it stops at one that fails)
Tab Complete a name (after a letter), or indent
Shift+Alt+F Format the cell (or what is selected)
Shift+Tab Indent back
Ctrl+Space List the names that fit here
Esc Leave the cell, for the keys below

After Esc, on the selected cell:

Keys What they do
Enter Edit it
↑ ↓ (or K J) Go to the cell above or below
A, B Add a cell above, below
D D Delete it. Z brings it back
S, P, M Make it SQL, Python or Markdown
L Turn Live on or off
O Hide its output, or show it again
I I Interrupt its Python (the variables stay)
0 0 Restart Python
? Show these keys (Settings, Keys)

Anywhere on the page:

Keys What they do
Ctrl+S Save the file or notebook in front
Shift+Alt+F Format the SQL or Python file in front (or what is selected)
Ctrl+K Search tables, files and commands
Ctrl+B, Ctrl+J, Ctrl+Alt+B Show or hide the left pane, the bottom panel, the right pane
↑ ↓ ← → Move in a tree (open, close) or along the tabs; Delete closes a tab
Alt+Page Down, Alt+Page Up The next page of an answer’s rows, the one before (in its grid)

The top of the page says where the cells run: the database, this node’s role (leader, follower or reader), how many nodes the lake’s cluster has, and how many live queries are open. Hover over it for the nodes’ addresses. A query the node spreads over the cluster runs from the console like any other: see Clusters.

The page is light the first time; Settings makes it dark, or follow the system’s setting.

The console is built to be added to: its own sections, tabs, kinds of cell and actions are registered through the same small API an extension uses, window.pondra. Give a node the scripts in PONDRA_CONSOLE_EXTENSIONS (separated as PATH is), and the page loads them after its own:

Terminal window
PONDRA_CONSOLE_EXTENSIONS=./history.js pondra serve lake
const { register, on, ui, api } = window.pondra;
// A section on the left (a view: its ⋯ moves it to the right, as the console's own).
register.section({ id: 'history', title: 'History', render: box => box.append(ui.h('div', {}, '…')) });
// A kind of file: what opens in a tab for these paths.
register.doc({ id: 'dash', match: path => path.endsWith('.dash'), open: async path => myDashboard(path) });
// A tab of the panel on the right: what was picked on the left.
register.panel({ id: 'sample', title: 'Sample', render: async (box, picked) =>
picked?.type === 'object' ? (await api.rows(`SELECT * FROM ${picked.t.q} LIMIT 5`)).map(r => ui.h('pre', {}, JSON.stringify(r))) : [] });
// A view of an answer: the first one that matches draws it.
register.renderer({ id: 'figure', order: 15, match: r => r.kind === 'rows' && r.rows.length === 1, render: r => ui.h('b', {}, String(r.rows[0][0])) });
// An action: a button at the top, or an item of a ⋯ menu there (it shows once one is added).
register.action({ id: 'share', menu: true, icon: 'copy', title: 'Share', run: () => ui.toast('Shared') });
// Events: 'start', 'run', 'ran', 'pick', 'refresh', 'changed'.
on('ran', (cell, answer) => console.log(cell.src, answer.kind));
register. Adds
view({ id, title, side, render(box, picked), tools, order }) a view, on the left (side: 'left') or a tab on the right
section({ id, title, render(box), tools, order }) a view on the left
panel({ id, title, render(box, picked), order }) a view on the right
doc({ id, match(path), open(path) }) a kind of file, opened in a tab
cellKind({ id, label, language, placeholder, run(text, signal) }) a kind of cell
renderer({ id, match(answer), render(answer, cell), order }) a view of an answer
action({ id, label or icon, title, run(), primary, menu, order }) a button at the top, or an item of the top bar’s ⋯ (shown once an extension adds one)
nav({ id, label, icon, run() }) a place in a rail at the far left (it shows once one is added)
key({ keys, title, run(cell), group }) a key on a cell, listed under ?
setting({ id, title, group, icon, about, rows(), order }) a section of Settings: rows() gives [name, what it does, its control], under the heading group
jobKind({ id, title, load(), item(x), empty, order }) a section of Jobs: what load() gives, each drawn by item

pondra.configure({ fetch, token, headers }) changes how the page reaches the node, for a gateway or a sign-in of your own. pondra.api runs SQL (run, rows, call), and pondra.ui holds the page’s own pieces (h, icon, line, button, toast, menu, pick, add), so an extension looks like the rest of the page. pondra.ui.notebook() gives the notebook as .ipynb JSON and pondra.ui.open(nb, name) opens one, for an extension that keeps notebooks elsewhere; pondra.ui.openFile(path) opens a lake file in its tab, and pondra.docs() lists the open ones. A whole example is in the repository: examples/console-extension.js. The console’s files are at /console/ (console.js and the modules it loads, some the first time they are needed, console.css, the fonts), and an extension at /console/ext/0.js, 1.js, …; the browser keeps them until they change.