mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
The fork already renamed its internals - the built binary is datastore, the packages are datastore-server and friends, and the shipped config.xml already says Datastore - but the surfaces a human reads still said ClickHouse. Issue templates asked which ClickHouse version was affected, the PR template told contributors to close upstream's issues, and CI job names, log lines, error messages and argparse descriptions named a product this repo does not ship. Only prose moved: display names, log and error messages, comments, docstrings and form labels. Everything a machine resolves stayed exactly as it was, because renaming those breaks a build rather than a sentence - binary and path tokens such as clickhouse-client and /etc/clickhouse-server, python and shell identifiers, CLICKHOUSE_* environment variables, the /ClickHouse container mount, docker images in a namespace we do not own, S3 buckets, maven and pypi coordinates, and every URL pointing at upstream. The attribution in NOTICE and README is untouched. Two links were wrong rather than merely stale, so they are corrected here. The PR template's Closes and Related placeholders sent contributors to upstream's tracker instead of this one, and an earlier pass had rewritten a source reference to github.com/Datastore/Datastore, which does not resolve. Co-authored-by: Hanzo Dev <[email protected]>
633 lines
28 KiB
Python
633 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Autogenerate Datastore reference documentation (settings, functions, ...) from
|
|
a Datastore binary's introspection tables. Python port of clickhouse-docs's
|
|
`scripts/settings/autogenerate-settings.sh`, living under ci/jobs/scripts/docs
|
|
so it can run as an optional step of the docs check.
|
|
|
|
Each generator runs a `.sql` file (in ./sql) through `<binary> local`, letting
|
|
it write its `INTO OUTFILE` markdown into a scratch dir, then places that output
|
|
into the docs tree -- either overwriting a page or replacing the region between
|
|
`<!--AUTOGENERATED_START-->` / `<!--AUTOGENERATED_END-->` markers.
|
|
|
|
Legacy-link bridge: the SQL emits Docusaurus-style paths (destination files and
|
|
in-content links, e.g. `/operations/settings/settings#x`). While `--remap-legacy`
|
|
is on (the default, for the migration period), both are translated to their
|
|
Mintlify equivalents via `_migration/slug-map.csv`. Once the generation source
|
|
is updated to emit Mintlify-native paths, run with `--no-remap-legacy`. Until
|
|
then that mode cannot produce any page (every generator's destination and links
|
|
are still Docusaurus-style), so it is rejected up front rather than failing
|
|
mid-run -- see the guard in `main`.
|
|
|
|
Modes: `--write` updates the docs tree in place; `--check` regenerates to a
|
|
scratch copy and diffs against the committed docs, failing on drift (so CI can
|
|
flag reference pages that are stale relative to the binary).
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
SQL_DIR = os.path.join(SCRIPT_DIR, "sql")
|
|
|
|
# The autogenerated region markers. Docusaurus source uses HTML comments
|
|
# (`<!--AUTOGENERATED_START-->`); migrated Mintlify pages use MDX comments
|
|
# (`{/*AUTOGENERATED_START*/}`), since MDX parses `<!-- -->` as JSX. Match either
|
|
# form and preserve whichever the page already uses.
|
|
START_RE = re.compile(r"<!--\s*AUTOGENERATED_START\s*-->|\{/\*\s*AUTOGENERATED_START\s*\*/\}")
|
|
END_RE = re.compile(r"<!--\s*AUTOGENERATED_END\s*-->|\{/\*\s*AUTOGENERATED_END\s*\*/\}")
|
|
|
|
# A settings generator:
|
|
# sql -- the .sql file(s) run in order; their INTO OUTFILE lands in scratch
|
|
# outfile -- the OUTFILE name the SQL writes
|
|
# dest -- Docusaurus destination path (the slug-map key -> Mintlify file)
|
|
# method -- how to place the content:
|
|
# "markers" -> replace between the AUTOGENERATED markers
|
|
# "body" -> keep the Mintlify page's frontmatter, replace the
|
|
# rest with the generated body
|
|
# deps -- files the SQL reads via file() relative to CWD, staged into
|
|
# scratch as {staged_name: repo-root-relative source path}
|
|
SETTINGS_GENERATORS = [
|
|
{
|
|
"name": "beta-and-experimental",
|
|
"sql": ["beta-settings.sql", "experimental-settings.sql"],
|
|
"outfile": "experimental-beta-settings.md",
|
|
"dest": "docs/about-us/beta-and-experimental-features.md",
|
|
"method": "markers",
|
|
},
|
|
{
|
|
"name": "session-settings",
|
|
"sql": ["session-settings.sql"],
|
|
"outfile": "settings.md",
|
|
"dest": "docs/operations/settings/settings.md",
|
|
"method": "body",
|
|
"deps": {"Settings.cpp": "src/Core/Settings.cpp"},
|
|
},
|
|
{
|
|
"name": "format-settings",
|
|
"sql": ["format-settings.sql"],
|
|
"outfile": "settings-formats.md",
|
|
"dest": "docs/operations/settings/settings-formats.md",
|
|
"method": "body",
|
|
"deps": {"FormatFactorySettings.h": "src/Core/FormatFactorySettings.h"},
|
|
},
|
|
{
|
|
"name": "server-settings",
|
|
"sql": ["global-server-settings.sql"],
|
|
"outfile": "server_settings.md",
|
|
"dest": "docs/operations/server-configuration-parameters/settings.md",
|
|
"method": "body",
|
|
"deps": {
|
|
"_server_settings_outside_source.md":
|
|
"docs/reference/settings/server-settings/_server_settings_outside_source.mdx",
|
|
},
|
|
},
|
|
{
|
|
"name": "mergetree-settings",
|
|
"sql": ["mergetree-settings.sql"],
|
|
"outfile": "generated_merge_tree_settings.md",
|
|
"dest": "docs/operations/settings/merge-tree-settings.md",
|
|
"method": "markers", # preserves frontmatter + hand-written intro
|
|
"fixups": True, # backtick-wrap angle-bracket text so it is valid MDX
|
|
},
|
|
]
|
|
|
|
# mergetree descriptions contain `<= 0`, `<part>/...` etc. that MDX would parse
|
|
# as JSX. Backtick-wrap them, mirroring the sed fixups in the original script.
|
|
MERGETREE_FIXUPS = [
|
|
("Limit the max number of partitions that can be accessed in one query. <= 0 means unlimited.",
|
|
"Limit the max number of partitions that can be accessed in one query. `<=` 0 means unlimited."),
|
|
("this merge is created when set min_age_to_force_merge_seconds > 0 and min_age_to_force_merge_on_partition_only = true",
|
|
"this merge is created when set `min_age_to_force_merge_seconds > 0` and `min_age_to_force_merge_on_partition_only = true`"),
|
|
("If >= 1, columns will be always written in full serialization.",
|
|
"If `>= 1`, columns will be always written in full serialization."),
|
|
("<candidate partitions for mutations only (partitions that cannot be merged)>/<candidate partitions for mutations>",
|
|
"`<candidate partitions for mutations only (partitions that cannot be merged)>/<candidate partitions for mutations>`"),
|
|
("(<part>/columns and <part>/checksums)",
|
|
"`(<part>/columns and <part>/checksums)`"),
|
|
]
|
|
|
|
|
|
def apply_fixups(content):
|
|
for old, new in MERGETREE_FIXUPS:
|
|
content = content.replace(old, new)
|
|
return content
|
|
|
|
|
|
# Regular-function docs: one page per category. generate-functions.sql takes the
|
|
# category as a param and always writes temp-functions.md; each category's output
|
|
# is inserted between the AUTOGENERATED markers of its function page. The dest
|
|
# basenames are not a mechanical lowercase of the category (e.g. Arrays ->
|
|
# array-functions, Dictionary -> ext-dict-functions, Map -> tuple-map-functions).
|
|
FUNCTION_CATEGORIES = [
|
|
("AI", "ai-functions"), ("Arithmetic", "arithmetic-functions"),
|
|
("Arrays", "array-functions"), ("Bit", "bit-functions"),
|
|
("Bitmap", "bitmap-functions"), ("Comparison", "comparison-functions"),
|
|
("Conditional", "conditional-functions"), ("Distance", "distance-functions"),
|
|
("Dates and Times", "date-time-functions"), ("Dictionary", "ext-dict-functions"),
|
|
("Encoding", "encoding-functions"), ("Financial", "financial-functions"),
|
|
("Null", "functions-for-nulls"), ("Encryption", "encryption-functions"),
|
|
("Hash", "hash-functions"), ("Introspection", "introspection"),
|
|
("IP Address", "ip-address-functions"), ("JSON", "json-functions"),
|
|
("Logical", "logical-functions"), ("Machine Learning", "machine-learning-functions"),
|
|
("Mathematical", "math-functions"), ("Natural Language Processing", "nlp-functions"),
|
|
("Random Number", "random-functions"),
|
|
("NumericIndexedVector", "numeric-indexed-vector-functions"),
|
|
("String Splitting", "splitting-merging-functions"), ("Rounding", "rounding-functions"),
|
|
("String Search", "string-search-functions"), ("Time Series", "time-series-functions"),
|
|
("Other", "other-functions"), ("Time Window", "time-window-functions"),
|
|
("String Replacement", "string-replace-functions"), ("String", "string-functions"),
|
|
("Tuple", "tuple-functions"), ("Type Conversion", "type-conversion-functions"),
|
|
("Map", "tuple-map-functions"), ("ULID", "ulid-functions"),
|
|
("URL", "url-functions"), ("UUID", "uuid-functions"),
|
|
]
|
|
|
|
FUNCTION_GENERATORS = [
|
|
{
|
|
"name": f"functions:{cat}",
|
|
"sql": ["generate-functions.sql"],
|
|
"params": {"category": cat},
|
|
"outfile": "temp-functions.md",
|
|
"dest": f"docs/sql-reference/functions/{dest}.md",
|
|
"method": "markers",
|
|
}
|
|
for cat, dest in FUNCTION_CATEGORIES
|
|
]
|
|
|
|
def aggregate_generators(docs_dir):
|
|
# Aggregate functions are driven by the existing doc pages (one per function)
|
|
# rather than a fixed list: for each page that has the AUTOGENERATED markers,
|
|
# regenerate its body via generate-aggregate-functions.sql. Pages whose name
|
|
# is not an aggregate function produce empty SQL output and are skipped.
|
|
agg_dir = os.path.join(docs_dir, "reference", "functions", "aggregate-functions")
|
|
gens = []
|
|
for name in sorted(os.listdir(agg_dir)) if os.path.isdir(agg_dir) else []:
|
|
if not name.endswith(".mdx") or name == "index.mdx":
|
|
continue
|
|
fn = name[:-len(".mdx")]
|
|
with open(os.path.join(agg_dir, name), encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
gens.append({
|
|
"name": f"aggregate:{fn}",
|
|
"sql": ["generate-aggregate-functions.sql"],
|
|
"params": {"function_name": fn},
|
|
"outfile": "temp-aggregate-function.md",
|
|
"dest": f"docs/sql-reference/aggregate-functions/reference/{fn}.md",
|
|
"method": "markers",
|
|
"skip_if_empty": True, # non-aggregate name -> empty output -> skip
|
|
})
|
|
return gens
|
|
|
|
|
|
# Pages whose basename does not normalise to the engine name (the SQL matches
|
|
# case/separator-insensitively, but cannot bridge a genuinely different word).
|
|
TABLE_ENGINE_PAGE_ALIASES = {"generate": "GenerateRandom"}
|
|
|
|
|
|
def table_engine_generators(docs_dir, file_map):
|
|
# One page per table engine, discovered from the migrated docs tree the
|
|
# same way as aggregate functions: every engine reference page carrying the
|
|
# AUTOGENERATED markers is regenerated from system.table_engines, whose
|
|
# `description` column holds the full Markdown body of that page. Concept
|
|
# pages living under the same tree (replication, annindexes, ...) match no
|
|
# engine, produce empty output, and are skipped. Discovery relies on the
|
|
# slug-map (file_map), so this runs only with --remap-legacy (the default).
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "/engines/table-engines/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"table-engine:{basename}",
|
|
"sql": ["generate-table-engines.sql"],
|
|
"params": {"engine": TABLE_ENGINE_PAGE_ALIASES.get(basename, basename)},
|
|
"outfile": "temp-table-engine.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True, # concept page -> no matching engine -> skip
|
|
"full_transform": True, # description is a whole Docusaurus page body
|
|
})
|
|
return gens
|
|
|
|
|
|
DATABASE_ENGINE_PAGE_ALIASES = {"datalake": "DataLakeCatalog"}
|
|
|
|
|
|
def database_engine_generators(docs_dir, file_map):
|
|
# One page per database engine, discovered like table engines from the
|
|
# migrated docs tree: every database-engine reference page carrying the
|
|
# AUTOGENERATED markers is regenerated from system.database_engines.
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "/engines/database-engines/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"database-engine:{basename}",
|
|
"sql": ["generate-database-engines.sql"],
|
|
"params": {"engine": DATABASE_ENGINE_PAGE_ALIASES.get(basename, basename)},
|
|
"outfile": "temp-database-engine.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True,
|
|
"full_transform": True,
|
|
})
|
|
return gens
|
|
|
|
|
|
DATA_TYPE_PAGE_ALIASES = {
|
|
"boolean": "Bool", "newjson": "JSON",
|
|
# Multi-family pages: one family's Documentation is the whole page body.
|
|
"float": "Float64", "int-uint": "Int32", "geo": "Point",
|
|
}
|
|
|
|
|
|
def data_type_generators(docs_dir, file_map):
|
|
# One page per data-type family, discovered like engines from the migrated
|
|
# docs tree: every data-type reference page carrying the AUTOGENERATED
|
|
# markers is regenerated from system.data_type_families. Pages that map to
|
|
# no single family (index, binary-encoding, or multi-type pages like
|
|
# int-uint) match nothing / are simply not given markers, and are skipped.
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "/data-types/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"data-type:{basename}",
|
|
"sql": ["generate-data-types.sql"],
|
|
"params": {"type": DATA_TYPE_PAGE_ALIASES.get(basename, basename)},
|
|
"outfile": "temp-data-type.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True,
|
|
"full_transform": True,
|
|
})
|
|
return gens
|
|
|
|
|
|
def format_generators(docs_dir, file_map):
|
|
# One page per format, discovered like engines from the migrated docs tree:
|
|
# every format reference page carrying the AUTOGENERATED markers is
|
|
# regenerated from system.formats. The index page matches no format and is
|
|
# skipped.
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "reference/formats/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"format:{basename}",
|
|
"sql": ["generate-formats.sql"],
|
|
"params": {"format": basename},
|
|
"outfile": "temp-format.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True,
|
|
"full_transform": True,
|
|
})
|
|
return gens
|
|
|
|
|
|
# Page basenames that are not a case/separator-insensitive match of the table
|
|
# function name (the generate() page documents the generateRandom function).
|
|
TABLE_FUNCTION_PAGE_ALIASES = {"generate": "generateRandom"}
|
|
|
|
|
|
def table_function_generators(docs_dir, file_map):
|
|
# One page per table function, discovered like engines from the migrated
|
|
# docs tree: every table-function reference page carrying the AUTOGENERATED
|
|
# markers is regenerated from system.table_functions.
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "/functions/table-functions/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"table-function:{basename}",
|
|
"sql": ["generate-table-functions.sql"],
|
|
"params": {"tf": TABLE_FUNCTION_PAGE_ALIASES.get(basename, basename)},
|
|
"outfile": "temp-table-function.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True,
|
|
"full_transform": True,
|
|
})
|
|
return gens
|
|
|
|
|
|
def window_function_generators(docs_dir, file_map):
|
|
# One page per window function, discovered like the others from the migrated
|
|
# docs tree: every window-function reference page carrying the AUTOGENERATED
|
|
# markers is regenerated from system.functions (window functions live in the
|
|
# aggregate-function factory). Pages whose function has no description (e.g.
|
|
# first_value/last_value, which are aliases of any/anyLast) match nothing and
|
|
# are skipped, staying hand-written.
|
|
gens = []
|
|
for docu, mint in sorted(file_map.items()):
|
|
if "/functions/window-functions/" not in mint or not mint.endswith(".mdx"):
|
|
continue
|
|
page = os.path.join(docs_dir, mint)
|
|
if not os.path.isfile(page):
|
|
continue
|
|
with open(page, encoding="utf-8") as f:
|
|
if not START_RE.search(f.read()):
|
|
continue
|
|
basename = os.path.basename(mint)[: -len(".mdx")]
|
|
gens.append({
|
|
"name": f"window-function:{basename}",
|
|
"sql": ["generate-window-functions.sql"],
|
|
"params": {"fn": basename},
|
|
"outfile": "temp-window-function.md",
|
|
"dest": docu,
|
|
"method": "markers",
|
|
"skip_if_empty": True,
|
|
"full_transform": True,
|
|
})
|
|
return gens
|
|
|
|
|
|
ALL_GENERATORS = SETTINGS_GENERATORS + FUNCTION_GENERATORS
|
|
|
|
|
|
FRONTMATTER_RE = re.compile(r"\A---\n.*?\n---\n?", re.DOTALL)
|
|
|
|
|
|
def import_migrate(docs_dir):
|
|
# The Docusaurus->Mintlify body transforms (imports @theme->/snippets,
|
|
# HTML->MDX comments, link rewriting) are reused from the migration script
|
|
# so generated pages match migrated ones exactly -- a single source of truth.
|
|
sys.path.insert(0, os.path.join(docs_dir, "_migration"))
|
|
import migrate # noqa: E402
|
|
return migrate
|
|
|
|
|
|
def build_file_map(migrate, slug_map):
|
|
# docusaurus_file -> mintlify_file, plus migrate's link/import Lookups.
|
|
lk, rows = migrate.build_lookups(Path(slug_map))
|
|
file_map = {}
|
|
for r in rows:
|
|
mfile = (r.get("mintlify_file") or "").split(" | ")[0].strip()
|
|
if r.get("status") == "matched" and mfile:
|
|
file_map[(r.get("docusaurus_file") or "").strip()] = mfile
|
|
return lk, file_map
|
|
|
|
|
|
def transform_body(migrate, content, src_docu, dest_path, lk):
|
|
# Generated fragments contain the same Docusaurus-flavoured Markdown as
|
|
# whole-page descriptions, so run the complete Mintlify body pipeline.
|
|
return migrate.transform_body_content(
|
|
content,
|
|
title=None,
|
|
lk=lk,
|
|
issues=[],
|
|
source_docu_path=Path(src_docu),
|
|
dest_path=Path(dest_path),
|
|
source_docu_file=src_docu,
|
|
row={"docusaurus_file": src_docu},
|
|
)
|
|
|
|
|
|
TITLE_RE = re.compile(r"^title:\s*(.+?)\s*$", re.MULTILINE)
|
|
|
|
|
|
def frontmatter_title(page_text):
|
|
# The generated body carries no frontmatter -- Mintlify renders the title
|
|
# from the dest page's frontmatter -- so read it there to feed
|
|
# drop_redundant_h1 (which strips a duplicate leading H1 from the body).
|
|
m = FRONTMATTER_RE.match(page_text)
|
|
if not m:
|
|
return None
|
|
t = TITLE_RE.search(m.group(0))
|
|
return t.group(1).strip().strip("'\"") if t else None
|
|
|
|
|
|
def transform_full_body(migrate, content, src_docu, dest_path, lk, title):
|
|
# Whole-page bodies (e.g. a table engine's `Documentation` string, which is
|
|
# the full Markdown body of its reference page) need the complete migrate
|
|
# body pipeline -- drop the redundant H1, convert admonitions/details/tabs,
|
|
# rewrite links and imports -- not just transform_body's narrow subset.
|
|
# Reuse migrate_file's shared entry point so generated pages match migrated
|
|
# ones exactly, from a single source of truth.
|
|
return migrate.transform_body_content(
|
|
content,
|
|
title=title,
|
|
lk=lk,
|
|
issues=[],
|
|
source_docu_path=Path(src_docu),
|
|
dest_path=Path(dest_path),
|
|
source_docu_file=src_docu,
|
|
row={"docusaurus_file": src_docu},
|
|
)
|
|
|
|
|
|
def run_sql(binary, sql_path, cwd, params=None):
|
|
# Run the generator SQL; it writes its INTO OUTFILE markdown into `cwd`.
|
|
cmd = [binary, "local", "--queries-file", sql_path]
|
|
for k, v in (params or {}).items():
|
|
cmd.append(f"--param_{k}={v}")
|
|
subprocess.run(cmd, cwd=cwd, check=True)
|
|
|
|
|
|
def replace_between_markers(page, content):
|
|
start = START_RE.search(page)
|
|
end = END_RE.search(page, start.end()) if start else None
|
|
if not start or not end:
|
|
raise ValueError("destination is missing the AUTOGENERATED markers")
|
|
# Keep the marker lines themselves; replace only the region between them.
|
|
return page[:start.end()] + "\n" + content.strip("\n") + "\n" + page[end.start():]
|
|
|
|
|
|
def generate(gen, binary, docs_dir, repo_root, migrate, lk, file_map, remap):
|
|
scratch = tempfile.mkdtemp(prefix="autogen-")
|
|
# Stage the files the SQL reads via file() (C++ sources, etc.) into scratch.
|
|
for staged, src in gen.get("deps", {}).items():
|
|
shutil.copyfile(os.path.join(repo_root, src), os.path.join(scratch, staged))
|
|
for sql in gen["sql"]:
|
|
run_sql(binary, os.path.join(SQL_DIR, sql), scratch, gen.get("params"))
|
|
with open(os.path.join(scratch, gen["outfile"]), encoding="utf-8") as f:
|
|
content = f.read()
|
|
if gen.get("skip_if_empty") and not content.strip():
|
|
return None, None # e.g. a page whose name is not an aggregate function
|
|
|
|
dest_rel = file_map.get(gen["dest"]) if remap else re.sub(r"^docs/", "", gen["dest"])
|
|
if not dest_rel:
|
|
raise ValueError(f"no Mintlify destination for {gen['dest']}")
|
|
dest = os.path.join(docs_dir, dest_rel)
|
|
|
|
if gen.get("fixups"):
|
|
content = apply_fixups(content)
|
|
if remap:
|
|
if gen.get("full_transform"):
|
|
with open(dest, encoding="utf-8") as f:
|
|
title = frontmatter_title(f.read())
|
|
content = transform_full_body(migrate, content, gen["dest"], dest, lk, title)
|
|
else:
|
|
content = transform_body(migrate, content, gen["dest"], dest, lk)
|
|
|
|
if gen["method"] == "markers":
|
|
with open(dest, encoding="utf-8") as f:
|
|
new = replace_between_markers(f.read(), content)
|
|
elif gen["method"] == "body":
|
|
# Keep the Mintlify page's frontmatter; replace the rest with the
|
|
# generated body (its own Docusaurus frontmatter stripped).
|
|
with open(dest, encoding="utf-8") as f:
|
|
fm = FRONTMATTER_RE.match(f.read())
|
|
body = FRONTMATTER_RE.sub("", content, count=1).lstrip("\n")
|
|
new = (fm.group(0).rstrip("\n") + "\n\n" + body) if fm else body
|
|
else: # overwrite
|
|
new = content
|
|
return dest, new
|
|
|
|
|
|
def main(argv=None):
|
|
p = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
p.add_argument("--binary", default="build/programs/clickhouse")
|
|
p.add_argument("--docs-dir", default="docs")
|
|
p.add_argument("--slug-map",
|
|
help="default: <docs-dir>/_migration/slug-map.csv")
|
|
mode = p.add_mutually_exclusive_group()
|
|
mode.add_argument("--write", action="store_true", help="update docs in place")
|
|
mode.add_argument("--check", action="store_true",
|
|
help="fail if regenerated output differs from committed docs")
|
|
p.add_argument("--no-remap-legacy", dest="remap", action="store_false",
|
|
help="emit paths/links as the SQL produces them (Mintlify-native)")
|
|
p.add_argument("--only", help="only run generators whose name contains this substring")
|
|
args = p.parse_args(argv)
|
|
|
|
binary = os.path.abspath(args.binary)
|
|
docs_dir = os.path.abspath(args.docs_dir)
|
|
repo_root = os.path.dirname(docs_dir)
|
|
slug_map = args.slug_map or os.path.join(docs_dir, "_migration", "slug-map.csv")
|
|
|
|
# The component-reference families (table/database engines, data types, formats,
|
|
# table/window functions) discover their pages by iterating the slug map
|
|
# (file_map), so build it even for a --no-remap-legacy run: those generators'
|
|
# names are what the fast-fail check needs to tell whether the current selection
|
|
# targets a family such a run cannot produce. The link/import remapping itself
|
|
# only runs under --remap-legacy, so drop `migrate`/`lk` afterwards in that mode.
|
|
migrate = import_migrate(docs_dir)
|
|
lk, file_map = build_file_map(migrate, slug_map)
|
|
if not args.remap:
|
|
migrate = lk = None
|
|
|
|
remap_only_families = [
|
|
table_engine_generators,
|
|
database_engine_generators,
|
|
data_type_generators,
|
|
format_generators,
|
|
table_function_generators,
|
|
window_function_generators,
|
|
]
|
|
|
|
all_generators = ALL_GENERATORS + aggregate_generators(docs_dir)
|
|
legacy_generators = []
|
|
for builder in remap_only_families:
|
|
legacy_generators += builder(docs_dir, file_map)
|
|
|
|
# `--only` is a plain substring match on generator names; apply exactly that in
|
|
# both the fast-fail check and the final selection so the two can never disagree
|
|
# (a substring that targets a family, e.g. `--only newjson`, must be caught).
|
|
def selected(gens):
|
|
return [g for g in gens if not args.only or args.only in g["name"]]
|
|
|
|
if args.remap:
|
|
all_generators += legacy_generators
|
|
else:
|
|
# --no-remap-legacy is a forward-looking mode for after the generation
|
|
# source is updated to emit Mintlify-native paths (see the module
|
|
# docstring). Today no family can be produced in it:
|
|
# * the component-reference families (table/database engines, data
|
|
# types, formats, table/window functions) are discovered through the
|
|
# slug map and need its link/path remapping to even be enumerated;
|
|
# * settings, functions and aggregate carry hard-coded Docusaurus
|
|
# `dest` paths (e.g. docs/operations/settings/settings.md,
|
|
# docs/sql-reference/aggregate-functions/reference/<fn>.md) whose
|
|
# Mintlify page lives elsewhere, so the naive `re.sub(r"^docs/", "")`
|
|
# in generate() resolves them to files that do not exist in the
|
|
# Mintlify tree -- open(dest) would fail with FileNotFoundError once
|
|
# the SQL has run.
|
|
# So fail fast for the whole selection (a full run, or an --only that
|
|
# matches any generator) instead of dying mid-run, or -- for an --only
|
|
# that matches nothing -- silently generating nothing.
|
|
blocked = selected(all_generators) + selected(legacy_generators)
|
|
if blocked:
|
|
families = sorted({g["name"].split(":", 1)[0] for g in blocked})
|
|
raise SystemExit(
|
|
"error: --no-remap-legacy cannot generate the "
|
|
f"{', '.join(families)} pages -- the generation source still "
|
|
"emits Docusaurus-style paths and links, which only "
|
|
"--remap-legacy (the default) translates to their Mintlify "
|
|
"equivalents via the slug map (_migration/slug-map.csv). Re-run "
|
|
"with --remap-legacy until the generation source is updated to "
|
|
"emit Mintlify-native paths.")
|
|
|
|
generators = selected(all_generators)
|
|
# A selector that matches nothing is a mistake (a typo, or a family unavailable
|
|
# in this mode); never report success while doing nothing.
|
|
if args.only and not generators:
|
|
raise SystemExit(f"error: --only '{args.only}' matched no generator; nothing to do.")
|
|
|
|
drift = 0
|
|
for gen in generators:
|
|
dest, new = generate(gen, binary, docs_dir, repo_root, migrate, lk, file_map, args.remap)
|
|
if new is None: # skipped (e.g. non-aggregate page)
|
|
continue
|
|
rel = os.path.relpath(dest, docs_dir)
|
|
if args.check:
|
|
current = open(dest, encoding="utf-8").read() if os.path.isfile(dest) else ""
|
|
if current != new:
|
|
print(f"DRIFT: {rel} is stale")
|
|
drift += 1
|
|
else:
|
|
print(f"ok: {rel}")
|
|
elif args.write:
|
|
with open(dest, "w", encoding="utf-8") as f:
|
|
f.write(new)
|
|
print(f"wrote: {rel}")
|
|
else:
|
|
print(f"[dry-run] would update: {rel}")
|
|
return 1 if drift else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|