Skip to content

API reference · More

serve, servePg, serveFlight

Servers that answer SQL over your tables through HTTP and OData, the PostgreSQL protocol and Arrow Flight SQL.

NodeBrowserEdge

vectrojs/serve puts tables in front of other programs. serve() answers SQL over HTTP in JSON, CSV, JSON lines or Arrow, plus an OData v4 feed (Excel, Power BI’s Web and OData connectors, Google Sheets, pandas, curl). servePg() speaks the PostgreSQL wire protocol as a read-only PostgreSQL 16 (Power BI, Tableau, Metabase, Grafana, DBeaver, psql, every driver). serveFlight() speaks Arrow Flight SQL over gRPC (pandas, Polars and DuckDB through ADBC). All three only read, listen on 127.0.0.1 unless told otherwise, and can share one set of tables made by openTables(). The vectro serve command is these functions with flags. Node only. See Serving and BI tools.

import { serve, servePg, serveFlight, openTables } from 'vectrojs/serve';

Tables to serve

Each server takes its tables one of three ways: tables (tables and datasets from your code), root (a data folder), or catalog (the result of openTables(), shared between servers). With tables or root the server opens its own catalog and closes it on close().

openTables(options?)

Returns a promise of a catalog: the tables given in code plus one table per loadable file directly in root.

Name Type Default Meaning
tables object {} { name: table or dataset }. These win over files of the same name.
root string none A data folder (~ is expanded).
engine Engine the tables’ engine, else a new one The engine files are loaded into.
log function none Called with a message for each file skipped.

Files load by extension: .csv and .tsv (text that rarely repeats is loaded as categories up to 5,000,000 rows, so it can be filtered and grouped), .jsonl and .ndjson (one flat object per line; column kinds from the first 1,000 lines), .arrow, .feather and .ipc, and .vectro datasets, which are opened on disk rather than loaded. A table is named after its file: daily sales.csv becomes daily_sales. Files over 2 GB other than .vectro are refused, and only regular files directly in the folder are read (no subfolders, no symbolic links).

The catalog has:

Name Meaning
engine The engine.
entries A Map of name to { table, source, owned }; source is 'given' or the file path.
root The folder’s real path, or null.
version Goes up by one on each reload.
named() { name: table }, as sql() takes it.
reload() Reads the folder again (file tables are released and reloaded; new files become tables). Returns { loaded, failed }, failed as [{ file, error }]. Throws without a root.
close() Releases the file tables, and closes the engine if the catalog made it.

HTTP and OData

serve(options?)

Starts an HTTP server and returns a promise of it once it is listening.

Name Type Default Meaning
tables, root, catalog, engine The tables, as above.
port number 8787 0 picks a free port.
host string '127.0.0.1' The address to listen on.
token string none Requests need Authorization: Bearer <token>, Basic with the token as the password (any user name), or ?token= in the URL.
cors boolean false Allow pages from other sites (Access-Control-Allow-Origin: *).
odataPage number 50000 Rows per OData page; further pages come through @odata.nextLink.
log function none Called with a line per query and per error.
Endpoint Gives
GET /sql?q=... The answer to one SELECT. format is json (default), csv, ndjson or arrow; limit=n caps the rows.
POST /sql The same, with the SQL as the body, or JSON { "sql": "...", "format": "csv", "limit": 100 }. Bodies are limited to 1 MB.
GET /tables { tables: [{ name, rows, columns, file?, on_disk? }] }.
GET /tables/<name> One table’s description.
GET /tables/<name>/rows A whole table, with format and limit.
POST /reload Reads the data folder again; returns { loaded, failed }.
GET /odata, /odata/$metadata, /odata/<table> An OData v4 feed: $filter, $select, $orderby, $top, $skip, $count, $apply=groupby(...)/aggregate(...) run in Vectro.
GET / The endpoints and table names.

JSON answers are { columns: [{ name, type }], rows: [[...], ...], row_count, ms }, type being number, text or date (ISO text). Errors are JSON { error } with status 400 (bad SQL or request), 401 (token), 404, 405 or 413.

The returned server has url, port, server (the Node http.Server), engine, tables (the catalog’s entries), catalog, reload() and close(), which closes open connections and, unless the catalog was passed in, the catalog.

