Skip to content

Commit 2a596ce

Browse files
committed
bump pigsty.io catalog generator
1 parent 66ab1db commit 2a596ce

File tree

9 files changed

+3630
-12
lines changed

9 files changed

+3630
-12
lines changed

cli/io_attr.go

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
/*
2+
Copyright 2018-2025 Ruohang Feng <rh@vonng.com>
3+
4+
IO Attr Generator - generates extension attribute pages for pigsty.io (English only)
5+
Sub-pages: load (Preload Required), ddl (Headless Extensions), deps (Dependencies), multi (Multi-Extension Packages), fork (Kernel Fork Extensions)
6+
*/
7+
package cli
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"path/filepath"
13+
"sort"
14+
"strings"
15+
16+
"github.com/sirupsen/logrus"
17+
)
18+
19+
// IOAttrGenerator generates extension attribute pages for pigsty.io
20+
type IOAttrGenerator struct {
21+
Cache *ExtensionCache
22+
OutputDir string
23+
}
24+
25+
// NewIOAttrGenerator creates a new IO Attr generator
26+
func NewIOAttrGenerator(cache *ExtensionCache, outputDir string) *IOAttrGenerator {
27+
return &IOAttrGenerator{
28+
Cache: cache,
29+
OutputDir: outputDir,
30+
}
31+
}
32+
33+
// GenerateAllAttrPages generates all attribute sub-pages and the index page
34+
func (g *IOAttrGenerator) GenerateAllAttrPages() error {
35+
attrDir := filepath.Join(g.OutputDir, "attr")
36+
37+
// Generate _index.md (only if not existing)
38+
if err := g.generateIndexPage(filepath.Join(attrDir, "_index.md")); err != nil {
39+
return fmt.Errorf("failed to generate attr index: %w", err)
40+
}
41+
42+
type pageGen struct {
43+
name string
44+
fn func(string) error
45+
}
46+
pages := []pageGen{
47+
{"load", g.GenerateLoadPage},
48+
{"ddl", g.GenerateDDLPage},
49+
{"deps", g.GenerateDepsPage},
50+
{"multi", g.GenerateMultiPage},
51+
{"fork", g.GenerateForkPage},
52+
}
53+
54+
for _, p := range pages {
55+
if err := p.fn(filepath.Join(attrDir, p.name+".md")); err != nil {
56+
return fmt.Errorf("failed to generate %s page: %w", p.name, err)
57+
}
58+
logrus.Infof("Generated attr page: %s", p.name)
59+
}
60+
61+
logrus.Infof("Successfully generated all attr pages")
62+
return nil
63+
}
64+
65+
// generateIndexPage generates the attr/_index.md page
66+
func (g *IOAttrGenerator) generateIndexPage(outputPath string) error {
67+
if _, err := os.Stat(outputPath); err == nil {
68+
logrus.Infof("Keeping existing %s", outputPath)
69+
return nil
70+
}
71+
content := `---
72+
title: "Attributes"
73+
linkTitle: "Attributes"
74+
description: "Extensions filtered by attributes"
75+
weight: 300
76+
icon: fa-solid fa-tags
77+
---
78+
`
79+
return WriteMarkdownFile(outputPath, content)
80+
}
81+
82+
// GenerateLoadPage generates the load.md page (extensions needing shared_preload_libraries)
83+
func (g *IOAttrGenerator) GenerateLoadPage(outputPath string) error {
84+
var exts []*Extension
85+
for _, ext := range g.Cache.ReadyExtensions() {
86+
if ext.NeedLoad {
87+
exts = append(exts, ext)
88+
}
89+
}
90+
91+
var b strings.Builder
92+
b.WriteString(`---
93+
title: "Preloading"
94+
linkTitle: "Preloading"
95+
description: "PostgreSQL extensions that require dynamic loading"
96+
weight: 10
97+
---
98+
99+
`)
100+
b.WriteString(fmt.Sprintf("The following **%d** extensions require loading in [`shared_preload_libraries`](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SHARED-PRELOAD-LIBRARIES) to function properly.\n\n", len(exts)))
101+
b.WriteString("You need to modify the [`shared_preload_libraries`](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SHARED-PRELOAD-LIBRARIES) parameter in `postgresql.conf`, add the extension library, and restart the database.\n\n")
102+
103+
b.WriteString("| **Extension** | **Library** | **Description** |\n")
104+
b.WriteString("|:-----------|:-------------|:---------|\n")
105+
for _, ext := range exts {
106+
desc := SanitizeText(ext.GetEnDesc())
107+
libName := fmt.Sprintf("`%s`", ext.GetLibName())
108+
b.WriteString(fmt.Sprintf("| [`%s`](/ext/e/%s) | %s | %s |\n",
109+
ext.Name, ext.Name, libName, desc))
110+
}
111+
b.WriteString("{.ext-table}\n\n")
112+
113+
return WriteMarkdownFile(outputPath, b.String())
114+
}
115+
116+
// GenerateDDLPage generates the ddl.md page (extensions not needing CREATE EXTENSION)
117+
func (g *IOAttrGenerator) GenerateDDLPage(outputPath string) error {
118+
var exts []*Extension
119+
for _, ext := range g.Cache.ReadyExtensions() {
120+
if !ext.NeedDDL {
121+
exts = append(exts, ext)
122+
}
123+
}
124+
125+
var b strings.Builder
126+
b.WriteString(`---
127+
title: "Headless"
128+
linkTitle: "Headless"
129+
description: "PostgreSQL extensions that do not require CREATE EXTENSION"
130+
weight: 20
131+
slug: ddl
132+
---
133+
134+
`)
135+
b.WriteString(fmt.Sprintf("The following **%d** extensions can be used without running `CREATE EXTENSION`.\n\n", len(exts)))
136+
b.WriteString("These extensions typically exist as shared libraries (hooks) or standalone tools that take effect through configuration parameters.\n\n")
137+
138+
b.WriteString("| **Extension** | **Package** | **Version** | **Attr** | **Description** |\n")
139+
b.WriteString("|:-----------|:-------------|:--------:|:--------:|:---------|\n")
140+
for _, ext := range exts {
141+
version := ext.GetVersion()
142+
desc := SanitizeText(ext.GetEnDesc())
143+
pkgLink := ext.GetPkgURLLink()
144+
attr := fmt.Sprintf("`%s`", ext.GetAttributeBadge())
145+
b.WriteString(fmt.Sprintf("| [`%s`](/ext/e/%s) | %s | `%s` | %s | %s |\n",
146+
ext.Name, ext.Name, pkgLink, version, attr, desc))
147+
}
148+
b.WriteString("{.ext-table}\n\n")
149+
150+
return WriteMarkdownFile(outputPath, b.String())
151+
}
152+
153+
// GenerateDepsPage generates the deps.md page (extensions with dependencies)
154+
func (g *IOAttrGenerator) GenerateDepsPage(outputPath string) error {
155+
allExts := g.Cache.ReadyExtensions()
156+
157+
// Upstream: extensions that require other extensions
158+
var upstream []*Extension
159+
for _, ext := range allExts {
160+
if len(ext.Requires) > 0 {
161+
upstream = append(upstream, ext)
162+
}
163+
}
164+
165+
// Downstream: extensions that are required by other extensions
166+
var downstream []*Extension
167+
for _, ext := range allExts {
168+
if len(ext.RequireBy) > 0 {
169+
downstream = append(downstream, ext)
170+
}
171+
}
172+
173+
var b strings.Builder
174+
b.WriteString(`---
175+
title: "Dependencies"
176+
linkTitle: "Dependencies"
177+
description: "PostgreSQL extensions with dependency relationships"
178+
weight: 30
179+
---
180+
181+
`)
182+
b.WriteString(fmt.Sprintf("**%d** extensions depend on other extensions, **%d** extensions are depended upon by others.\n\n", len(upstream), len(downstream)))
183+
184+
// Upstream table
185+
b.WriteString("## Upstream Dependencies\n\n")
186+
b.WriteString(fmt.Sprintf("The following **%d** extensions require other extensions to be installed first:\n\n", len(upstream)))
187+
b.WriteString("| **Extension** | **Requires** | **Description** |\n")
188+
b.WriteString("|:-----------|:-------------|:---------|\n")
189+
for _, ext := range upstream {
190+
b.WriteString(g.depsRow(ext, ext.Requires))
191+
}
192+
b.WriteString("{.ext-table}\n\n")
193+
194+
// Downstream table
195+
b.WriteString("## Downstream Dependencies\n\n")
196+
b.WriteString(fmt.Sprintf("The following **%d** extensions are depended upon by other extensions:\n\n", len(downstream)))
197+
b.WriteString("| **Extension** | **Required By** | **Description** |\n")
198+
b.WriteString("|:-----------|:-------------|:---------|\n")
199+
for _, ext := range downstream {
200+
b.WriteString(g.depsRow(ext, ext.RequireBy))
201+
}
202+
b.WriteString("{.ext-table}\n\n")
203+
204+
return WriteMarkdownFile(outputPath, b.String())
205+
}
206+
207+
// depsRow generates a table row with dependency links
208+
func (g *IOAttrGenerator) depsRow(ext *Extension, deps []string) string {
209+
desc := SanitizeText(ext.GetEnDesc())
210+
211+
// Format dependency links
212+
depLinks := make([]string, 0, len(deps))
213+
for _, dep := range deps {
214+
depLinks = append(depLinks, CCExtLink(dep))
215+
}
216+
depStr := strings.Join(depLinks, " ")
217+
218+
return fmt.Sprintf("| [`%s`](/ext/e/%s) | %s | %s |\n",
219+
ext.Name, ext.Name, depStr, desc)
220+
}
221+
222+
// GenerateMultiPage generates the multi.md page (packages containing multiple extensions)
223+
func (g *IOAttrGenerator) GenerateMultiPage(outputPath string) error {
224+
// Group extensions by package, only consider packages with multiple extensions
225+
pkgExts := make(map[string][]*Extension)
226+
for _, ext := range g.Cache.ReadyExtensions() {
227+
pkgExts[ext.Pkg] = append(pkgExts[ext.Pkg], ext)
228+
}
229+
230+
// Collect packages with multiple extensions, sorted by lead extension ID
231+
type multiPkg struct {
232+
Pkg string
233+
Lead *Extension
234+
Exts []*Extension
235+
}
236+
var pkgs []multiPkg
237+
for pkg, exts := range pkgExts {
238+
if len(exts) < 2 {
239+
continue
240+
}
241+
// Find lead extension
242+
var lead *Extension
243+
for _, ext := range exts {
244+
if ext.Lead {
245+
lead = ext
246+
break
247+
}
248+
}
249+
if lead == nil {
250+
lead = exts[0]
251+
}
252+
pkgs = append(pkgs, multiPkg{Pkg: pkg, Lead: lead, Exts: exts})
253+
}
254+
sort.Slice(pkgs, func(i, j int) bool {
255+
return pkgs[i].Lead.ID < pkgs[j].Lead.ID
256+
})
257+
258+
// Count total extensions
259+
totalExts := 0
260+
for _, p := range pkgs {
261+
totalExts += len(p.Exts)
262+
}
263+
264+
var b strings.Builder
265+
b.WriteString(`---
266+
title: "Multi-Ext PKG"
267+
linkTitle: "Multi-Ext PKG"
268+
description: "PostgreSQL packages containing multiple extensions"
269+
weight: 40
270+
---
271+
272+
`)
273+
b.WriteString(fmt.Sprintf("The following **%d** packages contain multiple extensions, totaling **%d** extensions.\n\n", len(pkgs), totalExts))
274+
b.WriteString("When installing these packages, you will get all extensions in the package. The lead extension is shown in bold.\n\n")
275+
276+
for _, p := range pkgs {
277+
b.WriteString(fmt.Sprintf("### %s\n\n", p.Pkg))
278+
279+
pkgLink := fmt.Sprintf("[`%s`](/ext/e/%s)", p.Pkg, p.Lead.Name)
280+
b.WriteString(fmt.Sprintf("Package %s contains **%d** extensions:\n\n", pkgLink, len(p.Exts)))
281+
282+
b.WriteString("| **ID** | **Extension** | **Version** | **Attr** | **Schema** | **Description** |\n")
283+
b.WriteString("|:------:|:-----------|:--------:|:--------:|:---------|:---------|\n")
284+
for _, ext := range p.Exts {
285+
version := ext.GetVersion()
286+
desc := SanitizeText(ext.GetEnDesc())
287+
attr := fmt.Sprintf("`%s`", ext.GetAttributeBadge())
288+
289+
schema := "-"
290+
if len(ext.Schemas) > 0 {
291+
schema = fmt.Sprintf("`%s`", ext.Schemas[0])
292+
}
293+
294+
name := fmt.Sprintf("[`%s`](/ext/e/%s)", ext.Name, ext.Name)
295+
if ext.Lead {
296+
name = fmt.Sprintf("[**`%s`**](/ext/e/%s)", ext.Name, ext.Name)
297+
}
298+
b.WriteString(fmt.Sprintf("| %d | %s | `%s` | %s | %s | %s |\n",
299+
ext.ID, name, version, attr, schema, desc))
300+
}
301+
b.WriteString("{.ext-table}\n\n")
302+
}
303+
304+
return WriteMarkdownFile(outputPath, b.String())
305+
}
306+
307+
// GenerateForkPage generates the fork.md page (extensions forked from kernel)
308+
func (g *IOAttrGenerator) GenerateForkPage(outputPath string) error {
309+
// Group extensions by kernel
310+
kernelExts := make(map[string][]*Extension)
311+
for _, ext := range g.Cache.ReadyExtensions() {
312+
if ext.Extra == nil {
313+
continue
314+
}
315+
kernel, ok := ext.Extra["kernel"]
316+
if !ok {
317+
continue
318+
}
319+
kernelStr, ok := kernel.(string)
320+
if !ok || kernelStr == "" {
321+
continue
322+
}
323+
kernelExts[kernelStr] = append(kernelExts[kernelStr], ext)
324+
}
325+
326+
if len(kernelExts) == 0 {
327+
return WriteMarkdownFile(outputPath, "---\ntitle: \"Kernel Forks\"\nweight: 50\n---\n\nNo kernel fork extensions found.\n")
328+
}
329+
330+
// Sort kernel names for stable output
331+
kernelNames := make([]string, 0, len(kernelExts))
332+
for k := range kernelExts {
333+
kernelNames = append(kernelNames, k)
334+
}
335+
sort.Strings(kernelNames)
336+
337+
// Count total
338+
totalExts := 0
339+
for _, exts := range kernelExts {
340+
totalExts += len(exts)
341+
}
342+
343+
var b strings.Builder
344+
b.WriteString(`---
345+
title: "Kernel Forks"
346+
linkTitle: "Kernel Forks"
347+
description: "Extensions based on PostgreSQL kernel forks"
348+
weight: 50
349+
---
350+
351+
`)
352+
b.WriteString(fmt.Sprintf("The following **%d** extensions are based on **%d** different PostgreSQL kernel forks.\n\n", totalExts, len(kernelNames)))
353+
b.WriteString("These extensions require a specific PostgreSQL kernel fork, not the vanilla PostgreSQL kernel.\n\n")
354+
355+
for _, kernel := range kernelNames {
356+
exts := kernelExts[kernel]
357+
info, known := knownKernels[kernel]
358+
if known {
359+
b.WriteString(fmt.Sprintf("## %s\n\n", info.Name))
360+
b.WriteString(fmt.Sprintf("The following extensions are based on the [**%s**](%s) kernel fork:\n\n", info.Name, info.Link))
361+
} else {
362+
b.WriteString(fmt.Sprintf("## %s\n\n", kernel))
363+
b.WriteString(fmt.Sprintf("The following extensions are based on the **%s** kernel fork:\n\n", kernel))
364+
}
365+
b.WriteString(IOExtensionTable(exts))
366+
}
367+
368+
return WriteMarkdownFile(outputPath, b.String())
369+
}

0 commit comments

Comments
 (0)