# Podman-Testumgebung

> Lokale Testumgebung mit Podman: DataBridge-Container, Azurite, wahlweise MSSQL, PostgreSQL, MySQL und Oracle, HTTP-Mocks für Business Central, NAV und OpenAPI sowie ein Service-Bus-Emulator — inklusive Beispieldaten, Connections, Jobs und Mappings.

---

Site [llms.txt](/llms.txt)

---

Die Testumgebung unter `dev/` startet eine vollständig vorkonfigurierte DataBridge auf dem eigenen
Rechner — ohne Azure-Ressourcen und ohne Kosten. Sie ist gedacht, um Connectoren gegen **echte
Datenbanken** und gegen **HTTP-Mocks** (Business Central, NAV, OpenAPI) zu testen, ohne vorher
Mappings und Jobs von Hand anzulegen.

> **Nur für Entwicklung**
>
> Die Umgebung nutzt feste Passwörter und einen festen Function Key (`local`). Sie ist nicht für
> den Betrieb gedacht und darf nicht aus dem Internet erreichbar sein.



## Was gestartet wird

```mermaid
flowchart LR
    subgraph db["Datenbanken (wählbar)"]
        mssql[(MSSQL)]
        pg[(PostgreSQL)]
        my[(MySQL)]
        ora[(Oracle)]
    end
    init["&lt;db&gt;-init<br/>Schema + Testdaten"] --> db
    seed["seed<br/>connections, jobs, mappings"] --> azurite[(Azurite<br/>Table Storage)]
    azurite -- "Konfiguration" --> bridge["databridge<br/>Function App :7062"]
    db -- "SQL-Connectoren" --> bridge
    wiremock["wiremock :8089 (wählbar)<br/>BC / NAV / OpenAPI"] -- "HTTP-Connectoren" --> bridge
    subgraph sb["Service Bus (wählbar: servicebus/getchanges/process)"]
        sbdb[(servicebus-db)] --> emulator["servicebus<br/>Emulator :5672 / :5300"]
        emulator --> explorer["servicebus-explorer<br/>Web-UI :8090 (wählbar)"]
    end
    bridge -- "GetChanges legt Nachrichten ab" --> emulator
    emulator -- "Process*QueueItems liest" --> bridge
```

Die Pfeile zeigen, wer wen versorgt: Init und Seed befüllen Datenbanken und Azurite. Die DataBridge
liest ihre Konfiguration aus Azurite und greift über ihre Connectoren auf die Datenbanken und auf
WireMock zu. Ist das Profil `servicebus` aktiv, legt `GetChanges` erkannte Änderungen auf die
Queues des Emulators, `Process*QueueItems` liest sie von dort. Die Connectoren lesen und schreiben.

