Skip to content

feat: apso import supabase — generate .apsorc from a Supabase/Postgres database#68

Open
cultron wants to merge 1 commit into
mainfrom
feat/import-supabase-schema
Open

feat: apso import supabase — generate .apsorc from a Supabase/Postgres database#68
cultron wants to merge 1 commit into
mainfrom
feat/import-supabase-schema

Conversation

@cultron

@cultron cultron commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 of migrating off Supabase: a new apso import supabase command that connects to an existing Supabase (Postgres) database via a connection string, introspects its schema read-only, and writes a local .apsorc. From there the existing apso generate / apso deploy flow rebuilds the structure on Apso.

This is step 1 of the two-step plan agreed up front (schema now, table-data copy later). It mirrors the existing apso schema pull shape: introspect → ApsorcOutput → write with backup.

How it works

apso import supabase                      # masked prompt for connection string
apso import supabase -c postgresql://…  # or a flag / SUPABASE_DB_URL / DATABASE_URL
apso import supabase --dry-run            # introspect + summary, write nothing

Pipeline:

  1. Introspect (src/lib/import/introspect.ts) — read-only SELECTs over information_schema/pg_catalog; SSL on by default; skips Supabase-managed schemas (auth, storage, …) and views. pg is lazy-imported.
  2. Convert (src/lib/import/pg-to-apsorc.ts, pure) — reverse type map (inverse of fieldTypeToColumnType, full fidelity: uuid/timestamp/bigint/jsonb preserved; arrays + unknown types → text + warning); PK detection (uuid/serial/text id; composite/non-id PKs kept as primary fields + flagged); created_at/updated_at detection; FK → ManyToOne with the FK column omitted and to_name round-trip-verified against getRelationshipIdField; enums; default translation; unique/index handling.
  3. Report — prints what was imported and an explicit list of anything lossy/skipped (arrays→text, composite keys, dropped defaults, join tables, …).
  4. Write — backs up any existing .apsorc; never logs or persists the connection string.

Decisions (confirmed during planning)

  • Full type fidelity (not down-mapped to the narrow authoring union).
  • Composite / non-id PKs imported as-is + warned (never silently rewritten).
  • Entity names kept verbatim; FKs → unidirectional ManyToOne (lossless).

Verification

  • New unit suites for the converter (every type, all PK variants, FK name round-tripping incl. snake_case→camelCase, enums, defaults, unique/index, lossy-array + type invariants) and for buildSchema row assembly (FK grouping, enum detection) — no DB needed. Command helpers tested with a fake introspector.
  • End-to-end: a generated .apsorc was fed to apso migrate (PGlite sandbox) and produced valid DDL (uuid PKs, enum type, unique/FK constraints, array→text).
  • Full suite: 248 tests pass, lint clean (npm test incl. the posttest eslint hook).

Notes / follow-ups

  • introspect.ts's live-DB path is covered only by the pure buildSchema tests here; a guarded integration test against a real database is a sensible follow-up.
  • Phase 2 (data copy) will reuse IntrospectedSchema and the FK graph; the Introspector interface and src/lib/import/ layout leave room for a sibling data reader without touching the converter.
  • One nuance observed: a ManyToOne with cascadeDelete: true renders as ON DELETE NO ACTION in the sandbox SQL — that's existing relationship/generator behavior, not introduced here; worth a separate look.

🤖 Generated with Claude Code

…s database

Phase 1 of migrating off Supabase: introspect an existing Postgres
database (read-only) and write a local .apsorc, after which the normal
apso generate/deploy flow rebuilds it on Apso. Mirrors the existing
'apso schema pull' shape (introspect -> ApsorcOutput -> write with backup).

- src/lib/import/introspect.ts: read-only PgIntrospector over
  information_schema/pg_catalog (lazy pg import, SSL on, skips Supabase
  system schemas and views). buildSchema() assembles raw rows and is
  unit-tested without a DB.
- src/lib/import/pg-to-apsorc.ts: pure converter with a reverse type map
  (inverse of fieldTypeToColumnType, full fidelity), PK detection
  (uuid/serial/text id; composite/non-id kept as primary fields + warned),
  created_at/updated_at detection, FK -> ManyToOne (FK column omitted;
  to_name derived and round-trip-verified against getRelationshipIdField),
  enum values, default translation, unique/index handling, and an import
  report of everything lossy/skipped.
- src/commands/import/supabase.ts: connection string via flag /
  SUPABASE_DB_URL / DATABASE_URL / masked prompt; --schema/--out/--dry-run/
  --yes; never logs or persists the connection string; backs up an
  existing .apsorc.
- pg + @types/pg added; 'import' topic registered.

Verified end-to-end: generated .apsorc passes apso migrate (PGlite
sandbox) producing valid DDL. 188 new assertions; full suite 248 tests,
lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant