Przejdź do treści

Rozszerzenie języka: storage

storage deklaruje logiczny model danych oraz nazwane odczyty i mutacje. Nie wybiera silnika bazy. Parser, projekcja planu, wywołanie z zadania i wykonanie przez provider są odrębnymi etapami; przyjęcie deklaracji nie dowodzi zapisu danych.

Składnia modelu

storageDeclaration
    : STORAGE anyIdentifier LBRACE storageMember* RBRACE
    ;
storageMember
    : storageEntityDeclaration
    | storageQueryDeclaration
    | storageMutationDeclaration
    ;
storageEntityDeclaration
    : ENTITY anyIdentifier LBRACE storageEntityMember* RBRACE
    ;
storageEntityMember
    : storageModelField
    | storagePrimaryKey
    ;
storageModelField
    : anyIdentifier COLON typeReference SEMICOLON?
    ;
storagePrimaryKey
    : KEY LPAREN anyIdentifier (COMMA anyIdentifier)* RPAREN SEMICOLON?
    ;

S01 — encje i klucz

Pole ma nazwę i typ; nie ma w tej regule wartości domyślnej. Średnik jest opcjonalny. Gramatyka dopuszcza wiele deklaracji key, ale projekcja źródła wymaga dokładnie jednej. Klucz może być złożony; jego pola muszą istnieć, a pola klucza nie mogą być nullable. Kontrole nazw i modelu obejmują powtórzenia encji i pól oraz błędne odwołania do klucza. Akceptacja typeReference przez parser nie wystarcza do przyjęcia typu w modelu storage.

S02 — przenośne typy

StorageSourceProjector mapuje następujące typy:

Typ źródłowy Typ modelu storage
Int, i8, i16, i32, i64, u8, u16, u32 int64
Float, f32, f64 float64
Bool, Text, Bytes, Decimal odpowiednio bool, text, bytes, decimal
Time, Uuid, Json odpowiednio timestamp, uuid, json
T | Null nullable odpowiedniego typu T

Unia nullable musi zawierać dokładnie jeden konkretny obsługiwany typ i jeden Null. u64 jest odrzucany: model int64 nie zachowuje całego jego zakresu. Dowolne struktury, kolekcje, typy komunikacyjne i inne unie nie należą do tego mapowania. Rozszerzanie tabeli na podstawie samej składni byłoby nieuprawnione.

Składnia odczytu

storageQueryDeclaration
    : QUERY anyIdentifier LPAREN storageQueryParameterList? RPAREN
      FROM anyIdentifier LBRACE storageQueryStep* RBRACE
    ;
storageQueryParameterList
    : storageQueryParameter (COMMA storageQueryParameter)*
    ;
storageQueryParameter
    : anyIdentifier COLON typeReference
    ;
storageQueryStep
    : storageFilterStep
    | storageProjectStep
    | storageSortStep
    | storageLimitStep
    ;
storageFilterStep
    : FILTER expression SEMICOLON?
    ;
storageProjectStep
    : PROJECT storageProjection (COMMA storageProjection)* SEMICOLON?
    ;
storageProjection
    : anyIdentifier (AS anyIdentifier)?
    ;
storageSortStep
    : SORT storageSortKey (COMMA storageSortKey)* SEMICOLON?
    ;
storageSortKey
    : anyIdentifier (ASC | DESC)?
    ;
storageLimitStep
    : LIMIT INTEGER_LITERAL (OFFSET INTEGER_LITERAL)? SEMICOLON?
    ;

S03 — źródło i parametry

query Nazwa(...) from Encja wskazuje encję w modelu i listę typowanych parametrów. Nawiasy są wymagane również bez parametrów. W predykacie zwykła nazwa oznacza pole aktualnego kształtu rekordu, a params.nazwa — parametr operacji. Samo podobieństwo nazw pola i parametru nie zmienia tej reguły.

S04 — kolejność kroków

Kroki są stosowane w kolejności źródła, a nie automatycznie przestawiane:

Krok Znaczenie w planie
filter predykat Wybór rekordów według obsługiwanego predykatu.
project pole [as alias], ... Nowy kształt wyniku, obejmujący wskazane pola i ich nazwy wynikowe.
sort pole [asc|desc], ... Sortowanie po dostępnych polach; brak kierunku oznacza ascending w projekcji planu.
limit n [offset m] Ograniczenie liczby wyników i przesunięcie.

Po projekcji dalsze kroki widzą jej wynik. project name; sort id jest błędem, jeżeli projekcja nie zachowała id. Alias projekcji jest nazwą pola wyniku. limit i offset przyjmują literały całkowite, nie parametry ani wyrażenia. Projekcja wymaga zakresu Int32; walidator odrzuca niedodatni limit i ujemny offset. Nie należy zakładać ustalonej kolejności rekordów przy braku sort ani reguły rozstrzygania remisów wyłącznie na podstawie obecności tego kroku.

S05 — podzbiór wyrażeń

Gramatyka przyjmuje ogólne expression, ale projektor dopuszcza ograniczony podzbiór: porównania, logiczne and, or, not, pola, parametry i obsługiwane literały. Walidator planu sprawdza również typy i dostępność nazw. Arytmetyka w operandzie filtra, dowolne wywołania funkcji i dostęp do obcego obiektu nie stają się przez to operacjami providera. Nie dodajemy tu własnych zasad porównań null ani konwersji typów.

Składnia mutacji

storageMutationDeclaration
    : MUTATION anyIdentifier LPAREN storageQueryParameterList? RPAREN
      storageMutationKind storageMutationCardinality anyIdentifier
      LBRACE storageMutationAssignment* storageMutationWhere? RBRACE
    ;
storageMutationKind
    : INSERT
    | UPDATE
    | DELETE
    ;
storageMutationCardinality
    : SINGLE
    | MANY
    ;
storageMutationAssignment
    : SET anyIdentifier ASSIGN expression SEMICOLON?
    ;
storageMutationWhere
    : WHERE expression SEMICOLON?
    ;

S06 — rodzaj i jawny zasięg

Zapis wymaga insert, update albo delete oraz jawnego single albo many. Przypisania set występują przed opcjonalnym składniowo where. Walidacja planu zaostrza wymagania parsera:

Operacja Wymagania
insert single Tworzenie jednego rekordu, bez where; wymagane przypisanie wszystkich pól non-nullable. Pole nullable można pominąć.
update single / update many Co najmniej jedno set, obowiązkowy predykat; zakaz zmiany pól klucza.
delete single / delete many Obowiązkowy predykat, brak przypisań set.

insert many jest odrzucane. many jest jawnym wyborem zasięgu, ale nie zwalnia z obowiązku predykatu. Nazwy query i mutation współdzielą przestrzeń operacji modelu; kolizja jest diagnozowana jako SMP080.

S07 — wartości i bezpieczeństwo single

Wartość set może pochodzić z params.nazwa albo literału. Nie może być odczytem bieżącego pola ani dowolnym obliczeniem. Kontrolowane są nieznane i wielokrotnie przypisane pola, zgodność typów i nullability.

Dla update/delete single walidator wymaga związania wszystkich pól klucza przez równość z nie-nullable parametrem albo literałem. Nie oznacza to „weź pierwszy pasujący rekord” ani „rekord na pewno istnieje”. Brak pełnego ograniczenia klucza jest błędem SMP041, zamiast niejawnej zmiany na many.

Wywołanie nazwanej operacji z zadania

S08 — symbole i wynik

Z modelu Data i query Names powstają symbole Data_Names(...) oraz Data_NamesRow. Query zwraca List<Data_NamesRow>; pola wiersza wynikają z końcowego kształtu zapytania, w tym aliasów projekcji.

Z modelu Data i mutation Add powstają Data_Add(...) oraz Data_AddResult. Wynik mutacji zawiera outcome: Text i affectedRows: Int, a nie pełny rekord encji. Opis taskowego kontraktu rozróżnia applied i not-applied; błędu lub nieznanego skutku nie należy utożsamiać z not-applied.

Wywołania są dozwolone w jednoznacznym zadaniu @server. Argumenty mogą być pozycyjne w kolejności deklaracji albo nazwane; semantyka sprawdza ich liczbę, nazwy i typy. Rodziny diagnostyk SQC i SMC dotyczą odpowiednio query i mutation. Obsługa aliasów i importów ma własne ograniczenia opisane w modułach. Nie przenosimy dowodu dla importowanych query na wszystkie warianty importowanych mutacji.

Sprawdzone przykłady

Każda para jest sprawdzana przez CLI w osobnym projekcie. To kontrola źródła i planów, bez otwierania bazy lub uruchamiania providera.

Przykład poprawny

storage Measurements {
    entity Reading {
        id: Int
        value: Float
        key(id)
    }

    query Recent() from Reading {
        project value
        limit 10
    }
}

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Przykład błędny

storage Measurements { entity Reading { id: Int key(id) } query Recent() from Reading { limit -1 } }

Powód: Gramatyka limit wymaga literału całkowitego bez znaku minus.

Kontrola: przykład odrzucony przez check; kody: P0001.

Pełny zakres weryfikacji przykładów.

Przykład: Przenośny typ pola

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
 }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: u64 key(id) }
 }

Powód: u64 nie mieści się bezstratnie w przenośnym int64.

Kontrola: przykład odrzucony przez check; kody: SQP013, SQP072.

Pełny zakres weryfikacji przykładów.

Przykład: Kolejność projekcji i sortowania

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Names() from Item { project name sort name } }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Names() from Item { project name sort id } }

Powód: Po projekcji name pole id nie należy już do kształtu wyniku.

Kontrola: przykład odrzucony przez check; kody: SQP060.

Pełny zakres weryfikacji przykładów.

Przykład: Podzbiór predykatów

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Find() from Item { filter id > 1 } }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Find() from Item { filter id + 1 > 2 } }

Powód: Arytmetyka w operandzie filtra nie należy do obsługiwanego podzbioru.

Kontrola: przykład odrzucony przez check; kody: SQP077.

Pełny zakres weryfikacji przykładów.

Przykład: Zasięg single i pełny klucz

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
mutation Remove(id: Int) delete single Item { where id == params.id } }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: Int name: Text key(id) }
mutation Remove(name: Text) delete single Item { where name == params.name } }

Powód: Warunek nie wiąże pola klucza id; single nie oznacza arbitralnego pierwszego rekordu.

Kontrola: przykład odrzucony przez check; kody: SMP041.

Pełny zakres weryfikacji przykładów.

Przykład: Przypisanie z parametru

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
mutation Rename(id: Int, name: Text) update single Item { set name = params.name where id == params.id } }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: Int name: Text key(id) }
mutation Rename(id: Int) update single Item { set name = name where id == params.id } }

Powód: set dopuszcza parametr albo literał; nie odczyt bieżącego pola.

Kontrola: przykład odrzucony przez check; kody: SMP074.

Pełny zakres weryfikacji przykładów.

Przykład: Wywołanie query z tasku

Poprawny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Names() from Item { project name } }
task Read() @server -> List<Data_NamesRow> { return Data_Names() }

Kontrola: przykład przyjęty przez check. Nie jest to test wykonania.

Błędny zapis

storage Data { entity Item { id: Int name: Text key(id) }
query Names() from Item { project name } }
task Read() @report { Data_Names() }

Powód: Wywołanie query wymaga jednoznacznego zadania @server.

Kontrola: przykład odrzucony przez check; kody: SQC001.

Pełny zakres weryfikacji przykładów.

Dodatkowe próby graniczne

Próba Wynik check
Encja bez klucza błąd SQP012
Nullable pole klucza błąd SQP014
limit 0 błąd SQP042
insert many błąd SMP030
Update pola klucza błąd SMP036
delete many bez where błąd SMP040
Sortowanie bez asc/desc po istniejącym polu przyjęte

Granice wykonania i jawne luki

Deklaracja jest obniżana do neutralnego modelu oraz planu query lub mutation. Provider i jego konfiguracja są osobną warstwą. check nie tworzy tabel, nie wykonuje migracji i nie zmienia danych.

Repozytorium zawiera osobne kontrakty wykonania przez memory, SQLite i PostgreSQL oraz scenariusze wywołania z wygenerowanego Go. Nie oznacza to jednak, że każdy przyjęty program został wykonany na każdym providerze. W tym przeglądzie sprawdzamy źródła, plany i typowanie; nie wykonujemy prób na rzeczywistych bazach.

Kontrakty taskowego bootstrapu opisują zatrzymanie wykonania przy błędzie Runtime, providera lub nieznanym skutku mutacji; nie obiecują automatycznego retry. Jawna obsługa takich błędów w Junexis wymaga osobnego kontraktu.

Poza ustalonym tu podzbiorem pozostają:

  • relacje, joiny, agregacje i podzapytania w składni storage;
  • surowy SQL, upsert i transakcje obejmujące wiele operacji;
  • jawna obsługa błędów i wyniku nieznanego w kodzie zadania;
  • idempotency key i polityki bezpiecznego ponawiania mutacji;
  • pełna równoważność wszystkich typów i operacji we wszystkich providerach;
  • gwarancje trwałości, izolacji i zachowania po awarii wynikające wyłącznie z deklaracji języka;
  • pełny kontrakt importów/aliasów mutacji oraz wykonania na @embedded i @report.

Nie ustanawiamy tych właściwości na podstawie samej obecności deklaracji, wygenerowanego klienta lub poprawnego planu.

Podstawa w repozytorium

  • grammar/JunexisLang.g4: reguły storage przytoczone powyżej.
  • compiler/JunexisLang.Compiler.IR/StorageSourceProjector.cs: mapowanie typów, kształt kroków i projekcja mutacji.
  • compiler/JunexisLang.Compiler.IR/StorageMutationPlanIR.cs: walidacja zapisów, przypisań i pełnego klucza.
  • compiler/JunexisLang.Compiler.Tests/IR/StorageSourceProjectorTests.cs, StorageQueryPlanTests.cs, StorageMutationSourceProjectorTests.cs, StorageMutationPlanTests.cs: model, kolejność kroków i ograniczenia planów.
  • compiler/JunexisLang.Compiler.Tests/Semantics/StorageQueryTaskCallTests.cs i StorageMutationTaskCallTests.cs: typowanie wywołań z zadań.
  • spec/43-storage-source-projection.md, spec/56-storage-mutation-source-projection.md, spec/51-storage-task-query-binding.md, spec/64-storage-task-mutation-binding.md: kontrakty kolejnych etapów. Historyczne ograniczenie pojedynczego etapu nie oznacza automatycznie braku funkcji w późniejszym etapie.