| Container | Aufgabe | Läuft |
|---|---|---|
| `mssql`, `postgres`, `mysql`, `oracle` | Datenbankserver, nur die gewählten | dauerhaft |
| `<db>-init` | Legt je Datenbank Schema und Testdaten an | einmalig pro Start |
| `azurite` | Emulator für Azure Table/Blob/Queue Storage | dauerhaft |
| `wiremock` | HTTP-Mock für Business Central, NAV und eine OpenAPI-Beispiel-API, nur die gewählten (siehe [HTTP-Mocks](#http-mocks-wiremock)) | dauerhaft, nur wenn mindestens ein Mock gewählt ist |
| `servicebus` | [Service-Bus-Emulator](#service-bus), nur wenn `servicebus`/`getchanges`/`process` gewählt sind | dauerhaft, nur wenn gewählt |
| `servicebus-db` | Eigener SQL Server als Speicher des Emulators (nicht die Dev-Datenbank `mssql`) | dauerhaft, nur wenn `servicebus` gewählt ist |
| `servicebus-explorer` | Grafische Web-Oberfläche für den Emulator ([Service Bus einsehen](../debuggen/podman/#service-bus-einsehen)) | dauerhaft, nur wenn `servicebus` gewählt ist |
| `seed` | Schreibt `connections`, `jobs` und `mappings` nach Azurite | einmalig pro Start |
| `databridge` | Die DataBridge selbst, gebaut aus dem aktuellen Quellcode (`src/Dockerfile`). Fehlt im [Host-Modus](../debuggen/podman/) (`--host`) | dauerhaft, außer im Host-Modus |

Die DataBridge startet erst, wenn alle Init-Container und der Seed erfolgreich durchgelaufen
sind. Timer- und Queue-Trigger (`GetChanges`, `Process*QueueItems`) sind standardmäßig
deaktiviert, lassen sich aber gezielt zuschalten (siehe [Verarbeitung schalten](#verarbeitung-schalten)).
HTTP-Funktionen und der MCP-Server laufen immer normal.

## Voraussetzungen

- **Podman** 5 oder neuer.
- **Compose-Provider** für `podman compose`: `docker-compose` 2.20 oder neuer (empfohlen) bzw. ein
  aktuelles `podman-compose`. Benötigt werden `depends_on` mit `condition` und `required: false`.
- **Windows/macOS:** eine eingerichtete `podman machine`. `up.ps1` und `down.ps1 -Reset` starten sie
  bei Bedarf selbst. Die PowerShell-Skripte laufen unter PowerShell 7 und Windows PowerShell 5.1.
- **Arbeitsspeicher:** abhängig von der Auswahl, siehe [Speicherbedarf](#speicherbedarf).
- **Freie Ports:** 7062 (DataBridge — im [Host-Modus](../debuggen/podman/) stattdessen vom lokalen
  Prozess belegt), 10000–10002 (Azurite) sowie die Ports der gewählten Datenbanken (siehe
  [Datenbanken](#datenbanken)). Mit `servicebus`/`getchanges`/`process` zusätzlich 5672 (AMQP) und
  5300 (Management) für den [Service-Bus-Emulator](#service-bus) — diese beiden Ports sind fest und
  nicht über `dev/.env` änderbar — sowie `SERVICEBUS_EXPLORER_PORT` (Standard `8090`) für dessen
  [Web-Oberfläche](../debuggen/podman/#service-bus-einsehen).

### Speicherbedarf

Gemessene Werte im laufenden Betrieb (`podman stats`, gerundet):

| Container | RAM | Läuft |
|---|---|---|
| `databridge` + `azurite` | ~0,3 GB | immer |
| `postgres` | ~0,1 GB | wenn gewählt |
| `mysql` | ~0,5 GB | wenn gewählt |
| `mssql` | ~1,5 GB | wenn gewählt |
| `oracle` | ~2,5 GB | wenn gewählt |
| `wiremock` (alle Mocks in einem Container) | ~0,2 GB | wenn ein Mock gewählt ist |
| `servicebus` + `servicebus-db` | ~0,5 GB + ~1,5 GB | wenn `servicebus`/`getchanges`/`process` gewählt sind (siehe [Service Bus](#service-bus)) |
| `servicebus-explorer` | ~0,2 GB (ungefähr, .NET-Web-App) | wenn `servicebus`/`getchanges`/`process` gewählt sind |

Die Init-Container und `seed` beenden sich nach dem Start und belegen danach keinen Speicher.

`up all` (ohne Service Bus) kommt damit auf rund **5 GB**, mit `getchanges`/`process`/`servicebus`
zusätzlich rund **2 GB** für den Emulator und seine SQL-DB — macOS/Windows-Leser bitte beachten:
`servicebus-db` läuft **zusätzlich** zu `mssql`, wenn beide gewählt sind (getrennte Container, siehe
[Service Bus](#service-bus)). Unter Windows und macOS laufen alle Container in der VM der
`podman machine`, die zusätzlich Kernel und Dateicache belegt und Speicher nur zögerlich an das
Host-System zurückgibt. Der Prozess `vmmem` bzw. die VM wächst daher deutlich darüber hinaus und
kann den Rechner spürbar ausbremsen. Unter Linux laufen die Container ohne VM und der Stack
merklich flüssiger.

**Im Normalfall nur das starten, was man gerade testet**, also die Datenbank bzw. den Mock des
Connectors, an dem man arbeitet (z. B. `dev\up.ps1 postgres` oder `dev\up.ps1 mssql bc`). Die
Auswahl lässt sich dauerhaft über `DATABRIDGE_DBS` / `DATABRIDGE_MOCKS` in `dev/.env` festlegen.
`all` ist für den gelegentlichen Gesamttest gedacht und braucht einen Rechner mit 16 GB oder mehr.

Unter Windows ist der größte Posten meist nicht der Container selbst, sondern der
**Dateicache der VM** (Images, Datenbankdateien, Volumes). Linux zählt ihn als verfügbar,
Windows sieht ihn aber als belegt, weil WSL ihn nicht von selbst zurückgibt. `vmmemWSL` kann so
auf 13 GB und mehr wachsen, obwohl die Container nur rund 5 GB brauchen. Prüfen lässt sich das mit
`podman machine ssh free -h` (Spalte `buff/cache`).

Cache sofort freigeben (laufende Container bleiben unberührt):

```bash
podman machine ssh "sync; echo 3 | sudo tee /proc/sys/vm/drop_caches"
```

Dauerhaft in `%USERPROFILE%\.wslconfig` die VM begrenzen und ungenutzten Cache automatisch an
Windows zurückgeben lassen (danach `wsl --shutdown`):

```ini
[wsl2]
memory=8GB

[experimental]
autoMemoryReclaim=gradual
```

Beim ersten Start lädt Podman die Images (mehrere GB, vor allem Oracle und MSSQL) und baut die
DataBridge. Das dauert einige Minuten; folgende Starts sind deutlich schneller.

## Starten und beenden

Alle Befehle werden aus dem Repository-Stammverzeichnis aufgerufen.




<ul class="nav nav-tabs" id="tabs-1" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-01-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-01-00" role="tab"
          data-td-tp-persist="windows (powershell)" aria-controls="tabs-01-00" aria-selected="true">
        Windows (PowerShell)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-01-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-01-01" role="tab"
          data-td-tp-persist="linux / macos" aria-controls="tabs-01-01" aria-selected="false">
        Linux / macOS
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-1-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-01-00" role="tabpanel" aria-labelled-by="tabs-01-00-tab" tabindex="1">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span>dev\up.ps1                          <span style="color:#75715e"># Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev\.env</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 mssql oracle              <span style="color:#75715e"># nur die genannten Datenbanken, keine Mocks</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 mssql bc openapi          <span style="color:#75715e"># Datenbanken und Mocks gemischt</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 bc                        <span style="color:#75715e"># nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev\.env</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 all                       <span style="color:#75715e"># alle vier Datenbanken und alle drei Mocks</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 mssql <span style="color:#66d9ef">process</span>             <span style="color:#75715e"># Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 mssql getchanges          <span style="color:#75715e"># Datenbank + Timer-Trigger GetChanges an</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 servicebus                <span style="color:#75715e"># nur den Emulator dazu, ohne Verarbeitung</span>
</span></span><span style="display:flex;"><span>dev\up.ps1 -HostMode mssql <span style="color:#66d9ef">process</span>   <span style="color:#75715e"># Host-Modus: kein databridge-Container, Debugger auf Port 7062</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>dev\down.ps1                         <span style="color:#75715e"># alles stoppen, Daten bleiben erhalten (VM aus: nichts zu tun)</span>
</span></span><span style="display:flex;"><span>dev\down.ps1 -Reset                  <span style="color:#75715e"># alles stoppen und alle Daten löschen (startet die VM bei Bedarf)</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-01-01" role="tabpanel" aria-labelled-by="tabs-01-01-tab" tabindex="1">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dev/up.sh                    <span style="color:#75715e"># Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev/.env</span>
</span></span><span style="display:flex;"><span>dev/up.sh mssql oracle       <span style="color:#75715e"># nur die genannten Datenbanken, keine Mocks</span>
</span></span><span style="display:flex;"><span>dev/up.sh mssql bc openapi   <span style="color:#75715e"># Datenbanken und Mocks gemischt</span>
</span></span><span style="display:flex;"><span>dev/up.sh bc                 <span style="color:#75715e"># nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev/.env</span>
</span></span><span style="display:flex;"><span>dev/up.sh all                <span style="color:#75715e"># alle vier Datenbanken und alle drei Mocks</span>
</span></span><span style="display:flex;"><span>dev/up.sh mssql process      <span style="color:#75715e"># Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)</span>
</span></span><span style="display:flex;"><span>dev/up.sh mssql getchanges   <span style="color:#75715e"># Datenbank + Timer-Trigger GetChanges an</span>
</span></span><span style="display:flex;"><span>dev/up.sh servicebus         <span style="color:#75715e"># nur den Emulator dazu, ohne Verarbeitung</span>
</span></span><span style="display:flex;"><span>dev/up.sh --host mssql process  <span style="color:#75715e"># Host-Modus: kein databridge-Container, Debugger auf Port 7062</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>dev/down.sh                  <span style="color:#75715e"># alles stoppen, Daten bleiben erhalten</span>
</span></span><span style="display:flex;"><span>dev/down.sh -v               <span style="color:#75715e"># alles stoppen und alle Daten löschen (kompletter Reset)</span>
</span></span></code></pre></div>
    </div>
</div>


**Auswahlregeln:**

- Argumente gehen vor `dev/.env`. Datenbanken (`mssql`, `postgres`, `mysql`, `oracle`), Mocks
  (`bc`, `nav`, `openapi`) und Toggles (`getchanges`, `process`, `servicebus`) dürfen in
  beliebiger Reihenfolge gemischt werden.
- Wer Argumente angibt, bekommt **nur** die genannten Mocks bzw. Toggles. `dev/up.sh mssql`
  startet also ohne WireMock und ohne Verarbeitung, auch wenn `DATABRIDGE_MOCKS`/
  `DATABRIDGE_GETCHANGES`/`DATABRIDGE_PROCESS` in `dev/.env` gesetzt sind.
- Mindestens eine Datenbank läuft immer mit, weil Seed und Jobs eine brauchen. Nennt man nur
  Mocks oder Toggles, kommen die Datenbanken aus `DATABRIDGE_DBS`.
- `getchanges` oder `process` aktivieren automatisch das Profil `servicebus`. `servicebus` allein
  ist erlaubt, um die Queues z. B. per MCP zu inspizieren, ohne dass etwas verarbeitet wird.
- Unbekannte Namen brechen mit einer Fehlermeldung ab.

### Was `up` im Einzelnen tut

1. Prüft, ob `podman` und `podman compose` verfügbar sind (unter Windows/macOS auch die
   `podman machine`). Unter Windows räumt `up.ps1` vor dem Start der `podman machine` eine
   liegengebliebene Socket-Datei weg (siehe [Fehlersuche](#fehlersuche)) und wartet, bis
   `podman compose` die VM erreicht.
2. Legt `dev/.env` aus `dev/.env.example` an, falls sie fehlt.
3. Stoppt Datenbank-Container, die **nicht** gewählt sind, WireMock, wenn kein Mock gewählt ist,
   den Service-Bus-Emulator, wenn weder `servicebus` noch `getchanges`/`process` gewählt sind, und
   im [Host-Modus](../debuggen/podman/) (`--host`) den `databridge`-Container. Daten-Volumes
   bleiben erhalten.
4. Baut die DataBridge aus dem aktuellen Quellcode und startet alle gewählten Container.
5. Läuft der Service-Bus-Emulator mit, wartet `up` zusätzlich, bis dessen Management-API antwortet
   (kein Docker-Healthcheck möglich, siehe [Service Bus](#service-bus)).
6. Ohne Host-Modus: startet die DataBridge nach dem Seed neu, weil sie ihre Konfiguration nur beim
   Start liest. Im Host-Modus: generiert stattdessen `src/DataBridge/local.settings.json` (siehe
   [Debuggen](../debuggen/podman/)) und beendet sich, ohne auf einen Container zu warten.
7. Ohne Host-Modus: wartet, bis `GetInfo` antwortet, und gibt MCP-Endpoint und Zugangsdaten aus.

`up` kann beliebig oft aufgerufen werden. Nach einer Code-Änderung genügt ein erneutes `up`:
Das Image wird neu gebaut, unveränderte Schichten kommen aus dem Cache.

### Konfiguration (`dev/.env`)

| Variable | Standard | Bedeutung |
|---|---|---|
| `DATABRIDGE_DBS` | `mssql` | Datenbanken, wenn `up` ohne Argument aufgerufen wird (kommagetrennt) |
| `DEV_DB_PASSWORD` | `DataBridge_Dev_2026` | Ein Passwort für alle Datenbanken (SA, App-User, Root) |
| `DATABRIDGE_MOCKS` | leer | Mocks, wenn `up` ohne Argument aufgerufen wird (kommagetrennt: `bc,nav,openapi`). Leer heißt: kein WireMock, keine Mock-Connections und -Jobs |
| `DATABRIDGE_GETCHANGES` | `off` | `an`/`off` — Timer-Trigger `GetChanges`, wenn `up` ohne Argument aufgerufen wird (siehe [Verarbeitung schalten](#verarbeitung-schalten)) |
| `DATABRIDGE_PROCESS` | `off` | `an`/`off` — die drei `Process*QueueItems`-Funktionen, wenn `up` ohne Argument aufgerufen wird |
| `DATABRIDGE_PORT` | `7062` | Port der DataBridge |
| `WIREMOCK_PORT` | `8089` | Host-Port von WireMock (9090 ist unter Fedora oft von Cockpit belegt) |
| `SERVICEBUS_EXPLORER_PORT` | `8090` | Host-Port der Service-Bus-Explorer-Weboberfläche (siehe [Service Bus einsehen](../debuggen/podman/#service-bus-einsehen)) |
| `MSSQL_PORT`, `POSTGRES_PORT`, `MYSQL_PORT`, `ORACLE_PORT` | `1433`, `5432`, `3306`, `1521` | Host-Ports der Datenbanken |

Das Passwort enthält bewusst keine Sonderzeichen außer `_`, damit es in allen Connection-Strings
und Kommandozeilen-Clients ohne Quoting funktioniert. Es muss die Komplexitätsregeln von SQL
Server erfüllen (mindestens 8 Zeichen, 3 von 4 Zeichenklassen). `dev/.env` ist nicht eingecheckt.

## Datenbanken

| Argument | Connection | Image | Host-Port | Benutzer | Datenbank / Schema |
|---|---|---|---|---|---|
| `mssql` | `mssql-dev` (MSSQL) | `mssql/server:2022-latest` | 1433 | `sa` | `DataBridgeTest`, Schema `dbo` |
| `postgres` | `postgres-dev` (PGSQL) | `postgres:17` | 5432 | `databridge` | `databridgetest`, Schema `public` |
| `mysql` | `mysql-dev` (MYSQL) | `mysql:8.4` | 3306 | `databridge` | `databridgetest` |
| `oracle` | `oracle-dev` (ORACLE) | `gvenzl/oracle-free:23-slim-faststart` | 1521 | `databridge` | Service `FREEPDB1`, Schema `DATABRIDGE` |

Mit den Host-Ports kann man sich auch mit eigenen Werkzeugen verbinden (SSMS, Azure Data Studio,
DBeaver, `psql`, SQL Developer …). Azurite ist im Azure Storage Explorer über
`UseDevelopmentStorage=true` erreichbar — Details siehe [Azurite einsehen](#azurite-einsehen).

**Hinweise je Datenbank:**

- **PostgreSQL** legt unquotierte Bezeichner in Kleinschreibung an. Tabellen und Spalten heißen
  dort `account`, `accountid` usw. Die DataBridge vergleicht Namen ohne Rücksicht auf
  Groß-/Kleinschreibung, `Account` funktioniert also genauso.
- **MySQL** unterscheidet unter Linux bei Tabellennamen Groß-/Kleinschreibung. Die Tabellen heißen
  exakt `Account`, `Opportunity` und `OpportunityProduct`.
- **Oracle** legt unquotierte Bezeichner in Großschreibung an (`ACCOUNT`, `ACCOUNTID`). Es wird
  Oracle 23ai Free verwendet, weil erst diese Version `BOOLEAN`, `JSON` und
  `CREATE TABLE IF NOT EXISTS` kennt.

## Beispieldaten

In jeder gewählten Datenbank werden dieselben drei Tabellen mit denselben Daten angelegt. Die
Skripte liegen unter `dev/<db>/init/`:

- `01_schema.sql` legt die Tabellen an, **nur wenn sie fehlen**.
- `02_data.sql` löscht die Testdaten und fügt sie neu ein, **bei jedem Start**. Wer in einer
  Testdatenbank Daten verändert hat, bekommt mit dem nächsten `up` den Ausgangszustand zurück.

Schemaänderungen an den Skripten wirken erst nach einem Reset (`down -v`), weil bestehende
Tabellen nicht verändert werden.

### Tabellen

```mermaid
erDiagram
    Account ||--o{ Opportunity : "AccountId"
    Opportunity ||--o{ OpportunityProduct : "OpportunityId"
```

Die Spalten decken bewusst möglichst viele Datentypen ab. Jede Datenbank verwendet dafür ihren
nativen Typ:

**Account** — Firmen (3 Datensätze)

| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
| `AccountId` (PK) | `uniqueidentifier` | `uuid` | `char(36)` | `varchar2(36)` |
| `AccountNumber` (eindeutig) | `varchar(20)` | `varchar(20)` | `varchar(20)` | `varchar2(20)` |
| `Name` | `nvarchar(160)` | `varchar(160)` | `varchar(160)` | `nvarchar2(160)` |
| `Description` | `nvarchar(max)` | `text` | `text` | `nclob` |
| `CountryCode` | `char(2)` | `char(2)` | `char(2)` | `char(2)` |
| `NumberOfEmployees` | `int` | `integer` | `int` | `number(10)` |
| `IndustryCode` | `smallint` | `smallint` | `smallint` | `number(5)` |
| `Rating` | `tinyint` | `smallint` | `tinyint` | `number(3)` |
| `Revenue` | `money` | `numeric(19,4)` | `decimal(19,4)` | `number(19,4)` |
| `CreditLimit` | `decimal(18,2)` | `numeric(18,2)` | `decimal(18,2)` | `number(18,2)` |
| `Latitude` | `float` | `double precision` | `double` | `binary_double` |
| `Longitude` | `real` | `real` | `float` | `binary_float` |
| `IsActive` | `bit` | `boolean` | `boolean` | `boolean` |
| `FoundedOn` | `date` | `date` | `date` | `date` |
| `OpeningTime` | `time(0)` | `time(0)` | `time` | `interval day to second` |
| `LastContactAt` | `datetime2(3)` | `timestamp(3)` | `datetime(3)` | `timestamp(3)` |
| `CreatedOnUtc` | `datetimeoffset(3)` | `timestamptz(3)` | `timestamp(3)` | `timestamp(3) with time zone` |
| `Logo` | `varbinary(max)` | `bytea` | `blob` | `blob` |
| `Attributes` (JSON) | `nvarchar(max)` | `jsonb` | `json` | `json` |
| `ModifiedOn` (Watermark) | `datetime2(3)` | `timestamp(3)` | `datetime(3)` | `timestamp(3)` |

**Opportunity** — Verkaufschancen (4 Datensätze)

| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
| `OpportunityId` (PK) | `uniqueidentifier` | `uuid` | `char(36)` | `varchar2(36)` |
| `AccountId` (FK) | `uniqueidentifier` | `uuid` | `char(36)` | `varchar2(36)` |
| `Name` | `nvarchar(200)` | `varchar(200)` | `varchar(200)` | `nvarchar2(200)` |
| `EstimatedValue` | `decimal(19,4)` | `numeric(19,4)` | `decimal(19,4)` | `number(19,4)` |
| `Probability` | `tinyint` | `smallint` | `tinyint` | `number(3)` |
| `EstimatedCloseDate` | `date` | `date` | `date` | `date` |
| `StatusCode` | `int` | `integer` | `int` | `number(10)` |
| `IsWon` | `bit` | `boolean` | `boolean` | `boolean` |
| `ActualCloseAt` | `datetime2(0)` | `timestamp(0)` | `datetime` | `timestamp(0)` |
| `ExternalRef` | `bigint` | `bigint` | `bigint` | `number(19)` |
| `ModifiedOn` (Watermark) | `datetime2(3)` | `timestamp(3)` | `datetime(3)` | `timestamp(3)` |

**OpportunityProduct** — Positionen der Verkaufschancen (6 Datensätze)

| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
| `OpportunityProductId` (PK, Identity) | `bigint identity` | `bigint identity` | `bigint auto_increment` | `number(19) identity` |
| `OpportunityId` (FK) | `uniqueidentifier` | `uuid` | `char(36)` | `varchar2(36)` |
| `LineNumber` | `int` | `integer` | `int` | `number(10)` |
| `ProductName` | `nvarchar(200)` | `varchar(200)` | `varchar(200)` | `nvarchar2(200)` |
| `Quantity` | `decimal(18,4)` | `numeric(18,4)` | `decimal(18,4)` | `number(18,4)` |
| `PricePerUnit` | `decimal(19,4)` | `numeric(19,4)` | `decimal(19,4)` | `number(19,4)` |
| `DiscountPercent` | `float` | `double precision` | `double` | `binary_double` |
| `TaxRate` | `real` | `real` | `float` | `binary_float` |
| `IsOptional` | `bit` | `boolean` | `boolean` | `boolean` |
| `DeliveryDate` | `date` | `date` | `date` | `date` |
| `ModifiedOn` (Watermark) | `datetime2(3)` | `timestamp(3)` | `datetime(3)` | `timestamp(3)` |

### Datensätze

Die Werte sind in allen Datenbanken identisch, einschließlich der Schlüssel. So lassen sich
Ergebnisse zwischen den Datenbanken direkt vergleichen.

| Account | Nummer | Land | Besonderheit |
|---|---|---|---|
| Müller GmbH | ACC-1000 | DE | Alle Felder gefüllt, inkl. Logo (Binärdaten) und JSON |
| Schmidt & Söhne KG | ACC-1001 | AT | Umlaute und `&` im Namen, keine Beschreibung |
| Weber AG | ACC-1002 | CH | Inaktiv, viele leere Felder (`NULL`) |

| Verkaufschance | Account | Status | Positionen |
|---|---|---|---|
| Neue Fertigungslinie | Müller GmbH | offen | 3 (davon 1 optional) |
| Wartungsvertrag 2027 | Müller GmbH | gewonnen | 1 |
| Ersatzteile Q4 | Schmidt & Söhne KG | offen | 1 |
| Rahmenvertrag | Weber AG | verloren | 1 |

Die Schlüssel sind feste GUIDs (`a0000000-…-000000000001` für Accounts,
`b0000000-…-000000000001` für Verkaufschancen) bzw. die Positionsnummern 1–6. Die Identity-Spalte
von `OpportunityProduct` ist so gesetzt, dass neue Zeilen ab 7 weiterzählen.

## Connections, Jobs und Mappings

Der Container `seed` schreibt die Konfiguration nach Azurite — passend zu den gewählten
Datenbanken. Die Vorlagen liegen unter `dev/seed/`:

### Azurite einsehen

Anders als der Service-Bus-Emulator bringt Azurite keine eigene Web-Oberfläche mit. Der übliche
Weg ist der kostenlose **[Microsoft Azure Storage Explorer](https://azure.microsoft.com/en-us/products/storage/storage-explorer/)**
(Windows/macOS/Linux):

1. Links im Baum unter **„Local & Attached“ → „Storage Accounts“** — Azurite ist dort meist
   automatisch als **„(Emulator - Default Ports) (Key)“** gelistet.
2. Falls nicht: **Rechtsklick auf „Storage Accounts“ → „Connect to Azure Storage“ → „Connection
   string“** und den Connection-String einfügen, den `up.sh`/`up.ps1` beim Start ausgeben (Zeile
   `Azurite`) — er verwendet den öffentlich dokumentierten Azurite-Well-known-Key, ist also kein
   Geheimnis.
3. Unter **„Tables“** liegen `connections`, `jobs`, `mappings` und `watermark` — Zeilen lassen
   sich dort auch bearbeiten, z. B. um einen Job per Hand auf `IsActive: true` zu setzen (siehe
   [Verarbeitung schalten](#verarbeitung-schalten)), ganz ohne MCP.

Alternativen: die VS-Code-Erweiterung **„Azure Storage“** (derselbe Explorer-Baum direkt in der
IDE) oder `az storage … --connection-string "…"` für Skripte.

| Datei | Inhalt |
|---|---|
| `connections.json` | Eine Connection je Datenbank (Name, Typ, Connection-String mit `${DEV_DB_PASSWORD}`) |
| `tables.json` | Die drei Tabellen mit Schlüssel, Watermark-Feld und den gemappten Feldern |
| `seed.py` | Erzeugt daraus die Einträge und gleicht die Azure Tables ab |

Einträge, die nicht mehr zur Auswahl passen, entfernt der Seed. Wer von `all` auf `mssql`
wechselt, hat danach also auch nur noch die MSSQL-Connection und ihre Jobs.

### Connections

Je gewählter Datenbank eine Connection: `mssql-dev`, `postgres-dev`, `mysql-dev`, `oracle-dev`
(siehe [Datenbanken](#datenbanken)). Je gewähltem Mock kommt `bc-dev`, `nav-dev` bzw.
`openapi-dev` hinzu (siehe [HTTP-Mocks](#http-mocks-wiremock)).

### Jobs

Für jede Kombination aus Quell- und Zieldatenbank entsteht **ein Job je Tabelle**. Die Namen
folgen dem Muster `<quelle>-to-<ziel>-<tabelle>`:

| Gewählte Datenbanken | Kombinationen | Jobs | Beispiel |
|---|---|---|---|
| 1 (z. B. `mssql`) | Datenbank auf sich selbst | 3 | `mssql-to-mssql-account` |
| 2 | 2 | 6 | `mssql-to-postgres-opportunity` |
| 3 | 6 | 18 | `mysql-to-oracle-opportunityproduct` |
| 4 (`all`) | 12 | 36 | `oracle-to-mssql-account` |

Alle Jobs haben diese Einstellungen:

| Feld | Wert |
|---|---|
| `Source` / `Target` | Tabellenname, z. B. `Account` |
| `Mapping` | `dev-<tabelle>`, z. B. `dev-account` |
| `WatermarkType` / `WatermarkField` | `datetime` / `ModifiedOn` |
| `Priority` | `Standard` |
| `IsActive` | `false` |

> **Jobs sind inaktiv**
>
> Die Jobs dienen als Testobjekte für `list_jobs`/`get_job` und als Vorlage. Sie laufen nicht, und
> die DataBridge prüft inaktive Jobs beim Start nicht — deshalb steht bei ihnen `isComplete: false`.
> Das ist kein Fehler. Aktiviert man einen Job, wird er beim nächsten Start geprüft.



### Mappings

Weil Tabellen- und Spaltennamen in allen Datenbanken gleich sind, gibt es nur **ein Mapping je
Tabelle**, das alle Jobs gemeinsam nutzen. Jedes Feld wird auf das gleichnamige Feld abgebildet:

| Mapping | Tabelle | Felder | Schlüssel (`TargetKey`) |
|---|---|---|---|
| `dev-account` | `Account` | 19 (alle Spalten außer `ModifiedOn`) | `AccountId` (`PrimaryKey`) |
| `dev-opportunity` | `Opportunity` | 10 | `OpportunityId` (`PrimaryKey`) |
| `dev-opportunityproduct` | `OpportunityProduct` | 10 | `OpportunityProductId` (`PrimaryKey`) |

`ModifiedOn` wird nicht gemappt, weil es das Watermark-Feld ist und die Zieldatenbank es selbst
setzt.

## HTTP-Mocks (WireMock)

Der Container `wiremock` spielt bis zu drei HTTP-Systeme gleichzeitig. Die DataBridge merkt keinen
Unterschied, sie bekommt nur eine andere URL in der Connection. Die Regeln (Stubs) sind so gebaut, dass der
jeweilige Connector vollständig durchläuft: Verbindungstest, Metadaten, Lesen mit Paging,
Upsert (Lookup → PATCH oder POST) und Delete.

| Connection | Typ | Tabelle | Auth | Metadaten |
|---|---|---|---|---|
| `bc-dev` | `D365BC` | `v2.0/customers` | OAuth2 Client Credentials gegen `/oauth/token` (Mock) | EDMX `wiremock/__files/bc/metadata.xml` |
| `nav-dev` | `D365NAV` | `Customers` | Basic (`dev`/`dev`) | EDMX `wiremock/__files/nav/metadata.xml` |
| `openapi-dev` | `OPENAPI` | `customers` | API-Key-Header `X-API-Key: dev-api-key` | Spec `wiremock/__files/openapi/spec.json` |

### Wie ein Container drei Systeme spielt

WireMock ist ein einzelner HTTP-Server mit einer Liste von Regeln (Stubs). Zu jeder Anfrage sucht
er die passende Regel und schickt deren Antwort. Welches System gespielt wird, entscheidet allein
das **Pfad-Präfix** der URL aus der Connection:

| Präfix | System | Connection-URL (aus Sicht der DataBridge) |
|---|---|---|
| `/bc/...` | Business Central | `http://wiremock:8080/bc/v2.0/sandbox` |
| `/nav/...` | NAV | `http://wiremock:8080/nav/ODataV4/Company('CRONUS')/` |
| `/openapi/...` | OpenAPI | `http://wiremock:8080/openapi` |
| `/oauth/token` | Token-Endpoint (gemeinsam) | `tokenUrl` von `bc-dev` |

Innerhalb des Compose-Netzes heißt der Container `wiremock` (Port 8080). Vom eigenen Rechner aus
erreicht man ihn unter `localhost:8089`.

**Nur die gewählten Mocks werden geladen.** Die Regeln liegen je System in einem eigenen Ordner:

```text
dev/wiremock/
├── mappings/            Regeln
│   ├── common/          immer geladen (OAuth-Token)
│   ├── bc/  nav/  openapi/
└── __files/             Antwortdateien, auf die Regeln per bodyFileName verweisen
    ├── bc/metadata.xml  nav/metadata.xml  openapi/spec.json
```

Der Ordner ist schreibgeschützt nach `/mocks` gemountet. Beim Start kopiert der Container `common`
und die Ordner der Mocks aus `DATABRIDGE_MOCKS` in sein Arbeitsverzeichnis und startet erst dann
WireMock. Ein nicht gewählter Mock antwortet deshalb mit 404. Der Seed legt Connections, Jobs und
Mappings nur für die gewählten Mocks an und entfernt die der anderen. Ändert sich die Auswahl,
baut `up` den Container neu auf.

**Aufbau einer Regel:** `request` beschreibt, worauf sie passt, `response`, was sie antwortet.

```json
{
  "name": "BC customer by key - existing record (update path)",
  "priority": 1,
  "request":  { "method": "GET", "urlPath": "/bc/.../customers(a0000000-0000-0000-0000-000000000001)" },
  "response": { "status": 200, "jsonBody": { "id": "a0000000-…", "number": "ACC-1000" } }
}
```

- **Treffer:** über `urlPath` (exakt), `urlPathPattern` (Regex), `queryParameters` (z. B.
  `$skiptoken` fehlt → Seite 1) und `headers` (z. B. `If-Match: *`, `X-API-Key`).
- **Priorität:** Passen mehrere Regeln, gewinnt die kleinste `priority`. So liefert der bekannte
  Kunde einen Datensatz, alle anderen Schlüssel fallen auf die allgemeine 404-Regel zurück.
  Daraus ergibt sich „vorhanden → PATCH, unbekannt → POST“, ohne dass WireMock sich etwas merkt.
- **Templates** (`"transformers": ["response-template"]`): `{{{request.body}}}` gibt den Body zurück
  (POST, PATCH). `{{request.baseUrl}}` baut den absoluten Link auf die nächste Seite, passend zur
  aufgerufenen Adresse (`wiremock:8080` oder `localhost:8089`).

### Datenbestand, Jobs und Mappings

Jeder Mock bildet alle drei Beispieltabellen ab — `Account` (Kunden, acht Felder: Guid, String,
Decimal, Int32, Boolean, Date, Zeitstempel), `Opportunity` und `OpportunityProduct` (jeweils
eigene Entität, z. B. bei BC `opportunities`/`opportunityLines`). Paging ist bewusst aktiv
(`maxPageSize: 2`, zwei Seiten mit drei Datensätzen): BC/NAV über `@odata.nextLink`, OpenAPI über
`meta.next_page`.

**Fester Datenbestand für Upsert-Tests:** Zu jeder Tabelle existiert ein bekannter Datensatz mit
fester ID (z. B. `AccountId a0000000-…-0001` bzw. `AccountNumber ACC-1000` für `Account`). Ein
Lookup liefert ihn, danach folgt ein PATCH. Jeder andere Schlüssel liefert 404, danach folgt ein
POST. POST und PATCH geben den Request-Body unverändert zurück. Das reicht für Response-Jobs.

**Ausnahme `OpportunityProduct`:** `OpportunityProductId` ist in allen Datenbanken eine
Ganzzahl (Identity), BC (`id`), NAV (`SystemId`) und OpenAPI (`id`) erwarten dort aber eine Guid. Das
Feld wird deshalb auf ein zusätzliches, nur im Mock vorhandenes Textfeld gemappt (BC
`externalLineNo`, NAV `External_Line_No`, OpenAPI `external_line_no`). Die Jobs nach BC und OpenAPI
haben keinen Ziel-Schlüssel und legen jede Position per POST neu an, der NAV-Job sucht weiterhin über
`Line_No`.

**Jobs und Mappings:** Je gewählter Datenbank, Mock und Tabelle (sofern der Mock sie kennt)
entstehen zwei inaktive Jobs:

| Job | Mapping | Richtung |
|---|---|---|
| `<db>-to-<mock>-<tabelle>` | `dev-<tabelle>-to-<mock>` | SQL-Tabelle → Mock-Entität (Schreibpfad des Connectors) |
| `<mock>-to-<db>-<tabelle>` | `dev-<mock>-to-<tabelle>` | Mock-Entität → SQL-Tabelle (Lesepfad, mit Watermark-Filter) |

Für `Account` lauten die Jobnamen unverändert `<db>-to-<mock>-account` (Kompatibilität zu
bestehenden Notizen/Screenshots). Die Feldzuordnung je Tabelle steht in `dev/seed/mocks.json`
unter `entities.<Account|Opportunity|OpportunityProduct>.fields`, z. B. für `Account.Name` →
`displayName` (BC), `Name` (NAV), `name` (OpenAPI).

**Was hat die DataBridge wirklich geschickt?** Das Anfragen-Protokoll von WireMock zeigt jeden
Request samt Body:




<ul class="nav nav-tabs" id="tabs-3" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-03-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-03-00" role="tab"
          data-td-tp-persist="windows (powershell)" aria-controls="tabs-03-00" aria-selected="true">
        Windows (PowerShell)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-03-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-03-01" role="tab"
          data-td-tp-persist="linux / macos" aria-controls="tabs-03-01" aria-selected="false">
        Linux / macOS
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-3-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-03-00" role="tabpanel" aria-labelled-by="tabs-03-00-tab" tabindex="3">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span>(Invoke-RestMethod localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests).requests |
</span></span><span style="display:flex;"><span>  Select-Object @{n=<span style="color:#e6db74">&#39;m&#39;</span>;e={$_.request.method}}, @{n=<span style="color:#e6db74">&#39;url&#39;</span>;e={$_.request.url}}, @{n=<span style="color:#e6db74">&#39;body&#39;</span>;e={$_.request.body}}, @{n=<span style="color:#e6db74">&#39;status&#39;</span>;e={$_.response.status}}
</span></span><span style="display:flex;"><span>Invoke-RestMethod localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests/unmatched   <span style="color:#75715e"># Requests, für die es keinen Stub gibt</span>
</span></span><span style="display:flex;"><span>Invoke-RestMethod -Method Delete localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests   <span style="color:#75715e"># Protokoll leeren</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-03-01" role="tabpanel" aria-labelled-by="tabs-03-01-tab" tabindex="3">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s localhost:8089/__admin/requests | jq <span style="color:#e6db74">&#39;.requests[] | {m: .request.method, url: .request.url, body: .request.body, status: .response.status}&#39;</span>
</span></span><span style="display:flex;"><span>curl -s localhost:8089/__admin/requests/unmatched     <span style="color:#75715e"># Requests, für die es keinen Stub gibt</span>
</span></span><span style="display:flex;"><span>curl -s -X DELETE localhost:8089/__admin/requests     <span style="color:#75715e"># Protokoll leeren</span>
</span></span></code></pre></div>
    </div>
</div>


Damit lässt sich ein Mapping inhaltlich prüfen: Man vergleicht den PATCH- oder POST-Body mit den
Beispieldaten aus `02_data.sql`.

> **Grenzen**
>
> - Die Mocks merken sich nichts. Ein POST legt nichts an, ein anschließender GET liefert weiter 404.
> - Filter werden nicht ausgewertet. Die Listen-Stubs liefern immer denselben Bestand.
> - Dataverse (`D365CE`) ist nicht dabei: Der `ServiceClient` nutzt das SDK-Protokoll mit OAuth und
>   lässt sich nicht sinnvoll per HTTP-Stub abbilden.
> - Mit den Mocks allein (ohne `servicebus`/`getchanges`/`process`) laufen Jobs nicht Ende-zu-Ende.
>   Nutzbar sind dann Verbindungstest, Metadaten (`list_connector_tables`,
>   `get_connector_table_schema`) und `CheckList`. Für den vollen Ablauf siehe
>   [Service Bus](#service-bus) und [Verarbeitung schalten](#verarbeitung-schalten).



**Eigene Stubs:** JSON-Dateien unter `dev/wiremock/mappings/<system>/` ablegen und WireMock neu
starten (`podman compose -f dev/compose.dev.yml restart wiremock`). Beim Neustart werden die
Dateien neu kopiert. Worauf die Connectoren
bestehen, steht als Kommentar im `name` der Stubs. Die wichtigsten Punkte:

- BC/NAV: POST muss genau `201` liefern, PATCH genau `200` (nicht `204`).
- BC/NAV: Jede Antwort braucht einen JSON-Body, auch ein 404.
- `nextLink` muss absolut sein.
- Jeder gelieferte Datensatz braucht das Watermark-Feld als Zeitstempel.

## Typische Abläufe

**Nach einer Code-Änderung testen:** erneut `dev/up.sh` (bzw. mit denselben Datenbanken wie
zuvor). Das Image wird neu gebaut, die DataBridge neu gestartet.

**Seed-Vorlagen geändert** (`dev/seed/*.json`): erneut `dev/up.sh`. Der Seed läuft bei jedem
Start und gleicht die Tabellen ab.

**Schema-Skripte geändert** (`dev/<db>/init/01_schema.sql`): `dev/down.sh -v`, dann `dev/up.sh`.

**Mock-Regeln geändert** (`dev/wiremock/**`): `podman compose -f dev/compose.dev.yml restart wiremock`.
Ein neues `up` ist nicht nötig, weil die DataBridge die Mocks erst bei Bedarf anfragt.
Ausnahme: Metadaten und die OpenAPI-Spec cacht sie. Nach einer Änderung an `__files` deshalb
auch `databridge` neu starten.

**Einen Connector gegen den Mock prüfen:**




<ul class="nav nav-tabs" id="tabs-5" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-05-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-05-00" role="tab"
          data-td-tp-persist="windows (powershell)" aria-controls="tabs-05-00" aria-selected="true">
        Windows (PowerShell)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-05-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-05-01" role="tab"
          data-td-tp-persist="linux / macos" aria-controls="tabs-05-01" aria-selected="false">
        Linux / macOS
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-5-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-05-00" role="tabpanel" aria-labelled-by="tabs-05-00-tab" tabindex="5">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span>dev\up.ps1 mssql bc
</span></span><span style="display:flex;"><span>Invoke-RestMethod -Method Delete localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests   <span style="color:#75715e"># Protokoll leeren</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...</span>
</span></span><span style="display:flex;"><span>(Invoke-RestMethod localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests).requests |
</span></span><span style="display:flex;"><span>  Select-Object @{n=<span style="color:#e6db74">&#39;m&#39;</span>;e={$_.request.method}}, @{n=<span style="color:#e6db74">&#39;url&#39;</span>;e={$_.request.url}}, @{n=<span style="color:#e6db74">&#39;status&#39;</span>;e={$_.response.status}}
</span></span><span style="display:flex;"><span>Invoke-RestMethod localhost<span style="color:#960050;background-color:#1e0010">:</span><span style="color:#ae81ff">8089</span>/__admin/requests/unmatched   <span style="color:#75715e"># Anfragen ohne passende Regel</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-05-01" role="tabpanel" aria-labelled-by="tabs-05-01-tab" tabindex="5">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dev/up.sh mssql bc
</span></span><span style="display:flex;"><span>curl -s -X DELETE localhost:8089/__admin/requests          <span style="color:#75715e"># Protokoll leeren</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...</span>
</span></span><span style="display:flex;"><span>curl -s localhost:8089/__admin/requests | jq <span style="color:#e6db74">&#39;.requests[] | {m: .request.method, url: .request.url, status: .response.status}&#39;</span>
</span></span><span style="display:flex;"><span>curl -s localhost:8089/__admin/requests/unmatched         <span style="color:#75715e"># Anfragen ohne passende Regel</span>
</span></span></code></pre></div>
    </div>
</div>


## Logging

Die Testumgebung setzt kein `APPLICATIONINSIGHTS_CONNECTION_STRING` — es gibt also keine
Application Insights, an die etwas gesendet werden könnte. Das gepufferte Sampling aus
[Betrieb → Logging](/docs/betrieb/logging/) (Flush-Regeln, `TELEMETRY_INFO_SAMPLE_RATE`,
`TELEMETRY_CONTROL`) betrifft ausschließlich diesen Versand und greift hier nicht: **alles**, was
die DataBridge loggt, landet unverändert und sofort im **Container-Log** (`stdout`/`stderr`).

`src/DataBridge/host.json` setzt für die Kategorien `DataBridge` und `Function` den Log-Level
`Trace` — lokal ist also die ausführlichste Stufe aktiv, inklusive der Einträge, die produktiv nie
über die Konsole hinauskommen (siehe [„Trace-Level erreicht Application Insights
nie“](/docs/betrieb/logging/#trace-level-erreicht-application-insights-nie)).




<ul class="nav nav-tabs" id="tabs-6" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-06-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-06-00" role="tab"
          data-td-tp-persist="windows (powershell)" aria-controls="tabs-06-00" aria-selected="true">
        Windows (PowerShell)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-06-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-06-01" role="tab"
          data-td-tp-persist="linux / macos" aria-controls="tabs-06-01" aria-selected="false">
        Linux / macOS
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-6-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-06-00" role="tabpanel" aria-labelled-by="tabs-06-00-tab" tabindex="6">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span>podman compose <span style="color:#f92672">-f</span> dev/compose.dev.yml logs <span style="color:#f92672">-f</span> databridge          <span style="color:#75715e"># laufend mitlesen</span>
</span></span><span style="display:flex;"><span>podman compose <span style="color:#f92672">-f</span> dev/compose.dev.yml logs seed mssql-init        <span style="color:#75715e"># abgeschlossene Init-Container</span>
</span></span><span style="display:flex;"><span>podman compose <span style="color:#f92672">-f</span> dev/compose.dev.yml logs <span style="color:#f92672">-f</span> databridge | Select-String <span style="color:#e6db74">&#34;mssql-to-postgres-account&#34;</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-06-01" role="tabpanel" aria-labelled-by="tabs-06-01-tab" tabindex="6">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>podman compose -f dev/compose.dev.yml logs -f databridge          <span style="color:#75715e"># laufend mitlesen</span>
</span></span><span style="display:flex;"><span>podman compose -f dev/compose.dev.yml logs seed mssql-init        <span style="color:#75715e"># abgeschlossene Init-Container</span>
</span></span><span style="display:flex;"><span>podman compose -f dev/compose.dev.yml logs -f databridge | grep <span style="color:#e6db74">&#34;mssql-to-postgres-account&#34;</span>
</span></span></code></pre></div>
    </div>
</div>


Ohne `-f` gibt der Befehl das bisherige Log aus und beendet sich; mit `-f` bleibt er offen und
zeigt neue Zeilen laufend an (<kbd>Strg</kbd>+<kbd>C</kbd> zum Beenden). Weil jede
Funktionsausführung ihren Job- bzw. Verbindungsnamen loggt, lässt sich damit gezielt nach einem
Job filtern, ohne `operation_Id` oder Application Insights zu brauchen.

Beim Debuggen im [Host-Modus](../debuggen/podman/) läuft die DataBridge nicht im Container — dort
erscheinen dieselben Log-Zeilen stattdessen direkt in der IDE (Rider-Run-Fenster,
Visual-Studio-Ausgabefenster bzw. das Terminal von `func start`), siehe
[Debuggen → Gegen Podman](../debuggen/podman/#logging).

## Service Bus

Profil `servicebus` startet einen [Azure Service Bus Emulator](https://learn.microsoft.com/en-us/azure/service-bus-messaging/test-locally-with-service-bus-emulator)
mit eigenem SQL Server als Speicher (`servicebus-db`, **nicht** die Dev-Datenbank `mssql`).
Damit laufen Jobs erstmals lokal von Anfang bis Ende: `GetChanges` erkennt Änderungen, legt sie
auf die passende Queue, `Process*QueueItems` verarbeitet sie.

| Container | Ports | Zweck |
|---|---|---|
| `servicebus` | 5672 (AMQP), 5300 (Management/Health) | Der Emulator selbst |
| `servicebus-db` | intern | SQL Server als Speicher des Emulators |
| `servicebus-explorer` | `SERVICEBUS_EXPLORER_PORT` (Standard 8090) | Grafische Web-Oberfläche, siehe [Service Bus einsehen](../debuggen/podman/#service-bus-einsehen) |

Die sieben Queues (`sbqexport`, `sbqhighexport`, `sbqlowexport`, `sbqignored`,
`sbqtransitivehigh`, `sbqtransitivestandart`, `sbqtransitivelow`) sind in `dev/servicebus/config.json`
mit denselben Eigenschaften wie in Azure vorkonfiguriert (`LockDuration PT30S`,
`DeadLetteringOnMessageExpiration true`, `MaxDeliveryCount` 3 bzw. 6 für die transitiven Queues)
— mit einer Ausnahme: `DefaultMessageTimeToLive` steht auf `PT1H` (1 Stunde) statt der in Azure
üblichen `P14D`, weil der Emulator maximal 1 Stunde unterstützt und mit einem längeren Wert gar
nicht erst startet.

Aktivieren: `dev/up.sh servicebus` (nur der Emulator) oder automatisch über `getchanges`/`process`
(siehe [Verarbeitung schalten](#verarbeitung-schalten)). Die DataBridge bekommt dann den
Connection-String `Endpoint=sb://servicebus;...;UseDevelopmentEmulator=true;` statt des
Dummy-Strings. Mit dem Profil läuft automatisch auch `servicebus-explorer` mit — eine grafische
Web-Oberfläche für die Queues, siehe [Service Bus einsehen](../debuggen/podman/#service-bus-einsehen).

> **Admin-Client braucht Port 5300**
>
> Der `ServiceBusAdministrationClient` (Queue-Status/MCP-Diagnose) spricht mit dem Emulator über
> dessen Management-API auf Port 5300, der normale `ServiceBusClient` (Senden/Empfangen) über den
> AMQP-Port 5672. `ServiceBusService` hängt den Port deshalb nur beim Erzeugen des Admin-Clients an
> den Endpoint an, wenn der Connection-String `UseDevelopmentEmulator=true` enthält und noch keinen
> Port hat. In Azure passiert dort nichts, weil der volle Hostname bereits einen impliziten
> Standardport hat und `UseDevelopmentEmulator` nicht gesetzt ist.



Grenzen des Emulators: kein AMQP over WebSockets, keine Entra-ID-Authentifizierung. Beides nutzt
die DataBridge lokal nicht.

## Verarbeitung schalten

Standardmäßig sind `GetChanges` (Timer) und die drei `Process*QueueItems`-Funktionen deaktiviert
— die Umgebung startet, ohne dass etwas verarbeitet wird. Zwei unabhängige Schalter:

| Variable (`dev/.env`) | Argument | Wirkung |
|---|---|---|
| `DATABRIDGE_GETCHANGES` | `getchanges` | `AzureWebJobs.GetChanges.Disabled` |
| `DATABRIDGE_PROCESS` | `process` | `AzureWebJobs.Process{Standard,High,Low}PriorityQueueItems.Disabled` |

Werte `an`/`off`. Beide aktivieren automatisch das Profil `servicebus` (siehe oben). Die
Durable-Functions-Kette (`StartGetChangesOrchestration`, `GetChangesOrchestrator`,
`GetChangesActivity`, `JobState`) bleibt in der Testumgebung immer deaktiviert.

### Einen Job manuell ausführen (`ExecuteJob`)

Der schnellste Weg, einen Job Ende-zu-Ende zu testen, ohne auf den Timer zu warten: die HTTP-Function
`ExecuteJob` löst für **einen** Job genau das aus, was `GetChanges` sonst für alle aktiven Jobs im
Timer-Takt tut — Änderungen erkennen und auf die passende Service-Bus-Queue legen. Ist `process`
an, greift `Process*QueueItems` (läuft dauerhaft im Hintergrund) die Nachricht Sekunden später
automatisch ab und schreibt ins Ziel.

1. `dev/up.sh mssql process` (Windows: `dev\up.ps1 mssql process`) — Datenbank, Verarbeitung und
   Service-Bus-Emulator starten. `getchanges` wird dafür **nicht** gebraucht.
2. Job-Namen ermitteln — Muster `<quelle>-to-<ziel>-<tabelle>` (siehe [Jobs](#jobs)), z. B.
   `mssql-to-mssql-account` bei nur einer Datenbank, oder per MCP `list_jobs` nachsehen.
3. Auslösen:




<ul class="nav nav-tabs" id="tabs-8" role="tablist">
  <li class="nav-item">
      <button class="nav-link active"
          id="tabs-08-00-tab" data-bs-toggle="tab" data-bs-target="#tabs-08-00" role="tab"
          data-td-tp-persist="windows (powershell)" aria-controls="tabs-08-00" aria-selected="true">
        Windows (PowerShell)
      </button>
    </li><li class="nav-item">
      <button class="nav-link"
          id="tabs-08-01-tab" data-bs-toggle="tab" data-bs-target="#tabs-08-01" role="tab"
          data-td-tp-persist="linux / macos" aria-controls="tabs-08-01" aria-selected="false">
        Linux / macOS
      </button>
    </li>
</ul>

<div class="tab-content" id="tabs-8-content">
    <div class="tab-body tab-pane fade show active"
        id="tabs-08-00" role="tabpanel" aria-labelled-by="tabs-08-00-tab" tabindex="8">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span>Invoke-RestMethod -Method Post <span style="color:#e6db74">&#39;http://localhost:7062/api/ExecuteJob?code=local&#39;</span> `
</span></span><span style="display:flex;"><span>  -ContentType <span style="color:#e6db74">&#39;application/json&#39;</span> -Body <span style="color:#e6db74">&#39;{&#34;JobName&#34;:&#34;mssql-to-mssql-account&#34;}&#39;</span>
</span></span></code></pre></div>
    </div>
    <div class="tab-body tab-pane fade"
        id="tabs-08-01" role="tabpanel" aria-labelled-by="tabs-08-01-tab" tabindex="8">
        <div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s -X POST <span style="color:#e6db74">&#39;http://localhost:7062/api/ExecuteJob?code=local&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -H <span style="color:#e6db74">&#39;Content-Type: application/json&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  -d <span style="color:#e6db74">&#39;{&#34;JobName&#34;:&#34;mssql-to-mssql-account&#34;}&#39;</span>
</span></span></code></pre></div>
    </div>
</div>


> **`IsActive` ist hier nicht nötig**
>
> Anders als der Timer prüft `ExecuteJob` `IsActive` nicht — es sucht den Job nur über den Namen
> (`ProcessJobsForRetrievalInOrderService.ExecuteByJobName`). Die Seed-Jobs bleiben also bewusst
> `IsActive=false` (siehe [Jobs](#jobs)) und lassen sich trotzdem einzeln per `ExecuteJob` testen.
> `IsActive: true` (per MCP `update_job` oder direkt in Azurite) ist erst nötig, wenn stattdessen der
> **Timer** (`getchanges`) selbst anhand des Cron-Ausdrucks erkennen soll, welche Jobs fällig sind —
> Verarbeitung bleibt so ein bewusster Opt-in, kein Automatismus.



**Nachschauen, ob's geklappt hat:**

- **Log** ([Logging](#logging)) zeigt beide Schritte durch — `GetChanges` erkennt X Änderungen,
  `ProcessStandardQueueItems` verarbeitet die Nachricht.
- Bei einem Mock-Job zusätzlich das **WireMock-Protokoll**
  (siehe [„Was hat die DataBridge wirklich geschickt?“](#http-mocks-wiremock)), ob der PATCH/POST
  ankam.
- Per MCP `list_queue_statuses`/`get_queue_status`, ob die Nachricht auf der Queue schon
  verarbeitet ist (Anzahl zurück auf 0).

## Debuggen

Für Breakpoints und Step-Through läuft die DataBridge lokal als Prozess statt im Container —
sowohl gegen diesen Podman-Stack (`dev/up.sh --host`) als auch gegen eine echte Azure-Instanz.
Eigener Artikel: [Debuggen](../debuggen/).

## Fehlersuche

| Problem | Ursache und Lösung |
|---|---|
| `up` bricht mit Port-Konflikt ab | Ein lokaler Datenbankserver oder Azurite belegt den Port. Port in `dev/.env` ändern oder den anderen Dienst stoppen. |
| `up` meldet „Timeout“ | Das Skript zeigt die letzten Logzeilen aller Container. Meist ist ein Init-Container an einem SQL-Fehler gescheitert. |
| Init-Container kann `/init` nicht lesen (Fedora/RHEL) | SELinux. Die Mounts in `compose.dev.yml` tragen dafür das Flag `:z` — bei eigenen Mounts ergänzen. |
| Skripte scheitern unter Windows mit merkwürdigen Fehlern | Zeilenenden. `.gitattributes` erzwingt LF für `dev/**/*.sh`, `*.sql` und `*.py`; bei einem vorhandenen Checkout die Dateien neu auschecken. |
| PowerShell zeigt bei `podman compose` rot „Executing external compose provider …“ (`NativeCommandError`) | Nur ein Hinweis von Podman auf stderr, kein echter Fehler. Windows PowerShell 5.1 zeigt ihn trotzdem als Fehler an. `up.ps1`/`down.ps1` schalten ihn selbst ab und kapseln Aufrufe, bei denen `podman compose` seinen Fortschritt auf stderr schreibt (z. B. „Restarting“). Bei direkten Aufrufen vorher `$env:PODMAN_COMPOSE_WARNING_LOGS = 'false'` setzen oder dauerhaft in `%APPDATA%\containers\containers.conf` unter `[engine]` `compose_warning_logs = false` eintragen. |
| Windows: `podman machine start` meldet `win-sshproxy.exe failed to start`, danach scheitert `podman compose` mit „failed to connect to the docker API at npipe:////./pipe/podman-machine-default“ | Ohne `win-sshproxy` gibt es keine Docker-API-Pipe, über die `docker-compose` spricht. Ursache ist eine liegengebliebene Socket-Datei `%TEMP%\podman\podman-machine-default-api.sock` (im Windows-Ereignisprotokoll, Quelle „.NET Runtime“: `bind: Only one usage of each socket address`). Sie bleibt bei jedem `podman machine stop` liegen und lässt sich als Unix-Socket weder per `Remove-Item` noch per `del` löschen. `up.ps1` und `down.ps1` benennen den Ordner deshalb vor dem VM-Start in `%TEMP%\podman.stale-<Zeitstempel>` um; Podman legt ihn neu an. Von Hand: VM stoppen, Ordner umbenennen, VM starten. Die `podman.stale-*`-Ordner können jederzeit gelöscht werden. |
| Windows PowerShell 5.1 zeigt Umlaute verstümmelt („fÃ¼r“) | PowerShell 5.1 liest Skripte ohne BOM als ANSI. `up.ps1` und `down.ps1` sind deshalb als UTF-8 **mit BOM** gespeichert — beim Bearbeiten so lassen. PowerShell 7 ist davon nicht betroffen. |
| 401 beim Aufruf der API | Der Function Key fehlt. Er ist fest auf `local` gesetzt (`?code=local`). |
| Oracle braucht lange | Der erste Start legt die Datenbank an. Der Init-Container wartet bis zu 2,5 Minuten auf den App-User. |
| Geänderte Daten sind nach `up` weg | Gewollt: Die Testdaten werden bei jedem Start zurückgesetzt. |
| WireMock startet nicht, Port-Konflikt auf 8089 | `WIREMOCK_PORT` in `dev/.env` ändern. 9090 ist unter Fedora oft von Cockpit belegt. |
| Mock antwortet mit 404 auf alles | Der Mock ist nicht gewählt (`dev/up.sh … bc`), oder die Anfrage passt auf keine Regel. `/__admin/requests/unmatched` zeigt, welche Anfragen keinen Treffer hatten, mit einem Vergleich zur nächstliegenden Regel. |
| `CheckList` meldet für `openapi-dev` „could not find table customers“ | Bekannter Fehler im OpenAPI-Connector: `GetTableNames` liefert Spec-Pfade (`/v1/customers`) statt Ressourcennamen. Das liegt nicht am Mock. |
| `up` hängt bei „Warte auf Service-Bus-Emulator“ | Der Emulator startet erst, wenn `servicebus-db` (SQL Server als Speicher) healthy ist — das dauert beim ersten Start etwas. Bei echtem Timeout: `podman compose -f dev/compose.dev.yml logs servicebus servicebus-db` prüfen, häufig ein Fehler in `dev/servicebus/config.json`. |
| MCP-Tools `list_queue_statuses`/`get_queue_status` liefern einen Verbindungsfehler | Profil `servicebus` läuft nicht (kein `servicebus`/`getchanges`/`process` beim letzten `up`-Aufruf angegeben), `ExportQueueServiceBus` zeigt dann auf den Dummy-Connection-String. |
