SQLite sidekick
A SQLite 3 database viewer for Visual Studio Code: read-only unless you turn on
Allow Changes for a file.
Open a .sqlite, .sqlite3, .db3, or .db file normally. The viewer checks
SQLite's binary header before starting a worker, so an unrelated file using the
same .db suffix is refused rather than parsed. For any other extension, or
none, run PK SQLite: Open File as SQLite Database.
CSV, TSV, PSV, and Excel files open too, imported into a temporary SQLite
copy so SQL, the profile, filters, and exports all work on them — see
CSV, TSV, PSV, and Excel files.
No native dependency, and nothing bundled. The engine is node:sqlite,
built into the Node that VS Code already ships, so one platform-neutral package
of about 110 KB serves every platform.
Remote files work too. Databases reached over Remote - SSH, WSL, Dev
Containers, or Codespaces open the same way, as do those in a virtual workspace
such as GitHub Remote Repositories — see
Remote and virtual workspaces, which covers
where the extension is best installed and the one case where a database can be
missing its most recent rows.
Requirements
VS Code built on Node 24 or newer, which is where node:sqlite became available
without a command-line flag. On an older build the extension reports this and
registers nothing, rather than failing later.
Features
- Every table and view appears as a workbook-style bottom tab. Drag a tab off
the strip to open it in a window of its own.
- Explorer panel listing the database, its tables, views, and their columns,
plus its indexes and triggers.
- Find in database searches every table and column for a value at once, and
a result opens that table already filtered to the rows it matched.
- Foreign-key navigation: jump from a key to the row it references, or to the
rows that reference it.
- SQL suggestions for keywords, types, SQLite's 172 built-in functions, tables,
views, and columns, aware of the clause and of
FROM aliases.
- Virtualized rows and columns for large databases.
- Column names and declared types remain visible while scrolling.
- Repeating 16-color, color-blind-friendly column palette.
- A Theme toolbar button that cycles between following VS Code and a
colorblind-friendly light or dark theme, built from the Okabe-Ito palette
with text held at WCAG AA contrast. The choice is the
pkSqlite.theme
setting, so every open viewer follows it and it survives a reload.
- Optional thousands separators for numeric columns, without changing copied or
exported values.
REAL columns show one consistent number of decimals so the decimal points
line up, without the noise digits binary floating point prints.
- Integers are exact at any size, including beyond 2^53.
BLOB columns are shown as a size and never decoded as text.
- On-demand, wrapping data profiles with key statistics, missing and exact
unique counts, distributions, and complete PNG download.
- Three-state sorting, Excel-style cell/range/row/column selection and keyboard
navigation, Tail 10, resizing, auto-fit, and freeze panes.
- Stacked row filters, run inside SQLite so they narrow the whole table rather
than the page on screen, and writable into the SQL editor as the query they
stand for.
- Selection-aware TSV copy and selected-column CSV, TSV, PSV, or JSON export.
- Focus cell crosshair with matching row and column-header highlights.
- Session SQL editor with Ctrl/Cmd+Enter execution and cancellation, and syntax
highlighting for keywords, types, strings, quoted identifiers, numbers,
function calls, and comments.
- One-click Prettify reformats the query into a consistent clause-per-line
layout with indented subqueries.
- A four-layer read-only guard, described below, and an opt-in Allow
Changes mode that lets the SQL editor change the file while ATTACH,
extensions, and pragma changes stay blocked.
- SQL history remains in memory for the current open-document session.
- Selected history exports to a Python/JupySQL
.ipynb notebook that needs no
database driver installed, to a plain .sql script, or to Markdown with one
sql code block per query.
- Active tables and the active SQL query result can be exported as CSV. A query
result export reruns the query so the file holds every row, not only the rows
shown under the display limit.
- Open databases reload after external changes, including changes that reach
only the write-ahead log.
- Works on databases held remotely — Remote - SSH, WSL, Dev Containers,
Codespaces — and in virtual workspaces, which are read through VS Code rather
than off the local disk.
Unless you turn on Allow Changes, the database, its -wal, and its -journal
are never modified.
Read-only by default, and how
Viewing must never change a database, so the guarantee is enforced four times
over rather than once:
- The connection is opened read-only.
INSERT, CREATE, and DROP fail
inside SQLite itself, below any code of ours.
- An authorizer allows only reads. This is a security control, not just a
correctness one: a read-only SQLite connection can still
ATTACH another
database and read it, so without this the SQL box would be an
arbitrary-file reader. Everything outside a read allowlist is denied, so a
future SQLite release cannot introduce a write-capable action that quietly
passes.
- Defensive mode is on and extension loading is never enabled.
- Exactly one statement, and it must return rows. SQLite's
prepare()
silently ignores anything after the first statement, so a second one is
reported rather than dropped.
SELECT, WITH, VALUES, EXPLAIN, and read-only PRAGMA calls are
accepted. INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, ATTACH,
DETACH, VACUUM, REINDEX, ANALYZE, transaction control, and pragma
assignments are all refused.
Allowing changes
The lock button in the toolbar, or PK SQLite: Allow Changes / Make
Read-Only, lets the SQL editor change the open file. It is off whenever a file
is opened, and turning it on asks you to confirm. Every viewer on the same file
follows it, and clicking it again makes the viewer read-only.
With changes allowed, the SQL editor also runs INSERT, UPDATE, DELETE,
REPLACE, CREATE and DROP of tables, indexes, views, and triggers,
ALTER TABLE, ANALYZE main, and REINDEX main.…. The SQL status and the
history show how many rows changed. A RETURNING clause shows its rows as a
result tab. After each change the explorer, tabs, and grid reload.
- Each statement is saved as it runs. There is no Save step and no undo, so
make a copy first if you may want the old data back.
- Still one statement at a time, and
BEGIN, COMMIT, and SAVEPOINT are
refused, so every statement is its own transaction.
- Still refused:
ATTACH, DETACH, VACUUM, pragma assignments,
temporary tables, virtual tables, and load_extension. The SQL box can change
the file it shows, and can still neither read nor write any other.
- Only the SQL editor writes. The grid, the profile, and the exports stay
read-only, and exporting or profiling a query never runs it as a change.
- Tables added to the viewer are not changed. A statement that would write
to them is refused. Unqualified
ANALYZE and REINDEX are refused while any
are added, because they act on every attached database.
- Not available for a CSV, TSV, PSV, or Excel import, a file in a remote or
virtual workspace, or a temporary recovered copy. In each of these, a change
would land in a temporary copy rather than your file.
- Cancel and the timeout cannot interrupt a change that has already started.
It either completes or never begins, and the history says a cancelled change
may have been saved.
CSV, TSV, PSV, and Excel files
Right-click a .csv, .tsv, .tab, .psv, or .xlsx file in the Explorer
and choose Open in SQLite Sidekick, or run PK SQLite: Open CSV, TSV, PSV,
or Excel in SQLite Sidekick. These files keep their usual editor otherwise;
Reopen Editor With… also offers the viewer.
The file is imported into a temporary SQLite database, which then opens
read-only like any other. The source file is only ever read. A CSV, TSV, or PSV
becomes one table named after the file; a workbook becomes one table per
worksheet, named after the sheet.
- Delimiters. TSV and TAB are tab-separated and PSV is pipe-separated. A
.csv is checked for commas, semicolons, tabs, and pipes, so a
semicolon-separated export from a decimal-comma locale reads correctly; the
toolbar names any delimiter other than a comma. Quoted fields may contain
delimiters, quotes, and line breaks. A UTF-8 or UTF-16 byte-order mark is
honored; text without one is read as UTF-8.
- Column names come from the first row (turn off
pkSqlite.tabularHeader
when a file has none). Blank names become column1, column2, …, and
repeats gain _2. A row longer than the header adds columns rather than
losing fields; a shorter one is padded with NULL.
- Types. A column becomes
INTEGER or REAL only when every value in it
fits. One stray value keeps it TEXT, and so do leading zeros (007) and
integers too large for SQLite (such as 20-digit account numbers), so no value
is ever changed by the import. Empty fields are NULL. Set
pkSqlite.tabularAllText to import everything as TEXT.
- Excel. Cells keep their types; formulas contribute their last calculated
value; error cells such as
#N/A are NULL; TRUE/FALSE are 1 and 0.
Dates and times, as Excel formats them, become ISO text such as 2024-03-15
or 2024-03-15 14:30:00. Data may start anywhere on the sheet: leading
empty rows and columns are skipped. Reading stops at the first fully empty
row after the data unless pkSqlite.excelStopAtEmpty is off. Older .xls
and binary .xlsb workbooks are not supported.
Adding files to an open viewer
Any open viewer, a database or an imported file, can take more files as new
tabs, so you can join a CSV against a database table without leaving it:
- Shift+drag CSV, TSV, TAB, PSV, or XLSX files from the VS Code Explorer
onto the viewer. VS Code only lets a drop reach a viewer while Shift is held.
- Or run PK SQLite: Add Data Files to Viewer….
Added tables are imported the same way, into a separate session database the
viewer attaches read-only as added; the source database is never written.
They carry a file glyph and a dashed left edge — where the Query Result tab's
solid edge says "not a relation at all", the dashed one says "not part of this
file" — their tooltip names the file they came from, and they appear under
added in the Explorer. Each is named clear of the source's
tables — a dropped orders.csv next to an orders table becomes orders_2 —
so plain names in SQL always mean one table, and added.orders_2 works too.
Adding files reloads the viewer, which clears the current query result.
Double-click an added tab (or press F2 on it) to rename its table: a box
opens over the label, Enter commits, Escape puts it back. The name is refused
if it is empty, begins with sqlite_, is already another added table's, or is
one of the source's own relations — a plain name in SQL has to keep meaning one
table. Renaming and closing are session-only; the file on disk is untouched.
Each added tab carries an × that closes it: the table is dropped from the
session database and the viewer reloads without it. The source file it came
from is untouched, so dropping it in again brings it back. Added tables
otherwise last until the viewer is closed, and survive the source being
reloaded. Files dragged in from the operating system's file manager do not
work, because a webview is not given their path; drag them from the VS Code
Explorer instead, or use the command. Notebook export reconnects to the source
only, so a query that reads an added table will not run there.
Editing the source file re-imports it, just as an externally changed database
reloads. PK SQLite: Save as SQLite Database (also in the Explorer menu)
writes the same import out as a permanent .sqlite file. Notebook export needs
a real database, so save the file as SQLite first to use it.
Row counts
SQLite has no stored row count — count(*) reads every row — so the grid never
waits for one. The first page appears immediately, and the toolbar's count
carries a leading … — the tab reads … rows — until the count arrives.
Above pkSqlite.exactRowCountLimit (five million by default) the count is
reported as ≈ N from max(rowid), which is an upper bound that can
overcount after deletions; its tooltip says so.
Deep scrolling in a very large table slows down linearly, because SQLite still
visits the rows an OFFSET skips.
Grid controls
Click a column header to select and sort it. Repeated clicks cycle through
ascending, descending, and the original order; an arrow beside the name shows
the active direction. Tables are sorted inside SQLite before pages are loaded,
with rowid as a stable tie-breaker where one exists. The Query Result tab
sorts the rows currently displayed. Each tab remembers its own sort while the
viewer remains open.
Drag the right edge of a column header to resize that column. Double-click the
edge to auto-fit just that column to its cached values, or click the
Auto-fit all columns button to fit every column to the values currently
loaded. Auto-fit is deliberately cache-based, so it does not scan a large
table merely to measure text.
Click and drag cells to select a rectangle. Shift+click extends from the active
cell. Shift+click a row or column header for a contiguous range, or Ctrl/Cmd+
click to toggle individual headers. The top-left corner or Ctrl/Cmd+A selects
the displayed grid; Escape or an empty-grid click clears the selection.
With the grid focused, arrows, Home, End, Page Up, Page Down, Tab, and Enter
move the active cell. Shift extends a cell or whole-row/column selection, and
Ctrl/Cmd jumps to an edge. Ctrl/Cmd+Space selects the active column,
Shift+Space the active row, and both modifiers select all cells. Tail 10
shows the final ten rows in the active sort; Show all restores the grid.
Ctrl/Cmd+C copies the selected rectangle or rows/columns as escaped TSV.
Whole-column table copies include headers and every row, not only cached pages;
query-result copies use all displayed rows. Ctrl/Cmd+Shift+C adds headers to a
row selection. Nulls remain empty and exact raw values are used.
Export in the toolbar opens one menu for both kinds of export: All
columns as CSV saves the current table or query result, and the Selected
columns entries stream the selected column headers as CSV, TSV, PSV, or a
typed JSON array in the active sort order. Right-click a column selection for
the same four formats in the context menu.
Delimited files include a header and UTF-8 BOM. Query-result exports rerun the
successful query so they are not capped by the display limit.
The menu's All tables and views section exports every tab at once. A
checklist asks which tables and views to include (all are checked), then
Excel workbook writes one .xlsx with a sheet per relation, CSV files
writes one .csv per relation into a folder you choose, asking before it
replaces files already there, and SQLite database writes one .sqlite
holding them all. Both are unfiltered, whatever filter the
current tab has. Each sheet has a bold, frozen header row with an autofilter.
Integers past 2^53 are written as text so Excel cannot round them, blobs as hex
literals, and a sheet stops at Excel's 1,048,575 data rows (the message names
any sheet that was cut short). The Query Result tab is not included; export it
on its own.
A SQLite export includes tabs added from files, which is the only way to
keep those past the session, and offers to open the result when it is done.
Every value travels exactly, blobs and large integers included. It is a
snapshot of what the tabs hold rather than a copy of the schema: a view
becomes a table of its rows, declared types travel as their affinity
(VARCHAR(20) becomes TEXT), and primary keys, indexes, constraints, and
triggers are not carried.
Focus cell highlights the active cell's row and column, including the
matching row and column headers and any frozen copies. Column colors are
temporarily turned off and their button disabled while focus cell mode is
active; leaving the mode restores the previous color setting.
The comma icon toggles thousands separators for numeric cells. This and the
column palette are display controls only: tooltips, copy, SQL, and CSV continue
to use exact raw values.
Find in database
Click the magnifier, press Ctrl/Cmd+Shift+H, or run PK SQLite: Find in
Database to open the search panel. Type a value and press Enter: every table
and view is read, and the columns holding a match are listed with how many
values each holds.
Results appear as each table finishes rather than at the end, so a large
database shows what it has found while it is still looking, and Stop
abandons the rest. Clicking a result opens that table with a contains filter
on the matching column already applied, so the grid shows exactly the rows the
count stood for.
Some details worth knowing:
- Matches are values, not rows. A row matching in two columns is counted by
both, because each column is counted separately in one pass over the table.
The filter a result opens is per column, so its row count is the number that
was clicked.
BLOB columns are never searched. The viewer does not decode a blob as
text anywhere else, and a search would be the one place it changed its mind.
A column declared with no type is searched, because it holds whatever was
put into it.
- Stopping is a request, not a promise. As with a cancelled query, the
search worker is destroyed rather than asked to stop, and that can only
happen between tables — a single very large table finishes first.
- Searching is case-insensitive for ASCII, and
% and _ are ordinary
characters, exactly as in the filter bar.
Filter rows
Click the funnel, press Ctrl/Cmd+Shift+F, or run PK SQLite: Filter Rows to
open the filter bar. A condition is a column — or Any column, which any
field of the row may satisfy — an operator, and a value:
| Operator |
Matches |
| contains, starts with, ends with |
text anywhere, at the start, or at the end |
| equals |
the whole value, ignoring case |
| is greater than, is less than |
numbers when the value is a number, text otherwise |
| is empty / is not empty |
NULL or an empty string, and its opposite |
| is NULL / is not NULL |
only a true NULL |
+ Condition stacks another one; a row must satisfy every condition in the
stack. Apply or Enter runs it, Clear empties it, and the funnel carries
the number of active conditions. % and _ are matched literally rather than
as wildcards. Right-click a cell for Filter by "value", which stacks that
value onto whatever the bar is holding.
Filtering from a column header
Point at a column header and click its funnel to filter that column the way a
spreadsheet does:
- Text and other columns list their most frequent values with how many
rows hold each; tick the ones to keep. Type in Contains… to narrow the
list, which also finds values past the first 200, and tick Every value
containing … to keep all of them at once.
- Number and date columns take a From and To instead, either of
which can be left open; the column's own lowest and highest values show as
placeholders. Dates compare as text, so write them the way the column holds
them (
2024-01-31). A range never keeps an empty cell.
Apply replaces whatever filtered that column before; Clear filter
removes it. A filtered column's funnel stays lit, and its filter appears in the
bar as one sentence — click it to change it, or its ✕ to remove it. Filters on
several columns combine with each other and with the conditions typed in the
bar. Right-click a cell for Filter Column… and Clear Filter on Column.
The filter becomes a WHERE clause inside SQLite, so it narrows the whole
table and not just the rows already loaded — paging, the row count, the data
profile, copy, and every export all read through it, and the row count says
"filtered" beside the source name. Each tab keeps its own filter while the
viewer is open.
A SQL query result is filtered by its own SQL: add a WHERE clause and run it
again. Its rows are capped by the display limit, so a filter there would search
only the part that fitted rather than the result it appears to.
Turning a filter into SQL
SQL in the filter bar — or PK SQLite: Write Row Filter as SQL — writes
the filter into the SQL editor as the query it stands for, and opens the editor
on it:
SELECT *
FROM buyers
WHERE name LIKE 'A%' ESCAPE '\'
AND spend > 100
ORDER BY spend DESC;
It is the same conditions the grid is reading, one per line, plus the active
sort, so running it returns the table on screen. From there it is an ordinary
query: join to another table, group it, project fewer columns, keep it in the
session history, or export it to a notebook. The bar can only express a WHERE
clause, so this is the way out of the bar rather than a second way to run it.
While the bar is open it writes what the rows currently say, applied or not;
with the bar closed it writes the filter the grid is showing. The editor's
previous contents are replaced, as they are when a history entry is clicked.
Freeze panes
Select a cell and click Freeze panes. Everything above and to the left of
it stays put while the rest of the grid scrolls, exactly as in Excel — so
selecting the cell in row 3, column C pins the first two rows and the first two
columns.
The button then reads Unfreeze; clicking it again removes the split. It is
also available from the command palette as PK SQLite: Freeze or Unfreeze
Panes at Active Cell, which makes it a single keybinding if you want one.
To move a split, unfreeze first and then freeze at the new cell. Selecting the
very first cell freezes nothing, since there is nothing above or to its left,
and a split always leaves at least one row and one column free, so the grid can
never be pinned solid.
Each table remembers its own split, so switching tabs and coming back keeps it.
One difference from Excel: columns you scroll past slide underneath the
frozen ones rather than stopping beside them. Freezing the leftmost columns to
keep an identifier in view works as expected; freezing many columns and then
scrolling far right will hide some behind the frozen block.
Number display
REAL columns are formatted for reading. Every row in a column gets the same
number of decimals, so the decimal points line up:
| Stored value |
Shown |
1499.95, 20, 3.5 |
1499.95, 20.00, 3.50 |
0.30000000000000004 |
0.3 |
0.0000012345678 |
0.000001235 |
Trailing digits like those in 0.1 + 0.2 are an artifact of binary floating
point rather than your data, and are always dropped. Genuine precision beyond
that is capped by pkSqlite.maxDecimalDigits, which defaults to 4. A column
whose values are too small for the cap widens instead of rounding to zero, and
scientific notation is never used.
Integers are never converted to JavaScript numbers or rounded, at any size.
A value beyond 2^53 keeps every digit in the grid, tooltip, copy, and CSV. When
the comma toggle is on, separators are inserted directly into the display text,
so 9007199254740993 is shown as 9,007,199,254,740,993 without losing
precision.
Formatting is display only. Ctrl/Cmd+C copies the exact stored value, hovering a
cell shows it, and CSV export is unaffected.
Types, and values that disagree with them
SQLite is dynamically typed: a declared column type is an affinity, and any
row may store any type. Two consequences are visible in the grid.
A column with no declared type, and every computed expression, reports no type
at all. The viewer infers one from the first values it loads and marks it with a
trailing ? in a dimmed style — INTEGER? is a guess, INTEGER is a
declaration.
A column whose values disagree with its declared affinity is legal and is not
flagged. Text in a REAL column is simply shown as text.
BLOB columns
A BLOB cell shows ‹BLOB 1,024 bytes› and is never decoded as text.
Blobs of 1 KB or less travel to the viewer whole, so hovering shows a complete
x'…' literal and Ctrl/Cmd+C copies one that pastes straight back into a query.
Above that, only the first pkSqlite.blobPreviewBytes (32 by default) are sent,
the tooltip says they are leading bytes, and copying yields the size label
instead of a partial value that would look complete.
CSV export always writes every byte, whatever the size, because it re-reads
the value rather than using the copy the grid holds.
Remote and virtual workspaces
The viewer supports databases opened through VS Code Remote - SSH, WSL, Dev
Containers, Codespaces, and desktop virtual workspaces such as GitHub Remote
Repositories. Install the extension in the location offered by VS Code, then
open the database normally or use Open With.
What decides the behavior is whether the extension is running on the same
machine as the database:
- Installed on the remote — with Install in SSH: …, or in the container
or Codespace — the database is an ordinary local file and is opened directly.
Nothing is copied.
- Installed locally against a remote workspace, or in a virtual workspace
where no file exists anywhere, the extension reads the resource through VS
Code and opens a disposable local snapshot instead. The snapshot is refreshed
when the source changes and removed when the viewer closes.
Either way the viewer works. Read-only repositories still support viewing, SQL,
profiles, and CSV export to a separate writable location. VS Code offers the
install location when you open the database; taking the remote one avoids the
copy, and with it the caveat below.
A snapshot copies the database and its sidecars, but a write-ahead log is not
always readable that way. So a snapshotted database is the one case that can
show stale rows: if it has an un-checkpointed -wal, commits made since the
last checkpoint may be missing, and the viewer says so once on opening. Where
the file is opened directly there is no such limitation — a database another
process is actively writing opens fine and shows that writer's committed rows.
Notebook export is the one feature that needs a directly-opened file rather
than a snapshot, because the exported notebook has to reopen the database by
path, outside VS Code. It is therefore available on a remote host when the
extension is installed there, and unavailable in a virtual workspace.
Opening a file from a link
PK SQLite: Open URL…, or the link button beside the file name in a
viewer's toolbar, downloads a data file from a shared link and opens it in the
viewer for its kind:
- a SQLite database
- an Excel workbook (a Google Sheet opens as a workbook, with every tab)
- a CSV, TSV, or PSV file
The box offers the link on your clipboard when there is one. It takes links
from:
| Service |
Link to paste |
| Google Sheets |
A sheet shared with "Anyone with the link", or published to the web |
| Google Drive |
A file shared with "Anyone with the link" |
| OneDrive and SharePoint |
Share ▸ "Anyone with the link can view" (1drv.ms/…, …sharepoint.com/:x:/…) |
| Dropbox |
Share ▸ Copy link |
| iCloud Drive |
Share ▸ Share Options ▸ "Anyone with the link" |
| Box |
A shared link (app.box.com/s/…) with downloading allowed |
| GitHub |
A file in a public repository (github.com/…/blob/… or raw.githubusercontent.com/…) |
| Amazon S3, Google Cloud Storage, Azure Blob Storage |
A public object, or a presigned / signed / SAS URL (s3://… and gs://… work too) |
| Any web address |
A direct link that downloads the file in a browser |
Nothing is downloaded until you ask, and only from the link you gave.
- Not shared: a link that isn't shared publicly gets a message saying how
to share it on that service, followed by every address tried and what each
one answered.
- Encodings: a CSV saved in your computer's legacy code page, such as
Windows-874 for Thai, is converted to UTF-8 as it downloads.
- DuckDB and Parquet: these files are pointed at DuckDB sidekick.
The tab is named after the file, and the file stays read-only: Allow Changes
needs a file on disk.
- Reload from Link: PK SQLite: Reload from Link downloads the file
again, and every viewer on it shows the new copy.
- Restarts: downloads are kept in the extension's storage, so a tab
reopened after a restart shows the last copy without going online. A copy
nobody has opened for 30 days is removed.
Explorer
Click Explorer in the viewer toolbar to show or hide the tree of the
database, its tables, views, and columns. Selecting a table or view opens it in
the grid and highlights its bottom tab, so the tree and the tabs stay in step.
The tree is read from SQLite's schema, so expanding a table never scans it,
however large it is. Tables and views are grouped separately; relations whose
names begin with sqlite_ are internal and hidden.
Indexes and Triggers get groups of their own, shown only when the
database has any. Expanding an index lists the columns it covers, in key order;
expanding a trigger shows the statement that created it. An index SQLite made
for itself — to enforce a UNIQUE constraint or a non-INTEGER PRIMARY KEY
— is shown too, marked implicit, because that a column is indexed is worth
knowing however the index came about. Turn the group off with
pkSqlite.showIndexes.
Right-click a cell for Show schema, which writes that table's CREATE
statement into the SQL editor as a comment block.
Right-click the database, a table, or a view in the tree for Generate DDL,
which opens its CREATE statements in a new, unsaved SQL editor tab. A table
brings its indexes and triggers along with it; the database gives every table,
view, index, and trigger, in an order that recreates the schema when run on an
empty database. Nothing is written until you save the tab.
Following a foreign key
Right-click a cell in a column that is part of a foreign key and the menu
offers to follow it:
- Go to
<table>.<column> opens the referenced table filtered to the row
this value points at.
- Show
<table> referencing this goes the other way, listing the rows in
another table that point back at this one.
Both are offered only when there is somewhere to go: a key whose target table
is missing, a NULL in any part of the key, and a key that references its
target's primary key without naming the columns are all described rather than
navigated. Composite keys are followed on every column at once.
Click Data profile in the viewer toolbar — or run PK SQLite: Toggle Data
Profile — to open the profile pane beside the grid. Drag its left edge to
resize it, and close it from the × in its header. Profiles are calculated
lazily and cached until the database changes. The Query Result tab is also
profileable: its successful SELECT is rerun to calculate statistics over the
full result rather than only the displayed row limit.
The pane has three tabs, all following the active cell:
- Record reads the selected row down the page instead of across it, one
labelled field per column — the view that makes a forty-column table
legible. Step rows with the arrows beside the position, and click a field to
jump the grid to that column.
- Column profiles just the active cell's column, at full pane height.
- All columns is the card grid: every column at once. Click a card to
select that column in the grid.
Every card shows the column's missing, unique, minimum, and maximum values;
numeric columns add the mean, the median, and a twelve-bin histogram, while
other columns show their eight most frequent values as bars, each with its
share of the values present. PNG saves the complete theme-aware card grid,
including off-screen cards; it is offered on the All columns tab only.
SQLite has no approximate-distinct function, so unique counts are exact — and
exact means a full scan with a sort or hash per column. They are computed every
time regardless, so on a very large table expect the profile to take a while.
SQL and notebook export
Click SQL in the viewer toolbar, enter one read-only query (or, with
changes allowed, one statement of any kind), and select
Run or press Ctrl/Cmd+Enter. The editor shows line numbers, and the panel is
resized by dragging its bottom edge.
Because SQLite offers no way to interrupt a running statement, Cancel and
the pkSqlite.queryTimeoutSeconds timeout stop a query by destroying the thread
running it and starting a fresh one. This is safe because nothing is being
written, or, with changes allowed, because each change is one statement that
either completes or never begins. It cannot disturb the grid, the tabs, or the
explorer, which are served by a separate worker.
Suggestions appear as you type and can be requested with Ctrl+Space. What is
offered first depends on the clause: relations after FROM and JOIN, columns
after SELECT, WHERE, ON, GROUP BY, and ORDER BY, and keywords at the
start of a statement. Typing alias. narrows to that relation's columns, and
main. lists the database's relations. Use ↑ and ↓ to move, Enter or Tab to
accept, and Escape to dismiss; Ctrl/Cmd+Enter still runs the query. Names that
need quoting are inserted quoted. Set pkSqlite.sqlSuggestions to false to
turn the popup off.
Prettify
Prettify in the SQL panel, Shift+Alt+F in the editor, right-click in the
editor, or PK SQLite: Prettify SQL reformats the query in place:
SELECT prefs.purchase_purpose,
prefs.preference,
prefs.rating
FROM (
SELECT b.purchase_purpose,
p.preference,
p.rating,
RANK() OVER (PARTITION BY b.purchase_purpose ORDER BY p.rating DESC) AS rank
FROM buyers AS b
JOIN prefs AS p
ON b.buyer_id = p.buyer_id
) AS prefs
WHERE prefs.rank <= 5
ORDER BY prefs.purchase_purpose,
prefs.rating DESC;
Each clause starts a line. The first item stays on the clause's line and later
items — separated by a comma, or by AND or OR — wrap to one extra level of
indentation. A subquery's parentheses open an indent level and its closing
parenthesis returns to the clause indent, so ) AS prefs reads as one line.
Parentheses holding an expression never break, which keeps a window
specification such as OVER (PARTITION BY x ORDER BY y) intact.
Keywords and type names are uppercased. Identifiers, quoted identifiers, string
literals, function names, and comments keep the spelling you gave them, and the
caret stays with the token it was on.
Expand SELECT *
Shift+Alt+E in the editor, right-click in the editor, or PK SQLite: Expand
SELECT * writes each star in the statement out as the columns it stands for:
SELECT * FROM sales -- becomes
SELECT id, region, channel, amount, "order date" FROM sales
It works on the selection if there is one, otherwise on the statement the
caret is in, and nothing is run. s.* becomes s.id, s.region, …. A bare *
over a join qualifies only the names both sides have, and a key merged by
USING stays bare. Stars inside CTEs and subqueries are expanded where they
stand. The list stays on the star's line while that line fits in 100
characters, and otherwise goes one column per line. A star SQLite cannot read
is left as it is, and the status line says why. One Undo puts every star back.
Join Lines
Ctrl+J (Cmd+J on macOS) in the SQL editor joins the line the caret is on to
the one below it, or, with several lines selected, joins them all into one. The
first line's trailing spaces and the next line's indentation are dropped and
one space goes between them, so SELECT id, over name becomes
SELECT id, name. The caret stays at the join, so pressing it again keeps
joining downward, and one Undo splits the lines back. The same keys run VS
Code's own Join Lines in the cells of a SQL script notebook.
Select and delete lines
Ctrl+L (Cmd+L on macOS) selects the caret's whole line, line break included,
as VS Code's Expand Line Selection does; press it again to take in the next
line, and with a selection it widens to whole lines. Ctrl+U deletes from the
caret back to the start of its line and Ctrl+K from the caret to its end; at
the very start or end of a line the line break goes instead, joining the two
lines, and with a selection both delete the selection. Ctrl+U and Ctrl+K are
Ctrl on macOS too. One Undo puts the text back. In a SQL script notebook's cells these keys keep VS Code's
own meanings.
Alt+Up and Alt+Down (Option on macOS) move the caret's line, or every line the
selection touches, one line up or down, as VS Code's Move Line Up and Move
Line Down do; the selection moves with the lines, and one Undo puts them back.
Shift+Alt+Down (Shift+Option on macOS) duplicates the caret's line, or every
line the selection touches, below itself and moves the selection to the copy,
as VS Code's Copy Line Down does; pressing it again copies the copy.
Ctrl+/ (Cmd+/ on macOS) comments the caret's line, or every line the selection
touches, out with --, as VS Code's Toggle Line Comment does; when every one
already is, it comments them back in. The -- goes in at the lines' smallest
indentation, so an indented block stays aligned, and blank lines are left
alone. One Undo puts the text back. In a SQL script notebook's cells the same
keys run VS Code's own toggle.
Right-clicking the SQL editor opens a menu with Prettify SQL, Expand
SELECT * and Run SQL alongside Cut, Copy, Paste, and Select All. Each entry shows its keyboard
shortcut, which remains available if your platform refuses clipboard access to
the viewer.
Ctrl+wheel (Cmd+wheel on macOS) over the SQL editor zooms its text, a pixel
per notch, between 6 and 48 pixels; a trackpad pinch works too. The line
numbers and suggestions zoom with it. The size lasts until the panel closes,
and a new panel starts at VS Code's editor.fontSize.
The History panel records each command, status, duration, and displayed row
count until the document closes. The history export button in the toolbar
opens a menu of three formats; each asks which queries to include, with the
successful ones preselected:
- Jupyter notebook (.ipynb) creates a JupySQL notebook that reopens the
database read-only through a SQLite URI. Python's
sqlite3 is in the
standard library, so the notebook runs without installing a database driver.
It needs a local SQLite file, so it is refused for a CSV or Excel file.
- SQL script (.sql) writes the queries one after another, each ending in a
semicolon, under a comment giving its number, status, time, and row count.
- Markdown with SQL blocks (.md) gives each query a heading and its own
```sql block, ready to paste into notes or a README.
Each file records when it was exported, in local time with its UTC offset. The
button is disabled until the session has at least one query to export.
Running a SQL script as a notebook
The scroll button beside the history buttons in the toolbar, or PK SQLite:
Open SQL Script as Notebook…, opens a notebook that runs against the file
in that viewer. It takes either:
- a
.sql file, with a cell for each statement
- a Markdown file of
```sql blocks, with a cell for each block and
the text between them shown as Markdown
After a .sql or .md history export, Open as Notebook in the export
message does the same for the file you just wrote. In a .sql file, a comment
directly above a statement stays in its cell. A comment set apart by a blank
line becomes a text cell.
Run a cell with Ctrl/Cmd+Enter or Shift+Enter, or use Run All, which stops
at the first cell that fails. Each cell shows its result under it, up to
1,000 rows. A cell holding several statements runs them in order and shows the
last one's result.
Cells run exactly as the SQL editor runs them:
- read-only unless you allow changes
- under the same timeout
- recorded in the same history
The notebook's kernel is the file it runs against. It is named after that
file and stays with the notebook when you switch viewers. To run the script
against another open file, pick that file's kernel.
Save writes the script back in its own format, line endings, and byte
order mark, so an unedited script saves unchanged.
Save also writes each cell's last result under it, as a Jupyter notebook
keeps its outputs: the first 20 rows as a table with the row count, or the
error. In Markdown the table renders in any preview; in a .sql file it is
written as comments. Reopen the script and each cell shows its saved result
until you run it again. Re-running unchanged SQL leaves the file unchanged,
since no timings are saved. The pkSqlite.scriptOutputRows setting sets how
many rows are kept; 0 saves no results. DuckDB sidekick and SQLens save results
the same way, so a script opens with its results in any of them.
.sql and .md files still open as text unless you use the button, the
command, or Reopen Editor With….
Opening a tab in its own window
To see two tables side by side, or to put one on another monitor, pull its tab
off the strip: drag it up, clear of the tab strip, and let go. A chip under
the pointer says Release to open in a new window once it will open. Dragging
along the strip, or pressing Escape, leaves the tab where it is. The Query
Result tab comes off the same way, with the rows it already has — the query is
not run again. You can also right-click a tab → Open in New Window, or run
PK SQLite: Open Current Tab in New Window from the Command Palette.
The new window is a complete second viewer on the same file, opened on that
tab. It has its own grid, filter, profile, and SQL editor, and it shares the
first viewer's session: the same SQL history and the same added tables. Adding,
renaming, or closing an added tab in either window reloads both, and each keeps
the tab it was showing. The tab stays in the window it came from too, because
tearing it off opens a second view of the file, not a move. VS Code for the Web
has no separate windows, so there the new viewer opens beside the current one.
Commands
- PK SQLite: Toggle Explorer
- PK SQLite: Toggle SQL Editor
- PK SQLite: Run SQL
- PK SQLite: Cancel SQL
- PK SQLite: Allow Changes / Make Read-Only
- PK SQLite: Prettify SQL
- PK SQLite: Expand SELECT *
- PK SQLite: Show SQL History
- PK SQLite: Export SQL History as Jupyter Notebook
- PK SQLite: Export SQL History as SQL Script
- PK SQLite: Export SQL History as Markdown
- PK SQLite: Clear SQL History
- PK SQLite: Open SQL Script as Notebook…
- PK SQLite: Open URL…
- PK SQLite: Reload from Link
- PK SQLite: Export Active Table or Query Result as CSV
- PK SQLite: Export All Tables to Excel Workbook
- PK SQLite: Export All Tables as CSV Files
- PK SQLite: Export All Tables to a SQLite Database
- PK SQLite: Auto-fit All Columns
- PK SQLite: Freeze or Unfreeze Panes at Active Cell
- PK SQLite: Toggle Data Profile
- PK SQLite: Filter Rows
- PK SQLite: Write Row Filter as SQL
- PK SQLite: Find in Database
- PK SQLite: Open Current Tab in New Window
- PK SQLite: Switch Viewer Theme (VS Code, Light, Dark)
- PK SQLite: Open File as SQLite Database
- PK SQLite: Open CSV, TSV, PSV, or Excel in SQLite Sidekick
- PK SQLite: Save as SQLite Database
- PK SQLite: Add Data Files to Viewer…
Not supported
Editing cells or rows in the grid (changes are made with SQL, when allowed);
multi-statement scripts and transactions; charting; encrypted databases
(SQLCipher, SEE); opening a database over HTTP or an object store; and FTS5 or
R*Tree shadow tables presented as first-class relations. Parquet, CSV, and Excel
are a different product — see DuckDB sidekick.
Contributing
Building the extension from source, running tests, and packaging a release are
covered in DEV.md.
License
MIT