Skip to content

Commit 0549033

Browse files
rustyconoverclaude
andcommitted
release: bump version 0.11.1 -> 0.12.0
feat(landing): surface catalog macros in describe.json + landing-page fixes - describe.json producer now collects SCALAR_MACRO/TABLE_MACRO and folds them into the schema `functions` list (scalar macro -> "scalar", table macro -> "table"). Defaulted macro params render as named args with their default; untyped params show as "ANY". Fixes workers whose callable functions are declared as macros (e.g. vgi-volcanos) showing 0 functions on the landing page. Golden updated 162 -> 165 functions. - Vendored landing.html: Markdown rendering for catalog/table/view docs, Arrow->DuckDB column/arg type display, table/view descriptions shown only on expand, footer schema-version line removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 3cbce58 commit 0549033

5 files changed

Lines changed: 207 additions & 20 deletions

File tree

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-python"
3-
version = "0.11.1"
3+
version = "0.12.0"
44
description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
55
readme = "README.md"
66
keywords = [

‎tests/landing/describe.expected.json‎

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{
44
"attach_options": [],
55
"counts": {
6-
"functions": 162,
6+
"functions": 165,
77
"schemas": 2,
88
"tables": 44,
99
"views": 3
@@ -1114,6 +1114,49 @@
11141114
"returns": "string",
11151115
"type": "scalar"
11161116
},
1117+
{
1118+
"args": [
1119+
{
1120+
"desc": "Value to clamp",
1121+
"name": "val",
1122+
"type": "ANY"
1123+
},
1124+
{
1125+
"default": "0",
1126+
"desc": "Lower bound (inclusive)",
1127+
"name": "lo",
1128+
"named": true,
1129+
"type": "int64"
1130+
},
1131+
{
1132+
"default": "100",
1133+
"desc": "Upper bound (inclusive)",
1134+
"name": "hi",
1135+
"named": true,
1136+
"type": "int64"
1137+
}
1138+
],
1139+
"doc": "Clamp a value between lo and hi (defaults: 0..100)",
1140+
"name": "vgi_clamp",
1141+
"type": "scalar"
1142+
},
1143+
{
1144+
"args": [
1145+
{
1146+
"desc": "First factor",
1147+
"name": "x",
1148+
"type": "ANY"
1149+
},
1150+
{
1151+
"desc": "Second factor",
1152+
"name": "y",
1153+
"type": "ANY"
1154+
}
1155+
],
1156+
"doc": "Multiply two values",
1157+
"name": "vgi_multiply",
1158+
"type": "scalar"
1159+
},
11171160
{
11181161
"args": [
11191162
{
@@ -2206,12 +2249,6 @@
22062249
"name": "struct_settings",
22072250
"type": "table"
22082251
},
2209-
{
2210-
"args": [],
2211-
"doc": "Distributed sum using simple callback API",
2212-
"name": "sum_all_columns_simple_distributed",
2213-
"type": "table_in_out"
2214-
},
22152252
{
22162253
"args": [],
22172254
"doc": "Generates 10000 integers from 0 to 9999",
@@ -2361,6 +2398,18 @@
23612398
"name": "versioned_data_scan",
23622399
"type": "table"
23632400
},
2401+
{
2402+
"args": [
2403+
{
2404+
"desc": "Number of rows to generate",
2405+
"name": "n",
2406+
"type": "ANY"
2407+
}
2408+
],
2409+
"doc": "Table macro returning range of values",
2410+
"name": "vgi_range_table",
2411+
"type": "table"
2412+
},
23642413
{
23652414
"args": [],
23662415
"doc": "buffer_input variant using batch_index to reconstruct order",
@@ -2610,6 +2659,12 @@
26102659
"doc": "Computes column-wise sums across all batches",
26112660
"name": "sum_all_columns",
26122661
"type": "table_in_out"
2662+
},
2663+
{
2664+
"args": [],
2665+
"doc": "Distributed sum using simple callback API",
2666+
"name": "sum_all_columns_simple_distributed",
2667+
"type": "table_in_out"
26132668
}
26142669
],
26152670
"name": "main",

‎uv.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vgi/http/describe_json.py‎

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from __future__ import annotations
1919

20+
import contextlib
2021
import json
2122
import logging
2223
from importlib.metadata import version as _pkg_version
@@ -37,6 +38,8 @@
3738
CatalogInterface,
3839
FunctionInfo,
3940
FunctionType,
41+
MacroInfo,
42+
MacroType,
4043
SchemaObjectType,
4144
TableInfo,
4245
ViewInfo,
@@ -190,6 +193,68 @@ def _function_args(fn: FunctionInfo) -> list[dict[str, Any]]:
190193
return args
191194

192195

196+
def _macro_display_type(m: MacroInfo) -> str:
197+
# A scalar macro is invoked exactly like a scalar function in SQL, and a
198+
# table macro like a table function; surface them in the same buckets so the
199+
# landing page lists a catalog's full callable surface (VGI workers commonly
200+
# expose their "functions" as declarative macros — see vgi-volcanos).
201+
return "scalar" if m.macro_type == MacroType.SCALAR else "table"
202+
203+
204+
def _macro_args(m: MacroInfo) -> list[dict[str, Any]]:
205+
# Defaulted parameters are optional and callable by name in DuckDB, so we
206+
# present them as named args (with their default); the rest are positional.
207+
defaults: dict[str, Any] = {}
208+
if m.parameter_default_values is not None:
209+
batch = m.parameter_default_values
210+
for col_name in batch.schema.names:
211+
with contextlib.suppress(Exception):
212+
defaults[col_name] = batch.column(col_name)[0].as_py()
213+
214+
raw = m.arguments_schema
215+
if raw is None:
216+
schema = None
217+
elif isinstance(raw, pa.Schema):
218+
# In-process catalog interfaces hand back a live schema; only the
219+
# serialized wire form is IPC bytes.
220+
schema = raw
221+
else:
222+
schema = _read_schema(bytes(raw))
223+
fields = list(schema) if schema is not None else None
224+
225+
args: list[dict[str, Any]] = []
226+
names = [f.name for f in fields] if fields is not None else list(m.parameters)
227+
field_by_name = {f.name: f for f in fields} if fields is not None else {}
228+
for name in names:
229+
field = field_by_name.get(name)
230+
# Macro parameters are untyped unless a typed default pins them; show
231+
# ANY rather than the Arrow null placeholder.
232+
if field is not None and not pa.types.is_null(field.type):
233+
arg: dict[str, Any] = {"name": name, "type": str(field.type)}
234+
else:
235+
arg = {"name": name, "type": "ANY"}
236+
doc = _meta(field, VGI_DOC_KEY) if field is not None else None
237+
if doc:
238+
arg["desc"] = doc
239+
if name in defaults:
240+
arg["named"] = True
241+
try:
242+
arg["default"] = json.dumps(defaults[name])
243+
except Exception: # noqa: BLE001
244+
arg["default"] = str(defaults[name])
245+
args.append(arg)
246+
return args
247+
248+
249+
def _macro_to_dict(m: MacroInfo) -> dict[str, Any]:
250+
return {
251+
"name": m.name,
252+
"type": _macro_display_type(m),
253+
"doc": m.comment or "",
254+
"args": _macro_args(m),
255+
}
256+
257+
193258
# ---------------------------------------------------------------------------
194259
# Catalog contents
195260
# ---------------------------------------------------------------------------
@@ -230,6 +295,9 @@ def _build_schemas(
230295
SchemaObjectType.AGGREGATE_FUNCTION,
231296
):
232297
funcs_raw.extend(_schema_contents(iface, attach_opaque_data, si.name, kind))
298+
macros_raw: list[MacroInfo] = []
299+
for kind in (SchemaObjectType.SCALAR_MACRO, SchemaObjectType.TABLE_MACRO):
300+
macros_raw.extend(_schema_contents(iface, attach_opaque_data, si.name, kind))
233301

234302
tables = []
235303
for t in sorted(tables_raw, key=lambda x: x.name):
@@ -257,8 +325,12 @@ def _build_schemas(
257325
"args": _function_args(fn),
258326
**({"returns": r} if (r := _function_returns(fn)) else {}),
259327
}
260-
for fn in sorted(funcs_raw, key=lambda f: (f.function_type.value, f.name))
328+
for fn in funcs_raw
261329
]
330+
functions.extend(_macro_to_dict(m) for m in macros_raw)
331+
# Deterministic ordering across functions + macros (both fold into the
332+
# same scalar/table/aggregate/table_in_out buckets on the landing page).
333+
functions.sort(key=lambda d: (d["type"], d["name"]))
262334

263335
schemas.append({"name": si.name, "tables": tables, "views": views, "functions": functions})
264336
totals["schemas"] += 1

‎vgi/http/landing.html‎

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@
7373
.cat-card .cc-title { display: flex; align-items: center; gap: 9px; font-size: 18px; font-weight: 650; letter-spacing: -0.01em; }
7474
.cat-card .cc-title .ic { width: 19px; height: 19px; color: var(--accent); }
7575
.cat-card .cc-doc { color: var(--muted); font-size: 13.5px; margin-top: 5px; }
76+
.cat-card .cc-doc .md-h { font-size: 14px; font-weight: 650; color: var(--ink); margin: 8px 0 4px; }
77+
.cat-card .cc-doc .md-p { margin: 4px 0; }
78+
.cat-card .cc-doc .md-p:first-child { margin-top: 0; }
79+
.cat-card .cc-doc .md-ul { margin: 4px 0; padding-left: 18px; }
80+
.cat-card .cc-doc code { font-family: var(--mono); background: var(--chip); padding: 1px 5px; border-radius: 4px; font-size: .92em; }
81+
.cat-card .cc-doc a { color: var(--accent); }
7682
.badges { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 13px; }
7783
.badge { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; padding: 5px 11px; border: 1px solid var(--line-strong); border-radius: 8px; text-decoration: none; color: var(--ink); background: var(--panel); }
7884
.badge:hover { border-color: var(--accent); color: var(--accent); }
@@ -190,10 +196,17 @@
190196
.t-name, .fn-name { font-family: var(--mono); font-size: 12.5px; font-weight: 550; }
191197
.t-meta { color: var(--muted); font-size: 11.5px; font-family: var(--mono); font-variant-numeric: tabular-nums; flex: none; }
192198
.fn-sig { color: var(--muted); font-size: 12px; font-family: var(--mono); flex: none; }
193-
.t-comment, .fn-doc { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; margin-left: auto; text-align: right; }
199+
.fn-doc { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; margin-left: auto; text-align: right; }
194200

195201
/* level 4: body inset */
196202
.t-body, .fn-body { padding: 9px 13px 12px; background: color-mix(in srgb, var(--kc) 4%, var(--bg)); border-radius: 0 0 var(--row-radius) var(--row-radius); }
203+
.t-desc { color: var(--ink); font-size: 12.5px; line-height: 1.5; margin: 0 0 11px; }
204+
.t-desc .md-h { font-size: 13px; font-weight: 650; margin: 8px 0 4px; }
205+
.t-desc .md-p { margin: 4px 0; }
206+
.t-desc .md-p:first-child { margin-top: 0; }
207+
.t-desc .md-ul { margin: 4px 0; padding-left: 18px; }
208+
.t-desc code { font-family: var(--mono); background: var(--chip); padding: 1px 5px; border-radius: 4px; font-size: .92em; }
209+
.t-desc a { color: var(--accent); }
197210
.args-label { font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); font-weight: 700; margin: 0 0 6px; }
198211
.cols { display: flex; flex-direction: column; gap: 6px; }
199212
.col-item { border: 1px solid color-mix(in srgb, var(--kc) 22%, var(--line)); border-radius: 7px; padding: 7px 10px; background: color-mix(in srgb, var(--kc) 4%, var(--panel)); }
@@ -225,7 +238,7 @@
225238
</style>
226239
</head>
227240
<body>
228-
<!-- vgi-landing-asset v1 -->
241+
<!-- vgi-landing-asset v2 -->
229242

230243
<a class="skip" href="#contents">Skip to contents</a>
231244

@@ -312,6 +325,55 @@ <h1 id="worker-name">—</h1>
312325
const esc = s => String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
313326
const sqlHi = s => esc(s).replace(/\b(SELECT|FROM|WHERE|GROUP BY|ORDER BY|INTERVAL|AND|OR|AS|JOIN|ON|YEAR)\b/g, m=>'<span class="kw">'+m+'</span>');
314327

328+
function mdToHtml(src){
329+
if(!src) return '';
330+
var lines = String(src).replace(/\r\n?/g,'\n').split('\n'), out=[], i=0;
331+
function inl(t){
332+
t = esc(t);
333+
t = t.replace(/`([^`]+)`/g,'<code>$1</code>');
334+
t = t.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>');
335+
t = t.replace(/(^|[^*])\*([^*]+)\*/g,'$1<em>$2</em>');
336+
t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
337+
return t;
338+
}
339+
while(i<lines.length){
340+
var ln=lines[i];
341+
if(/^\s*$/.test(ln)){ i++; continue; }
342+
var h=ln.match(/^(#{1,6})\s+(.*)$/);
343+
if(h){ var lv=Math.min(h[1].length,6); out.push('<h'+lv+' class="md-h">'+inl(h[2])+'</h'+lv+'>'); i++; continue; }
344+
if(/^\s*[-*+]\s+/.test(ln)){
345+
var items=[];
346+
while(i<lines.length && /^\s*[-*+]\s+/.test(lines[i])){ items.push('<li>'+inl(lines[i].replace(/^\s*[-*+]\s+/,''))+'</li>'); i++; }
347+
out.push('<ul class="md-ul">'+items.join('')+'</ul>'); continue;
348+
}
349+
var para=[ln]; i++;
350+
while(i<lines.length && !/^\s*$/.test(lines[i]) && !/^(#{1,6}\s|\s*[-*+]\s)/.test(lines[i])){ para.push(lines[i]); i++; }
351+
out.push('<p class="md-p">'+inl(para.join(' '))+'</p>');
352+
}
353+
return out.join('');
354+
}
355+
var _DDB_TYPES = {int8:'TINYINT',int16:'SMALLINT',int32:'INTEGER',int64:'BIGINT',
356+
uint8:'UTINYINT',uint16:'USMALLINT',uint32:'UINTEGER',uint64:'UBIGINT',
357+
halffloat:'FLOAT',float:'FLOAT',float32:'FLOAT',double:'DOUBLE',float64:'DOUBLE',
358+
bool:'BOOLEAN',boolean:'BOOLEAN',string:'VARCHAR',large_string:'VARCHAR',utf8:'VARCHAR',large_utf8:'VARCHAR',
359+
binary:'BLOB',large_binary:'BLOB',date32:'DATE','date32[day]':'DATE',date64:'DATE',null:'NULL',
360+
month_day_nano_interval:'INTERVAL'};
361+
function duckdbType(t){
362+
if(!t) return t; t=String(t); var lt=t.toLowerCase();
363+
if(_DDB_TYPES[lt]) return _DDB_TYPES[lt];
364+
var d=t.match(/^dictionary<values=([^,>]+)/i); if(d) return duckdbType(d[1].trim());
365+
var l=t.match(/^(?:large_)?list<(?:item:\s*)?(.+)>$/i); if(l) return duckdbType(l[1].trim())+'[]';
366+
if(/^fixed_size_binary/i.test(t)) return 'BLOB';
367+
var ts=t.match(/^timestamp\[([^,\]]+)(?:,\s*tz=([^\]]+))?\]$/i); if(ts) return ts[2]?'TIMESTAMPTZ':'TIMESTAMP';
368+
if(/^time(32|64)\[/i.test(t)) return 'TIME';
369+
if(/^duration\[/i.test(t)) return 'INTERVAL';
370+
var dec=t.match(/^decimal(?:128|256)\((\d+),\s*(\d+)\)$/i); if(dec) return 'DECIMAL('+dec[1]+','+dec[2]+')';
371+
if(/^struct</i.test(t)) return 'STRUCT';
372+
if(/^map</i.test(t)) return 'MAP';
373+
return t;
374+
}
375+
376+
315377
// ── identity pill ────────────────────────────────────────────────────────────────
316378
function renderIdentity(){
317379
if (!describe.oauth) return;
@@ -377,7 +439,7 @@ <h1 id="worker-name">—</h1>
377439
? '<div class="cc-by">'+[t.author?'by '+esc(t.author):'', t.copyright?esc(t.copyright):''].filter(Boolean).join(' · ')+'</div>' : '';
378440
document.getElementById('cat-card').innerHTML =
379441
'<div class="cc-title">'+icon('catalog')+esc(t.title || c.name)+'</div>' +
380-
(t.doc_md ? '<div class="cc-doc">'+esc(t.doc_md)+'</div>' : '') +
442+
(t.doc_md ? '<div class="cc-doc">'+mdToHtml(t.doc_md)+'</div>' : '') +
381443
(badges ? '<div class="badges">'+badges+'</div>' : '') + kw + by;
382444
}
383445

@@ -482,9 +544,9 @@ <h1 id="worker-name">—</h1>
482544
const kcls = kind==='view' ? ' view k-view' : '';
483545
return '<details class="tbl'+kcls+'" data-name="'+esc(t.name.toLowerCase())+'" data-kind="'+kind+'" data-catalog="'+esc(catName)+'" data-schema="'+esc(s.name)+'" data-table="'+esc(t.name)+'">'+
484546
'<summary>'+icon('caret','tri')+icon(iconName,'row-icon')+
485-
'<span class="t-name">'+esc(t.name)+'</span><span class="t-meta">'+t.cols+' cols</span>'+
486-
(t.comment? '<span class="t-comment">'+esc(t.comment)+'</span>':'')+'</summary>'+
547+
'<span class="t-name">'+esc(t.name)+'</span><span class="t-meta">'+t.cols+' cols</span></summary>'+
487548
'<div class="t-body">'+
549+
(t.comment ? '<div class="t-desc">'+mdToHtml(t.comment)+'</div>' : '')+
488550
(kind==='view' && t.def ? '<div class="args-label">Definition</div><pre class="view-sql">'+sqlHi(t.def)+'</pre>' : '')+
489551
'<div class="args-label">Columns</div><div class="cols"><div class="skeleton">Loading columns…</div></div></div></details>';
490552
}
@@ -497,7 +559,7 @@ <h1 id="worker-name">—</h1>
497559
const def = (a.default!==undefined && a.default!==null) ? '<span class="arg-def">default '+esc(a.default)+'</span>' : '';
498560
const desc = a.desc ? esc(a.desc) : '';
499561
const meta = (desc||def) ? '<div class="col-comment">'+desc+(desc&&def?' · ':'')+def+'</div>' : '';
500-
return '<div class="col-item"><div class="col"><span class="col-name">'+esc(a.name)+(a.named?'<span class="arg-kind"> :=</span>':'')+'</span><span class="col-type">'+esc(a.type)+'</span></div>'+meta+'</div>';
562+
return '<div class="col-item"><div class="col"><span class="col-name">'+esc(a.name)+(a.named?'<span class="arg-kind"> :=</span>':'')+'</span><span class="col-type">'+esc(duckdbType(a.type))+'</span></div>'+meta+'</div>';
501563
}
502564
function fnRow(f, iconName){
503565
const pos = f.args.filter(a=>!a.named), named = f.args.filter(a=>a.named);
@@ -563,7 +625,7 @@ <h1 id="worker-name">—</h1>
563625
const body = d.querySelector('.cols');
564626
fetchColumns(d.dataset.catalog, d.dataset.schema, d.dataset.table).then(info=>{
565627
body.innerHTML = info.columns.map(col=>
566-
'<div class="col-item"><div class="col"><span class="col-name">'+esc(col.name)+'</span><span class="col-type">'+esc(col.type)+'</span></div>'+
628+
'<div class="col-item"><div class="col"><span class="col-name">'+esc(col.name)+'</span><span class="col-type">'+esc(duckdbType(col.type))+'</span></div>'+
567629
(col.comment? '<div class="col-comment">'+esc(col.comment)+'</div>':'')+'</div>').join('');
568630
});
569631
}, true);
@@ -604,9 +666,7 @@ <h1 id="worker-name">—</h1>
604666
'<a href="'+prefix+'/describe">RPC API</a><span class="dot">·</span>'+
605667
'<a href="https://github.com/query-farm">Source</a><span class="dot">·</span>'+
606668
'<a href="https://query.farm">Query.Farm</a>'+
607-
'<div class="foot-meta">vgi-rpc (<code>'+esc(describe.worker.lang)+'</code>) · v'+esc(describe.worker.version)+' · <code>'+esc(describe.server_id)+'</code></div>'+
608-
'<div class="foot-meta">landing_schema_version '+describe.landing_schema_version+
609-
' · shared page, identical across Python / Go / Rust / TypeScript / Java</div>';
669+
'<div class="foot-meta">vgi-rpc (<code>'+esc(describe.worker.lang)+'</code>) · v'+esc(describe.worker.version)+' · <code>'+esc(describe.server_id)+'</code></div>';
610670
}
611671

612672
function renderAll(){ renderCatBar(); renderCatCard(); renderConnect(); updateCupola(); renderContents(); }

0 commit comments

Comments
 (0)