# Importing a data source's schema

> Writes an entity for every table a registered data source holds — at its address, with its key — as ordinary source you review, grant and commit. The provider describes itself on the local platform, inside your compiled app.

<!-- id: local-importing-a-schema · area: local · stability: stable · html: https://osysharp.com/reference/local/importing-a-schema/ -->

## Summary        {#summary}

Once an app registers a data source, `osy import-schema` asks that source what it holds and writes an entity for each
table: `[DataSource]`, its `[ExternalName]` address, a member per column with its type, and `[Key]` on the columns that
identify a row. What it writes is source like any other — `Order` is then a type your functions and pages use, that
`osy model` shows and the editor completes.

## Signature      {#signature}

```console
osy import-schema <source> [path] [--table <name>] [--schema <name>] [--as <entity>] [-o <file>] [--force] [--list] [--json]
```

## Description    {#description}

### What it reads   {#reads}

The source's own `DescribeSchema()` — the provider class the app registered, built from its entry in
`app.DataSources` with its secrets, running on the local platform. So the source has to be in the **compiled** app:
add it to `app.DataSources`, `osy compile`, then import. The CLI never holds the credentials.

### What it writes   {#writes}

One entity per table, into `<source>.osy` in the project (or `-o <file>`):

```osy syntax
// Generated by `osy import-schema Analytics` from what data source `Analytics` describes.
// Ordinary source: review it, trim it, grant reads, and commit it. A later import never overwrites it.

[DataSource(DataSource.Analytics), ExternalName("SALES.ORDERS")]
entity Orders {
  [Key, ExternalName("ORDER_ID")] string OrderId;
  [ExternalName("REGION")] string? Region;
  [ExternalName("TOTAL")] decimal Total;
  [ExternalName("CREATED_AT")] DateTimeOffset CreatedAt;

  // Nobody can read these rows until you say who may — for example:
  //   security { allow read when IsAuthenticated; }
}
```

- **Names** are PascalCase, with `[ExternalName]` carrying the exact column whenever the two differ. The entity is
  named after the table exactly — `ORDERS` becomes `Orders`, never a guessed singular — and `--as Order` renames it.
  A column named `ID` becomes `IdColumn`, because every entity's `Id` is the platform's.
- **Types** are the ones a source hands values back in: `string`, `bool`, `int`, `long`, `decimal`, `bigdecimal`,
  `double`, `DateTime`, `DateOnly`, `TimeOnly`, `DateTimeOffset`, `Guid` and `Geography`. A nullable column is
  written `T?`.
- **A column of any other type is never dropped silently**: it is written as a `⚠ SKIPPED` comment naming it, and
  listed when the command finishes.
- **Keys** come from the source marking its key columns. A table with none is written with a `⚠ NO KEY` comment and
  does not compile until you mark the column or columns that identify a row with `[Key]`.

### Nothing is granted   {#security}

The generated entities have no `security { }` block, so under secure-by-default nobody can read them. Who may see a
source's data is your decision, not a generator's: write the rule in each entity before you ship it.

### Choosing tables   {#tables}

`--list` prints the tables the source holds and writes nothing. `--table <name>` imports one table — by its full name
or its last part, so `ORDERS` finds `SALES.ORDERS` — and repeats for more. `--schema SALES` limits the import to that
schema. `--json` prints the generated source (or, with `--list`, the tables) instead of writing.

### It never overwrites   {#overwrite}

The file is yours once it is written: you trim columns, rename members and add security. So an import refuses to
replace an existing file. `-o` a different file to compare, or `--force` when you mean to start again.

### Asking whether it is still true   {#check}

Because the file is yours, a second import cannot tell you anything useful by rewriting it — the interesting question
is whether what you wrote is still true of the source. `--check` answers exactly that and writes nothing:

```console
osy import-schema Analytics --check
```

It reports, per entity:

| | |
|---|---|
| ✗ **a column that is gone** | with the nearest name at the source, because a drop and a RENAME look identical from here and the remedies are opposites |
| ✗ **a table that is gone**, a **type that no longer fits**, a column that **started admitting NULL**, a **key that moved** | each one meaning a read will fail, or already does on some row |
| ⓘ **a column you do not read**, one that **stopped admitting NULL** | information, never a problem — trimming the import is the point of it being source |

`--json` gives the same findings with a `kind` and a `refusal` flag per entry. The exit code is non-zero when
anything in the first two rows was found.

⚠ **Widening is not drift.** A `decimal` column read into a `bigdecimal` member, or an `int` into a `long`, is fine
and is not reported — an author who widened a member ahead of the warehouse has not broken anything. Nor is a member
typed as your own `enum` over a code column: that is a hand-edit the generator could not have made, and the check
does not punish it.

### `osy compile` refuses drift   {#compile-gate}

The same comparison runs as a pre-flight on every `osy compile`, before a version is minted, and fails the compile
naming the column. Without it the first sign of a dropped column is a read failing in a provider's words, on whoever
opens the page first.

⚠ **A source that cannot be reached never fails a compile.** A warehouse is somebody else's machine, and it is down
sometimes; a gate that turned "I could not ask" into "your app is broken" would stop the whole team compiling —
including the fix — for an outage they did not cause. Only an answer refuses.

⚠ **And `osy validate` does not run it**, because validate is offline by design. Its footer says so rather than
letting a clean local run read as schema-checked.

## Examples       {#examples}

What an import leaves you with, beside the provider it came from — with the read the author then granted:

```osy test app=importing-a-schema
using Osysharp.Sources;

class Warehouse : IEntitySource {
  public const QueryFeatures Pushdown = QueryFeatures.Filter | QueryFeatures.OrderBy | QueryFeatures.Paging;
  public const string SecurityKitVersion = "1";

  public stream<SourceRow> Query(QueryPlan plan) { yield return new SourceRow(); }

  public SourceSchema DescribeSchema() {
    return new SourceSchema { Tables = [
      new SourceTable { Name = "SALES.ORDERS", Columns = [
        new SourceColumn { Name = "ORDER_ID", Type = "string", Key = true },
        new SourceColumn { Name = "TOTAL", Type = "decimal" },
      ] },
    ] };
  }
}

app.DataSources = [ new Warehouse("Analytics") ];

// Generated by `osy import-schema Analytics`, then reviewed: the author added the read.
[DataSource(DataSource.Analytics), ExternalName("SALES.ORDERS")]
entity Orders {
  [Key, ExternalName("ORDER_ID")] string OrderId;
  [ExternalName("TOTAL")] decimal Total;

  security { allow read when IsAuthenticated; }
}
```

And the commands that write it:

```console
osy import-schema Analytics --list
osy import-schema Analytics --schema SALES --table ORDERS --as Order -o model/orders.osy
osy import-schema Analytics --table ORDERS --json
osy import-schema Analytics --check
```

## See also       {#see-also}
- [data held somewhere else (app.DataSources)](https://osysharp.com/reference/config/data-sources/) — registering a source, and the entities whose rows live in one
- [Importing data](https://osysharp.com/reference/local/importing-data/) — loading your app's own rows, rather than describing a source's
- [Compiling your app](https://osysharp.com/reference/local/compiling-your-app/) — a source is read from the compiled app; and its pre-flight refuses a column the source has dropped
