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
Copy link
Copy Markdown
Contributor

@os-zhuang os-zhuang commented May 30, 2026

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.allowWrites and object.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 sqlite3 peer 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
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

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

Project Deployment Actions Updated (UTC)
spec Error Error May 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-actions github-actions Bot added dependencies Pull requests that update a dependency file size/xl and removed size/l labels May 30, 2026
Comment thread packages/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-zhuang os-zhuang changed the title feat: External Datasource Federation (ADR-0015) — Phase 1 feat: 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-zhuang os-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 thread packages/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 main May 30, 2026
9 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants