Virtual Tables¶
Virtual table to typowana tabela rekordów, którą organizacja trzyma dla swoich agentów, workflow i integracji: zamówienia do uzgodnienia, pliki do przetworzenia, leady do obsłużenia.
Tabele to metadane plus JSONB. Nic nie tworzy fizycznej tabeli SQL, więc założenie
tabeli kosztuje jeden wiersz, zmiana nazwy kolumny nie zmienia żadnego rekordu, a
żaden tenant nie może rozrastać katalogu bazy danych. Każdy odczyt i zapis przechodzi
przez jeden serwis, VirtualTableService, więc konsola, narzędzia agenta, węzły
workflow i publiczne API mają te same reguły. Ta strona opisuje serwis i jego trasy
HTTP pod /api/v1/tables; dokument OpenAPI jest ich kontraktem.
Obszar tabeli domyślnie wypełnia dostępne miejsce, bez przycisku rozszerzania. Panel rekordu ma osobny przycisk Full screen, który powiększa wszystkie pola bez utraty edycji; Exit full screen przywraca panel boczny. Wiersze leżą bezpośrednio pod paskiem, bez drugiej ramki i pustego panelu z marginesami. Katalog wypełnia dostępną wysokość strony, ma własny przycisk pełnego ekranu i pokazuje paginację dopiero przy wielu stronach.
Każda tabela pokazuje zablokowane pola created_at, updated_at, created_by i updated_by z rekordu. To te same właściwości, które są nad Markdownem, a nie edytowalne kopie w komórkach. Przed pierwszą aktualizacją updated_at pokazuje datę utworzenia. Pola pozostają widoczne po ukryciu kolumn użytkownika. Ich nazwy są zarezerwowane przy dodawaniu i zmianie nazw kolumn; istniejące starsze nazwy pozostają czytelne.
Typowaną Wartość domyślną ustawisz w ustawieniach lub przy dodawaniu kolumny. Nowe rekordy używają jej tylko po pominięciu pola; jawna wartość lub null ma pierwszeństwo. Domyślne opcje select mogą przy wysyłaniu schematu wskazywać ID albo etykiety; serwer zapisuje ID. Kolor opcji wybierzesz na liście opcji; Automatyczny zachowuje dotychczasową paletę. Zmiana koloru zachowuje identyfikatory i rewizję znaczenia kolumny.
Szybkie dodawanie jest bezpośrednio za załadowanymi wierszami, w ich obszarze przewijania. Eksport CSV w konsoli zawiera cztery właściwości rekordu. Eksport API zachowuje dotychczasowy format, chyba że podasz include_metadata: true. Przy tworzeniu tabeli z eksportowanego CSV pola systemowe są pomijane; importowane rekordy dostają nowe metadane systemowe.
W dokumencie Markdown wpisz / na początku wiersza, aby wyszukać blok: zadanie, nagłówek, listę, cytat, kod, separator, tabelę, zdjęcie, wideo lub link. Strzałki i Enter wybierają blok; Escape zamyka menu. Skopiowane zdjęcia można wkleić bezpośrednio do edytora. Zdjęcia PNG, JPEG, GIF i WebP do 5 MiB są przechowywane z tabelą, wliczane do jej limitu miejsca i objęte jej bieżącymi uprawnieniami dostępu. Pozostają dostępne po archiwizacji tabeli, również przez odwołania zachowane w historii rekordów.
Typy kolumn user i agent wskazują jednego członka organizacji lub dostępnego agenta AI; multi_user i multi_agent wskazują do 100 różnych osób lub agentów. Wybierasz je po nazwie z podpowiedzi z avatarami. Zapisane wartości to stabilne UUID, a członkostwo lub dostęp do agenta są sprawdzane przy zapisie i ustawianiu wartości domyślnej. Pojedyncze referencje obsługują eq, ne, in i is_null, a wielokrotne contains i is_null. Kolumn tożsamości nie można sortować.
Szablony dokumentów w polu Markdown otwierają bibliotekę tej kolumny. Zapisz bieżący dokument, dostosuj gotowy szablon, edytuj lub usuń nazwane szablony i wybierz domyślny. Domyślny szablon automatycznie uzupełnia nowe rekordy; jego edycja nie zmienia istniejących dokumentów. Każda kolumna mieści do 10 szablonów o łącznej długości do 100 000 znaków. Zwykłe właściwości rekordu są pokazywane nad treścią dokumentów w panelu rekordu.
Zaznaczanie rekordów pokazuje pływający pasek akcji bez przesuwania tabeli. Przycisk otwierania rekordu pozostaje przypięty obok pola zaznaczenia, także po przewinięciu tabeli w poziomie. Każdy panel rekordu pokazuje daty i autorów utworzenia oraz ostatniej zmiany, wraz z avatarami. Panel boczny rozpoczyna w trybie Preview, a dokument na pełnej stronie w Write.
Klienci zewnętrzni¶
Źródło zapisu jest rejestrowane oddzielnie od osoby udzielającej uprawnień. Nowe zapisy przechowują created_source i updated_source: rodzaj, identyfikator i nazwę osoby, agenta, klucza API, workflow lub systemu. Konsola pokazuje avatar osoby, portret agenta albo ikonę API/workflow obok nazwy w kolumnach systemowych i właściwościach dokumentu. Utworzenie ustawia oba źródła; późniejsze zapisy zachowują twórcę. Starsze rekordy zachowują zapisaną osobę bez zgadywania historycznego źródła. Tych pól nie można ustawić przez dane rekordu ani Markdown.
Klucze API organizacji działają na /api/v1/tables. Wyślij
Authorization: Bearer aos_…; organizacja pochodzi z klucza. Odczyt wymaga
tables:view, zapis rekordów i schematu tables:edit, a utworzenie tabeli
tables:create. Obowiązują zarówno bieżący dostęp wystawcy, jak i zakres klucza.
Historia i potwierdzenia wskazują wystawcę; klucz nie rozszerza jego dostępu.
Niedostępna tabela zwraca 404, brak uprawnienia kolekcji 403, stara rewizja 409,
a nieprawidłowe zapytanie lub rekord 422. Ponowiony zapis z tym samym
Idempotency-Key i treścią zwraca pierwotny wynik bez kolejnego zdarzenia utworzenia.
POST /api/v1/tables/{id}/records/query przyjmuje columns (do 100 identyfikatorów
aktywnych kolumn) obok filtrów, sortowania, skip i limit. Odpowiedź zawiera tylko
wybrane komórki i etykiety powiązań. null zachowuje cały rekord; [] zwraca tylko
metadane. Kolumny obliczane są wyliczane przed projekcją, więc ich zależności nie
muszą być wybrane. Te same rekordy od razu widać w konsoli; asynchroniczne uruchomienie
wyzwalacza tabeli nie opóźnia odpowiedzi CRUD.
Jak zbudowana jest tabela¶
| Element | Czym jest | Tożsamość |
|---|---|---|
| Table | Nazwa, właściciel, widoczność i grants, jak w pliku kontekstowym | id, stałe |
| Schema version | Niezmienna migawka kolumn. Zmiana dodaje wersję N+1 | version |
| Column | Etykieta, typ, informacja czy może być pusta, opcjonalna wartość domyślna | id, stałe |
| Option | Jeden wybór kolumny typu select | id, stałe |
| Record | Wartości komórek kluczowane id kolumny, revision i opcjonalny external_id |
id, stałe |
Wartości są kluczowane id kolumny, nigdy jej etykietą. Zmiana nazwy przepisuje więc jedną wersję schematu i żaden rekord. Rekord pamięta wersję schematu, w której był ostatnio zapisany.
Rekord przechowuje tylko komórki, które mają wartość. Komórka wysłana jako null
zostaje wyczyszczona, a odczyt nic dla niej nie pokazuje. Przy create wartość domyślna
kolumny wypełnia tylko komórki, które pominiesz; komórka wysłana jako null zostaje
pusta.
Typy kolumn¶
| Typ | Zapisywany jako | Filtry |
|---|---|---|
text |
Tekst do 1000 znaków | eq ne contains starts_with in is_null |
long_text |
Tekst do 100 000 znaków | tak samo jak text |
rich_text |
Źródło Markdown do 100 000 znaków | tak samo jak text |
email |
Adres z jednym @ i kropką w domenie, do 254 znaków |
tak samo jak text |
url |
Adres http lub https z hostem, do 2048 znaków |
tak samo jak text |
phone |
Cyfry, spacje i + ( ) - . /, co najmniej pięć cyfr |
tak samo jak text |
number |
Skończona liczba | eq ne lt lte gt gte in is_null |
integer |
Liczba całkowita, co najwyżej 2^53 - 1 | tak samo jak number |
rating |
Liczba całkowita od 1 do 5 | tak samo jak number |
boolean |
true lub false |
eq ne is_null |
date |
YYYY-MM-DD |
tak samo jak number |
datetime |
ISO 8601 ze strefą czasową, zapisywany jako UTC | tak samo jak number |
single_select |
Id opcji | eq ne in is_null |
multi_select |
Lista id opcji | contains is_null |
link |
Lista do 100 id rekordów tabeli, z którą łączy | contains (id rekordu) is_null |
lookup, rollup |
Nic: wyliczane przy odczycie rekordu | brak |
formula |
Nic: wyliczana z pozostałych kolumn rekordu przy odczycie | brak |
Tekst jest zapisywany dokładnie tak, jak został wysłany. Spacje na początku i na końcu, podziały wierszy i wartości złożone z samych spacji to dane użytkownika, więc nie są przycinane. Obowiązuje tylko limit długości, a znak NUL jest odrzucany, w komórkach i tak samo w nazwach, etykietach, opisach i external id, bo PostgreSQL nie potrafi go zapisać.
Porównania pasują tylko do komórek, które mają wartość. Puste znajdziesz przez is_null.
contains i starts_with przyjmują fragment wartości, więc @example.com znajdzie
w kolumnie email adresy w tej domenie.
Narzędzia tabel agenta i kroki tabel w workflow nazywają wybór etykietą opcji, bez rozróżniania wielkości liter: po etykiecie zapisują i filtrują, a wybory rekordu odczytują jako etykiety, także opcji zarchiwizowanej. Wartość, która nie wskazuje żadnej aktywnej opcji, jest odrzucana wraz z listą opcji kolumny.
Tekst formatowany i Markdown¶
Wybierz Rich text (Markdown) dla notatek z nagłówkami, listami, zadaniami, tabelami, linkami i kodem. Kliknięcie komórki otwiera rekord. Write oferuje pole dokumentu i pasek pogrubienia, kursywy, linków, obrazów i filmów. Preview używa renderera czatu dla linków i obrazów; bezpośrednie linki HTTP(S) do MP4, WebM lub Ogg stają się filmami z kontrolkami, bez autoodtwarzania. Surowy HTML nie jest wykonywany. Expand editor otwiera większy obszar pracy. Zapisane dokumenty zaczynają od podglądu; Write przełącza do edycji.
Kanban pokazuje kartę pod kursorem i docelową kolumnę, animuje przejście i od razu przenosi kartę na czas zapisu. Odmowa zapisu przywraca poprzednią kolumnę i zachowuje obsługę konfliktu. Menu Move to pozostaje dostępne z klawiatury.
Właściwości dokumentu są nad edytorem Markdown. Utworzono, Ostatnia zmiana i Autor pochodzą z rekordu i nie można ich zmienić przez Markdown. Data ostatniej zmiany uwzględnia zapis całego rekordu, także innych komórek; przed pierwszą zmianą pokazuje datę utworzenia.
Właściwości dokumentu przechowuj w typowanych kolumnach tabeli. Edytor Markdown nie dodaje ani nie edytuje już właściwości YAML. Nowe szablony notatnika zawierają tylko Markdown. Dotychczasowe nagłówki importowanych dokumentów są zachowywane podczas edycji treści, aby nie utracić danych historycznych. Automatyczne daty i tożsamości autorów nadal pochodzą z rekordu.
Domyślny szablon dokumentu wybierzesz przy tworzeniu kolumny rich text lub w Columns. Dostępne są pusty dokument, notatka, spotkanie i opis projektu; treść można dostosować. Szablon korzysta z istniejącego default kolumny: konsola, API i workflow stosują go przy tworzeniu, jeśli pominięto komórkę. Jawnie podana wartość, także pusty tekst, ma pierwszeństwo. Zmiana szablonu wpływa wyłącznie na nowe rekordy. Użyj szablonu kolumny zastępuje treść istniejącego dokumentu po potwierdzeniu i zachowuje właściwości. Zapytania tekstowe i konwersje działają jak wcześniej.
Powiązania, odczyty i podsumowania¶
Kolumna link przechowuje rekordy innej tabeli albo tej samej, wskazanej przez
link_table_id. Zapis sprawdza, czy każdy rekord istnieje w tej tabeli i czy
zapisujący ją widzi, więc powiązanie nie pozwala sprawdzić, jakie id ma
prywatna tabela. Rekord zwraca swoje powiązania nazwane, w links, pierwszą
wartością tekstową każdego powiązanego rekordu, a w drugiej kolejności jego
zewnętrznym id. Powiązany rekord usunięty w międzyczasie po prostu znika.
lookup czyta jedną kolumnę powiązanych rekordów przez kolumnę powiązania tej
samej tabeli (link_column_id, target_column_id): wartości, a wybór po
etykiecie. rollup coś z nich wylicza: count powiązań albo sum, avg, min
lub max kolumny liczb - min i max także dat. Obie są wyliczane przy
odczycie rekordu i nigdy nie są zapisywane, filtrowane ani sortowane.
To, co pokazuje odczyt, nie sięga dalej niż czytelnik: powiązana tabela, której czytelnik nie widzi, zostawia powiązania bez nazw, a każdy odczyt i każde podsumowanie przez nie puste. Powiązanie zachowuje swoją tabelę na zawsze. Odczyt czyta kolumnę z wartościami, nie inne powiązanie ani odczyt, a archiwizacja kolumny powiązania jest odrzucana, dopóki czyta przez nią odczyt lub podsumowanie. Eksport zapisuje powiązanie nazwami jego rekordów, a odczyt tym, co przeczytał.
Formuły¶
Kolumna formula wylicza wartość z pozostałych kolumn rekordu:
{Price} * {Quantity}, IF({Total} > 100, "large", "small"),
ROUND({Total} / {Count}, 2), DAYS({Due}, TODAY()). Kolumnę wskazuje się jej
etykietą w nawiasach klamrowych, a zmiana nazwy przepisuje każdą formułę, która ją
wskazuje. Formuła liczy przez + - * / %, porównuje przez = <> < <= > >=, łączy
warunki przez and, or i not oraz wywołuje IF, ROUND, ABS, MIN, MAX,
CONCAT, LEN, UPPER, LOWER, DAYS i TODAY. Wybór czyta się jego etykietą.
Może czytać każdą żywą kolumnę z wartościami, także wynik odczytu lub podsumowania, ale nie powiązanie ani inną formułę, a kolumny czytanej przez formułę nie można zarchiwizować. Zapis schematu sprawdza formułę i mówi, co jest w niej nie tak. Wartość, której nie da się wyliczyć - dzielenie przez zero, tekst w sumie, pusta komórka - zostawia komórkę tego rekordu pustą. Formuła jest wyliczana przy odczycie rekordu, jak odczyt z powiązania, i nigdy nie jest zapisywana, filtrowana ani sortowana.
Zmiana schematu¶
PUT /tables/{id}/schema przyjmuje pełną listę kolumn, jakie tabela ma mieć, oraz
expected_version, czyli wersję, którą wywołujący ostatnio odczytał. Serwis uzgadnia
ją z bieżącymi kolumnami:
- Kolumna z
idto ta kolumna. Kolumna bez niego jest nowa. - Typ kolumny zmienia się tylko na taki, jako który dają się odczytać jej wartości, a zmiana przepisuje wartość w każdym rekordzie. Każda wartość staje się tekstem; tekst staje się liczbą, tak/nie, datą albo pojedynczym wyborem, gdy to mówi - wybór dostaje opcję na każdą inną wartość; liczba całkowita staje się liczbą i z powrotem, gdy jest całkowita; data staje się datą z czasem; jeden wybór staje się kilkoma i z powrotem, gdy jest jeden. Każda inna zmiana jest odrzucana, podobnie jak taka, której któraś wartość nie przetrwa - z liczbą i przykładem, a nic nie zostaje zapisane. Coś, co używa kolumny, odrzuca zmianę tak jak archiwizację. Rewizja rekordu idzie dalej, więc edycja według starej wartości jest konfliktem.
- Tekst staje się kolumną
email,url,phonelubrating, gdy każda wartość się nią czyta, a liczba staje się oceną, gdy jest liczbą całkowitą od 1 do 5. Każda z nich wraca do tekstu, a ocena także do liczby. - Nic nie jest usuwane. Pominięta kolumna lub opcja zostaje zarchiwizowana: jej
wartości pozostają czytelne i filtrowalne, a zapis do niej jest odrzucany kodem
ARCHIVED_COLUMN. - Zarchiwizowane liczą się do limitów. Tabela ma co najwyżej 100 kolumn, a kolumna typu
select co najwyżej 100 opcji, wliczając zarchiwizowane. Zmiana, która przekroczyłaby
któryś, jest odrzucana kodem
INVALID_SCHEMAi nie dodaje wersji, więc nie można zastąpić pełnej listy opcji; dodaj zamiast tego nową kolumnę. - Nowa kolumna wymagana potrzebuje wartości domyślnej, bo istniejące rekordy nic w niej nie mają. Istniejąca kolumna nie może stać się wymagana, dopóki jakikolwiek rekord nie ma w niej wartości, a wymagana kolumna nie może wrócić z archiwum bez wartości domyślnej, bo rekordy zapisane w czasie archiwizacji nie mogły jej mieć.
- Nieaktualne
expected_versiontoSCHEMA_VERSION_CONFLICT. - Zgłoszenie identyczne z bieżącymi kolumnami, z tymi samymi id, kolejnością, etykietami i opcjami, nie zmienia niczego: nie powstaje wersja, a zwracana jest bieżąca tabela. Zmiana kolejności lub etykiety jest zmianą.
Rekordy nie są przepisywane. Rekord zapisany w wersji 1 pozostaje czytelny i edytowalny w wersji 4; wymagana kolumna, której nigdy nie miał, dostaje wartość domyślną przy najbliższej edycji rekordu.
Każda kolumna ma tylko do odczytu revision, równe 0 dla nowej kolumny i dla
każdej kolumny, która istniała, zanim je dodano. Zlicza zmiany tego, co wartości
kolumny oznaczają, żeby opublikowana strona mogła
rozpoznać, że kolumna, względem której ją opublikowano, nie jest już tą samą. Rośnie o jeden przy:
- konwersji typu;
- zmianie formuły, porównywanej po tym, jak przez nią przepisano zmianę nazwy, więc zmiana nazwy kolumny, którą czyta formuła, się nie liczy, ale przejęcie etykiety przez inną kolumnę tak;
- innym celu linku, lookupu albo rollupu, albo innym agregacie;
- archiwizacji albo przywróceniu kolumny, albo archiwizacji albo przywróceniu jednej z jej opcji wyboru.
Nowa etykieta, wartość domyślna czy wymaganie, zmiana etykiety opcji wyboru i nowa
opcja zostawiają je bez zmian. Filtr trzyma id opcji, więc żadna z tych zmian nie
zmienia tego, co filtr dopasowuje. Kolumnę TableRead można odesłać w zmianie schematu
tak, jak przyszła: pole jest przyjmowane i ignorowane, a ustawia je dopiero sama zmiana.
Zapis rekordu oraz zmiana schematu lub archiwizacja tej samej tabeli czekają na siebie: zapis czeka na trwającą zmianę i jest potem oceniany według tego, co ona zatwierdziła, więc rekord nigdy nie trafia do tabeli zarchiwizowanej chwilę wcześniej.
Archiwizacja kolumny lub całej tabeli najpierw pyta każdy zarejestrowany checker
zależności, czy coś, co wywołujący może zarówno zobaczyć, jak i zmienić, jeszcze z
niej korzysta. Zapisane widoki (zobacz Zapisane widoki) i workflow,
które czytają lub zapisują tabelę, rejestrują swoje w
app/services/virtual_tables/dependencies.py, podobnie wyzwalacze tabeli, które
blokują każdego, kto archiwizuje: wyzwalacz filtrujący po kolumnie, której już nie ma,
wróciłby zepsuty.
Odmowa wymienia każdą zależność w SCHEMA_DEPENDENCY z jej kind,
id i name, a name jest null dla wyzwalacza w workflow, którego wywołujący nie
może otworzyć. Konsola pokazuje je w dialogu, który pytał, workflow jako link do
niego. Każda inna zależność nigdy nie jest wymieniana i nigdy nie blokuje
wywołującego: jej funkcja sama radzi sobie ze zmianą.
Archiwizacja i przywracanie tabeli¶
Tabela nigdy nie jest usuwana; jest archiwizowana. Zarchiwizowana tabela zachowuje
rekordy, kolumny, widoki i historię, a każdy odczyt nadal działa. Każdy zapis jest
odrzucany kodem TABLE_ARCHIVED, niezależnie od tego, kto pyta: żaden rekord nie
powstaje, nie zmienia się ani nie znika, a jej nazwa i kolumny też się nie zmieniają.
Jej nazwa jest wolna dla nowej tabeli, a ona sama nadal wlicza się do
limitu tabel organizacji.
W konsoli członek, który może edytować tabelę, znajdzie Archive table w menu
More actions na jej stronie. Dialog mówi, co robi archiwizacja, zanim cokolwiek
się zmieni. Opublikowany workflow, który nadal czyta lub zapisuje tabelę i który
wywołujący może edytować, odrzuca archiwizację kodem SCHEMA_DEPENDENCY, a dialog
pokazuje go jako link. Zapisane widoki i wyzwalacze blokują tylko archiwizację kolumny.
Strona zarchiwizowanej tabeli mówi, kiedy ją zarchiwizowano. Dla członka, który może edytować tabelę, komunikat zawiera Restore, które przywraca ją pod jej nazwą; każdy inny czyta jej rekordy i dowiaduje się, kto może ją przywrócić. Katalog pokazuje aktywne tabele, dopóki nad nim nie wybierze się Archived included, a karta zarchiwizowanej tabeli mówi, że jest zarchiwizowana.
| Akcja | Trasa | Kod odmowy |
|---|---|---|
| Archiwizacja | POST /tables/{id}/archive |
SCHEMA_DEPENDENCY (409), gdy coś używa tabeli. Ponowna archiwizacja nie jest błędem |
| Przywrócenie | POST /tables/{id}/unarchive |
BAD_REQUEST (400), gdy tabela nie jest zarchiwizowana; ALREADY_EXISTS (409), gdy w międzyczasie aktywna tabela zajęła jej nazwę |
Obie wymagają prawa do edycji tej tabeli, z roli lub z grantu na nią, a każdy inny
dostaje 404. can_edit jest false dla zarchiwizowanej tabeli, więc to
TableRead.can_restore mówi, czy ten wywołujący może ją przywrócić. Każda trafia do
audytu, jako table.archived i table.unarchived, i liczy się do limitu
częstotliwości zapisów tabel. Nazwę zajętą w międzyczasie zmienia się
najpierw w aktywnej tabeli, bo zarchiwizowanej nie można przemianować.
Rekordy i revisions¶
Każdy rekord ma revision, która zaczyna się od 1 i rośnie z każdą zmianą.
| Operacja | Trasa | Wymaga expected_revision |
|---|---|---|
| Utworzenie | POST /tables/{id}/records |
Nie |
| Zmiana wskazanych komórek | PATCH /tables/{id}/records/{record_id} |
Tak |
| Usunięcie | DELETE /tables/{id}/records/{record_id}?expected_revision= |
Tak |
| Upsert | PUT /tables/{id}/records/by-external-id/{external_id} |
Tylko gdy rekord istnieje |
| Odczyt, exists | GET .../records/{record_id}, .../by-external-id/{external_id}, .../exists |
Nie |
Aktualizacja lub usunięcie ze starą revision jest odrzucane kodem REVISION_CONFLICT
(409) i details.current_revision; nic nie zostaje nadpisane. Odczytaj rekord ponownie
i spróbuj jeszcze raz. Upsert, który znajdzie istniejący rekord i nie dostanie
expected_revision, otrzymuje REVISION_REQUIRED (428), znów z revision do wysłania.
External id ma od 1 do 255 znaków i może zawierać /, jak w 2026/ORD-1. Nie może zawierać NUL ani znaku nowego wiersza. Serwis sprawdza to
tak samo jak trasy. Przez HTTP trasa odrzuca to pierwsza, kodem VALIDATION_ERROR;
wywołujący, który używa serwisu bezpośrednio, dostaje INVALID_RECORD.
Równoległe upserty tego samego external id tworzą jeden rekord. Przegrywający go znajduje i jest obsługiwany jak aktualizacja: potrzebuje revision albo dowiaduje się, którą wysłać.
Aktualizacja, która zostawiłaby każdą komórkę bez zmian, nie zmienia niczego. Revision
zostaje, nie powstaje wiersz historii ani receipt, a zwracany jest bieżący rekord.
Nieaktualne expected_revision to nadal konflikt, bo jest sprawdzane najpierw. Upsert,
który znajdzie rekord, podlega tej samej regule.
Usunięcie jest twarde. Historia rekordu zostaje, dopóki nie usunie jej retencja.
Edycja rekordów w konsoli¶
Katalog oraz widoki Table, Kanban i List mają wspólny panel z wyszukiwaniem i zapisanymi widokami. Add record używa dwóch kolumn na większym ekranie i jednej na telefonie; długi tekst i powiązania zachowują pełną szerokość. Panel rekordu korzysta ze wspólnej obsługi klawiatury i fokusu konsoli. Columns zwija istniejące opcje wyboru pod ich liczbą. Błąd pobierania rekordów daje Retry.
Karty katalogu mówią, ile rekordów i aktywnych kolumn ma każda tabela, kto ma do niej dostęp i kiedy ostatnio się zmieniła. Na stronie tabeli każdy nagłówek pokazuje typ kolumny ikoną, liczby są wyrównane do prawej, a daty zapisane tak, jak zapisuje je twój język. Panel rekordu ma w tytule pierwszą kolumnę rekordu i mówi, kiedy go dodano i kiedy ostatnio zmieniono.
Członek, który może edytować tabelę, dodaje, zmienia i usuwa rekordy z jej strony.
Add record prosi o wartość dla każdej aktywnej kolumny, w typie tej kolumny.
Kolumna wymagana i bez wartości domyślnej jest oznaczona * i trzeba ją wypełnić,
zanim rekord zostanie zapisany; każda inna pusta kolumna przyjmuje wartość domyślną.
Kliknięcie komórki edytuje ją w miejscu: Enter lub kliknięcie obok zapisuje, Escape
zostawia ją bez zmian, a pole tak/nie, które nie może być puste, przełącza się jednym
kliknięciem. Strzałki przenoszą między komórkami, a przycisk rozwinięcia na końcu
wiersza otwiera cały rekord.
Linia pod siatką dodaje rekordy podczas pisania: to, co wpiszesz, trafia do pierwszej kolumny tekstowej, a Enter tworzy rekord i zostawia linię gotową na następny. Tabela z inną wymaganą kolumną otwiera zamiast tego Add record z wpisaną wartością, więc nic nie zostaje zapisane, dopóki nie ma wszystkich wymaganych wartości.
Każda edycja to jeden PATCH względem rewizji widocznej na ekranie, więc edycja, która
przegrywa z nowszą zmianą, zostaje odrzucona, a nie zapisana na niej. Rekord otwiera się
wtedy z odrzuconą wartością zachowaną obok Reload and reapply. Zaznaczenie wierszy
udostępnia Delete dla wszystkich naraz, każdy względem własnej rewizji: rekord,
który ktoś w międzyczasie zmienił, zostaje, a konsola mówi, ile takich było. Panel
rekordu usuwa pojedynczy rekord w ten sam sposób.
Usunięcie czeka kilka sekund z Undo w komunikacie, zanim zostanie wysłane; rekordy od razu znikają ze wszystkich widoków, a Undo przywraca je nietknięte. Członek, który może tylko oglądać tabelę, widzi tę samą siatkę tylko do odczytu, a kliknięcie wiersza otwiera rekord.
Członkowi, który może edytować tabelę, nagłówek kolumny otwiera menu. Sort ascending i Sort descending sortują siatkę według niej, a Hide in this view zdejmuje ją z ekranu, dopóki przycisk ukrytych kolumn nie pokaże jej z powrotem; Save view zapisuje jedno i drugie. Rename i Archive column zmieniają tabelę dla wszystkich, każda jako ta sama nowa wersja schematu, którą zapisałby dialog Columns, a archiwizacja czegoś, co jest jeszcze używane, zostaje odrzucona, a workflow, widoki i wyzwalacze pojawiają się w dialogu. + za ostatnią kolumną dodaje nową, na początek opcjonalną. Typ kolumny zmienia się w Columns, na typy, na które dają się przekonwertować jej wartości.
W Columns przełącznik Required sprawia, że kolumna wymaga wartości, a zarchiwizowana kolumna trafia do Archived columns, skąd Restore przywraca ją razem z jej wartościami.
Import i eksport¶
Eksport odczytuje najwyżej 4 MiB rozwiniętego JSON rekordów na stronę (albo jeden
rekord), maksymalnie 500 rekordów naraz. Organizacja może przesyłać jeden eksport
jednocześnie i rozpoczynać dziesięć na minutę. Kolejne żądania zwracają 429 z
Retry-After; ponów je po tym czasie. Bez Redis limit rozpoczęć zachowuje minimum
w każdym procesie; baza nadal wymusza jeden eksport na organizację naraz.
Jeden kursor serwera utrzymuje spójny snapshot rekordów eksportu, pobierając z
wyprzedzeniem najwyżej cztery rekordy. Zamyka się po zakończeniu lub anulowaniu;
kolejne strony nie skanują ponownie wcześniejszych rekordów.
Export, w menu … obok Share, zapisuje to, co pokazuje strona, jako plik CSV: rekordy pasujące do filtrów
i wyszukiwania, w kolejności siatki, w jej widocznych kolumnach. POST
/tables/{id}/records/export robi to samo dla wywołującego, z tymi samymi filtrami,
wyszukiwaniem, sortowaniem i listą columns. Opcja jest zapisywana jako jej etykieta,
kilka jako a; b, a komórka tekstowa, którą arkusz odczytałby jako formułę, zaczyna
się od '. Ponad 100 000 pasujących rekordów jest odrzucane z EXPORT_TOO_LARGE (413).
Import, w tym samym menu, czyta plik CSV rozdzielany przecinkiem lub średnikiem, którego pierwszy
wiersz nazywa kolumny. Każda kolumna pliku jest dopasowana do kolumny tabeli o tej
samej nazwie albo do External id i można ją skierować gdzie indziej lub pominąć.
Wartość, której nie da się odczytać w typie kolumny, odrzuca swój wiersz, zanim cokolwiek
zostanie wysłane. Reszta trafia po 200 do POST /tables/{id}/records/batch, który
zapisuje każdy rekord osobno i wymienia odrzucone z ich kodami, a konsola wypisuje każdy
nieudany wiersz z numerem linii. Każdy dodany rekord uruchamia wyzwalacze tabeli, jak
każdy inny.
Gdy zmapowane jest External id, Update records already here importuje plik
ponownie, żeby zaktualizować tabelę: wiersz, którego zewnętrzne id tabela już ma, zmienia
wskazane komórki tego rekordu zamiast zostać odrzucony jako zajęty (update_existing w
partii), a import podaje, ile dodał i ile zaktualizował.
New table zaczyna się od kolumny Name. Najpierw podaj nazwę tabeli i jej kolumny; opis oraz widoczność są pod More options, z domyślną widocznością prywatną. Zamiast tego może zacząć się od pliku: Start from a CSV file zamienia nagłówek na kolumny i daje każdej najwęższy typ, w którym dają się odczytać wszystkie jej wartości - liczba całkowita, liczba, tak/nie, data, data i godzina w ISO, adres e-mail albo link - a w przeciwnym razie tekst lub długi tekst, gdy wartość ma podział wiersza albo przekracza 1000 znaków. Nazwa pochodzi z pliku i obie rzeczy można zmienić przed utworzeniem, co kosztuje mniej niż późniejsza zmiana typu. Gdy tabela już istnieje, import otwiera się z każdą kolumną już przypisaną, a po nim otwiera się tabela.
Bezpieczne ponawianie¶
Każdy zapis rekordu przyjmuje nagłówek Idempotency-Key (co najwyżej 128 znaków).
Ponowienie z tym samym kluczem i tą samą treścią zwraca pierwszą odpowiedź, z
Idempotent-Replayed: true, i niczego nie zapisuje, nawet jeśli rekord zmienił się w
międzyczasie. Ten sam klucz z inną treścią jest odrzucany kodem
IDEMPOTENCY_KEY_REUSED.
Nagłówek oznacza powtórzone create, update lub upsert. Powtórzone usunięcie odpowiada 204 jak za pierwszym razem i nie jest oznaczane.
Klucz należy do wywołującego i do rodzaju zapisu, więc dwóch wywołujących może użyć tego samego ciągu, a jeden wywołujący może go użyć do create i do upsert. Zapisywane są tylko sukcesy: odrzucony zapis nie zostawia potwierdzenia, więc poprawiasz go i ponawiasz z tym samym kluczem.
Powtórzona odpowiedź trafia tylko do wywołującego, który nadal może edytować tabelę. Po cofnięciu dostępu to samo ponowienie to 404.
Receipt trwa 24 godziny. Potem klucz jest zapominany, a ten sam klucz z tą samą treścią to nowy zapis: wykonuje się ponownie, zamiast zwrócić pierwszą odpowiedź. Ponawiaj w tym oknie, a dłuższą przerwę traktuj jak nowe żądanie. Czas życia jest egzekwowany w chwili użycia klucza, więc obowiązuje co do godziny; codzienny sweep tylko odzyskuje miejsce po receipts, których nikt nie ponowił.
Listowanie i filtrowanie¶
GET /tables/{id}/records przegląda tabelę stronami; POST /tables/{id}/records/query
dodaje typowane filtry, z których wszystkie muszą być spełnione. Oba są ograniczone:
limit wynosi od 1 do 100, skip co najwyżej 10 000, a zapytanie ma co najwyżej 20
filtrów.
search to tekst, który rekord musi zawierać, bez względu na wielkość liter, w dowolnej
aktywnej kolumnie tekstowej lub długiego tekstu albo w etykiecie opcji wyboru, którą
przechowuje. Łączy się z filtrami, a puste wyszukiwanie niczego nie szuka. Zapisany
widok przechowuje je obok swoich filtrów. W konsoli wysyła je pole wyszukiwania, a
Filter zapisuje warunki: kolumnę, operator obsługiwany przez jej typ i wartość.
Każdy kompletny warunek od razu zawęża rekordy, a Save view zapisuje warunki,
wyszukiwanie i sortowanie w widoku na ekranie.
Kolejność jest całkowita. Po żądanym sortowaniu (created_at, updated_at lub
kolumna, którą można sortować) następuje id rekordu, więc strona nigdy nie powtarza ani
nie pomija rekordu w niezmienionej tabeli. Rekordy bez wartości w sortowanej kolumnie
są na końcu w obu kierunkach. Kolumny multi_select nie da się sortować. updated_at jest ustawiane przy utworzeniu
rekordu i przesuwa się z każdą edycją, więc rekordy, których nikt nie edytował,
sortują się według czasu utworzenia.
Listowanie nie ma total, bo liczenie przefiltrowanej tabeli nie jest tanie: has_more
mówi, czy następuje kolejna strona. POST /tables/{id}/records/count odpowiada na to
pytanie osobno, dla filtrów i wyszukiwania zapytania, i liczy najwyżej do 100 000;
capped mówi, że pasuje więcej. Konsola pokazuje tę liczbę obok zakładek widoków, a jej
siatka wczytuje po sto rekordów podczas przewijania i rysuje tylko widoczne wiersze. Za
10 000 rekordów, które zapytanie może pominąć, prosi o filtr lub wyszukiwanie.
Zapisane widoki¶
Sortuj obok Filtruj wybiera kolumnę oraz kierunek rosnący lub malejący dla tabeli, listy albo kart w każdej kolumnie Kanbana. Dostępne są też ukryte kolumny obsługujące sortowanie i daty systemowe. Przycisk pokazuje aktywną kolumnę i kierunek. Serwer sortuje przed stronicowaniem, także rekordy jeszcze niewczytane. Zapisz widok zachowuje kolejność razem z filtrami i wyglądem; + zapisuje nowy widok. Domyślna kolejność: najstarsze najpierw przywraca sortowanie według daty utworzenia.
Ustawienia widoku pozwalają zmieniać kolejność i widoczność kolumn, także systemowych. Przeciągnij kolumnę lub użyj strzałek; ukrycie pola systemowego zachowuje jego automatyczną wartość. Zawijaj tekst pokazuje pełny tekst i podziały wierszy; wyłączenie przywraca kompaktowe wiersze. Wybierz datę automatyczną, ISO, dzień–miesiąc–rok, miesiąc–dzień–rok lub pełną oraz godzinę automatyczną, 24-godzinną, 12-godzinną albo ukrytą. Sama data zachowuje dzień, a znaczniki czasu używają strefy odbiorcy. Zapisz widok zachowuje ustawienia wybranego widoku; + tworzy nowy. Formatowanie nie zmienia wartości ani filtrów.
Tablica kanban bez zapisanego grupowania automatycznie korzysta z jedynej aktywnej kolumny jednokrotnego wyboru. Przy kilku kolumnach wybierz, która tworzy tory. Zapis widoku zachowuje grupowanie. Wiersze listy nazywają pola pomocnicze i pokazują Unnamed record, gdy tytuł jest pusty. Triggers proponuje Open workflows, gdy żaden workflow nie startuje jeszcze z tej tabeli.
Widok to zachowany filtr, sortowanie i grupowanie nad rekordami jednej tabeli -
to, co zapisują ekrany konsoli table/kanban/list, żeby nikt nie budował tej samej
tablicy przy każdej wizycie. Jest podzasobem tabeli, a nie osobnym zasobem
współdzielonym: widok nie ma własnego właściciela ani grantów swojego rodzaju, a
shared oznacza wyłącznie "widoczny dla każdego, kto już ma tables:view na
tabeli nadrzędnej" - nigdy nie poszerza dostępu ponad to, na co pozwala sama
tabela.
W konsoli ekran bez wybranego widoku nazywa się All records, a + obok zapisuje to, co pokazuje ekran, jako nowy widok. Tablica kanban automatycznie wybiera jedyną aktywną kolumnę jednokrotnego wyboru; przy kilku pyta, która tworzy tory. Zapis widoku zachowuje grupowanie. Karta pokazuje pod tytułem do czterech kolumn ekranu, pomijając puste. Tabela bez kolumny jednokrotnego wyboru proponuje zamiast tego Add a column.
Właściciel widoku przełącza go między Private a Shared przyciskiem obok Rename. Ktoś, kto zarządza widokiem innego członka, na przykład administrator, nie dostaje tego przełącznika: o udostępnieniu czyichś filtrów decyduje ich autor.
GET/POST /tables/{id}/views oraz GET/PATCH/DELETE /tables/{id}/views/{view_id}
listują, tworzą, czytają, aktualizują i usuwają je. Lista jest stronicowana przez
skip i limit (najwyżej 100): najpierw własne widoki wywołującego, potem
współdzielone, w każdej grupie według nazwy; total liczy wszystkie. config to {filters, search, sort,
visible_columns, group_by, display} - RecordQuery oraz pola potrzebne tylko
renderowaniu konsoli: visible_columns (null oznacza każdą żywą kolumnę) i
group_by (żywa kolumna single_select, dla kolumn tablicy kanban).
Obiekt display przechowuje column_order, hidden_system_columns, wrap_text, date_format i time_format. Starsze widoki zachowują kompaktowy wygląd i formatowanie języka. Jawne visible_columns: [] ukrywa wszystkie kolumny niestandardowe.
| Pole | Znaczenie |
|---|---|
kind |
table, kanban lub list - widok jest zapisany dla jednego rodzaju |
visibility |
private (tylko właściciel) lub shared (każdy, kto widzi tabelę) |
can_manage |
Czy ten wywołujący może zmienić nazwę, przekonfigurować lub udostępnić widok |
can_delete |
Czy ten wywołujący może usunąć widok |
Listowanie, odczyt i usunięcie rozwiązują się względem tabeli (tables:view);
utworzenie lub zmiana widoku wymaga tables:edit na tabeli, więc właściciel,
któremu odebrano prawo edycji, nadal może usunąć swoje widoki, ale nie może już
ich przekształcić ani udostępnić.
Zmiana lub usunięcie widoku jest węższe:
tylko jego właściciel albo wywołujący, którego scope dla
tables:edit to ALL - nie "każdy, kto może edytować tabelę" - więc
współdzielony edytor nie może po cichu przestawić zapisanego filtra innego
członka. Odmowa działa tak samo jak przy każdym innym zapisie na jednym zasobie
tutaj: NOT_FOUND (404), nigdy 403, który ujawniłby istnienie widoku wywołującemu,
któremu go odmówiono. can_manage i can_delete mówią, co z tych dwóch ten
wywołujący może zrobić.
Zarchiwizowanie kolumny jest odrzucane z SCHEMA_DEPENDENCY, wskazując widok, gdy
widok, który wywołujący może zarówno zobaczyć, jak i zmienić, nadal używa jej do
filtrowania, sortowania lub grupowania: jeden z jego własnych albo - dla
wywołującego, którego scope dla tables:edit to ALL - współdzielony. Żaden inny
widok nie blokuje archiwizacji i nie jest wymieniany, bo wywołujący nie mógłby go
usunąć z drogi; dotyczy to każdego prywatnego widoku innego członka, którego nie
ujawnia się nawet wywołującemu ze scope ALL. Samo pokazywanie kolumny w
visible_columns też nie blokuje.
To, co widok nadal wskazuje z kolumny, która nie jest już żywa, jest pomijane przy
jego odczycie: filtr na niej znika, sortowanie po niej wraca do created_at,
grupowanie po niej jest czyszczone, a ona sama wypada z visible_columns - widok,
który nie pokazuje już żadnej z wybranych kolumn, pokazuje wszystkie żywe. Zapisana
konfiguracja nie jest przepisywana.
Strony i widoki¶
Opublikowany artefakt nie czyta przez widok. Jego bindingi
to zapytania strony: tabela, filtry, sortowanie, limit i pola, które strona
czyta, a które platforma zapisuje w wersji strony przy publikacji. Binding może
wystartować od zapisanego widoku — agent znajduje go przez list_views, które dla
tabel, które może czytać, zwraca id każdego widoku, jego kolumny i to, czy jest
współdzielony — ale filtry i sortowanie widoku są wtedy kopiowane do strony.
Późniejsza edycja, udostępnienie, zmiana nazwy czy usunięcie widoku niczego na
stronie nie zmienia, a visible_columns i group_by widoku są ignorowane: o kolumnach
decyduje fields bindingu.
Widoku z wyszukiwaniem pełnotekstowym nie można powiązać, bo wyszukiwanie dopasowuje każdą żywą kolumnę tekstową i poszerzałoby się wraz z dodawaniem kolumn; publikacja jest odrzucana i nazywa widok. Tak samo widok, którego filtr wskazuje kolumnę, której już nie ma.
O tym, kto może czytać rekordy strony, nie decyduje widok. Są czytane z dostępem
właściciela strony, gdy właściciel wyraził zgodę, a dostęp do tabeli jest wszystko
albo nic: read daje każdy wiersz i każdą żywą kolumnę. Zapytanie strony to sposób,
w jaki wiersze tabeli docierają do osób, które nie mogłyby jej otworzyć, i właściciel
jest o to najpierw pytany. Zobacz
Artefakty.
Zapytanie strony zapisuje revision każdej kolumny, do której sięga: jej pola,
kolumny filtra i sortowania, wejścia formuły, link i cel lookupu albo rollupu oraz
kolumnę, którą każda powiązana tabela etykietuje swoje rekordy. Binding staje się
unavailable (source_changed), gdy któraś z nich poszła dalej albo została
zarchiwizowana, albo gdy zarchiwizowano tabelę, do której sięga, a pozostałe bindingi
nadal działają. Nic nie blokuje z tego powodu zmiany tabeli: strona nie jest
zależnością, która blokuje archiwizację. share_view już nie istnieje; dla strony
nie ma prywatnego widoku do udostępnienia.
Wyzwalacze¶
Kolumny Kanbana wczytują po 25 kart. Wczytaj więcej zachowuje dotychczasowe karty; po błędzie strony dostępne jest ponowienie. Zmianę grupowania można zapisać w aktywnym widoku. Odczyt prywatnego widoku po zmianie typów kolumn przez innego członka usuwa niezgodne filtry, sortowanie i grupowanie ze zwracanej konfiguracji, bez nadpisywania zapisanego widoku właściciela. Zapytanie strony tracące filtr wymaga ponownej zgody.
Zdarzenia tabel używają zegara ściennego przy wstawianiu po blokadzie mutacji, zgodnego z aktywacją triggera. Transakcja rozpoczęta przed aktywacją może nadal utworzyć późniejsze, kwalifikujące się zdarzenie. Migracja 0229_table_event_wall_clock.py zmienia przyszłe znaczniki czasu; nie odtwarza wcześniej pominiętych zdarzeń, co mogłoby powielić efekty.
Workflow, którego węzłem wyzwalacza jest
Table record, po publikacji uruchamia się dla tego, co dzieje się z rekordami jego
tabeli - dodania, zmiany lub usunięcia, zależnie od events; domyślnie tylko dodania -
niezależnie od drogi: w konsoli, przez API, przez narzędzie tabel agenta albo krok tabeli
innego workflow. Upsert, który tworzy rekord, to dodanie, a taki, który go aktualizuje,
to zmiana. Publikacja wymaga dostępu do odczytu tabeli i prawa do uruchamiania workflow,
bo działa on jako członek, który go opublikował, nigdy jako autor rekordu. Dostęp tego
członka jest sprawdzany ponownie przy każdym zdarzeniu.
Wyzwalacz uruchamia wersję, która go włączyła, a następna publikacja przenosi go na nową wersję. Jego filtry używają operatorów z Listowania i filtrowania i są oceniane na rekordzie takim, jaki został dodany lub zmieniony, albo jaki był przed usunięciem. Zmiana uruchamia go tylko wtedy, gdy zmieniła się jedna z kolumn wybranych dla tabeli, o ile kolumny są wybrane.
Przekazuje runowi rekord: record_id, event, values według id kolumn, te same
wartości jako fields według etykiet i author_id, a przy zmianie lub usunięciu
także before oraz kolumny changed według etykiet. Triggers na stronie tabeli
wymienia workflow, które od niej startują, wstrzymuje i wznawia każdy z nich oraz
otwiera jego historię.
Wyzwalacz startuje tylko dla rekordów dodanych, gdy jest włączony. Włączenie -
publikacja albo wznowienie - bierze blokadę schematu tabeli, na którą czeka każdy zapis rekordu, więc żaden
rekord zatwierdzony przed tą chwilą go nie uruchomi, a nic dodanego, gdy był wyłączony,
nie zostanie odtworzone. Job workflow-table-triggers, uruchamiany co dziesięć sekund, odczytuje zdarzenie
outbox każdego nowego rekordu, więc wyzwalacz startuje w ciągu około dziesięciu sekund. Decyduje raz na wyzwalacz i zapisuje decyzję;
drugi przebieg albo drugi worker znajduje tę decyzję i niczego nie uruchamia.
Historia wymienia każdą decyzję, od najnowszej, bez wartości rekordu:
| Widoczne jako | Dlaczego |
|---|---|
| Rozpoczął uruchomienie | Każdy filtr był spełniony; run jest podlinkowany |
| Pominięty | Rekord nie pasował albo dodano go, zanim wyzwalacz włączono |
| Zablokowany | Uruchomiłby sam siebie ponownie, łańcuch przekroczył pięć wyzwalaczy w głąb albo 50 runów, albo run odrzucił limit przyjęć |
| Nie udało się uruchomić | Członek, jako który działa, nie może już czytać tabeli ani uruchamiać workflow, albo nie udało się odczytać utworzenia rekordu |
Workflow, który zapisuje do tabeli, może uruchomić jej wyzwalacze, i tak dalej przez kolejne tabele. Każdy run niesie łańcuch, do którego należy, a wyzwalacz, przez który łańcuch już przeszedł, jest blokowany zamiast uruchamiany ponownie - to powstrzymuje dwa workflow dodające rekordy do swoich tabel przed zapętleniem. Kolumny, którą wyzwalacz filtruje, nie da się zarchiwizować, dopóki wyzwalacz jego workflow nie przestanie po niej filtrować i nie zostanie opublikowany, nawet gdy jest wstrzymany, a tabeli, od której startuje opublikowany workflow, nie da się zarchiwizować, dopóki ten workflow nie zacznie startować inaczej.
Co zatwierdza się razem¶
Zapis rekordu, jego wiersz historii, jego potwierdzenie idempotencji i, dla create,
wiersz outbox table.record.created są zapisywane w jednej transakcji i zatwierdzane
lub wycofywane razem. Błąd na dowolnym kroku nie zostawia żadnego z nich. Zmiany
tabeli i schematu trafiają do audit log; zmiany rekordów trafiają do
historii per rekord, która przechowuje komórki dotknięte każdą zmianą.
Dwa z tych magazynów trzymają kopie tego, co zapisano. Receipt trzyma cały rekord tak, jak zwrócił go zapis, a history trzyma to, co się zmieniło, więc usunięcie rekordu usuwa bieżący wiersz i zostawia oba, dopóki nie usunie ich retencja. Wiersze outbox trzymają id. Traktuj wszystkie trzy jako dane osobowe, jeśli takie są komórki; zobacz ochronę danych oraz limity i retencję.
Wiersz outbox to przekazanie temu, co reaguje na nowy rekord: dziś są to wyzwalacze. Ich heartbeat pobiera niedostarczone wiersze we własnej sesji, ocenia każdy względem wyzwalaczy tabeli i oznacza go jako wysłany w tej samej transakcji. Niewysłany wiersz jest usuwany tylko przez własne, znacznie dłuższe okno retencji (poniżej) - to dead-letter cutoff dla workera, który tak długo nie działał, a nie deklaracja, że zdarzenie zostało kiedykolwiek odebrane.
Limity i retencja¶
Tenant może rozrosnąć wspólną bazę tylko tak, jak pozwala wdrożenie. Każdy limit to
ustawienie wdrożenia, obowiązuje per organizacja, więc użycie jednego tenanta nigdy nie
liczy się na konto innego, i jest odrzucany kodem QUOTA_EXCEEDED (402), gdy zapis by go
przekroczył.
| Ustawienie | Domyślnie | Ogranicza |
|---|---|---|
TABLES_MAX_PER_ORGANIZATION |
200 | Tabele organizacji. Zarchiwizowane się liczą, bo tabela nigdy nie jest usuwana |
TABLES_MAX_RECORDS_PER_TABLE |
100 000 | Rekordy w jednej tabeli. Aktualizacja rekordu w pełnej tabeli jest dozwolona |
TABLES_MAX_RECORD_BYTES |
1 000 000 | Zserializowane wartości jednego rekordu, w bajtach |
TABLES_MAX_STORAGE_BYTES_PER_ORGANIZATION |
1,073,741,824 | Bajty danych JSONB PostgreSQL na organizację łącznie dla rekordów, historii, potwierdzeń i outboxa; narzut tabel i indeksów jest osobny |
Łączny limit jest sprawdzany w savepoincie zmiany, pod blokadą organizacji, przed commitem. Przekroczenie cofa rekord i wszystkie zachowane kopie, także dla wiersza importu; pozostałe wiersze są raportowane jako odrzucone. Ponowienie z potwierdzenia, zapis bez zmian, odczyt i usuwanie nadal działają po obniżeniu limitu. Retencja zwalnia usuwane bajty.
Odmowa nazywa limit i jego pułap w details ({"quota": "records", "limit": 100000}),
nigdy treść, i zapisuje wpis table.quota_refused w audit log z tymi
samymi dwoma polami. Odrzucone żądanie niczego nie zapisuje. Zapisy są też ograniczone do
RATE_LIMIT_TABLE_WRITES_PER_MINUTE (300) na członka i organizację, w konsoli tak samo jak
przez API; członek ponad limit dostaje 429 z Retry-After. Zobacz
konfigurację.
Co trzyma history. Create trzyma cały rekord w after, a delete trzyma cały rekord w
before; limit rekordu ogranicza oba. Rekord sprzed limitu, albo zapisany zanim
TABLES_MAX_RECORD_BYTES obniżono, wciąż może go przekraczać - jego delete trzyma wtedy
before jako {"omitted": {"bytes": <jego rozmiar>, "limit": <limit>}} zamiast wartości,
i mimo to się udaje. Update trzyma tylko komórki, które się zmieniły: before zawiera ich
wcześniejsze wartości, a after nowe, a kolumna nieobecna po jednej stronie była tam
pusta. Edycja jednej komórki dużego rekordu kosztuje więc jedną komórkę, choćby powtarzana
bez końca.
Retencja. Codzienny sweep retencji usuwa też dane tabel, twardo i partiami, dla każdej organizacji:
| Co | Usuwane, gdy | Ustawienie |
|---|---|---|
| Receipts | Starsze niż 24 godziny | TABLES_RECEIPT_TTL_HOURS |
| Wiersze outbox | Wysłane ponad 3 dni temu | TABLES_OUTBOX_RETENTION_DAYS |
| Niewysłane wiersze outbox | Nigdy niewysłane i mające 30 dni: heartbeat wyzwalaczy nie działał tak długo i dla tych rekordów żaden wyzwalacz nie wystartuje | TABLES_OUTBOX_UNDISPATCHED_RETENTION_DAYS |
| History | Starsza niż 365 dni, dla usuniętego rekordu tak samo jak dla żywego | TABLES_HISTORY_RETENTION_DAYS |
Sweep zapisuje jeden wpis audytu na organizację, nazywający klasę (table_receipts,
table_outbox, table_history) i liczbę. To ustawienia wdrożenia, a nie per organizacja.
Samych rekordów i tabel sweep nigdy nie usuwa.
RATE_LIMIT_TABLE_WRITES_PER_MINUTE to przydział per member, więc budżet jednego przebiegu
dla każdej z trzech klas skaluje się zarówno z nim, jak i z liczbą aktywnych członków
organizacji, z zapasem, żeby istniejąca zaległość się kurczyła, a nie tylko utrzymywała na
stałym poziomie - każdy member piszący bez przerwy naraz nigdy nie wyprzedza sweepa, aż do
hojnego limitu na to, dla ilu członków budżet jednego przebiegu organizacji się skaluje.
Liczby są w konfiguracji. Zaległość
ponad to jest usuwana w kilku kolejnych przebiegach, tak jak w każdej innej klasie.
Kto co może¶
| Permission | Kto ją ma |
|---|---|
tables:view |
Owner, admin, builder i operator widzą wszystkie tabele; member i viewer widzą własne, widoczne dla organizacji i udostępnione |
tables:edit |
Owner i admin edytują wszystkie; builder edytuje własne i udostępnione; member edytuje własne |
tables:create |
Owner, admin, builder, member |
tables:view i tables:edit to permissions zasobowe, więc grant na
jedną tabelę poszerza rolę tylko dla tej tabeli: viewer z edit na jednej tabeli
edytuje tę tabelę i nic więcej. Udostępnianie używa tych samych tras
/tables/{id}/sharing co inne zasoby współdzielone. Rekordy dziedziczą dostęp swojej
tabeli, a schemat to wymusza: wiersz records, history lub outbox odwołuje się do swojej
tabeli także przez organizację, więc nie może wskazać tabeli innego tenanta.
Tabela innej organizacji i tabela, do której wywołujący nie ma dostępu, to w obu przypadkach 404. Kontekst bez zalogowanego podmiotu nie dociera do niczego.
Czy konkretny wywołujący może edytować konkretną tabelę, jest też bezpośrednio na
przewodzie: TableSummary.can_edit i TableRead.can_edit są rozwiązywane po
stronie serwera (scope roli lub jawny grant) i wysyłane przy każdym odczycie, tak
samo jak Agent.can_run - więc wiersz katalogu albo strona szczegółów nigdy nie
musi zgadywać, czy jej kontrolki edycji zostałyby odrzucone. Własne can_manage i
can_delete zapisanego widoku to ta sama idea, o jeden poziom niżej (zobacz
Zapisane widoki).
Błędy¶
Każda odmowa odpowiada {"error": {"code", "message", "details"}}, a code jest tym,
według czego klient się rozgałęzia.
| Kod | Status | Znaczenie |
|---|---|---|
REVISION_CONFLICT |
409 | Rekord zmienił się od odczytu |
REVISION_REQUIRED |
428 | Upsert istniejącego rekordu potrzebuje expected_revision |
SCHEMA_VERSION_CONFLICT |
409 | Schemat zmienił się od odczytu |
SCHEMA_DEPENDENCY |
409 | Coś zależy od tego, co zmiana usuwa |
TABLE_ARCHIVED |
409 | Tabela odrzuca zapisy |
BAD_REQUEST |
400 | Przywrócenie wskazało tabelę, która nie jest zarchiwizowana |
ALREADY_EXISTS |
409 | Nazwa tabeli, nazwa widoku lub external id jest zajęte |
INVALID_RECORD |
422 | Wartość nie pasuje do kolumny; details.fields wskazuje każdą |
ARCHIVED_COLUMN |
422 | Wartość wskazuje zarchiwizowaną kolumnę |
INVALID_QUERY |
422 | Filtr lub sortowanie, na które tabela nie odpowie |
INVALID_SCHEMA |
422 | Niespójna zmiana schematu |
IDEMPOTENCY_KEY_REUSED |
422 | Klucz został użyty dla innego żądania |
QUOTA_EXCEEDED |
402 | Zapis przekroczyłby limit przechowywania; details nazywa limit (tables, records, record_bytes, storage_bytes) i jego pułap |
RATE_LIMIT_EXCEEDED |
429 | Za dużo zapisów do tabel w ostatniej minucie; zobacz Retry-After |
VALIDATION_ERROR |
422 | Samo żądanie jest wadliwe: zły typ, nieznane pole, limit albo NUL, znak nowego wiersza lub osamotniony surogat w id, kluczu lub nazwie. Trasa odrzuca je, zanim uruchomi się serwis |
AUTHORIZATION_ERROR |
403 | Wywołujący nie ma permission, której wymaga trasa kolekcji (tables:view, tables:create) |
CONCURRENT_CHANGE |
409 | Upsert przegrał wyścig z usunięciem tego samego rekordu. Ponów go |
NOT_FOUND |
404 | Nie ma takiej tabeli ani rekordu albo wywołujący nie ma do nich dostępu |
Wywoływanie serwisu z Pythona¶
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),
)
Organizacja zawsze pochodzi z ctx, nigdy z argumentu. Serwis nigdy nie robi commit:
robi go sesja żądania, a worker ma własny zakres sesji.
Dodawanie typu kolumny¶
Typ kolumny to jeden wpis w COLUMN_TYPES w
backend/app/services/virtual_tables/types.py, z nazwą w ColumnTypeName w
backend/app/schemas/virtual_table.py. Wpis mówi, jak komórka jest odczytywana z
rekordu do porównań i sortowania (SqlKind), które operatory filtrów przyjmuje, czy
się sortuje, i jaki walidator przechodzi każdy zapis i każdy operand filtra. Walidator
zwraca wartość tak, jak zostanie zapisana, albo rzuca CellProblem z komunikatem dla
tego, kto ją zapisał. Każda powierzchnia woła ten sam serwis, więc konsola, API,
agenci i workflow przyjmują nowy typ od razu, a wyzwalacz filtruje po nim
według tych samych reguł.
Konsola potrzebuje typu w ColumnTypeName w frontend/src/types/tables.ts, edytora w
record-cell-editor.tsx, wyboru w dialogach schematu i tworzenia tabeli oraz etykiety
w trzech katalogach komunikatów. Testy należą do
backend/tests/test_virtual_table_types.py dla walidatora oraz do testu
integracyjnego, który zapisuje, filtruje i sortuje rekord nowego typu.
Przy tworzeniu pustej tabeli wymagane kolumny nie potrzebują wartości domyślnych: każdy nowy rekord musi podać ich wartości. Dodanie wymaganej kolumny do istniejącego schematu nadal wymaga wartości domyślnej, aby chronić istniejące rekordy.