Virtual Tables¶
Eine Virtual Table ist eine typisierte Tabelle mit Datensätzen, die eine Organisation für ihre Agents, Workflows und Integrationen führt: abzugleichende Bestellungen, zu verarbeitende Dateien, nachzufassende Leads.
Tabellen sind Metadaten plus JSONB. Nichts legt eine physische SQL-Tabelle an: Eine
Tabelle zu erstellen kostet eine Zeile, das Umbenennen einer Spalte ändert keinen
Datensatz, und kein Tenant kann den Datenbankkatalog wachsen lassen. Jeder Lese- und
Schreibzugriff läuft über einen einzigen Service, VirtualTableService, sodass die
Konsole, Agent-Tools, Workflow-Knoten und die öffentliche API dieselben Regeln teilen.
Diese Seite beschreibt den Service und seine HTTP-Routen unter /api/v1/tables; das
OpenAPI-Dokument ist ihr Vertrag.
Der Tabellenbereich füllt den verfügbaren Platz automatisch, ohne Vergrößerungsknopf. Das Datensatzpanel hat einen eigenen Full screen-Knopf, der alle Felder ohne Verlust laufender Änderungen vergrößert; Exit full screen stellt das Seitenpanel wieder her. Zeilen liegen direkt unter der Leiste, ohne zweiten Rahmen oder leeres gepolstertes Panel. Der Katalog füllt den verfügbaren Seitenbereich, bietet einen eigenen Vollbildknopf und zeigt Seitennavigation nur bei mehreren Seiten.
Jede Tabelle zeigt die gesperrten Datensatzfelder created_at, updated_at, created_by und updated_by. Es sind dieselben Eigenschaften wie über Markdown, keine bearbeitbaren Kopien in Zellen. Vor der ersten Aktualisierung zeigt updated_at das Erstellungsdatum. Die Felder bleiben sichtbar, wenn eigene Spalten ausgeblendet werden. Ihre Namen sind beim Hinzufügen und Umbenennen von Spalten reserviert; bestehende ältere Namen bleiben lesbar.
Lege einen typisierten Standardwert in den Spalteneinstellungen oder beim Hinzufügen einer Spalte fest. Neue Datensätze verwenden ihn nur, wenn das Feld fehlt; ein expliziter Wert oder null hat Vorrang. Auswahl-Standardwerte dürfen beim Einreichen eines Schemas IDs oder Bezeichnungen nennen; der Server speichert IDs. Wähle die Farbe jeder Option in ihrer Liste; Automatisch behält die bisherige Palette. Farbänderungen erhalten Options-IDs und die Bedeutungsrevision der Spalte.
Die Schnelleingabe steht direkt hinter den geladenen Zeilen in deren Scrollbereich. Der CSV-Export der Konsole enthält die vier Datensatzeigenschaften. API-Exporte behalten ihr bisheriges Format, sofern nicht include_metadata: true angefordert wird. Beim Erstellen einer Tabelle aus einem exportierten CSV werden Systemeigenschaften übersprungen; importierte Datensätze erhalten neue Systemmetadaten.
Geben Sie in einem Markdown-Dokument / am Zeilenanfang ein, um einen Block zu suchen: Aufgabe, Überschrift, Liste, Zitat, Code, Trennlinie, Tabelle, Bild, Video oder Link. Pfeiltasten und Eingabe wählen einen Block; Escape schließt das Menü. Kopierte Bilder lassen sich direkt einfügen. PNG-, JPEG-, GIF- und WebP-Bilder bis 5 MiB werden mit der Tabelle gespeichert, zählen zu ihrem Speicherlimit und unterliegen ihren aktuellen Zugriffsrechten. Sie bleiben nach der Archivierung verfügbar, auch über Verweise im Datensatzverlauf.
Die Spaltentypen user und agent verweisen auf ein Organisationsmitglied oder einen zugänglichen KI-Agenten; multi_user und multi_agent auf bis zu 100 verschiedene Identitäten. Wählen Sie nach Namen aus Vorschlägen mit Avataren. Gespeichert werden stabile UUIDs; Mitgliedschaft und Agentenzugriff werden beim Schreiben und bei Standardwerten geprüft. Einzelne Referenzen unterstützen eq, ne, in und is_null, mehrere Referenzen contains und is_null. Identitätsspalten sind nicht sortierbar.
Dokumentvorlagen im Markdown-Feld öffnen die Bibliothek dieser Spalte. Speichern Sie das aktuelle Dokument, passen Sie eine Startvorlage an, bearbeiten oder löschen Sie benannte Vorlagen und wählen Sie einen Standard. Dieser füllt neue Datensätze automatisch; Änderungen überschreiben keine vorhandenen Dokumente. Pro Spalte sind bis zu 10 Vorlagen mit insgesamt 100.000 Zeichen möglich. Normale Datensatzeigenschaften stehen im Datensatzpanel über den Dokumentinhalten.
Die Datensatzauswahl zeigt eine schwebende Aktionsleiste, ohne das Raster zu verschieben. Der Öffnen-Button bleibt neben dem Auswahlkästchen angeheftet, auch beim horizontalen Scrollen. Jedes Datensatzpanel zeigt Erstellungs- und Änderungszeiten sowie Autoren mit Avataren. Seitenpanels starten in Preview, ganzseitige Dokumente in Write.
Externe Clients¶
Die Herkunft eines Schreibvorgangs wird getrennt vom autorisierenden Mitglied gespeichert. Neue Schreibvorgänge setzen created_source und updated_source mit Art, ID und Name: Person, Agent, API-Schlüssel, Workflow oder System. Die Konsole zeigt den Avatar einer Person, das Porträt eines Agenten oder ein API-/Workflow-Symbol neben dem Namen in Systemspalten und Dokumenteigenschaften. Beim Erstellen werden beide Quellen gesetzt; spätere Änderungen erhalten den Ersteller. Alte Datensätze behalten ihr gespeichertes Mitglied, ohne eine historische Herkunft zu erfinden. Diese Felder lassen sich weder durch Eingabewerte noch Markdown setzen.
Organisations-API-Keys werden auf /api/v1/tables akzeptiert. Senden Sie
Authorization: Bearer aos_…; die Organisation kommt vom Key. Lesen erfordert
tables:view, Datensatz- und Schemaänderungen tables:edit, das Anlegen einer Tabelle
tables:create. Sowohl der aktuelle Zugriff des Ausstellers als auch die Scopes des
Keys gelten. Historie und Quittungen nennen den Aussteller; der Key erweitert dessen
Zugriff nicht. Eine unzugängliche Tabelle antwortet 404, eine fehlende
Sammlungsberechtigung 403, eine veraltete Revision 409, eine ungültige Abfrage
oder ein ungültiger Datensatz 422. Eine Wiederholung mit demselben
Idempotency-Key und Inhalt liefert das ursprüngliche Ergebnis ohne weiteres Created-Ereignis.
POST /api/v1/tables/{id}/records/query akzeptiert columns (bis zu 100 IDs aktiver
Spalten) neben Filtern, Sortierung, skip und limit. Nur diese Zellen und Link-Labels
erreichen die Antwort. null behält den ganzen Datensatz; [] liefert nur Metadaten.
Berechnete Spalten werden vor der Projektion ausgewertet; ihre Abhängigkeiten müssen
nicht ausgewählt sein. Dieselben Datensätze erscheinen sofort in der Konsole; die
asynchrone Ausführung des Tabellen-Triggers verzögert die CRUD-Antwort nicht.
Wie eine Tabelle aufgebaut ist¶
| Teil | Was es ist | Identität |
|---|---|---|
| Table | Ein Name, ein Besitzer, eine Sichtbarkeit und Grants, wie bei einer Context-Datei | id, stabil |
| Schema version | Ein unveränderlicher Schnappschuss der Spalten. Eine Änderung hängt Version N+1 an | version |
| Column | Eine Bezeichnung, ein Typ, ob sie leer sein darf, ein optionaler Standardwert | id, stabil |
| Option | Eine Auswahl einer Select-Spalte | id, stabil |
| Record | Zellwerte, nach Spalten-id geordnet, eine Revision und eine optionale external_id |
id, stabil |
Werte werden über die id der Spalte adressiert, nie über ihre Bezeichnung. Ein Umbenennen schreibt daher eine Schema-Version um und keinen Datensatz. Ein Datensatz merkt sich die Schema-Version, unter der er zuletzt geschrieben wurde.
Ein Datensatz speichert nur die Zellen, die einen Wert enthalten. Eine Zelle, die als
null gesendet wird, wird geleert, und ein Lesezugriff zeigt für sie nichts an. Bei einem
Create füllt der Standardwert einer Spalte nur die Zellen, die Sie weglassen; eine Zelle,
die Sie als null senden, bleibt leer.
Spaltentypen¶
| Typ | Gespeichert als | Filter |
|---|---|---|
text |
Text bis 1.000 Zeichen | eq ne contains starts_with in is_null |
long_text |
Text bis 100.000 Zeichen | wie text |
rich_text |
Markdown-Quelltext mit bis zu 100.000 Zeichen | wie text |
email |
Eine Adresse mit einem @ und einem Punkt in der Domain, bis 254 Zeichen |
wie text |
url |
Eine http- oder https-Adresse mit Host, bis 2.048 Zeichen |
wie text |
phone |
Ziffern, Leerzeichen und + ( ) - . /, mindestens fünf Ziffern |
wie text |
number |
Eine endliche Zahl | eq ne lt lte gt gte in is_null |
integer |
Eine ganze Zahl, höchstens 2^53 - 1 | wie number |
rating |
Eine ganze Zahl von 1 bis 5 | wie number |
boolean |
true oder false |
eq ne is_null |
date |
YYYY-MM-DD |
wie number |
datetime |
ISO 8601 mit Zeitzone, als UTC gespeichert | wie number |
single_select |
Die id einer Option | eq ne in is_null |
multi_select |
Eine Liste von Options-ids | contains is_null |
link |
Eine Liste von bis zu 100 Datensatz-ids der verknüpften Tabelle | contains (eine Datensatz-id) is_null |
lookup, rollup |
Nichts: beim Lesen eines Datensatzes ermittelt | keine |
formula |
Nichts: beim Lesen aus den übrigen Spalten des Datensatzes ermittelt | keine |
Text wird genau so gespeichert, wie er gesendet wurde. Führende und nachgestellte Leerzeichen, Zeilenumbrüche und Werte, die nur aus Leerzeichen bestehen, sind Daten des Benutzers und werden deshalb nicht beschnitten. Es gilt nur eine Längenbegrenzung, und das NUL-Zeichen wird abgelehnt, in Zellen ebenso wie in Namen, Bezeichnungen, Beschreibungen und external ids, weil PostgreSQL es nicht speichern kann.
Vergleiche treffen nur Zellen, die einen Wert enthalten. Mit is_null finden Sie die
leeren. contains und starts_with nehmen ein Stück eines Werts, sodass
@example.com in einer email-Spalte die Adressen einer Domain findet.
Die Tabellen-Tools eines Agenten und die Tabellenschritte eines Workflows benennen eine Auswahl stattdessen über das Label ihrer Option, ohne Beachtung der Groß- und Kleinschreibung: Sie schreiben und filtern per Label und lesen die Auswahl eines Datensatzes als Labels zurück, auch die einer archivierten Option. Ein Wert, der keine aktive Option nennt, wird mit den Optionen der Spalte abgelehnt.
Formatierter Text und Markdown¶
Wähle Rich text (Markdown) für Notizen mit Überschriften, Listen, Aufgaben, Tabellen, Links und Code. Ein Zellklick öffnet den Datensatz. Write bietet ein Dokumentfeld mit Werkzeugen für Fett, Kursiv, Links, Bilder und Video. Preview nutzt den Chat-Renderer für Links und Bilder; direkte HTTP(S)-Links auf MP4, WebM oder Ogg werden als Video mit Steuerelementen ohne Autoplay angezeigt. Rohes HTML wird nicht ausgeführt. Expand editor öffnet mehr Platz. Gespeicherte Dokumente starten in der Vorschau; Write wechselt zur Bearbeitung.
Kanban zeigt eine Karte unter dem Zeiger und markiert die Zielspalte; beim Speichern steht die Karte bereits dort. Bei abgelehntem Schreiben kehrt sie zurück und die Konfliktaktionen bleiben erhalten. Das Menü Move to ist per Tastatur erreichbar.
Dokumenteigenschaften stehen über dem Markdown-Editor. Erstellt, Zuletzt geändert und Erstellt von stammen aus dem Datensatz und lassen sich nicht über Markdown ändern. Das Änderungsdatum folgt Schreibvorgängen am gesamten Datensatz, auch an anderen Zellen; vor der ersten Änderung gilt das Erstellungsdatum.
Dokumenteigenschaften gehören in typisierte Tabellenspalten. Der Markdown-Editor erstellt oder bearbeitet keine YAML-Eigenschaften mehr. Neue Notizbuchvorlagen enthalten nur Markdown. Bereits importierte Header bleiben bei Inhaltsänderungen unverändert erhalten, damit historische Daten nicht verloren gehen. Automatische Datumsangaben und Schreibquellen stammen weiterhin aus dem Datensatz.
Wähle die Standardvorlage beim Erstellen einer Rich-Text-Spalte oder unter Columns. Leeres Dokument, Notiz, Besprechungsnotizen und Projektbeschreibung sind anpassbare Startpunkte. Die Vorlage nutzt den vorhandenen Spaltenwert default: Konsole, API und Workflow übernehmen ihn beim Erstellen, wenn die Zelle fehlt. Ein ausdrücklich angegebener Wert, auch eine leere Zeichenfolge, hat Vorrang. Eine Vorlagenänderung betrifft nur neue Datensätze. Spaltenvorlage verwenden ersetzt nach Bestätigung den Inhalt eines bestehenden Dokuments und behält seine Eigenschaften. Textabfragen und Konvertierungen behalten ihre bisherigen Verträge.
Verknüpfungen, Nachschlagen und Zusammenfassungen¶
Eine link-Spalte hält Datensätze einer anderen Tabelle oder derselben,
benannt durch link_table_id. Beim Schreiben wird geprüft, dass jeder Datensatz
in dieser Tabelle existiert und der Schreibende sie sehen kann, sodass eine
Verknüpfung nicht verrät, welche ids eine private Tabelle hat. Ein Datensatz
liefert seine Verknüpfungen benannt zurück, in links, nach dem ersten
Textwert jedes verknüpften Datensatzes, sonst nach seiner externen id. Ein
inzwischen gelöschter verknüpfter Datensatz fällt einfach weg.
Ein lookup liest eine Spalte der verknüpften Datensätze über eine
Verknüpfungsspalte derselben Tabelle (link_column_id, target_column_id): die
Werte, eine Auswahl nach ihrem Label. Ein rollup ermittelt daraus etwas:
count der Verknüpfungen oder sum, avg, min oder max einer Zahlenspalte -
min und max auch von Datumswerten. Beide werden beim Lesen ermittelt und nie
gespeichert, geschrieben, gefiltert oder sortiert.
Was ein Nachschlagen zeigt, reicht nicht weiter als der Leser darf: Eine verknüpfte Tabelle, die der Leser nicht sieht, lässt die Verknüpfungen unbenannt und jedes Nachschlagen und jede Zusammenfassung darüber leer. Eine Verknüpfung behält ihre Tabelle für immer. Ein Nachschlagen liest eine Spalte mit Werten, keine andere Verknüpfung und kein Nachschlagen, und das Archivieren einer Verknüpfungsspalte wird abgelehnt, solange etwas über sie liest. Ein Export schreibt eine Verknüpfung mit den Namen ihrer Datensätze und ein Nachschlagen mit dem Gelesenen.
Formeln¶
Eine formula-Spalte ermittelt einen Wert aus den übrigen Spalten des
Datensatzes: {Price} * {Quantity}, IF({Total} > 100, "large", "small"),
ROUND({Total} / {Count}, 2), DAYS({Due}, TODAY()). Eine Spalte wird mit ihrem
Label in geschweiften Klammern genannt, und eine Umbenennung schreibt jede Formel
um, die sie nennt. Eine Formel rechnet mit + - * / %, vergleicht mit
= <> < <= > >=, verbindet Bedingungen mit and, or und not und ruft IF,
ROUND, ABS, MIN, MAX, CONCAT, LEN, UPPER, LOWER, DAYS und TODAY
auf. Eine Auswahl liest sich als ihr Label.
Sie darf jede lebende Spalte mit Werten lesen, auch das Ergebnis eines Nachschlagens oder einer Zusammenfassung, aber keine Verknüpfung und keine andere Formel, und eine Spalte, die eine Formel liest, kann nicht archiviert werden. Beim Speichern des Schemas wird die Formel geprüft und gesagt, was nicht stimmt. Ein Wert, der sich nicht ermitteln lässt - eine Division durch null, Text in einer Summe, eine leere Zelle - lässt die Zelle dieses Datensatzes leer. Eine Formel wird beim Lesen ermittelt und nie gespeichert, geschrieben, gefiltert oder sortiert.
Ein Schema ändern¶
PUT /tables/{id}/schema nimmt die vollständige Liste der Spalten entgegen, die die
Tabelle haben soll, und die expected_version, die der Aufrufer zuletzt gelesen hat.
Der Service gleicht sie mit den aktuellen Spalten ab:
- Eine Spalte mit
idist diese Spalte. Eine Spalte ohneidist neu. - Der Typ einer Spalte ändert sich nur in einen, als den sich ihre Werte lesen lassen, und die Änderung schreibt den Wert jedes Datensatzes neu. Jeder Wert wird zu Text; Text wird zu einer Zahl, einem Ja/Nein, einem Datum oder einer Einfachauswahl, wenn er eines sagt - eine Auswahl erhält je unterschiedlichem Wert eine Option; eine ganze Zahl wird zur Zahl und zurück, wenn sie ganz ist; ein Datum wird zu Datum und Uhrzeit; eine Auswahl wird zu mehreren und zurück, wenn es eine ist. Jede andere Änderung wird abgelehnt, ebenso eine, die ein Wert nicht übersteht - mit Anzahl und Beispiel, und nichts wird geschrieben. Was die Spalte verwendet, lehnt sie wie eine Archivierung ab. Die Revision eines Datensatzes rückt weiter, sodass eine Bearbeitung gegen den alten Wert ein Konflikt ist.
- Text wird zu einer
email-,url-,phone- oderrating-Spalte, wenn jeder Wert sich so liest, und eine Zahl wird zu einer Bewertung, wenn sie eine ganze Zahl von 1 bis 5 ist. Jede davon wird wieder zu Text, eine Bewertung auch zu einer Zahl. - Nichts wird gelöscht. Eine weggelassene Spalte oder Option wird archiviert: Ihre
Werte bleiben les- und filterbar, und ein Schreibzugriff darauf wird mit
ARCHIVED_COLUMNabgelehnt. - Archivierte zählen zu den Grenzen. Eine Tabelle hat höchstens 100 Spalten und eine
Select-Spalte höchstens 100 Optionen, Archivierte eingeschlossen. Eine Änderung, die eine
davon überschreiten würde, wird mit
INVALID_SCHEMAabgelehnt und hängt keine Version an; eine volle Liste von Optionen lässt sich also nicht ersetzen. Fügen Sie stattdessen eine neue Spalte hinzu. - Eine neue Pflichtspalte braucht einen Standardwert, weil bestehende Datensätze nichts für sie enthalten. Eine bestehende Spalte kann nicht zur Pflichtspalte werden, solange ein Datensatz keinen Wert für sie hat, und eine Pflichtspalte kann ohne Standardwert nicht aus dem Archiv zurückkehren, weil Datensätze, die während der Archivierung geschrieben wurden, keinen halten konnten.
- Eine veraltete
expected_versionergibtSCHEMA_VERSION_CONFLICT. - Eine Übermittlung, die den aktuellen Spalten entspricht, mit denselben ids, derselben Reihenfolge, denselben Bezeichnungen und Optionen, ändert nichts: Es wird keine Version angehängt, und die aktuelle Tabelle wird zurückgegeben. Eine neue Reihenfolge oder eine geänderte Bezeichnung ist eine Änderung.
Datensätze werden nicht umgeschrieben. Ein unter Version 1 geschriebener Datensatz bleibt unter Version 4 lesbar und bearbeitbar; eine Pflichtspalte, die er nie hatte, erhält beim nächsten Bearbeiten des Datensatzes ihren Standardwert.
Jede Spalte trägt eine schreibgeschützte revision, die für eine neue Spalte und
für jede Spalte, die vor ihrem Hinzufügen existierte, 0 ist. Sie zählt die
Änderungen daran, was die Werte der Spalte bedeuten, sodass eine veröffentlichte
Seite erkennen kann, dass die Spalte, gegen die sie
veröffentlicht wurde, nicht mehr dieselbe ist. Sie steigt um eins bei:
- einer Typkonvertierung;
- einer Änderung an einer Formel, verglichen, nachdem eine Umbenennung durch sie hindurchgeschrieben wurde; eine Spalte umzubenennen, die eine Formel liest, zählt also nicht, wohl aber, wenn eine andere Spalte das Label übernimmt;
- einem anderen Link-, Lookup- oder Rollup-Ziel oder einem anderen Aggregat;
- dem Archivieren oder Wiederherstellen der Spalte oder dem Archivieren oder Wiederherstellen einer ihrer Select-Optionen.
Ein neues Label, ein neuer Standardwert oder eine neue Pflichtigkeit, eine
umbenannte Select-Option und eine neue Option lassen sie, wie sie war. Ein Filter
hält Options-IDs, daher ändert nichts davon, was ein Filter trifft. Eine
TableRead-Spalte kann in einer Schemaänderung so zurückgeschickt werden, wie sie
kam: Das Feld wird akzeptiert und ignoriert, und nur die Änderung selbst setzt es.
Ein Schreibzugriff auf einen Datensatz und eine Schemaänderung oder Archivierung derselben Tabelle kommen nacheinander dran: Der Schreibzugriff wartet auf eine laufende und wird dann an dem gemessen, was sie committet hat, sodass ein Datensatz nie in einer Tabelle landet, die einen Moment zuvor archiviert wurde.
Beim Archivieren einer Spalte oder der ganzen Tabelle wird zuerst jeder registrierte
Dependency-Checker gefragt, ob etwas, das der Aufrufer sowohl sehen als auch ändern
kann, sie noch verwendet. Gespeicherte Ansichten (siehe
Gespeicherte Ansichten) und die Workflows, die die Tabelle lesen oder
schreiben, registrieren ihre in app/services/virtual_tables/dependencies.py, ebenso
Tabellen-Trigger, die jeden blockieren, der archiviert: Ein Trigger, der nach einer
nicht mehr vorhandenen Spalte filtert, käme kaputt zurück.
Eine Ablehnung führt jeden
Abhängigen in SCHEMA_DEPENDENCY mit kind, id und name auf, und name ist null
für einen Trigger in einem Workflow, den der Aufrufer nicht öffnen kann. Die Konsole
zeigt sie im Dialog, der gefragt hat, einen Workflow als Link darauf. Jeder andere
Abhängige wird nie genannt und blockiert den Aufrufer nie: sein Feature kommt
stattdessen selbst mit der Änderung zurecht.
Eine Tabelle archivieren und wiederherstellen¶
Eine Tabelle wird nie gelöscht, sondern archiviert. Eine archivierte Tabelle behält
ihre Datensätze, Spalten, Ansichten und ihren Verlauf, und jedes Lesen antwortet
weiterhin. Jeder Schreibzugriff wird mit TABLE_ARCHIVED abgelehnt, egal wer fragt:
Kein Datensatz wird angelegt, geändert oder gelöscht, und weder ihr Name noch ihre
Spalten ändern sich. Ihr Name ist für eine neue Tabelle frei, und sie zählt weiterhin
zum Tabellenlimit der Organisation.
In der Konsole findet ein Mitglied, das die Tabelle bearbeiten darf, Archive table
im Menü More actions auf ihrer Seite. Der Dialog sagt, was das Archivieren
bewirkt, bevor sich etwas ändert. Ein veröffentlichter Workflow, der die Tabelle noch
liest oder schreibt und den der Aufrufer bearbeiten darf, lehnt die Archivierung mit
SCHEMA_DEPENDENCY ab, und der Dialog führt ihn als Link auf. Gespeicherte Ansichten
und Trigger blockieren nur das Archivieren einer Spalte.
Die Seite einer archivierten Tabelle sagt, wann sie archiviert wurde. Für ein Mitglied, das die Tabelle bearbeiten darf, trägt der Hinweis Restore, das sie unter ihrem Namen zurückholt; alle anderen lesen ihre Datensätze und erfahren, wer sie wiederherstellen kann. Der Katalog listet aktive Tabellen, bis darüber Archived included gewählt wird, und die Karte einer archivierten Tabelle sagt, dass sie archiviert ist.
| Aktion | Route | Abgelehnt mit |
|---|---|---|
| Archivieren | POST /tables/{id}/archive |
SCHEMA_DEPENDENCY (409), solange etwas die Tabelle verwendet. Zweimal archivieren ist kein Fehler |
| Wiederherstellen | POST /tables/{id}/unarchive |
BAD_REQUEST (400), wenn die Tabelle nicht archiviert ist; ALREADY_EXISTS (409), wenn eine aktive Tabelle inzwischen ihren Namen übernommen hat |
Beide brauchen das Recht, diese Tabelle zu bearbeiten, per Rolle oder per Grant auf
ihr, und alle anderen erhalten ein 404. can_edit ist auf einer archivierten Tabelle
false, daher sagt TableRead.can_restore, ob dieser Aufrufer sie wiederherstellen
darf. Beides wird auditiert, als table.archived und table.unarchived, und zählt
zum Schreib-Rate-Limit für Tabellen. Ein inzwischen vergebener Name wird zuerst an der
aktiven Tabelle geändert, weil eine archivierte nicht umbenannt werden kann.
Datensätze und Revisions¶
Jeder Datensatz hat eine revision, die bei 1 beginnt und mit jeder Änderung steigt.
| Operation | Route | Braucht expected_revision |
|---|---|---|
| Erstellen | POST /tables/{id}/records |
Nein |
| Genannte Zellen ändern | PATCH /tables/{id}/records/{record_id} |
Ja |
| Löschen | DELETE /tables/{id}/records/{record_id}?expected_revision= |
Ja |
| Upsert | PUT /tables/{id}/records/by-external-id/{external_id} |
Nur wenn der Datensatz existiert |
| Lesen, exists | GET .../records/{record_id}, .../by-external-id/{external_id}, .../exists |
Nein |
Ein Update oder Delete mit einer alten Revision wird mit REVISION_CONFLICT (409) und
details.current_revision abgelehnt; nichts wird überschrieben. Lesen Sie den Datensatz
erneut und versuchen Sie es noch einmal. Ein Upsert, der einen bestehenden Datensatz findet
und kein expected_revision erhält, bekommt REVISION_REQUIRED (428), wieder mit der
zu sendenden Revision.
Eine external id hat 1 bis 255 Zeichen und darf / enthalten, wie in 2026/ORD-1. Sie darf weder NUL noch einen Zeilenumbruch enthalten. Der Service
prüft das ebenso wie die Routen. Über HTTP lehnt die Route es zuerst ab, mit
VALIDATION_ERROR; ein Aufrufer, der den Service direkt nutzt, erhält INVALID_RECORD.
Gleichzeitige Upserts derselben external id erzeugen einen Datensatz. Der Verlierer findet ihn und wird wie ein Update beantwortet: Er braucht die Revision oder erfährt, welche er senden muss.
Ein Update, das jede Zelle unverändert ließe, ändert nichts. Die Revision bleibt, es wird
keine Historienzeile und kein Receipt geschrieben, und der aktuelle Datensatz wird
zurückgegeben. Eine veraltete expected_revision ist weiterhin ein Konflikt, weil sie
zuerst geprüft wird. Ein Upsert, der den Datensatz findet, folgt derselben Regel.
Ein Delete ist ein hartes Löschen. Die Historie des Datensatzes bleibt, bis ihre Aufbewahrung sie entfernt.
Datensätze in der Konsole bearbeiten¶
Katalog und die Ansichten Table, Kanban und List verwenden ein gemeinsames Panel mit Suche und gespeicherten Ansichten. Add record nutzt zwei Spalten auf großen Bildschirmen und eine auf dem Telefon; Langtext und Verknüpfungen behalten die volle Breite. Das Datensatz-Panel nutzt die gemeinsame Tastatur- und Fokussteuerung der Konsole. Columns klappt vorhandene Auswahloptionen hinter ihrer Anzahl ein. Ladefehler bei Datensätzen bieten Retry.
Die Karten des Katalogs sagen, wie viele Datensätze und aktive Spalten jede Tabelle hat, wer sie erreichen darf und wann sie sich zuletzt geändert hat. Auf der Seite einer Tabelle zeigt jede Kopfzeile den Typ ihrer Spalte als Symbol, Zahlen stehen rechtsbündig, und Daten lesen sich so, wie Ihre Sprache sie schreibt. Das Datensatz-Panel trägt die erste Spalte des Datensatzes als Titel und sagt, wann er hinzugefügt und zuletzt geändert wurde.
Ein Mitglied, das die Tabelle bearbeiten darf, fügt Datensätze auf ihrer Seite hinzu,
ändert und löscht sie. Add record fragt nach einem Wert je aktiver Spalte, im Typ
der Spalte. Eine Pflichtspalte ohne Standardwert ist mit * markiert und muss
ausgefüllt sein, bevor der Datensatz geschrieben wird; jede andere leere Spalte
übernimmt ihren Standardwert. Ein Klick auf eine Zelle bearbeitet sie direkt: Enter
oder ein Klick daneben speichert, Escape lässt sie unverändert, und ein Ja/Nein, das
nicht leer sein darf, wechselt mit einem Klick. Die Pfeiltasten wechseln zwischen Zellen, und die Schaltfläche zum Aufklappen am
Zeilenende öffnet den ganzen Datensatz.
Die Zeile unter dem Raster fügt Datensätze beim Tippen hinzu: Was du tippst, landet in der ersten Textspalte, und Enter legt den Datensatz an und lässt die Zeile für den nächsten bereit. Eine Tabelle mit einer weiteren Pflichtspalte öffnet stattdessen Add record mit dem getippten Wert, sodass nichts geschrieben wird, bevor alle Pflichtwerte da sind.
Jede Bearbeitung ist ein PATCH gegen die angezeigte Revision, daher wird eine
Bearbeitung, die gegen eine neuere Änderung verliert, abgelehnt statt darüber
geschrieben. Der Datensatz öffnet sich dann mit dem abgelehnten Wert neben Reload and
reapply. Markierte Zeilen bieten Delete für alle an, jede gegen ihre eigene
Revision: Ein zwischenzeitlich geänderter Datensatz bleibt erhalten, und die Konsole
sagt, wie viele es waren. Das Datensatz-Panel löscht einen einzelnen Datensatz auf
dieselbe Weise.
Ein Löschen wartet ein paar Sekunden mit Undo in seiner Meldung, bevor es gesendet wird; die Datensätze verschwinden sofort aus jeder Ansicht, und Undo bringt sie unverändert zurück. Wer die Tabelle nur ansehen darf, sieht dasselbe Raster schreibgeschützt, und ein Klick auf eine Zeile öffnet den Datensatz.
Für ein Mitglied, das die Tabelle bearbeiten darf, öffnet der Kopf einer Spalte ein Menü. Sort ascending und Sort descending sortieren das Raster nach ihr, und Hide in this view nimmt sie vom Bildschirm, bis die Schaltfläche für ausgeblendete Spalten sie wieder zeigt; Save view speichert beides. Rename und Archive column ändern die Tabelle für alle, jeweils als dieselbe neue Schemaversion, die der Dialog Columns schreiben würde, und eine Archivierung von etwas, das noch verwendet wird, wird abgelehnt, mit den Workflows, Ansichten und Triggern im Dialog. Das + nach der letzten Spalte fügt eine hinzu, anfangs optional. Der Typ einer Spalte ändert sich in Columns, in die Typen, in die sich ihre Werte umwandeln lassen.
In Columns macht Required eine Spalte zur Pflicht, und eine archivierte Spalte wandert unter Archived columns, wo Restore sie samt ihren Werten zurückbringt.
Import und Export¶
Exporte lesen höchstens 4 MiB des entpackten Datensatz-JSON pro Seite (oder
einen Datensatz), mit maximal 500 Datensätzen pro Seite. Eine Organisation kann
einen Export gleichzeitig übertragen und zehn pro Minute starten. Weitere
Anfragen erhalten 429 mit Retry-After; danach erneut versuchen. Die Startbegrenzung
bleibt bei ausgefallenem Redis pro Prozess erhalten; die Datenbank erzwingt
weiterhin einen gleichzeitigen Export pro Organisation.
Ein Servercursor hält den Datensatz-Snapshot des Exports stabil und lädt höchstens
vier Datensätze vorab. Er schließt nach Abschluss oder Abbruch; spätere Seiten
durchlaufen frühere Datensätze nicht erneut.
Export, unter … neben Share, speichert, was die Seite zeigt, als CSV-Datei: die Datensätze, die Filter und
Suche treffen, in der Reihenfolge des Rasters, unter seinen sichtbaren Spalten. POST
/tables/{id}/records/export tut dasselbe für einen Aufrufer, mit denselben Filtern,
Suche, Sortierung und einer Liste columns. Eine Option wird als ihre Bezeichnung
geschrieben, mehrere als a; b, und eine Textzelle, die eine Tabellenkalkulation als
Formel lesen würde, beginnt mit '. Mehr als 100.000 passende Datensätze werden mit
EXPORT_TOO_LARGE (413) abgelehnt.
Import, im selben Menü, liest eine komma- oder semikolongetrennte CSV-Datei, deren erste Zeile die
Spalten benennt. Jede Spalte der Datei wird der gleichnamigen Tabellenspalte oder
External id zugeordnet und kann umgelenkt oder übersprungen werden. Ein Wert, der
sich nicht als Typ seiner Spalte lesen lässt, lässt seine Zeile scheitern, bevor etwas
gesendet wird. Der Rest geht zu je 200 an POST /tables/{id}/records/batch, das jeden
Datensatz einzeln schreibt und die abgelehnten mit ihren Codes auflistet, und die Konsole
listet jede gescheiterte Zeile mit ihrer Zeilennummer. Jeder hinzugefügte Datensatz
startet die Trigger der Tabelle wie jeder andere.
Mit zugeordneter External id importiert Update records already here eine Datei
erneut, um die Tabelle zu aktualisieren: Eine Zeile, deren externe ID die Tabelle schon
hat, ändert die genannten Zellen dieses Datensatzes, statt als vergeben abgelehnt zu
werden (update_existing im Batch), und der Import zählt, was er hinzugefügt und was er
aktualisiert hat.
New table beginnt mit einer Spalte Name. Geben Sie zuerst den Tabellennamen und die Spalten an; Beschreibung und Sichtbarkeit stehen unter More options, standardmäßig privat. Stattdessen kann sie mit einer Datei beginnen: Start from a CSV file macht aus der Kopfzeile Spalten und gibt jeder den engsten Typ, als den sich alle ihre Werte lesen lassen - Ganzzahl, Zahl, Ja/Nein, Datum, Datum und Uhrzeit in ISO, eine E-Mail-Adresse oder ein Link - und sonst Text, oder langen Text, sobald ein Wert einen Zeilenumbruch hat oder länger als 1.000 Zeichen ist. Der Name kommt aus der Datei, und beides lässt sich vor dem Erstellen ändern, was weniger kostet als eine spätere Typänderung. Sobald die Tabelle existiert, öffnet sich der Import mit jeder Spalte schon zugeordnet, danach die Tabelle.
Sichere Wiederholungen¶
Jeder Schreibzugriff auf Datensätze akzeptiert einen Idempotency-Key-Header (höchstens
128 Zeichen). Eine Wiederholung mit demselben Schlüssel und demselben Body liefert die
erste Antwort mit Idempotent-Replayed: true und schreibt nichts, selbst wenn sich der
Datensatz inzwischen geändert hat. Derselbe Schlüssel mit einem anderen Body wird mit
IDEMPOTENCY_KEY_REUSED abgelehnt.
Der Header kennzeichnet ein wiederholtes Create, Update oder Upsert. Ein wiederholtes Delete antwortet wie beim ersten Mal mit 204 und wird nicht gekennzeichnet.
Ein Schlüssel gehört dem Aufrufer und der Art des Schreibzugriffs, sodass zwei Aufrufer dieselbe Zeichenfolge verwenden können und ein Aufrufer sie für ein Create und ein Upsert nutzen kann. Nur Erfolge werden gespeichert: Ein abgelehnter Schreibzugriff hinterlässt keine Quittung; Sie korrigieren ihn also und wiederholen ihn mit demselben Schlüssel.
Eine Wiederholung wird nur einem Aufrufer beantwortet, der die Tabelle noch bearbeiten darf. Nach dem Entzug des Zugriffs ist dieselbe Wiederholung ein 404.
Ein Receipt hält 24 Stunden. Danach ist der Schlüssel vergessen, und derselbe Schlüssel mit demselben Body ist ein neuer Schreibzugriff: Er wird erneut ausgeführt, statt die erste Antwort zurückzugeben. Wiederholen Sie innerhalb des Fensters und behandeln Sie eine längere Pause als neue Anfrage. Die Lebensdauer wird beim Verwenden des Schlüssels durchgesetzt und gilt daher auf die Stunde genau; der tägliche Sweep gibt nur den Platz von Receipts frei, die niemand wiederholt hat.
Auflisten und Filtern¶
GET /tables/{id}/records blättert durch eine Tabelle; POST /tables/{id}/records/query
fügt typisierte Filter hinzu, die alle zutreffen müssen. Beide sind begrenzt: limit
liegt zwischen 1 und 100, skip bei höchstens 10.000, und eine Abfrage hat höchstens 20
Filter.
search ist Text, den ein Datensatz enthalten muss, ohne Beachtung der Groß- und
Kleinschreibung, in einer aktiven Text- oder Langtextspalte oder in der Bezeichnung
einer Auswahloption, die er hält. Die Suche wird mit den Filtern kombiniert, und eine
leere Suche sucht nichts. Eine gespeicherte Ansicht bewahrt sie neben ihren Filtern auf.
In der Konsole sendet das Suchfeld sie, und Filter schreibt die Bedingungen: eine
Spalte, einen Operator, den ihr Typ unterstützt, und einen Wert. Jede vollständige
Bedingung grenzt die Datensätze sofort ein, und Save view speichert Bedingungen,
Suche und Sortierung in der angezeigten Ansicht.
Die Reihenfolge ist total. Auf die gewünschte Sortierung (created_at, updated_at
oder eine sortierbare Spalte) folgt die Datensatz-id, sodass eine Seite in einer
unveränderten Tabelle nie einen Datensatz wiederholt oder überspringt. Datensätze ohne
Wert in der sortierten Spalte kommen in beiden Richtungen zuletzt. Eine
multi_select-Spalte lässt sich nicht sortieren. updated_at wird beim Erstellen eines
Datensatzes gesetzt und rückt mit jeder Bearbeitung vor, sodass Datensätze, die niemand
bearbeitet hat, nach ihrer Erstellungszeit sortieren.
Eine Auflistung hat kein total, weil das Zählen einer gefilterten Tabelle nicht billig
ist: has_more sagt, ob eine weitere Seite folgt. POST /tables/{id}/records/count
beantwortet diese Frage getrennt, für die Filter und die Suche einer Abfrage, und zählt
höchstens bis 100.000; capped sagt, dass mehr passen. Die Konsole zeigt diese Zahl neben
den Ansichts-Tabs, und ihr Raster lädt beim Scrollen jeweils hundert Datensätze und
zeichnet nur die sichtbaren Zeilen. Jenseits der 10.000 Datensätze, die eine Abfrage
überspringen darf, bittet es um einen Filter oder eine Suche.
Gespeicherte Ansichten¶
Sortieren neben Filter wählt eine sortierbare Spalte und aufsteigende oder absteigende Reihenfolge für Tabelle, Liste oder Karten innerhalb jeder Kanban-Spalte. Ausgeblendete sortierbare Spalten und Systemdaten bleiben verfügbar. Die Schaltfläche zeigt Spalte und Richtung. Der Server sortiert vor der Seiteneinteilung, auch noch nicht geladene Datensätze. Ansicht speichern behält die Reihenfolge zusammen mit Filtern und Darstellung; + erstellt eine neue Ansicht. Standardreihenfolge: älteste zuerst stellt die Sortierung nach Erstellungsdatum wieder her.
Ansichtseinstellungen steuert Reihenfolge und Sichtbarkeit aller Spalten einschließlich Systemfelder. Ziehe Spalten oder nutze die Pfeile; ausgeblendete Systemfelder behalten ihre automatischen Werte. Text umbrechen zeigt vollständigen Text mit Zeilenumbrüchen, ausgeschaltet bleiben Zeilen kompakt. Wähle automatische, ISO-, Tag-zuerst-, Monat-zuerst- oder lange Datumsangaben und automatische, 24-Stunden-, 12-Stunden- oder ausgeblendete Uhrzeiten. Reine Datumswerte behalten ihren Tag; Zeitstempel verwenden die Zeitzone der betrachtenden Person. Ansicht speichern speichert die Auswahl für diese Ansicht; + erstellt eine neue. Formatierung ändert weder Werte noch Filter.
Ein Kanban-Board ohne gespeicherte Gruppierung nutzt die einzige aktive Einfachauswahl-Spalte automatisch. Bei mehreren wählen Sie die Spalte für die Bahnen. Eine gespeicherte Ansicht behält diese Gruppierung. Listenzeilen benennen zusätzliche Felder und zeigen Unnamed record bei leerem Titel. Triggers bietet Open workflows, wenn noch kein Workflow von der Tabelle startet.
Eine Ansicht ist ein gespeicherter Filter, eine Sortierung und eine Gruppierung
über die Datensätze einer Tabelle - was die Tabellen-/Kanban-/Listenansichten der
Konsole speichern, damit niemand bei jedem Besuch dasselbe Board neu aufbaut. Sie
ist eine Unterressource der Tabelle, keine eigene teilbare Ressource: Eine Ansicht
hat keinen eigenen Besitzer und keine eigenen Grants ihrer Art, und shared
bedeutet nur "sichtbar für jeden, der bereits tables:view auf der übergeordneten
Tabelle hält" - sie erweitert den Zugriff nie über das hinaus, was die Tabelle
selbst erlaubt.
In der Konsole heißt der Bildschirm ohne gewählte Ansicht All records, und + daneben speichert, was er zeigt, als neue Ansicht. Ein Kanban-Board ohne gespeicherte Gruppierung nutzt die einzige aktive Einfachauswahl-Spalte automatisch. Bei mehreren wählen Sie die Spalte für die Bahnen. Eine gespeicherte Ansicht behält diese Gruppierung. Eine Karte zeigt unter ihrem Titel bis zu vier Spalten des Bildschirms und lässt leere weg. Eine Tabelle ohne Einfachauswahl-Spalte bietet stattdessen Add a column an.
Der Eigentümer einer Ansicht schaltet sie mit der Schaltfläche neben Rename zwischen Private und Shared um. Wer die Ansicht eines anderen Mitglieds verwaltet, etwa ein Admin, bekommt diesen Schalter nicht: Ob jemandes eigene Filter geteilt werden, entscheidet diese Person selbst.
GET/POST /tables/{id}/views und GET/PATCH/DELETE /tables/{id}/views/{view_id}
listen, erstellen, lesen, aktualisieren und löschen sie. Die Liste ist mit skip
und limit (höchstens 100) seitenweise: zuerst die eigenen Ansichten des Aufrufers,
dann die geteilten, jeweils nach Name; total zählt alle. config ist {filters,
search, sort, visible_columns, group_by, display} - eine RecordQuery plus Darstellungsfelder, die
nur die Darstellung der Konsole braucht: visible_columns (null bedeutet jede
lebende Spalte) und group_by (eine lebende single_select-Spalte, für die
Spalten eines Kanban-Boards).
Das Objekt display enthält column_order, hidden_system_columns, wrap_text, date_format und time_format. Ältere Ansichten behalten kompakte Zeilen und sprachabhängige Formate. Ein ausdrückliches visible_columns: [] blendet alle benutzerdefinierten Spalten aus.
| Feld | Bedeutung |
|---|---|
kind |
table, kanban oder list - eine Ansicht wird für eine Art gespeichert |
visibility |
private (nur ihr Besitzer) oder shared (jeder, der die Tabelle sehen kann) |
can_manage |
Ob dieser Aufrufer sie umbenennen, umkonfigurieren oder teilen darf |
can_delete |
Ob dieser Aufrufer sie löschen darf |
Auflisten, Lesen und Löschen lösen sich gegen die Tabelle auf (tables:view); eine
Ansicht zu erstellen oder zu ändern braucht tables:edit auf der Tabelle, sodass ein
Besitzer, dem das Bearbeitungsrecht entzogen wurde, seine Ansichten noch löschen,
aber nicht mehr umbauen oder teilen kann.
Eine Ansicht zu ändern oder zu löschen ist
enger: nur ihr Besitzer, oder ein Aufrufer, dessen
Scope für tables:edit ALL ist - nicht "jeder, der die Tabelle
bearbeiten darf" - sodass ein geteilter Bearbeiter nicht stillschweigend den
gespeicherten Filter eines anderen Mitglieds umbiegen kann. Die Ablehnung
funktioniert wie bei jedem anderen Schreibzugriff auf eine einzelne Ressource
hier: NOT_FOUND (404), nie ein 403, der einem abgelehnten Aufrufer verraten
würde, dass die Ansicht existiert. can_manage und can_delete sagen, welches von
beidem dieser Aufrufer darf.
Das Archivieren einer Spalte wird mit SCHEMA_DEPENDENCY abgelehnt und nennt die
Ansicht, wenn eine Ansicht, die der Aufrufer sowohl sehen als auch ändern kann, sie
noch zum Filtern, Sortieren oder Gruppieren nutzt: eine seiner eigenen oder - für
einen Aufrufer, dessen Scope für tables:edit ALL ist - eine geteilte. Jede andere
Ansicht blockiert das Archivieren nicht und wird nicht genannt, weil der Aufrufer sie
nicht aus dem Weg räumen könnte; das gilt für jede private Ansicht eines anderen
Mitglieds, die selbst einem Aufrufer mit Scope ALL nicht offengelegt wird. Eine
Spalte nur in visible_columns anzuzeigen blockiert ebenfalls nicht.
Was eine Ansicht noch von einer Spalte nennt, die nicht mehr lebt, wird beim Lesen
der Ansicht weggelassen: ein Filter darauf entfällt, eine Sortierung danach fällt auf
created_at zurück, eine Gruppierung danach wird geleert, und sie verlässt
visible_columns - eine Ansicht, die keine ihrer gewählten Spalten mehr zeigt, zeigt
alle lebenden. Die gespeicherte Konfiguration wird nicht umgeschrieben.
Seiten und Ansichten¶
Ein veröffentlichtes Artefakt liest nicht über eine
Ansicht. Seine Bindings sind Seitenabfragen: eine Tabelle, Filter, eine
Sortierung, ein Limit und die Felder, die die Seite liest, die die Plattform beim
Veröffentlichen auf der Version der Seite speichert. Ein Binding kann von einer
gespeicherten Ansicht ausgehen - ein Agent findet eine mit list_views, das für
die Tabellen, die er lesen darf, die ID jeder Ansicht, ihre Spalten und ob sie
geteilt ist liefert -, aber die Filter und die Sortierung der Ansicht werden dann
in die Seite kopiert.
Die Ansicht danach zu bearbeiten, zu teilen, umzubenennen
oder zu löschen, ändert nichts an der Seite, und visible_columns und group_by
einer Ansicht werden ignoriert: Die fields des Bindings bestimmen die Spalten.
Eine Ansicht mit Freitextsuche lässt sich nicht binden, weil eine Suche jede aktive Textspalte trifft und mit jeder hinzugefügten Spalte breiter würde; das Veröffentlichen wird abgelehnt und nennt die Ansicht. Ebenso eine Ansicht, deren Filter eine nicht mehr vorhandene Spalte nennt.
Wer die Datensätze der Seite lesen darf, entscheidet nicht die Ansicht. Sie werden
mit dem Zugriff des Besitzers der Seite gelesen, sobald der Besitzer eingewilligt
hat, und der Zugriff auf eine Tabelle gilt ganz oder gar nicht: read gibt jede
Zeile und jede aktive Spalte. Eine Seitenabfrage ist der Weg, auf dem die Zeilen
einer Tabelle Personen erreichen, die sie nicht öffnen könnten, und der Besitzer
wird zuvor gefragt. Siehe
Artefakte.
Eine Seitenabfrage hält die revision jeder Spalte fest, die sie erreicht: ihre
Felder, ihre Filter- und Sortierspalten, die Eingaben einer Formel, den Link und das
Ziel eines Lookups oder Rollups und die Spalte, nach der jede verknüpfte Tabelle
ihre Datensätze beschriftet. Ein Binding wird unavailable (source_changed),
wenn eine davon sich weiterentwickelt hat oder archiviert wurde oder wenn eine
Tabelle, die es erreicht, archiviert wurde; die anderen Bindings arbeiten weiter.
Nichts hindert dafür eine Tabellenänderung: Eine Seite ist keine Abhängigkeit, die
ein Archivieren blockiert. share_view gibt es nicht mehr; es gibt keine private
Ansicht, die für eine Seite zu teilen wäre.
Trigger¶
Kanban-Spalten laden jeweils 25 Karten. Weitere laden behält vorhandene Karten; nach einem Seitenfehler ist ein erneuter Versuch möglich. Eine geänderte Gruppierung kann in der aktiven Ansicht gespeichert werden. Werden Spaltentypen durch ein anderes Mitglied geändert, entfernt das Lesen einer privaten Ansicht inkompatible Filter, Sortierung und Gruppierung aus der Antwort, ohne die gespeicherte Ansicht ihres Eigentümers zu überschreiben. Eine Seitenabfrage, die einen Filter verliert, erfordert erneute Zustimmung.
Tabellenereignisse verwenden beim Einfügen nach der Änderungssperre die Echtzeituhr, passend zur Trigger-Aktivierung. Eine vor der Aktivierung begonnene Transaktion kann daher ein späteres berechtigtes Ereignis erzeugen. Die Migration 0229_table_event_wall_clock.py ändert künftige Zeitstempel; sie spielt zuvor übersprungene Ereignisse nicht erneut ab, um doppelte Auswirkungen zu vermeiden.
Ein Workflow, dessen Trigger-Knoten
Table record ist, läuft nach der Veröffentlichung für das, was mit den Datensätzen
seiner Tabelle geschieht - hinzufügen, ändern oder löschen, je nachdem, was events
nennt; standardmäßig nur hinzufügen -, egal auf welchem Weg: in der Konsole, über die
API, durch das Tabellen-Tool eines Agents oder den Tabellenschritt eines anderen
Workflows. Ein Upsert, der einen Datensatz anlegt, ist ein Hinzufügen, einer, der ihn
aktualisiert, eine Änderung. Das Veröffentlichen braucht Lesezugriff auf die Tabelle und
das Recht, den Workflow auszuführen, denn er läuft als das Mitglied, das ihn
veröffentlicht hat. Dessen Zugriff wird bei jedem Ereignis erneut geprüft.
Der Trigger führt die Version aus, die ihn eingeschaltet hat, und die nächste Veröffentlichung verschiebt ihn auf die neue Version. Seine Filter nutzen die Operatoren aus Auflisten und Filtern und werden am Datensatz geprüft, wie er hinzugefügt oder geändert wurde, oder wie er vor dem Löschen war. Eine Änderung startet ihn nur, wenn sich eine der für die Tabelle gewählten Spalten geändert hat, sofern Spalten gewählt sind.
Er gibt dem Run den Datensatz: record_id, event, values nach Spalten-ID,
dieselben Werte als fields nach Beschriftung und author_id, bei einer Änderung
oder Löschung auch before und die changed Spalten nach Beschriftung. Triggers
auf der Seite der Tabelle listet die Workflows, pausiert und setzt jeden fort und
öffnet seinen Verlauf.
Ein Trigger startet nur für Datensätze, die hinzukommen, während er eingeschaltet ist.
Das Einschalten - eine Veröffentlichung oder das Fortsetzen - nimmt die Schema-Sperre der Tabelle, auf die jeder
Schreibzugriff wartet, sodass kein vorher committeter Datensatz ihn je startet und nichts
nachgeholt wird, was hinzukam, während er aus war. Der Job workflow-table-triggers, der alle zehn Sekunden läuft, liest das
Outbox-Ereignis jedes neuen Datensatzes, sodass ein Trigger nach etwa zehn Sekunden startet. Er entscheidet
einmal pro Trigger und hält die Entscheidung fest; ein zweiter Durchlauf oder ein zweiter
Worker findet sie und startet nichts.
Verlauf listet jede Entscheidung, die neueste zuerst, ohne die Werte des Datensatzes:
| Angezeigt als | Warum |
|---|---|
| Lauf gestartet | Jeder Filter galt; der Run ist verlinkt |
| Übersprungen | Der Datensatz passte nicht oder kam hinzu, bevor der Trigger eingeschaltet war |
| Blockiert | Er hätte sich erneut gestartet, die Kette ging mehr als fünf Trigger tief oder über 50 Runs, oder die Zulassungsquote lehnte den Run ab |
| Konnte nicht starten | Das Mitglied, als das er läuft, darf die Tabelle nicht mehr lesen oder den Workflow nicht mehr ausführen, oder das Anlegen des Datensatzes war nicht lesbar |
Ein Workflow, der in eine Tabelle schreibt, kann deren Trigger starten, und so weiter über weitere Tabellen. Jeder Run trägt die Kette, zu der er gehört, und ein Trigger, den die Kette schon durchlaufen hat, wird blockiert statt erneut gestartet - so laufen zwei Workflows, die Datensätze in die Tabellen des jeweils anderen schreiben, nicht im Kreis. Eine Spalte, auf die ein Trigger filtert, kann nicht archiviert werden, bis der Trigger seines Workflows nicht mehr auf sie filtert und veröffentlicht ist, auch wenn er pausiert ist, und eine Tabelle, mit der ein veröffentlichter Workflow startet, kann nicht archiviert werden, bis dieser Workflow anders startet.
Was gemeinsam committet¶
Ein Schreibzugriff auf einen Datensatz, seine Historienzeile, seine
Idempotenz-Quittung und bei einem Create eine Outbox-Zeile table.record.created
werden in einer Transaktion geschrieben und gemeinsam committet oder zurückgerollt. Ein
Fehler in irgendeinem Schritt hinterlässt keines davon. Änderungen an Tabelle und
Schema werden im Audit-Log festgehalten; Änderungen an Datensätzen in
der Historie pro Datensatz, die die von jeder Änderung berührten Zellen aufbewahrt.
Zwei dieser Speicher halten Kopien dessen, was geschrieben wurde. Ein Receipt enthält den ganzen Datensatz, wie ihn der Schreibzugriff zurückgab, und die Historie enthält, was sich geändert hat. Ein Löschen des Datensatzes entfernt also die aktuelle Zeile und lässt beide zurück, bis ihre Aufbewahrung sie entfernt. Outbox-Zeilen enthalten ids. Behandeln Sie alle drei als personenbezogene Daten, wenn es die Zellen sind; siehe Datenschutz und Limits und Aufbewahrung.
Die Outbox-Zeile ist die Übergabe an alles, was auf einen neuen Datensatz reagiert: heute die Trigger. Ihr Heartbeat holt sich nicht zugestellte Zeilen in einer eigenen Session, prüft jede gegen die Trigger der Tabelle und markiert sie in derselben Transaktion als versendet. Eine nicht zugestellte Zeile wird nur durch ihr eigenes, viel längeres Aufbewahrungsfenster entfernt (unten) - eine Dead-Letter-Frist für einen Worker, der so lange ausgefallen ist, keine Behauptung, das Ereignis sei je abgeholt worden.
Limits und Aufbewahrung¶
Ein Tenant kann die gemeinsame Datenbank nur so weit wachsen lassen, wie das Deployment es
zulässt. Jedes Limit ist eine Einstellung des Deployments, gilt je Organisation, sodass
die Nutzung eines Tenants nie auf einen anderen angerechnet wird, und wird mit
QUOTA_EXCEEDED (402) abgelehnt, wenn ein Schreibzugriff es überschreiten würde.
| Einstellung | Standard | Begrenzt |
|---|---|---|
TABLES_MAX_PER_ORGANIZATION |
200 | Tabellen einer Organisation. Archivierte zählen mit, weil eine Tabelle nie gelöscht wird |
TABLES_MAX_RECORDS_PER_TABLE |
100.000 | Datensätze in einer Tabelle. Ein Update eines Datensatzes in einer vollen Tabelle ist erlaubt |
TABLES_MAX_RECORD_BYTES |
1.000.000 | Die serialisierten Werte eines Datensatzes in Bytes |
TABLES_MAX_STORAGE_BYTES_PER_ORGANIZATION |
1,073,741,824 | Gespeicherte PostgreSQL-JSONB-Nutzdaten je Organisation über Datensätze, Historie, Quittungen und Outbox; Tabellen- und Index-Overhead zählt separat |
Die Gesamtgrenze wird im Savepoint der Änderung unter einer Organisationssperre vor dem Commit geprüft. Eine Überschreitung rollt den Datensatz und alle Kopien zurück, auch eine Importzeile; übrige Zeilen werden als abgelehnt gemeldet. Quittungswiederholungen, unveränderte Schreibvorgänge, Lesen und Löschen bleiben nach einer Absenkung möglich. Die Aufbewahrungsbereinigung gibt entfernte Bytes frei.
Die Ablehnung nennt das Limit und seine Obergrenze in details ({"quota": "records",
"limit": 100000}), nie den Inhalt, und schreibt einen Eintrag table.quota_refused in das
Audit-Log mit denselben zwei Feldern. Die abgelehnte Anfrage schreibt
nichts. Schreibzugriffe sind außerdem auf RATE_LIMIT_TABLE_WRITES_PER_MINUTE (300) je
Mitglied und Organisation begrenzt, in der Konsole ebenso wie über die API; ein Mitglied
darüber erhält ein 429 mit Retry-After. Siehe Konfiguration.
Was die Historie aufbewahrt. Ein Create hält den ganzen Datensatz in after, ein Delete
den ganzen Datensatz in before; das Datensatzlimit begrenzt beides. Ein Datensatz, der
älter als das Limit ist oder geschrieben wurde, bevor TABLES_MAX_RECORD_BYTES gesenkt
wurde, kann immer noch darüber liegen - sein Delete hält before dann als
{"omitted": {"bytes": <seine Größe>, "limit": <das Limit>}} statt der Werte, und gelingt
trotzdem. Ein Update hält nur die Zellen, die sich geändert haben: before enthält ihre
früheren Werte und after die neuen, und eine Spalte, die auf einer Seite fehlt, war dort
leer. Das Bearbeiten einer Zelle eines großen Datensatzes kostet daher eine Zelle, wie oft
es auch wiederholt wird.
Aufbewahrung. Der tägliche Aufbewahrungs-Sweep entfernt auch Tabellendaten, hart und in Batches, für jede Organisation:
| Was | Entfernt, wenn | Einstellung |
|---|---|---|
| Receipts | Älter als 24 Stunden | TABLES_RECEIPT_TTL_HOURS |
| Outbox-Zeilen | Vor mehr als 3 Tagen zugestellt | TABLES_OUTBOX_RETENTION_DAYS |
| Nicht zugestellte Outbox-Zeilen | Nie zugestellt und 30 Tage alt: Der Trigger-Heartbeat lief so lange nicht, und für diese Datensätze startet kein Trigger mehr | TABLES_OUTBOX_UNDISPATCHED_RETENTION_DAYS |
| Historie | Älter als 365 Tage, für einen gelöschten Datensatz ebenso wie für einen lebenden | TABLES_HISTORY_RETENTION_DAYS |
Der Sweep schreibt einen Audit-Eintrag je Organisation, der die Klasse (table_receipts,
table_outbox, table_history) und die Anzahl nennt. Es sind Einstellungen des Deployments,
keine je Organisation. Die Datensätze selbst und die Tabellen entfernt er nie.
RATE_LIMIT_TABLE_WRITES_PER_MINUTE ist ein Kontingent je Mitglied, daher skaliert das
Budget eines Durchlaufs für jede der drei Klassen sowohl damit als auch mit der Anzahl der
aktiven Mitglieder der Organisation, mit Spielraum, damit ein bestehender Rückstand schrumpft
statt nur gehalten zu werden - jedes Mitglied, das gleichzeitig ununterbrochen schreibt, läuft
dem Sweep nie davon, bis zu einer großzügigen Grenze, für wie viele Mitglieder sich der
Durchlauf einer Organisation bemisst. Die Zahlen stehen in der
Konfiguration. Ein Rückstand darüber
hinaus wird wie bei jeder anderen Klasse über mehrere Durchläufe abgearbeitet.
Wer was darf¶
| Permission | Wer sie hält |
|---|---|
tables:view |
Owner, Admin, Builder und Operator sehen alle Tabellen; ein Member oder Viewer sieht die eigenen, die organisationsweit sichtbaren und die geteilten |
tables:edit |
Owner und Admin bearbeiten alle; ein Builder bearbeitet die eigenen und geteilten; ein Member die eigenen |
tables:create |
Owner, Admin, Builder, Member |
tables:view und tables:edit sind Ressourcen-Permissions, daher erweitert ein
Grant auf eine Tabelle eine Rolle nur für diese Tabelle: Ein Viewer
mit edit auf einer Tabelle bearbeitet diese Tabelle und sonst nichts. Das Teilen
nutzt dieselben /tables/{id}/sharing-Routen wie die anderen geteilten Ressourcen.
Datensätze erben den Zugriff ihrer Tabelle, und das Schema erzwingt es: Eine Zeile in
Records, History oder Outbox verweist auch über die Organisation auf ihre Tabelle und kann
keine Tabelle eines anderen Tenants nennen.
Die Tabelle einer anderen Organisation und eine, die der Aufrufer nicht erreichen darf, sind beide ein 404. Ein Kontext ohne angemeldetes Subjekt erreicht nichts.
Ob ein bestimmter Aufrufer eine bestimmte Tabelle bearbeiten darf, steht auch
direkt auf dem Wire: TableSummary.can_edit und TableRead.can_edit werden
serverseitig aufgelöst (Rollen-Scope oder ein expliziter Grant) und bei jedem
Lesen mitgeschickt, genau wie Agent.can_run - sodass eine Katalogzeile oder eine
Detailseite nie raten muss, ob ihre Bearbeitungskontrollen abgelehnt würden. Das
eigene can_manage und can_delete einer gespeicherten Ansicht sind dieselbe Idee,
eine Ebene tiefer (siehe Gespeicherte Ansichten).
Fehler¶
Jede Ablehnung antwortet mit {"error": {"code", "message", "details"}}, und der
code ist das, wonach ein Client verzweigt.
| Code | Status | Bedeutung |
|---|---|---|
REVISION_CONFLICT |
409 | Der Datensatz hat sich seit dem Lesen geändert |
REVISION_REQUIRED |
428 | Ein Upsert eines bestehenden Datensatzes braucht expected_revision |
SCHEMA_VERSION_CONFLICT |
409 | Das Schema hat sich seit dem Lesen geändert |
SCHEMA_DEPENDENCY |
409 | Etwas hängt von dem ab, was die Änderung entfernt |
TABLE_ARCHIVED |
409 | Die Tabelle lehnt Schreibzugriffe ab |
BAD_REQUEST |
400 | Eine Wiederherstellung nannte eine Tabelle, die nicht archiviert ist |
ALREADY_EXISTS |
409 | Der Tabellenname, ein Ansichtsname oder die external id ist vergeben |
INVALID_RECORD |
422 | Ein Wert passt nicht zu seiner Spalte; details.fields nennt jeden |
ARCHIVED_COLUMN |
422 | Ein Wert nennt eine archivierte Spalte |
INVALID_QUERY |
422 | Ein Filter oder eine Sortierung, die die Tabelle nicht beantworten kann |
INVALID_SCHEMA |
422 | Eine widersprüchliche Schemaänderung |
IDEMPOTENCY_KEY_REUSED |
422 | Der Schlüssel wurde für eine andere Anfrage verwendet |
QUOTA_EXCEEDED |
402 | Der Schreibzugriff würde ein Speicherlimit überschreiten; details nennt das Limit (tables, records, record_bytes, storage_bytes) und seine Obergrenze |
RATE_LIMIT_EXCEEDED |
429 | Zu viele Tabellenschreibzugriffe in der letzten Minute; siehe Retry-After |
VALIDATION_ERROR |
422 | Die Anfrage selbst ist fehlerhaft: ein falscher Typ, ein unbekanntes Feld, eine Grenze oder NUL, ein Zeilenumbruch oder ein einzelnes Surrogat in einer id, einem Schlüssel oder Namen. Die Route lehnt sie ab, bevor der Service läuft |
AUTHORIZATION_ERROR |
403 | Dem Aufrufer fehlt die Permission, die eine Collection-Route verlangt (tables:view, tables:create) |
CONCURRENT_CHANGE |
409 | Ein Upsert hat ein Rennen mit dem Löschen desselben Datensatzes verloren. Wiederholen Sie ihn |
NOT_FOUND |
404 | Keine solche Tabelle oder kein solcher Datensatz, oder nicht erreichbar für den Aufrufer |
Den Service aus Python aufrufen¶
service = VirtualTableService(db)
table = await service.create_table(
ctx,
TableCreate(
name="Orders",
columns=[
ColumnInput(label="Customer", type="text"),
],
),
)
customer = str(table.columns[0].id)
written = await service.upsert_record(
ctx,
table.id,
"ORD-1042",
RecordUpsert(values={customer: "Acme"}),
operation_key="import-2026-09-21-row-17",
)
# Send the revision back to change it; a stale one raises RevisionConflictError.
await service.upsert_record(
ctx,
table.id,
"ORD-1042",
RecordUpsert(values={customer: "Acme Ltd"}, expected_revision=written.record.revision),
)
Die Organisation kommt immer aus ctx, nie aus einem Argument. Der Service committet
nie: Das tut die Session der Anfrage, und ein Worker besitzt seinen eigenen
Session-Scope.
Einen Spaltentyp hinzufügen¶
Ein Spaltentyp ist ein Eintrag in COLUMN_TYPES in
backend/app/services/virtual_tables/types.py, mit seinem Namen in ColumnTypeName in
backend/app/schemas/virtual_table.py. Der Eintrag legt fest, wie die Zelle zum
Vergleichen und Sortieren aus dem Datensatz gelesen wird (ein SqlKind), welche
Filteroperatoren sie nimmt, ob sie sortierbar ist, und welchen Validator jeder
Schreibzugriff und jeder Filteroperand durchläuft. Der Validator gibt den Wert so
zurück, wie er gespeichert wird, oder wirft CellProblem mit einer Meldung für den, der
ihn geschrieben hat. Jede Oberfläche ruft denselben Service, also nehmen Konsole, API,
Agents und Workflows den neuen Typ sofort an, und ein Trigger filtert mit
denselben Regeln darauf.
Die Konsole braucht den Typ in ColumnTypeName in frontend/src/types/tables.ts,
einen Editor in record-cell-editor.tsx, eine Auswahl in den Dialogen für Schema und
neue Tabelle und seine Beschriftung in den drei Meldungskatalogen. Tests gehören für den
Validator in backend/tests/test_virtual_table_types.py und in einen
Integrationstest, der einen Datensatz des neuen Typs schreibt, filtert und sortiert.
Beim Erstellen einer leeren Tabelle benötigen Pflichtspalten keine Vorgaben: Jeder neue Datensatz muss ihre Werte liefern. Neue Pflichtspalten in einem bestehenden Schema benötigen weiterhin einen Standardwert zum Schutz vorhandener Datensätze.