Skip to content

feat: External Datasource Federation (ADR-0015) — backend (P1–P4, P6) - #1390

Merged
os-zhuang merged 14 commits into
mainfrom
claude/external-data-source-impl-UzpGG
May 30, 2026
Merged

feat: External Datasource Federation (ADR-0015) — backend (P1–P4, P6)#1390
os-zhuang merged 14 commits into
mainfrom
claude/external-data-source-impl-UzpGG

Conversation

@os-zhuang

@os-zhuangos-zhuang commented May 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the backend of ADR-0015 — External Datasource Federation: connect a mature external database (Postgres, Snowflake, …), register its tables as ObjectStack objects without ever touching the remote schema, and let the whole stack (ObjectQL, REST, Views, AI) use them — enforced by three runtime gates.

A federated object stays a normal Object; its remote-ness is the datasource (schemaMode !== 'managed') + an object.external binding. All changes are additive and backward-compatible (schemaMode defaults to 'managed').

What's included

P1 — Spec + DDL gate (Gate 1) · @objectstack/spec, @objectstack/driver-sql

  • Datasource.schemaMode + Datasource.external; Object.external binding; cross-field superRefine.
  • shared/external-errors.ts: ExternalSchemaMismatchError / ExternalWriteForbiddenError / ExternalSchemaModeViolationError + SchemaDiffEntry.
  • driver-sql: assertSchemaMutable() blocks DDL (initObjects/syncSchema/dropTable) when not managed.

P2 — Service + type matrix + REST + CLI · @objectstack/spec, @objectstack/service-external-datasource (new), @objectstack/rest, @objectstack/cli, @objectstack/objectql

  • data/type-compat.ts: dialect-aware SQL↔field matrix (postgres/mysql/sqlite/snowflake/bigquery/mongo).
  • IExternalDatasourceService + ExternalDatasourceService: listRemoteTables / generateObjectDraft (reviewable *.object.ts) / validateObject / validateAll / refreshCatalog.
  • REST registerExternalDatasourceRoutes/api/v1/datasources/:name/external/* (tables, draft, refresh-catalog, validate), wired into the REST API plugin; 503 external_service_unavailable when the service is absent.
  • CLI os datasource list-tables | introspect | validate.
  • engine.introspectDatasource(name) end-to-end introspection.

P3 — Boot validation (Gate 2) + catalog type · @objectstack/runtime, @objectstack/spec

  • ExternalValidationPlugin (exported): on kernel:ready, enforces external.validation.onMismatch (fail/warn/ignore).
  • external_catalog metadata type + ExternalCatalogSchema.

P4 — AI awareness · @objectstack/service-ai

  • SchemaRetriever badges federated objects [external, read-only, datasource=…]; query_data already clamps limits.

P6 — Write gate (Gate 3) · @objectstack/objectql

  • assertWriteAllowed() blocks insert/update/delete on federated datasources unless datasource.external.allowWritesandobject.external.writable.

Out of scope

  • P5 (Studio UI) lives in the separate ../objectui repo (not in this environment).
  • P7 (extra driver adapters) is non-blocking extension work.
  • Boot wiring: ExternalDatasourceServicePlugin (service) + ExternalValidationPlugin (Gate 2) are exported for the host to .use(); not force-registered in runtime (consistent with other opt-in services). REST routes mount unconditionally and degrade gracefully; the CLI talks to them over HTTP.

Verification (per-package, local)

spec federation 151, driver-sql DDL gate 8, objectql write-gate/introspect 11, service-external-datasource 15, runtime boot-gate 7, service-ai 3195 tests, all green. Affected builds (incl. DTS) pass for spec, driver-sql, objectql, runtime, rest, cli, service-ai, service-external-datasource. Changeset fixed-group in sync (62 packages).

Known pre-existing CI failures (NOT from this PR)

Surfaced only because the new package changed the lockfile (path-filtered workflows that rarely run on main); each reproduced on a clean main checkout:

  • Test Coreobjectql/protocol-meta.test.ts provenance (_packageId/_provenance).
  • Build Docs — App-schema `{<id>}` breaks the new Turbopack/fumadocs MDX parser.
  • Validate Depspnpm audit high on tar via the sqlite3peer dep.

CodeQL findings in this PR's own code (ReDoS in type-compat; network-data-to-file in os datasource introspect --out, which now sandboxes --out to the CWD) are addressed.

Refs ADR-0015.

https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd

…, object.external, DDL gate
Implements Phase 1 of ADR-0015 (External Datasource Federation): the spec
foundation plus the driver-layer DDL gate, so a datasource can be marked as
a guest in a mature external database that ObjectStack must never run DDL
against. All changes are additive and backward-compatible (schemaMode
defaults to 'managed').
Spec:
- Datasource.schemaMode ('managed' | 'external' | 'validate-only') and
Datasource.external settings, with a superRefine enforcing the
cross-field invariant (external settings <-> non-managed mode).
- Object.external binding (remoteName/remoteSchema/writable/columnMap/
introspectedAt/ignoreColumns).
- shared/external-errors.ts: ExternalSchemaMismatchError /
ExternalWriteForbiddenError / ExternalSchemaModeViolationError with stable
`code`s, plus SchemaDiffEntry and a pure renderDiffMessage().
driver-sql (Gate 1 — DDL gate, ADR section 5.1):
- SqlDriverConfig gains an optional schemaMode (stripped before Knex).
- assertSchemaMutable() choke-point throws ExternalSchemaModeViolationError
when schemaMode != 'managed'; wired into initObjects (covers syncSchema)
and dropTable.
Tests: Zod refinements (datasource modes/settings, object binding), error
classes + diff rendering, and the DDL gate (managed allows DDL;
external/validate-only block create/alter/drop; schemaMode not leaked to
Knex). Adds the /shared subpath alias to driver-sql's vitest config.
Docs: docs/plans/external-datasource-federation-impl.md (phased plan +
progress tracker) and a changeset.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
@vercel

vercelBot commented May 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specErrorErrorMay 30, 2026 12:37pm

Request Review

…n P2 — type-compat matrix, service contract & impl
Implements the service core of Phase 2 of ADR-0015: the SQL↔field type
compatibility matrix, the IExternalDatasourceService contract, and a new
service package that introspects, drafts, and validates federated objects.
Spec:
- data/type-compat.ts: pure, dialect-aware matrix (canonicalizeSqlType,
suggestFieldType, isCompatible -> true | 'lossy' | false) for postgres,
mysql, sqlite, snowflake, bigquery, mongo. Independently unit-tested.
- contracts/external-datasource-service.ts: IExternalDatasourceService plus
RemoteTable, GenerateDraftOpts, ObjectDraft, SchemaValidationResult/Report.
Reuses the existing IntrospectedSchema (schema-diff-service) and
SchemaDiffEntry (external-errors).
New package @objectstack/service-external-datasource:
- ExternalDatasourceService implements the contract: listRemoteTables
(schema-qualified, allowedSchemas-filtered), generateObjectDraft (renders a
reviewable *.object.ts with // REVIEW: markers on lossy/unknown columns),
validateObject/validateAll (structured diffs; lossy = warning, hard
mismatch = error; honours columnMap + ignoreColumns), refreshCatalog
(snapshot shape; persistence lands with P3's external_catalog type).
- Decoupled from the kernel via injected I/O, so the logic is fully
unit-tested. ExternalDatasourceServicePlugin wires the live IDataEngine +
IMetadataService and registers it as the 'external-datasource' service.
Tests: type-compat matrix (canonicalization, suggestion, compatibility) and
the service (list/draft/validate/refresh) — 15 service cases + matrix cases.
REST routes and the `os datasource` CLI commands follow in the next slice.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
@github-actionsgithub-actionsBot added dependencies Pull requests that update a dependency file size/xl and removed size/l labels May 30, 2026
Comment threadpackages/spec/src/data/type-compat.ts Fixed
…icalization
CodeQL flagged the SQL-type canonicalizer's `\s*\([^)]*\)` and `\s+…$`
patterns as polynomial regular expressions on (library-controlled) input.
Replace them with linear substring operations (indexOf-slice for the
precision parenthetical, anchored endsWith for trailing qualifiers). Behaviour
is unchanged; type-compat tests still pass.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
@os-zhuangos-zhuang changed the title feat: External Datasource Federation (ADR-0015) — Phase 1feat: External Datasource Federation (ADR-0015) — Phases 1 & 2 (service core)May 30, 2026
…og metadata type
Registers the `external_catalog` metadata type (MetadataTypeSchema +
DEFAULT_METADATA_TYPE_REGISTRY, system domain) and adds
data/external-catalog.zod.ts (ExternalCatalogSchema / ExternalTableSchema /
ExternalColumnSchema) for persisting a cached remote-schema snapshot of a
federated datasource — consumed by refreshCatalog, the boot-validation gate,
and Studio's schema browser.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…on plumbing (ADR-0015)
- Write gate: insert/update/delete block writes to a federated datasource
(schemaMode != 'managed') unless BOTH datasource.external.allowWrites and
object.external.writable are true, throwing ExternalWriteForbiddenError.
registerDatasourceDef() records declarative ownership; manifests carrying
`datasources` are indexed during registerApp.
- engine.introspectDatasource(name) delegates to the named driver's
introspectSchema(), completing the external-datasource service path.
Tests: 8 write-gate + 3 introspect cases, real engine, all pass.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…insert)
The engine dispatches inserts via driver.create(); the test's fake driver
defined insert(), so the allow-path cases hit 'driver.create is not a
function'. Rename to create(). All 11 write-gate + introspect cases pass.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…ADR-0015)
Commit 0603646 landed the assertWriteAllowed() definition but the three call
sites were lost during an environment recovery, leaving the gate dead code and
a 'declared but never read' DTS build error. Re-add the calls. Write-gate
suite passes 11/11.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
claude added 2 commits May 30, 2026 11:46
…0015)
ExternalValidationPlugin subscribes to kernel:ready and runs the
external-datasource service's validateAll(), enforcing each datasource's
external.validation.onMismatch policy: fail (throw, default), warn (log), or
ignore. No-op when the service is not registered. 7 tests.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…, ADR-0015)
SchemaRetriever.renderSnippet appends an [external, read-only|writable,
datasource=<name>] badge for federated objects so the LLM does not propose
schema changes or unsafe writes. ObjectShape gains datasource + external;
managed objects unannotated. 3 new renderSnippet tests.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…DR-0015)
The badge edit was lost in a prior environment recovery while its tests landed,
leaving 3 failing tests. Re-add the [external, read-only|writable,
datasource=<name>] badge to renderSnippet. schema-retriever suite passes 14/14.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…n badge (P4, ADR-0015)
Prior environment recoveries left schema-retriever.ts corrupted (the
describeField helper was dropped, breaking 3 pre-existing tests). Restore the
file from main and re-apply only the minimal P4 changes: ObjectShape gains
datasource + external, and renderSnippet appends an [external,
read-only|writable, datasource=<name>] badge for federated objects. Suite
passes 12/12 (9 original + 3 federation).
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
…R-0015)
New os datasource group backed by the external federation REST routes:
list-tables, introspect (writes a reviewable *.object.ts), validate (non-zero
exit on mismatch).
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
@os-zhuangos-zhuang changed the title feat: External Datasource Federation (ADR-0015) — Phases 1 & 2 (service core)feat: External Datasource Federation (ADR-0015) — backend (P1–P4, P6)May 30, 2026
Comment threadpackages/cli/src/commands/datasource/introspect.ts Fixed
…facts after env churn (ADR-0015)
A series of container reclaims left several committed files half-applied.
This consolidates the repairs:
- objectql: add the missing assertWriteAllowed(object, 'delete') call site so
the write gate (Gate 3) covers delete (was insert/update only).
- runtime: export ExternalValidationPlugin + createExternalValidationPlugin
from index, and add the required init() to satisfy the Plugin interface.
- service-ai: rewrite schema-retriever.test.ts as a complete, self-contained
file (it had lost its imports + duplicated a describe block).
- cli: drop the broken ../../lib/api-client import (wrong path; the helper is
async and returns {client,token}); the os datasource commands now resolve
url/token inline via flags/env. introspect constrains --out to the CWD
(CodeQL: network-data-to-file).
Tests: objectql write-gate 11/11, service-ai schema-retriever 3/3, runtime
boot-gate 7/7. objectql/runtime/cli/service-ai builds (incl. DTS) green.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
this.error(`--out must be a relative path within the current directory: ${flags.out}`);
return;
}
await writeFile(target, draft.source, 'utf8');
Add registerExternalDatasourceRoutes mounting /api/v1/datasources/:name/
external/* (tables, tables/:remote/draft, refresh-catalog, validate), served
by the external-datasource service and wired into the REST API plugin next to
registerPackageRoutes. Routes return 503 external_service_unavailable when the
service is not registered, so they are safe to mount unconditionally.
Refs ADR-0015.
https://claude.ai/code/session_019FweBAH7Af5HzMWNQ84nPd
@os-zhuang
os-zhuang marked this pull request as ready for review May 30, 2026 12:53
@os-zhuang
os-zhuang merged commit 2faf9f2 into mainMay 30, 2026
9 of 13 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationprotocol:datasize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@os-zhuang@github-advanced-security@claude