콘텐츠로 이동

내장 언어와 schema

Unibase는 SQL·JSON·HTML·CSS 같은 다른 언어를 문자열에 숨기지 않고 소스에 직접 씁니다. 컴파일러가 해당 언어의 형태를 검사하고 타입이 있는 값으로 다룹니다.

식 위치의 내장 언어 블록
언어 {
내장 언어 소스
}
최상위 schema 선언
schema 종류 이름 {
선언
}
  • 바깥 중괄호는 Unibase의 블록 경계이고, 안쪽 중괄호는 내장 언어의 것입니다. JSON 객체는 그래서 json { { … } }처럼 중괄호가 두 겹입니다.
  • sql, json 블록 안의 문자열과 주석 속 중괄호는 경계로 세지 않습니다.
  • 블록을 대입·인수·반환값으로 쓰는 문장도 끝에 ;를 씁니다.
형태 상태 값의 타입
sql { … } 지원 SqlText (또는 SQL 문장 핸들)
sql"""…""" 지원 SqlText
직접 SQL 문장 SELECT …; 지원 문장 종류별 결과 타입
json { … } 지원 — JSON 형식 검사 JsonValue
html { … } 제한 지원 — 화면 코드 생성 대상 Widget
css { … } 제한 지원 — 화면 코드 생성 대상 StyleSheet
schema grpc 이름 { … } 지원 — 메시지·서비스 이름을 타입으로 등록 —
grpc { … } 식 구문만 — 사용하지 않음 GrpcSchema
schema graphql/http/state/workflow/validation/search … 구문만 — 내용 미검사 —
regex { … }, xml { … }, graphql { … } 식 로드맵 — 내장 언어로 인식하지 않음 —

SQL 블록·문장의 첫 단어는 대문자 SELECT, INSERT, UPDATE, DELETE, CREATE 중 하나여야 합니다. 소문자 select는 Invalid SQL embedded block: unsupported SQL statement kind. 오류입니다.

int id = 1;
var query = sql {
SELECT id, name FROM users WHERE id = :id
};
string text = query.text();
  • 값은 실행되지 않은 SQL 문장인 SqlText입니다. Database의 execute·query에 전달해 실행합니다.
  • :이름은 같은 범위의 변수 이름을 값 위치에 연결합니다. 없는 변수는 SQL bind variable 'x' is not defined., 정적 메서드에서 인스턴스 필드를 연결하면 Cannot bind instance field 'x' from static context. 오류입니다.
  • 변수 이름이 _로 시작하거나 타입이 SqlStatement<T>이면 실행 가능한 문장 핸들이 됩니다: var _select = sql { SELECT name FROM users WHERE id = :id }; var rows = _select.execute();
SqlText ddl = sql"""
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name TEXT
);
""";

sql"""로 시작해 """로 끝나는 여러 줄 SQL입니다. 앞뒤 공백과 끝의 ;는 제외하고 검사합니다. 닫지 않으면 Unterminated SQL text literal 오류입니다.

메서드 본문에서 대문자 SQL 키워드로 시작하는 문장은 래퍼 없이 SQL로 읽습니다. 값은 _이름 변수나 :이름으로 연결합니다.

전체 소스: DirectSql.uni
class DirectSql {
static void main() {
CREATE TABLE members (id BIGINT PRIMARY KEY, name TEXT);
string _name = "Kim";
INSERT INTO members (id, name) VALUES (1, _name);
int _id = 1;
ResultSet rows = SELECT name FROM members WHERE id = _id;
print("rows=" + rows.rowCount());
var _rename = UPDATE members SET name = 'Lee' WHERE id = _id;
UpdateResult renamed = _rename.execute();
print("updated=" + renamed.rowsAffected());
}
}
형태 의미
INSERT INTO …; 즉시 실행
ResultSet rows = SELECT …; 실행하고 결과를 변수에 받음
var _stmt = UPDATE …; 뒤 _stmt.execute() 문장 핸들을 만들고 나중에 실행
문장 결과 타입 필요한 형태
CREATE ExecuteResult CREATE TABLE [IF NOT EXISTS] 이름 ( 열 … ) — 테이블만 지원
INSERT InsertResult INSERT INTO 이름 … VALUES …
SELECT ResultSet SELECT 열 FROM 이름 …
UPDATE UpdateResult UPDATE 이름 … SET …
DELETE DeleteResult DELETE FROM 이름 …

필요한 부분이 빠지면 Invalid SQL syntax: SELECT requires a projection followed by FROM.처럼 문장별 진단이 표시됩니다. 컴파일러는 문장의 기본 형태만 검사하며 테이블·열의 존재는 실행할 때 확인됩니다. 문장 핸들이 아닌 변수에 execute()를 호출하면 execute() must be called on a SQL statement expression variable. 오류입니다.

선언 용도
Database.memory(), Database.open(path) 네이티브 임시·영속 DB 열기
ExecuteResult execute(SqlText sql) CREATE·INSERT·UPDATE·DELETE 실행
ResultSet query(SqlText sql) SELECT 실행
prepareInsert/prepareUpdate/prepareDelete/prepareSelect(string sql) SqlStatement<결과> 준비 선언; 네이티브 미지원
SqlStatement<T>.bind(string name, string value), execute(), execute(Database db) 값 연결과 실행
ResultSet.rowCount(), isEmpty() 조회 행 수·빈 결과 확인
getString/getInt/getLong/getDouble/getBool(int row, string column), isNull(int row, string column) 지정 행·열의 값을 타입별로 읽고 NULL 확인
rowsAffected(), InsertResult.generatedId(), hasGeneratedId() 변경 결과 읽기
close(), isClosed() 연결 종료

Database.memory()는 전용 임시 폴더를 사용하며 close()에서 제거하고, open(path)는 데이터를 보존합니다. 네이티브 DB는 빈 셀을 NULL로 취급하므로 빈 문자열과의 구별이 필요한 경우 주의합니다. 숫자·bool 읽기는 NULL·타입 불일치를 오류로 처리합니다. Go·NestJS의 long 스칼라 생성과 DB 지원은 별개이며 ResultSet.getLong은 거절합니다. NestJS는 Mapper 매개변수·행과 SQL 바인딩의 long도 지원하지 않습니다.

외부 MySQL SDK의 unibase.db.mysql.Database 선언은 위 네이티브 DB API와 별도입니다. import나 메서드 선언만으로 해당 실행 환경의 연결 지원을 판단하지 마세요. 실행 예는 SQL과 값 바인딩에 있습니다.

var payload = json {
{
"id": 1,
"name": "Kim",
"tags": ["admin", "user"],
"active": true
}
};
string text = payload.text();
  • 블록 안에는 표준 JSON 값 하나를 씁니다. 키는 큰따옴표 문자열이어야 하고 끝 쉼표·주석은 허용하지 않습니다.
  • 오류는 Invalid JSON embedded block: object keys must be strings.처럼 원인을 표시합니다.
  • 값은 JsonValue이며 get/has/at/size, asString/asLong/asDouble/asBool, set/add, toJson/toPrettyJson 등으로 조회·변환·수정합니다. JSON API와 참조 소스를 참고하세요. JSON 블록 안의 변수 보간은 지원하지 않습니다. JSON 블록은 지원 프로필에서 해당 텍스트를 파싱하는 형태로 생성합니다. Json.parse로 문자열을 직접 읽을 수도 있습니다.

html { … }은 화면 조각을 만드는 식이며 값의 타입은 Widget입니다. HTML 페이지·컴포넌트·라우터는 Web 화면 코드 생성(unic export-ui --target vue|react|svelte)과 미리보기 대상에서 사용합니다. 콘솔 native 프로그램의 출력 대상은 아닙니다.

전체 소스: Counter.uni
import unibase.core.ui.*;
import unibase.core.ui.window.*;
import unibase.core.ui.web.*;
public class CounterPage extends HtmlPage {
private int count = 0;
private string title = "Counter";
private final StyleSheet sheet = css {
.counter { display: flex; gap: 8px; }
@media (max-width: 640px) { .counter { flex-direction: column; } }
};
private void increase() {
this.count = this.count + 1;
}
public Widget build(HtmlRouteContext route) {
return this.sheet.apply(html {
<section class="counter" ub:id="counter-root">
<h1 title={this.title}>{this.title}</h1>
<p>Clicked {this.count} times</p>
<button type="button" on:click={this.increase}>Add</button>
</section>
});
}
}
public class AppRouter extends HtmlRouter {
public List<HtmlRoute> buildRoutes() {
return [{ path: "/", page: CounterPage() }];
}
}
public class Application {
public static void main() {
HtmlApplication application = HtmlApplication(AppRouter());
Window window = Window("Counter", 640.0, 480.0, "en", "ltr", application.build());
window.show();
}
}

목록·조건 표시와 실제 서버 호출은 HTML 목록과 HTTP 연결에서 다룹니다. ub:each, ub:key, ub:if와 행 범위 이벤트는 아래의 기본 속성 표를 확장합니다.

항목 형태 규칙
텍스트 값 <p>{this.count}</p> {식}으로 Unibase 값을 넣음. 글자 그대로의 중괄호는 &#123;, &#125;
고정 속성 class="title", disabled 값은 따옴표로 감쌈. 속성 사이에 공백 필요
값 연결 속성 title={this.title} 기본 태그에는 string·int·double·bool 값
단방향 연결 :value={this.name} 컴포넌트 입력을 한 방향으로만 전달
논리 속성 disabled={this.busy} disabled, readonly, required, multiple, checked, selected, hidden, open, controls는 bool
이벤트 on:click={this.save} 호출이 아닌 메서드 참조. void 인스턴스 메서드, 매개변수 없음 또는 HtmlEvent 하나
요소 식별자 ub:id="save-button" 문서 안에서 고유, 영문·숫자·-·_, 128자 이하
라우팅 <router-link to="/help">, <router-view /> router-link에는 href·on:* 사용 불가
  • 사용할 수 있는 기본 이벤트: click, input, change, submit, focus, blur, keydown, keyup, pointerenter, pointerleave, invalid.
  • 내용이 있는 요소는 닫는 태그가 필요합니다(<div/> 불가). <input>, <br> 같은 빈 요소와 컴포넌트는 />로 닫을 수 있습니다.
  • onclick=처럼 스크립트 속성, <script>, <style>, <iframe>, <svg> 등은 허용하지 않습니다. 스타일은 css { }를 사용합니다.
  • 오류는 HTML_SOURCE:로 시작하며 mismatched closing tag, attribute values must be quoted, HTML event requires a method reference, not an invocation처럼 원인을 표시합니다.

대문자로 시작하는 태그는 HtmlComponent를 상속한 사용자 컴포넌트입니다.

public class Badge extends HtmlComponent {
public string caption = "";
public Widget child;
public final HtmlComponentEvent<string> saved = HtmlComponentEvent<string>();
private void send() {
this.saved.emit(this.caption);
}
public Widget build() {
return html { <section><p>{this.caption}</p>{this.child}<button on:click={this.send}>Save</button></section> };
}
}
<Badge caption={this.name} on:saved={this.accept}>
<strong>Default slot</strong>
</Badge>
  • 컴포넌트 입력은 public이고 static·final이 아닌 필드입니다. 태그 안의 자식 내용은 public Widget child 필드로 전달됩니다.
  • 컴포넌트 이벤트는 public final HtmlComponentEvent<T> 필드이며 emit(값)으로 보냅니다. 받는 메서드는 T 매개변수 하나를 받을 수 있습니다.
  • 다른 Widget 필드는 header={html { <h2>Title</h2> }}처럼 이름 있는 영역으로 전달합니다.
  • 기본 제공 컴포넌트 <ub:Button>은 type, class, disabled, 자식 내용, on:click을 받습니다.

화면 코드 생성 프로젝트에서는 Home.uni, Home.html.uni, Home.css.uni처럼 한 클래스를 partial 클래스로 나눌 수 있습니다.

private final StyleSheet sheet = css {
.card { padding: 16px; border-radius: 8px; }
.card:hover { color: rgb(37, 99, 235); }
@media (max-width: 640px) { .card { padding: 8px; } }
};
  • 블록 안에는 선택자 { 속성: 값; } 규칙을 씁니다. 사용자 정의 속성 --name과 !important를 쓸 수 있습니다.
  • at-rule은 @media, @supports, @container만 허용합니다. 규칙 안의 중첩 선택자는 지원하지 않습니다.
  • 규칙은 해당 페이지·컴포넌트 범위로 제한됩니다. this.sheet.apply(html { … })로 적용하고, 전체 페이지 스타일은 application.registerGlobalStyle(css { body { … } })로 등록합니다.
  • 오류는 CSS_SOURCE:로 시작합니다(expected property colon, unsupported at-rule: @font-face 등).

database, requirements, qa, test, dictionary, layout은 본문 검사와 참조 분석이 있는 모델링 schema입니다. 파일 구성, realizes/verifies/covers, @Realizes는 모델링 DSL과 추적성에서 설명합니다. 이들의 제목 문자열은 이름 뒤에 둡니다. 아래 grpc 및 다른 schema 종류와 검사 범위를 구분하세요.

schema 형태
schema 종류 이름 {
…
}
  • 파일의 최상위(타입 선언 위치)에만 둡니다. 종류와 이름이 모두 필요합니다(schema grpc { }는 Invalid schema declaration).
  • 같은 이름의 schema나 타입이 있으면 Duplicate schema declaration 'X'., Schema declaration 'X' conflicts with an existing type. 오류입니다.
전체 소스: UserApi.uni
package app.users;
schema grpc UserApi {
message GetUserRequest {
id: int
}
message UserReply {
id: int
name: string
}
service UserService {
rpc GetUser(GetUserRequest) returns (UserReply)
}
}
class UserApiCheck {
static void main() {
print("schema ready");
}
}
schema grpc 본문 문법
message 이름 {
필드이름: 타입 // 줄마다 하나, 끝의 ; 또는 , 는 선택
}
service 이름 {
rpc 메서드이름(요청메시지) returns (응답메시지)
}
  • 필드 타입은 bool, byte, short, int, long, float, double, char, string 또는 같은 schema의 message입니다 (Unknown gRPC field type 'Missing'.).
  • rpc의 요청·응답은 같은 schema의 message 이름입니다.
  • message·service 이름은 패키지의 타입 이름으로 등록되므로 다른 타입과 겹치면 안 됩니다. 이름·필드·rpc 중복은 Duplicate gRPC … 오류입니다.
  • 현재는 이름 등록과 검사까지 지원합니다. 언어 명세의 생성 클라이언트·서버 API(UserService.client(...), Grpc.server(...))는 로드맵입니다.
  • grpc { service … } 식 블록도 구문은 통과하지만 언어 명세의 표준 형태가 아니므로 schema grpc를 사용하세요.

언어 명세는 schema graphql, schema http, schema validation, schema state, schema workflow, schema search를 계획하고 있습니다. 현재 컴파일러는 이 선언을 구문으로만 통과시키고 본문을 검사하거나 타입·API를 만들지 않습니다. 동작을 기대하고 작성하지 마세요.