import { Engine } from 'vectrojs';
import { serve } from 'vectrojs/serve';

const engine = new Engine();
const orders = engine.table({ region: ['EU', 'US', 'EU', 'APAC'], price: [120, 80, 45, 300] });
const server = await serve({ tables: { orders }, port: 0, token: 'secret' });

const q = encodeURIComponent('SELECT region, SUM(price) AS total FROM orders GROUP BY region ORDER BY total DESC');
const headers = { authorization: 'Bearer secret' };
console.log(await (await fetch(`${server.url}/sql?q=${q}`, { headers })).json());
console.log(await (await fetch(`${server.url}/sql?q=${q}&format=csv&token=secret`)).text());
console.log((await fetch(`${server.url}/tables`)).status);    // 401 without the token

const odata = await fetch(`${server.url}/odata/orders?$filter=${encodeURIComponent('price gt 100')}&$select=region`, { headers });
console.log((await odata.json()).value);

await server.close();
await engine.close();

PostgreSQL

servePg(options?)

Starts a PostgreSQL protocol server and returns a promise of it. It answers as PostgreSQL 16, read only: the simple and the extended query protocol (prepared statements, $1 parameters, text and binary values, portals), BEGIN and COMMIT, SET and SHOW, DECLARE and FETCH cursors, cancel requests, and the information_schema and pg_catalog queries drivers and BI tools send. Writes are refused.

Name Type Default Meaning
tables, root, catalog, engine The tables, as above.
port number 5433 0 picks a free port.
host string '127.0.0.1' Any address other than 127.0.0.1, ::1 or localhost needs a password.
user string none The only user name accepted. Without it any name is.
password string none Without it, anyone on this machine may connect.
auth 'scram', 'md5' or 'password' 'scram' How the password is checked (SCRAM-SHA-256 by default).
tls object none { cert, key } (PEM text or bytes) or { certFile, keyFile }, for encrypted connections.
database string 'vectro' The database name reported.
log function none Called with connection messages.
logQueries boolean false Also log every statement clients send.

Column types: whole-number columns are bigint, other numbers double precision, text text, dates timestamp (date when every value is at midnight). The returned server has port, host, server, catalog, url (postgresql://user@host:port/database) and close().

Arrow Flight SQL

serveFlight(options?)

Starts an Arrow Flight SQL server over gRPC (HTTP/2) and returns a promise of it. Served: statements and prepared statements (without parameters), the catalog (catalogs, schemas, tables with their Arrow schemas, table types), and sign-in with a user name and password (Handshake or Basic, then a bearer token). Answers are Arrow, column by column: whole numbers int64, other numbers float64, text utf8, dates timestamp (ms, UTC) or date32.

Options: tables, root, catalog, engine, port (default 31337), host, user, password, tls, database, log and logQueries, as for servePg (no auth). The returned server has port, host, server, catalog, url (grpc://host:port, or grpc+tls:// with tls) and close().

import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { serve, servePg, serveFlight, openTables } from 'vectrojs/serve';

const dir = mkdtempSync(join(tmpdir(), 'vectro-'));
writeFileSync(join(dir, 'daily sales.csv'), 'region,amount\nEU,10\nUS,20\nEU,5\n');

const catalog = await openTables({ root: dir });
const http = await serve({ catalog, port: 0 });
const pg = await servePg({ catalog, port: 0, user: 'me', password: 'secret' });
const flight = await serveFlight({ catalog, port: 0 });
console.log([...catalog.entries.keys()], pg.url, flight.url);

writeFileSync(join(dir, 'regions.tsv'), 'region\tname\nEU\tEurope\nUS\tUnited States\n');
console.log(await (await fetch(`${http.url}/reload`, { method: 'POST' })).json());
const q = encodeURIComponent('SELECT r.name, SUM(s.amount) AS total FROM daily_sales s JOIN regions r USING (region) GROUP BY 1');
console.log((await (await fetch(`${http.url}/sql?q=${q}`)).json()).rows);

await Promise.all([http.close(), pg.close(), flight.close()]);
await catalog.close();
rmSync(dir, { recursive: true, force: true });