Skip to main content

horizondb-idiosyncrasies

Work with Azure HorizonDB (preview) — its AI/embedding features, extension allow-listing, AGE/pgvector/pg_durable quirks, and the connection/TLS gotchas discovered while bringing up the horizon-facts incubator. Use when connecting to a HorizonDB cluster, enabling extensions, writing AGE Cypher, wiring in-DB embeddings via pg_durable df.http(), or debugging "extension not allow-listed" / "access to library not allowed" / "self-signed certificate" / Cypher syntax errors.

Quellinformationen

Repository
microsoft/PilotSwarm
Letzte Quellaktivität
4. November 2026 um 05:45
Erkannte Sprache von SKILL.md
Englisch
Sterne
14
Forks
7

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
horizondb-idiosyncrasies
description
Work with Azure HorizonDB (preview) — its AI/embedding features, extension allow-listing, AGE/pgvector/pg_durable quirks, and the connection/TLS gotchas discovered while bringing up the horizon-facts incubator. Use when connecting to a HorizonDB cluster, enabling extensions, writing AGE Cypher, wiring in-DB embeddings via pg_durable df.http(), or debugging "extension not allow-listed" / "access to library not allowed" / "self-signed certificate" / Cypher syntax errors.
# HorizonDB Idiosyncrasies Azure HorizonDB is a managed, preview Postgres-17 service (a.k.a. OrionDB internally). It bundles AI/graph/vector/durable-execution extensions but locks them behind an allow-list and several managed-PG restrictions that differ from vanilla Postgres. This skill captures the verified facts and the workarounds. > Reference implementation: `incubator/horizon-facts/` — a HorizonDB-only > enhanced facts interface. Its integration suite (`test/integration/`) runs > live against a HorizonDB cluster and exercises every capability below. ## Quick reference | Symptom | Cause | Fix | | --- | --- | --- | | `self-signed certificate in certificate chain` | pg v8 treats `sslmode=require` as `verify-full` | append `uselibpqcompat=true` to the connection string | | `extension "X" is not allow-listed for "azure_pg_admin" users` | empty/insufficient `azure.extensions` on the parameter group | add X to `azure.extensions` on a custom parameter group, attach it to the cluster's pool | | `access to library "age" is not allowed` | `age` is in `shared_preload_libraries`; explicit `LOAD 'age'` is forbidden | tolerate that error — the library is already preloaded | | `syntax error at or near "WHERE"` from `cypher_parser.c` | AGE doesn't support `any(x IN list WHERE pred)` | use `size([x IN list WHERE pred]) > 0` | | `schema "..." already exists` creating an AGE graph | graph name collides with an existing schema (`create_graph` makes a schema) | give the graph a name distinct from every other schema | | `relation "<label>" already exists` (`42P07`) **or** `duplicate key … "pg_class_relname_nsp_index"` (`23505`) under concurrent graph writes | AGE creates a label's backing table lazily on first `CREATE`/`MERGE`; concurrent first-references race the DDL (two detection points) | re-run the same idempotent statement — the label now exists (see §3 concurrency) | | `cannot cast type agtype to jsonb` | `agtype` has no jsonb cast | `RETURN` scalars/`label()`/`type()`, parse client-side | | `ECONNRESET` / `Connection terminated unexpectedly` mid-workload | HorizonDB resets idle pooled TLS connections | guard + bounded retry on a **fresh** connection (§1.1) | | AGE writes are 600 ms–2 s each under concurrency | small connection pool → ops queue (connection-queue wait, not query cost) | raise `poolMax` for the parallelism (default now 16, `HORIZON_POOL_MAX`); per-connection AGE setup; fewer round-trips per upsert (§3 performance) | ## 1. Connection & TLS HorizonDB requires SSL. The cluster's certificate chain is **not** in Node's default trust store, and modern `pg` (v8+) / `pg-connection-string` treat `sslmode=require` as `verify-full`, so a plain `?sslmode=require` URL fails with: ``` CONN FAIL: self-signed certificate in certificate chain ``` **Fix:** append `uselibpqcompat=true`, which restores libpq semantics where `require` means *encrypt but don't verify the CA*: ``` postgresql://USER:PW@<cluster>.<id>.<region>.horizondb.azure.com:5432/postgres?sslmode=require&uselibpqcompat=true ``` For production trust verification, install the Azure CA bundle and use `sslmode=verify-full` with an explicit `ca`. For dev/test against preview clusters, `uselibpqcompat=true` is the pragmatic path. The horizon-facts test harness normalizes this automatically — see `normalizeDbUrl()` in `incubator/horizon-facts/test/integration/_db.mjs`. ### 1.1 Idle pooled connections get reset — guard + retry on a fresh connection HorizonDB (preview) intermittently drops **idle pooled TLS connections**. Under a sustained workload this surfaces two ways: - an **uncaught** `error` event on a checked-out client (`ECONNRESET`) — this crashes a long-running Node process unless you attach a pool/client `error` handler; - a **rejected** query (`ENOTCONN` / `EPIPE` / `Connection terminated unexpectedly`) from `pool.query`. Neither partially commits, so re-running on a **fresh** connection is safe. Pattern (see `incubator/horizon-facts/src/db-retry.ts`): - `pool.on("error", …)` and a process-level guard that swallow **only** this transient socket class; - a bounded retry (`withDbRetry`) that re-acquires a connection and re-runs. **Keep two retry classifiers distinct** — they target different recovery actions: | Classifier | Matches | Retry action | | --- | --- | --- | | `isTransientDbError` | `ECONNRESET`/`ENOTCONN`/`EPIPE`/`Connection terminated` + pg `08xxx`/`57P0x` | re-run on a **fresh** connection | | `isLabelCreationRaceError` | `42P07` **or** `23505` on a `pg_catalog` index (`pg_class_*`/`pg_type_*`/`pg_namespace_*`) | re-run the **same** statement (AGE label race, §3) | Do **not** lump the label race into the connection regex, and do **not** retry a bare `23505` — a unique violation on a *user* constraint is a real error; only the **system-catalog** index variant is the label race. ## 2. Extension allow-list (the #1 blocker) HorizonDB rejects `CREATE EXTENSION` unless the extension name is in the `azure.extensions` server parameter, even for the admin user: ``` ERROR: extension "vector" is not allow-listed for "azure_pg_admin" users in Azure Database for PostgreSQL (SQLSTATE 0A000) ``` The **default parameter group** (`default_pg17`) ships with `azure.extensions` **empty** — only `plpgsql` is installed. The extensions exist on the image (check `pg_available_extensions`), they're just not allowed yet. ### Available AI/graph extensions (PG17 image, verified 2026-06) | Extension | Version | Purpose | | --- | --- | --- | | `vector` (pgvector) | 0.8.0 | embedding storage + HNSW ANN | | `age` (Apache AGE) | 1.6.0 | property graph + Cypher | | `pg_durable` | 0.2.1 | durable execution + `df.http()` for in-DB HTTP | | `pg_textsearch` | 1.3.0-dev | ranked lexical search | | `azure_ai` | 2.2.1 | native in-DB model inference (Azure-managed) | | `pg_diskann` | 0.7.1 | disk-based ANN index (azure.extensions ONLY — NOT preloadable) | ### Enabling extensions You **cannot** set `azure.extensions` per-session. You must create a custom **parameter group** (settings group), set the allow-list (and `shared_preload_libraries` for preload-required extensions), and attach it to the cluster's **pool**. Attaching restarts the server once (about 10 s down) when `shared_preload_libraries` changes. **Preload requirements:** `age` and `pg_durable` must be in `shared_preload_libraries`. `vector` and `pg_textsearch` only need to be in `azure.extensions`. `pg_diskann` is `azure.extensions`-ONLY — it is **rejected** in `shared_preload_libraries` (HorizonDB's allowed preload set is fixed and does not include it), yet works without preloading. #### API 2026-10-01-preview: pools own compute and the settings group The deploy module (`deploy/providers/azure/services/horizondb/bicep/main.bicep` plus `ensureHorizonDbParameterGroup` in `deploy/scripts/lib/horizondb.mjs`) uses API `2026-10-01-preview`. Its rules, each found by a failed deploy: | Rule | What happens otherwise | | --- | --- | | Compute (`computeModel`, `nodeCount`) and `parameterGroup` live on `clusters/{c}/pools/{pool}`, not on the cluster | the cluster PUT ignores them | | A new pool accepts only the built-in group `/subscriptions/{s}/providers/Microsoft.HorizonDb/locations/{region}/parameterGroups/default_pg17`; attach the custom group with a pool PATCH afterwards | pool create is rejected | | `clusters/{c}/authentications/passwordMethod` needs an existing pool | `Pool 'DefaultPool' was not found` | | `administratorLogin` can be sent once; later PUTs send only the password (a reset) | `administratorLogin cannot be specified because an administrator is already configured` | | PUT the settings group before the cluster, not in parallel: a PUT to a group that a pool uses keeps the cluster `Updating` for about a minute | cluster PUT fails with `ClusterNotReady` | | Read the host name from `clusters/{c}/endpoints/{c}` (role ReadWrite). New clusters use `{c}.rw.{id}.{region}.horizondb.azure.com` | the old API's `fullyQualifiedDomainName` does not resolve for new clusters | | One pool request may change only one property group (`nodeCount` + `highAvailability`, or `parameterGroup`). Pools of clusters migrated from the old API can show no `highAvailability` and no group, so a full pool PUT changes two groups. The deploy module PUTs the pool only to create it | `The request changes multiple Pool property groups ... Submit a separate request for each property group` | | Settings groups can be updated (PUT/PATCH); connected pools pick up the change | (API 2026-01-20-preview rejected updates with `ParameterGroupUpdateNotSupported`) | **Attach timing.** A pool PATCH takes about two minutes. During that time the pool GET still shows the **previous** group as `Succeeded` / `InSync`; the server restarts about a minute in, and the GET shows the new group only when the operation ends. Send **one** PATCH, then wait until the GET shows the new group with `syncStatus: InSync` (or follow the `Azure-AsyncOperation` URL). **The first PATCH soon after a pool is created can be dropped:** it ends `Succeeded`, no restart happens, and the pool stays on the built-in group. Seen twice, each time within a minute of the pool's create and login writes. A second PATCH then attached normally. `ensureHorizonDbParameterGroup` resends the PATCH when the GET still shows another group four minutes later (at most three PATCHes). `syncStatus` values: `Applying`, `InSync`, `PendingReboot` (static settings wait for `POST .../pools/{pool}/restart`), `FailedToApply`. With `applyMethod: Immediate` the service applied `shared_preload_libraries` without a manual restart in testing; the deploy still handles `PendingReboot`. #### Via ARM (az rest) ```bash SUB=<subscription-id>; RG=<resource-group>; API=2026-10-01-preview CLUSTER=<cluster-name>; PG=<cluster>-ext; REGION=<region> # 1. Create or update the settings group. Only the listed values are stored # as overrides (valueSource UserOverride); the rest are service defaults. cat > /tmp/pg-body.json <<EOF { "location": "$REGION", "properties": { "description": "Enable AI/graph extensions", "postgreSqlVersion": "17", "parameters": [ { "name": "azure.extensions", "value": "age,azure_ai,pg_diskann,pg_durable,pg_textsearch,vector" }, { "name": "shared_preload_libraries", "value": "age,pg_durable,pg_textsearch" } ] } } EOF az rest --method put \ --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.HorizonDb/parameterGroups/$PG?api-version=$API" \ --headers "Content-Type=application/json" --body @/tmp/pg-body.json # 2. Attach it to the pool (one restart). Then poll the pool GET until it # shows this group with syncStatus InSync (about two minutes). az rest --method patch \ --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.HorizonDb/clusters/$CLUSTER/pools/DefaultPool?api-version=$API" \ --headers "Content-Type=application/json" \ --body "{\"properties\":{\"parameterGroup\":{\"parameterGroupResourceId\":\"/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.HorizonDb/parameterGroups/$PG\",\"applyMethod\":\"Immediate\"}}}" ``` Notes: - The platform **re-injects its own system preload libs** (`azure`, `orion_storage`, `pg_availability`, `pg_qs`, `pgms_stats`, `pgms_wait_sampling`, and on newer builds `pgaadauth`, `horizondb_ext`) on top of yours — you only list the extras. - List the built-in groups with `GET /subscriptions/{s}/providers/Microsoft.HorizonDb/locations/{region}/parameterGroups?api-version=2026-10-01-preview`. - Extension versions depend on the server build. Clusters created at different times can run different builds (for example `age` 1.7.0 on one and 1.8.0 on a newer one); check `pg_available_extensions` on the cluster you test. ### Verify after restart ```js await pool.query(`show shared_preload_libraries`); // includes age,pg_durable,pg_textsearch await pool.query(`CREATE EXTENSION IF NOT EXISTS vector`); await pool.query(`CREATE EXTENSION IF NOT EXISTS age`); await pool.query(`CREATE EXTENSION IF NOT EXISTS pg_durable`); // df.http() should now exist: await pool.query(`select proname from pg_proc where proname='http' and pronamespace::regnamespace::text='df'`); ``` ## 3. Apache AGE quirks ### Don't `LOAD 'age'` when it's preloaded On vanilla Postgres you must `LOAD 'age'` per session. On HorizonDB `age` is in `shared_preload_libraries`, so it's already loaded and an explicit `LOAD` is **rejected**: ``` ERROR: access to library "age" is not allowed ``` Make the load tolerant — attempt it (for portability to vanilla PG) but swallow that one error: ```ts async function loadAge(client: any): Promise<void> { try { await client.query(`LOAD 'age'`); } catch (err: any) { if (!/access to library "age" is not allowed/i.test(String(err?.message ?? ""))) throw err; // already preloaded via shared_preload_libraries — nothing to do } } ``` Still run `SET search_path = ag_catalog, "$user", public` on every AGE session. ### Graph name must differ from every schema name `SELECT create_graph('g')` creates a **Postgres schema** named `g`. If you also have a facts/data schema named `g`, you get `schema "g" already exists`. Always use distinct names (e.g. data schema `hf_test_<r>`, graph `hf_graph_<r>`). ### Cypher: no `any(... WHERE ...)` AGE 1.6.0's Cypher parser rejects the list predicate `any(x IN list WHERE pred)` with `syntax error at or near "WHERE"` (`cypher_parser.c`). Use the list-comprehension + `size()` form instead: ```cypher -- BROKEN on AGE: WHERE any(a IN e.aliases WHERE toLower(a) CONTAINS 'tgl') -- WORKS: WHERE size([a IN e.aliases WHERE toLower(a) CONTAINS 'tgl']) > 0 ``` `UNWIND ... WITH x WHERE ...` also works. When in doubt, probe syntax against a throwaway graph before committing to a query shape.
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen