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
@embeddedi@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.csiStorageMutationTaskCallTests.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.