라이브러리
d93ee3a 기준입니다. 파일 경로는 저장소 루트 기준입니다.reylo는 .sql 파일을 직접 가져와, 매개변수와 결과 타입이 추론된 쿼리 객체로 쓰게 하는 TypeScript 라이브러리입니다. Postgres·MySQL·SQLite를 다루며, 실제 쿼리는 애플리케이션이 고른 드라이버로 실행합니다. 데이터베이스 서버를 대신 운영하거나 테이블을 자동 생성하는 제품은 아닙니다.
SQL과 데이터베이스 스키마를 읽어 타입을 정하고, 그 타입에 맞춰 실행 결과까지 정리하는 연결이 이 프로젝트의 핵심입니다. 따라서 공부할 대상도 SQL 문법 하나에 그치지 않습니다. 컴파일러 확장, Rust의 인터페이스 설계, 캐시 무효화, 여러 실행 환경을 하나의 API로 묶는 방법을 함께 볼 수 있습니다.
SQL은 데이터베이스가 읽는 주문서입니다. TypeScript는 보통 그 주문서 안에 어떤 입력이 들어가고 어떤 열이 돌아오는지 모릅니다. reylo는 데이터베이스에 주문서를 검사받고, 그 답을 TypeScript가 읽는 설명서로 옮깁니다.
실제로 주문할 때는 기존 배달원인 드라이버를 씁니다. 통역사는 설명서에 적힌 모양과 배달된 값의 모양을 맞추는 일도 담당합니다. 이 두 단계가 각각 타입 검사 경로와 실행 경로입니다.
다음은 이해를 위한 예제입니다. users.id가 integer primary key이고, name은 text not null, email은 NULL을 허용한다고 가정합니다. 테이블 정의가 달라지면 추론도 달라집니다.
-- get_user.sql: :id는 이름이 있는 매개변수
select id, name, email
from users
where id = :id::int
// main.ts: Postgres 드라이버와 런타임 로더 설정을 마친 뒤의 코드
import postgres from "postgres";
import getUser from "./get_user.sql";
const db = postgres(process.env.DATABASE_URL!);
const user = await getUser.run(db, { id: 1 });
// 추론되는 결과: { id: number; name: string; email: string | null } | null
if (user !== null) {
console.log(user.name);
}
await db.end();
결과 끝의 | null은 사용자가 없을 수 있다는 뜻입니다. email의 | null은 사용자는 있지만 이메일이 비어 있을 수 있다는 뜻입니다. 행의 존재 여부와 열의 NULL 가능성은 서로 다른 정보입니다.
| 내부 값 | TypeScript 결과 | 대표적인 조건 |
|---|---|---|
one | Row | 행을 제거하는 조건이 없는 단일 집계 등, 정확히 한 행으로 추론된 경우 |
maybeOne | Row | null | 유일 키 조건, 또는 limit 1 등 최대 한 행인 경우 |
many | Row[] | 다수 행 가능성이 있거나 더 좁게 판단하지 못한 경우 |
none | void | 결과 열을 돌려주지 않는 변경문 등 |
이 반환 규칙은 src/index.ts의 Materialize<Row, C> 조건부 타입과 defineQuery() 내부의 materialize() 분기에 함께 정의되어 있습니다. 정적 타입과 실제 반환 동작을 짝지어 읽는 것이 첫 번째 학습 과제입니다.
examples/node/src/main.ts → src/index.ts → src/loader.ts → src/analyze.ts 순으로 읽으세요. 사용자가 보는 결과에서 출발해, 그 결과를 만들기 위해 필요한 정보가 무엇인지 거꾸로 따라가면 됩니다.
선정 근거는 TrendShift Daily #21(2026-10-23)입니다. 2026-10-23 GitHub 기준, 저장소 생성일은 2026-10-21이며 스타는 776개입니다. 다만 이 숫자만으로 유입 경로, 사용자 수, 실제 도입률을 알 수는 없습니다. 다음 세 가지는 관심을 끌 이유에 대한 설계 관점의 해석입니다.
이를 가능하게 하는 기능이 콘텐츠 매퍼입니다. TypeScript가 외부 형식의 파일을 가상 TypeScript로 읽도록 변환기에 맡기는 방식입니다. 공식 제안에는 표준 입출력의 JSON-RPC 통신과 --runExternalCode가 설명되어 있고, reylo의 구현은 src/rpc.ts·src/mapper.ts에 있습니다. TypeScript 콘텐츠 매퍼 공식 제안
“생성할 타입 파일이 없다”와 “내부에서 코드를 전혀 만들지 않는다”는 서로 다른 주장입니다. src/mapper.ts는 export type Params와 declare const query를 포함한 텍스트를 만들고, src/loader.ts의 sqlModule()은 defineQuery(...)를 호출하는 실행 모듈을 만듭니다.
| 방법·도구 | 타입을 얻는 흐름 | 선택할 때 볼 차이 |
|---|---|---|
| 직접 작성한 타입 | SQL 결과와 별도로 인터페이스를 작성 | 설정은 단순하지만 실제 쿼리와 타입이 달라져도 직접 찾아야 함 |
| Prisma TypedSQL | SQL 파일을 작성하고 prisma generate --sql로 타입 있는 함수를 생성 | Prisma Client 흐름에 연결됨. 공식 문서는 생성 명령과 감시 옵션을 안내 |
| sqlc | 스키마·쿼리와 sqlc.yaml을 읽고 sqlc generate로 코드 출력 | 생성 결과를 사용한다는 경계가 명확함. DB 연결을 이용한 분석 옵션도 있음 |
| reylo | 콘텐츠 매퍼가 SQL 분석을 요청하고 타입 선언을 가상으로 전달 | SQL 직접 가져오기와 기존 드라이버 활용. TypeScript 7.1 및 로더 설정이 필요 |
비교는 기능 전체의 우열이나 성능 비교가 아닙니다. “SQL 수정 뒤 어떤 단계로 타입이 갱신되는가”에 한정했습니다. 경쟁 도구의 흐름은 Prisma TypedSQL 공식 문서와 sqlc 생성 명령 공식 문서 기준입니다. sqlc도 DB 기반 분석을 제공하므로 “DB에 물어보는 것은 reylo만의 기능”이라고 하면 부정확합니다.
README는 에이전트 친화성을 장점으로 내세웁니다. SQL·스키마·타입 선언을 여러 군데서 함께 수정할 필요를 줄인다는 주장입니다. 소스에서 확인 가능한 부분은 철자가 틀린 열이나 타입이 없는 매개변수를 진단으로 돌려주는 경로입니다. AI 모델을 호출하는 추론 엔진이 들어 있다는 뜻은 아닙니다.
src/module-types.ts의 moduleTypes()는 strictParams가 켜져 있고 매개변수 타입이 unknown이면 코드 12 진단을 만듭니다. SQL이 이미 다른 오류로 분석에 실패했다면 그 오류를 먼저 고치도록 중복 진단을 줄입니다. “타입이 틀렸다”뿐 아니라 어떤 오류를 먼저 보여줄지도 제품 설계라는 점을 배울 수 있습니다.
공개 저장소 생성일은 코드 개발 시작일과 다릅니다. Cargo.toml에는 2026-10-03 측정치를 적은 주석도 있습니다. 생성일과 스타 수만으로 “이틀 만에 완성했다”거나 도입이 빠르게 늘었다고 판단할 근거는 없습니다.
이 저장소에는 애플리케이션용 백엔드 서버와 프론트엔드가 한 쌍으로 존재하는 전형적인 웹 서비스 구조가 없습니다. 분석 엔진을 백엔드 역할로, TypeScript 패키지를 개발 도구와 실행 연결층으로 구분하면 이해하기 쉽습니다. React 프론트엔드는 이 라이브러리를 쓰기 위한 필수 런타임이 아니라 문서 사이트입니다.
| 부품 | 의존성 선언·버전 | 역할 |
|---|---|---|
| Rust 워크스페이스 | 각 엔진 crate의 edition = "2021" | 공통 모델과 DB별 분석 구현 분리. edition은 Rust 컴파일러의 정확한 버전이 아님 |
| 직렬화 | core의 serde = "1", serde_json = "1" | 호스트와 주고받는 요청·결과를 JSON으로 표현 |
| 설정 파일 | serde_yaml = "0.9" | .reylo.yaml 설정 해석. 잠금 버전은 0.9.34+deprecated |
| Postgres 파서 | pg_query = "18.0", 잠금 값 18.0.0 | Postgres 18 계열 구문 분석. 워크스페이스 패치가 준비된 로컬 복사본을 가리킴 |
| MySQL 파서 | engine/mysql/vendor/sqlparser/Cargo.toml의 0.63.0 | 필요 범위에 맞춘 저장소 내 파서 복사본 사용 |
| SQLite | engine/build.sh의 sqlite-amalgamation-3530400.zip | SQLite 3.53.4 소스에 훅을 붙여 해석된 구문 트리를 읽음 |
| C 연결 | Postgres의 bindgen = "0.66", cc = "1" | C 파서와 Rust 사이의 바인딩·컴파일 지원 |
이 값들은 engine/core/Cargo.toml과 DB별 Cargo.toml, Cargo.lock, engine/build.sh의 선언·잠금 값입니다. 예를 들어 serde_json의 잠금 버전은 1.0.151, bindgen은 0.66.1입니다. 허용 범위를 선언한 버전과 실제로 잠긴 버전을 구분해서 읽으세요.
wasm32-wasip1로 빌드하고 Node의 node:wasi로 불러옵니다. WASM이라는 이유만으로 모든 브라우저·엣지 환경에서 같은 로더가 동작하는 것은 아닙니다.| 설정 | 실제 값 | 의미 |
|---|---|---|
| Node 조건 | 루트 package.json: >=24 | 개발 도구·로더가 사용하는 실행 환경의 요구사항 |
| TypeScript | 루트: ^7.1.0-dev.20260918.1 | 콘텐츠 매퍼 지원을 전제로 함 |
| 예제의 컴파일러 | examples/node/package.json: 7.1.0-beta | 예제를 복제할 때 루트 개발 의존성과 혼동하지 않기 |
| DB 엔진 패키지 | @reylosql/postgres 등 선택적 peer 0.4.0 | 사용하는 DB의 엔진만 설치하며 reylo와 버전을 맞춤 |
| 드라이버 peer | pg >=8, postgres >=3, mysql2 >=3 | 선택적 의존성. 런타임은 드라이버의 필요한 메서드 형태를 검사 |
| 패키지 빌드 | esbuild ^0.28.2와 tsc | 타입 선언과 JavaScript를 만들고 배포 경로를 바꿈 |
scripts/build.ts는 소스를 dist/reylo로 컴파일하고 패키지의 내보내기 경로를 .ts에서 .js·.d.ts로 바꿉니다. 엔진은 dist/postgres 등으로 따로 복사합니다. 소스 패키지의 private: true는 우발적인 배포를 막는 설정이며, 배포본을 만드는 단계에서는 해당 속성을 제거합니다.
| 영역 | site/package.json의 값 | 역할 |
|---|---|---|
| UI | react ^19.2.0, react-dom ^19.2.0 | 문서 UI와 상호작용 컴포넌트 |
| 라우팅 | react-router 8.5.0 | 문서 경로와 빌드 시 페이지 생성 |
| 빌드·스타일 | vite 8.3.4, tailwindcss ^4.1.0 | 문서 빌드와 스타일 처리 |
| 문서 처리 | fumadocs-core ^16.15.1, fumadocs-mdx ^15.3.1 | MDX 콘텐츠와 문서 기능 |
| 사이트 타입 검사 | typescript ^5.9.0 | 라이브러리 이용 프로젝트의 7.1 조건과는 별개의 의존성 선언 |
site/react-router.config.ts는 ssr: false와 prerender 목록을 지정합니다. 빌드 때 문서와 Markdown 경로를 만들고, site/vercel.json은 build/client를 배포 대상으로 삼습니다. “React가 들어 있으니 DB 쿼리가 브라우저에서 직접 실행된다”는 해석은 이 구성과 맞지 않습니다.
| 구성 | 소스의 설정·동작 | 배울 점 |
|---|---|---|
| 엔진 배포 | DB별 engine.wasm을 저장소에 포함 | 일반 이용자는 엔진을 Rust로 다시 빌드할 필요가 없음 |
| 엔진 빌드 도구 | wasm32-wasip1, wasi-sdk 34 | 호스트에서 실행할 코드와 빌드 대상의 환경을 구분 |
| JavaScript CI | Node 24, nub 0.9.6, Postgres 18, MySQL 8.4 | 드라이버·통합 테스트를 실제 DB와 함께 검사 |
| Rust CI | 워크스페이스 빌드·crate 테스트·별도 oracle 작업 | 추론 결과와 실제 실행값의 관계까지 검증 |
| 릴리스 | .github/workflows/release.yml의 수동 실행과 CI 재사용 | 버전 변경·테스트·npm 배포·태그 생성을 별도 단계로 관리 |
| 컨테이너 | 저장소에 Dockerfile 없음. CI의 DB 서비스는 컨테이너 | 이 라이브러리용 운영 서버 이미지를 상상해서 추가하지 않기 |
사용자가 실행하는 SQL 함수와, 그 함수의 타입을 만드는 개발 도구는 서로 다른 의존성과 실행 시점을 가집니다. src/engine.ts의 WASI 호스팅과 src/index.ts의 쿼리 API를 나란히 읽으면 이 경계가 드러납니다.
중요한 구분은 쿼리 모듈이 만들어지는 때와 .run()을 호출하는 때입니다. Node의 등록 훅은 실행 프로세스에서 모듈을 불러올 때 분석 경로를 사용할 수 있고, 번들러는 미리 분석 정보를 담은 모듈을 만듭니다. “개발 도구 경로”가 언제나 별도의 빌드 서버에서만 돈다는 뜻은 아닙니다.
src/mapper.ts의 transform()은 분석 결과를 받아 가상 선언을 돌려줍니다. src/loader.ts의 sqlModule()은 같은 결과를 실행 코드로 바꿉니다. 동일한 분석 결과에 두 소비자가 붙어 있는 구조라고 읽으면 됩니다.
Rust 엔진은 직접 DB 연결을 열지 않습니다. 필요한 답이 없으면 “이 문장을 describe해 달라”, “이 카탈로그 쿼리를 실행해 달라”는 요청을 모읍니다. TypeScript 호스트가 그 일을 처리한 뒤, 지금까지의 답을 넣어 엔진을 다시 호출합니다.
engine/core/src/host.rs의 Host::describe()와 Host::query()를 보면 동작이 명확합니다. 답 목록에 해당 키가 있으면 그 값을 반환하고, 없으면 Need를 기록한 다음 None을 돌려줍니다. 이 파일의 Lazy<T>는 Option<T> 별칭이며, 이 문맥의 None은 “아직 답을 받지 못했다”는 뜻입니다.
// src/engine.ts / Engine.analyze()의 핵심 제어 흐름을 줄인 의사 코드
const answers = {};
for (let round = 0; round < 16; round++) {
const result = engine.call({ op: "analyze", sql, dialect, answers });
if (result.status === "analysis") return result.analysis;
if (result.status !== "need") throw new Error("분석 응답 오류");
await Promise.all(result.needs.map(async (need) => {
answers[need.id] = await fulfil(need, io);
}));
}
throw new Error("분석이 수렴하지 않음");
실제 구현은 중간에 AnswerCache.get()도 사용하며, 왕복 횟수를 최대 16회로 제한합니다. 한 차례의 독립 요청은 Promise.all()로 처리합니다. 모든 요청이 병렬로 실행된다고 단정할 수는 없습니다. 드라이버의 연결 수와 요청 간 의존성에 따라 실제 실행은 달라집니다.
엔진은 책상에서 계산하는 설계자이고, 호스트는 자료실을 오가는 조수입니다. 설계자는 “테이블 정의와 열 타입을 가져와 달라”고 목록을 줍니다. 조수가 자료를 가져오면, 설계자는 같은 문제를 더 많은 근거로 다시 풉니다. 계산 규칙을 네트워크 연결 코드와 분리하면 테스트에서 자료만 바꿔 넣기도 쉬워집니다.
Engine.call()은 JSON을 UTF-8 바이트로 바꾸고, WASM의 reylo_alloc으로 메모리를 확보한 뒤 reylo_call을 호출합니다. 결과 앞의 4바이트 길이를 읽어 JSON으로 복원하고 reylo_free로 해제합니다. 서로 다른 언어를 묶는 부분에서는 타입뿐 아니라 바이트 길이와 메모리 소유권도 계약이 됩니다.
읽을 파일: src/engine.ts · engine/core/src/host.rs · engine/core/src/abi.rs.
engine/core/src/database.rs의 Database trait는 DB별 분석 구현이 갖춰야 할 항목을 정합니다. 각 DB는 방언과 지문 상수, describe 응답의 타입, analyze()를 제공합니다. 공통 round()는 분석이 끝났으면 신선도 확인용 사실을 덧붙이고, 아직 답이 없으면 필요한 목록을 반환합니다.
// Database trait에서 핵심 항목만 발췌
pub trait Database {
const DIALECT: Dialect;
const FINGERPRINT: &'static str;
type Describe: Describe;
fn analyze(sql: &str, host: &Host<'_, Self::Describe>) -> Lazy<Analysis>;
// round(), tables(), unbundle() 등은 생략
}
각 WASM 진입 crate는 특정 DB를 골라 공통 API를 구체화합니다. 이 trait는 소스 주석이 밝히듯 정적 디스패치에 쓰입니다. 모든 DB를 하나의 거대한 바이너리에 넣고 실행 중 교체하는 구조와는 다릅니다. DB별 crate가 다른 DB crate에 의존하지 않는 점도 이 경계를 뒷받침합니다.
| DB | 분석의 주요 근거 | 찾아볼 파일 |
|---|---|---|
| Postgres | 서버 Parse/Describe, 시스템 카탈로그, 로컬 파서와 표현식 규칙 | engine/postgres/src/postgres.rs, engine/postgres/src/infer.rs |
| MySQL | 서버 prepare 메타데이터, sqlparser 트리, NULL·파생 테이블 분석 | engine/mysql/src/mysql.rs, engine/mysql/src/mysql_nulls.rs, engine/mysql/src/mysql_derived.rs |
| SQLite | DB 파일에서 읽은 스키마, 엔진 내 SQLite가 해석한 트리 | engine/sqlite/src/sqlite.rs, engine/sqlite/src/sqlite_tree.rs, engine/sqlite/csrc/reylo_sqlite.c |
따라서 README의 “DB가 prepare한 결과를 읽는다”는 요약을 세 DB에 똑같이 적용하면 세부 동작을 놓칩니다. 특히 SQLite는 애플리케이션이 쓰는 SQLite와 분석 엔진의 SQLite가 다를 수 있어, src/sqlite-drivers.ts가 프로젝트에서 찾은 드라이버로 추가 prepare 검사를 수행합니다. 이 추가 검사에도 지원 대상의 범위가 있습니다.
engine/postgres/src/postgres.rs::analyze()가 엔진 스택에서 분석을 시작하고, 구조화 매개변수 처리 뒤 일반 분석으로 들어갑니다.$1 형식으로 바꾸고 describe 응답에서 타입과 열의 출처를 읽습니다.NOT NULL 같은 사실을 읽고, 외부 조인이 NULL을 만들 수 있는지 반영합니다.infer::plan()·infer::resolve()의 규칙을 더합니다. JSON 모양은 engine/postgres/src/jsonshape.rs가 별도로 담당합니다.Engine::cardinality()는 exactly_one()을 먼저, 이어 at_most_one()을 시도해 반환 행 수 범주를 정합니다.예를 들어 count(*)의 결과 열은 일반 테이블 열처럼 출처를 추적할 수 없습니다. engine/postgres/src/infer.rs는 count가 NULL이 되지 않는 규칙을 알고 있지만, 일반적인 다른 집계는 빈 입력에서 NULL이 될 수 있다고 봅니다. coalesce, CASE, 외부 조인 등을 거치면 다시 다른 규칙이 필요합니다.
engine/postgres/src/cardinality.rs는 GROUP BY·HAVING·LIMIT·집합 반환 함수 등을 살펴봅니다. “집계니까 언제나 한 행”이라고 단순화하지 않습니다. 사용자 정의 함수나 연산자가 기본 이름을 가린 경우에는 Trust 정보로 기본 규칙의 적용을 제한합니다.
확실하지 않은 결과를 더 넓은 타입으로 남기는 선택이, 잘못 좁힌 타입 때문에 실행 중 깨지는 것보다 안전하다는 판단이 코드에 반복됩니다. 이를 읽을 때 “추론하지 못함”과 “그 값이 절대 나오지 않음”을 구별해야 합니다.
src/analyze.ts의 Session은 콘텐츠 매퍼·번들러·등록 훅·CLI가 함께 사용하는 진입점입니다. entryFile()은 .reylo/queries/ 아래 파일 위치를 만들고, #statementKey()는 캐시 형식 버전·엔진 지문·DB 방언·SQL을 해시합니다. 서버 식별값의 해시도 파일명에 붙입니다.
캐시 본문에는 version, dialect, schema, analysis, at를 저장합니다. 분석 결과가 있다는 사실만 보지 않고 어떤 스키마를 상대로 만든 결과인지를 함께 기록하는 것입니다. 같은 SQL이어도 스키마가 바뀌면 다시 분석해야 하기 때문입니다.
| 상황·설정 | 소스의 선택 | 실제로 뜻하는 것 |
|---|---|---|
| 스키마 지문 동일 | 해당 서버의 기존 분석 사용 | SQL 텍스트가 같아도 스키마 조건을 함께 확인 |
| 스키마 지문 변경 | 엔진으로 재분석 | 마이그레이션 이후 타입을 갱신하는 경로 |
| URL 없음·DB 연결 불가 | 해당 서버의 항목, 없으면 다른 서버의 최신 항목 사용 | 현재 DB와의 일치 여부를 새로 확인한 결과는 아님 |
| 오프라인·캐시 없음 | 분석 실패 진단 | 새 SQL의 타입을 근거 없이 만들어 내지 않음 |
SCHEMA_TTL | 5_000ms | 매 파일마다 스키마 지문을 다시 조회하지 않음 |
OFFLINE_TTL | 60_000ms | 접속 불가 세션의 반복 연결 비용을 줄임 |
BUILD_IDLE | 250ms | 빌드용 세션이 쉬면 연결을 놓도록 예약 |
이 TTL 값과 src/engine.ts의 AnswerCache는 구분해야 합니다. 후자는 엔진이 요청한 개별 답을 재사용하며 기본 TTL이 세션 범위 60초·문장 범위 5초입니다. 둘 다 시간을 쓰지만 저장하는 대상과 무효화 조건이 다릅니다.
site/content/sections/09-faq.mdx는 DB 없는 빌드를 아직 지원하지 않는다고 적습니다. 반면 Session.#analyze()는 URL이 없거나 DB에 연결할 수 없을 때 #offlineEntry()로 기존 분석을 찾습니다. 새 SQL·캐시 없는 환경까지 오프라인으로 동작한다는 뜻은 아니며, 누락된 엔진이나 치명적인 연결 설정 오류 등은 실패할 수 있습니다.
또한 런타임에는 별도의 신선도 확인이 있습니다. src/execute.ts는 기록된 카탈로그 사실을 재조회하지만 조회 실패를 잡아서 넘기는 분기도 있습니다. 캐시와 신선도 확인을 모든 스키마 변화에 대한 절대적인 보증으로 설명하면 안 됩니다.
sqlModule()은 타입 선언만 만들지 않습니다. 이름별 입력 타입, 드라이버에 전달할 순서, 열별 값 표현 방식, 행 수 범주를 실행 모듈에 넣습니다. defineQuery()는 이를 받아 .run()·.runSync()·.with()를 구성합니다.
src/execute.ts의 ClientFor<D>는 방언에 따라 허용되는 클라이언트 타입을 고릅니다. 실행 시에는 query()·unsafe()·prepare() 등 실제 메서드 형태를 보고 어댑터를 선택합니다. 드라이버 전체 클래스를 상속받기보다 필요한 메서드의 최소 형태를 계약으로 삼습니다.
여기서 어려운 부분은 메서드 이름만이 아닙니다. 드라이버에 따라 64비트 정수·날짜·JSON의 JavaScript 표현이 다릅니다. src/values.ts가 표현 방식을 모으고, src/render.ts·src/schema.ts·DB별 디코더가 이를 공유합니다.
| SQL 값의 종류 | reylo의 기본 표현 | 초보자가 놓치기 쉬운 이유 |
|---|---|---|
int8 계열(BIGINT 등) | bigint | JavaScript number로 큰 정수를 정확하게 표현할 수 없음 |
| 정확한 소수(Postgres·MySQL) | 문자열 | 부동소수점 변환으로 자릿수가 손실될 수 있음 |
| 날짜·시각 | 종류에 맞는 문자열 | Date 객체의 시간대 해석과 구분 필요 |
| 바이너리 | Uint8Array | Node의 Buffer와 다른 환경의 값을 함께 다뤄야 함 |
SQLite의 일반 INTEGER는 number이며, NUMERIC은 저장된 값에 따라 숫자나 문자열이 될 수 있습니다. 행 수도 실행 시 다시 셉니다. materialize()는 one인데 0행이나 2행이 오면 오류를 내고, maybeOne인데 2행 이상이면 오류를 냅니다. TypeScript의 조건부 타입을 실제 JavaScript 분기로 뒷받침하는 좋은 사례입니다.
읽을 파일: src/loader.ts · src/index.ts · src/values.ts.
아래 트리는 저장소 루트 기준입니다. src/는 TypeScript 라이브러리, engine/은 Rust·C 분석 엔진, oracle/은 실제 쿼리 결과로 추론을 검사하는 도구입니다.
| 경계 | 관찰할 것 | 학습 질문 |
|---|---|---|
core/ ↔ DB별 crate | 공통 타입은 공유하고 파서·카탈로그는 분리 | 새 DB를 넣을 때 무엇을 새로 구현해야 할까? |
engine/ ↔ src/ | 분석 규칙과 DB 연결 담당을 분리 | DB 응답을 고정하면 어느 부분을 독립 테스트할 수 있을까? |
src/ ↔ examples/ | 라이브러리 구현과 이용자의 설정을 분리 | 타입 검사만 설정하고 런타임 훅을 빠뜨리면 어디서 실패할까? |
tests/ ↔ oracle/ | API 동작과 추론의 실제 모순을 각각 검사 | 문자열 출력이 맞아도 실제 NULL이 나오면 잡을 수 있을까? |
site/ ↔ src/ | 문서에서 실제 분석·렌더링 함수를 재사용 | 문서 예제와 구현의 차이를 어떻게 줄일까? |
examples/와 lab/는 비슷해 보여도 목적이 다릅니다. 전자는 사용자가 복제할 설정 단위의 예제이고, 후자는 드라이버 조합·경계 사례를 검증하는 작업장입니다. 처음에는 examples/node나 examples/vite 한 곳만 고르는 편이 읽기 쉽습니다.
| 범위 | 파일 수 | 대략적인 줄 수 |
|---|---|---|
| Rust 전체 | 105 | 93,427 |
| 외부·복사 영역 제외 Rust | 58 | 36,152 |
| 같은 기준 TypeScript | 187 | 21,235 |
| 같은 기준 TSX | 22 | 1,329 |
| 같은 기준 C / Python | 2 / 1 | 959 / 37 |
| SQL 전체 | 2,184 | 4,085 |
| TypeScript 라이브러리만 | 38 | 6,819 |
| TypeScript 테스트만 | 30 | 6,394 |
수치는 커밋 d93ee3a의 확장자별 물리적 줄 수이며, 공백·주석·테스트도 포함합니다. “외부·복사 영역 제외”는 engine/mysql/vendor/, engine/postgres/vendor/, site/docs-kit/를 뺀 범위이며, 완벽한 저작권별 분류나 순수 구현 줄 수를 뜻하지 않습니다.
이 기준에서 Rust 엔진은 32,668줄, Rust oracle은 3,484줄입니다. SQL 중 oracle 아래 파일은 2,076개·3,473줄이고, oracle/cases/의 바로 아래 사례 폴더는 110개입니다. 이 수치는 테스트 통과 횟수나 실행된 쿼리 수와 다릅니다.
외부 파서 복사본은 전체 Rust 줄 수를 크게 늘립니다. 처음에는 209줄인 src/index.ts, 87줄인 src/loader.ts에서 시작하세요. 작은 연결층을 이해한 뒤 DB별 추론 규칙으로 내려가면, 큰 파일에서 무엇을 찾는지 알 수 있습니다.
src/index.ts의 Materialize는 행 수라는 문자열 리터럴을 반환 타입으로 바꿉니다. 같은 Row여도 C가 무엇인지에 따라 결과가 달라집니다. 제네릭을 “아무 타입이나 넣는 자리”로만 보지 말고, 입력 정보와 출력 타입의 관계를 보존하는 장치로 읽으세요.
// src/index.ts의 조건부 타입을 줄바꿈해 표시
export type Materialize<Row, C extends Cardinality> =
C extends "many" ? Row[] :
C extends "one" ? Row :
C extends "none" ? void :
Row | null;
실습 아이디어: Row = { id: number }라고 놓고 네 가지 C의 결과를 종이에 적습니다. 이어 materialize()를 읽고 0·1·2행을 넣었을 때 어떤 값 또는 오류가 나오는지 대응표를 만드세요. 타입 선언만 보고 끝내지 않는 연습입니다.
구조적 타이핑은 src/execute.ts의 PgQueryClient에서 볼 수 있습니다. 특정 드라이버의 클래스 이름 대신 필요한 query() 모양을 요구합니다. 덕분에 풀에서 꺼낸 연결이나 트랜잭션도 같은 계약으로 받을 여지가 생깁니다. 반면 메서드 이름이 겹치는 서로 다른 드라이버를 구별할 추가 조건도 필요합니다.
engine/core/src/api.rs의 요청·응답 모델과 engine/core/src/database.rs의 trait를 함께 보세요. “분석이 끝남”, “아직 정보가 필요함”, “오류”가 별개의 상태로 표현됩니다. 임의의 문자열과 빈 객체로 상태를 섞어 표현하는 것보다, 가능한 상태를 열거하는 모델이 호출자의 분기를 명확하게 만듭니다.
실습 아이디어: Host::describe()를 읽고 응답이 없을 때, 정상 응답일 때, DB 오류일 때의 반환값을 적습니다. 이 문맥에서 Option<Result<D, DbError>>는 미응답·성공·DB가 거절한 응답의 세 상태를 표현합니다. None과 Err를 같은 실패로 취급하면 다음 왕복을 설계할 수 없습니다.
users.name은 출처를 찾기 쉽지만, coalesce(name, '익명')은 표현식 규칙까지 봐야 합니다.테이블 열이 NOT NULL이어도 LEFT JOIN의 오른쪽 결과는 NULL일 수 있습니다. 오른쪽에 대응 행이 없을 때 DB가 빈 자리를 만들어 주기 때문입니다. “스키마에 NOT NULL이니 결과도 항상 값이 있다”는 생각을 깨는 데 좋은 예입니다.
-- posts.id가 NOT NULL이어도 LEFT JOIN 뒤의 p.id는 NULL 가능
select u.id as user_id, p.id as post_id
from users u
left join posts p on p.user_id = u.id
실습 아이디어: 게시글이 있는 사용자와 없는 사용자를 각각 하나씩 가정해 결과를 손으로 만듭니다. 이어 where p.id is not null을 더했을 때 결과가 어떻게 달라지는지 설명해 보세요. 데이터 예제와 engine/postgres/src/infer.rs의 규칙을 연결하는 것이 목표입니다.
src/mapper.ts는 positionEncoding: "utf-8"을 선택하고, mapToStatement()로 가상 선언 전체를 원래 SQL 문장에 연결합니다. 여기서는 정의로 이동할 때 SQL 파일을 열도록 하되, 이름 바꾸기가 잘못 TypeScript를 SQL에 써 버리는 기능은 매핑하지 않습니다.
src/module-types.ts의 withinFile()은 진단 범위를 파일의 바이트 길이 안으로 자릅니다. 범위를 벗어나면 컴파일러가 변환 응답 전체를 거절할 수 있기 때문입니다. “오류 메시지를 잘 쓴다”에는 문장뿐 아니라 위치·길이·인코딩의 정확성도 포함됩니다.
실습 아이디어: -- 사용자 조회 같은 한국어 주석 앞뒤의 문자 수와 UTF-8 바이트 수를 비교해 보세요. src/loader.ts의 utf16()은 엔진의 바이트 위치를 JavaScript 문자열 위치로 바꾸는 지점입니다. 한글이 들어가도 오류가 정확한 곳에 표시되는 이유를 설명할 수 있어야 합니다.
SQL 해시만 저장하면 테이블 변경을 놓칩니다. 스키마 지문만 보면 엔진의 추론 규칙 변경을 놓칠 수 있습니다. reylo는 두 종류의 정보와 서버 구분을 캐시 설계에 넣습니다. 캐시 키를 설계할 때는 결과를 바꿀 수 있는 모든 입력을 먼저 나열해야 한다는 교훈입니다.
실습 아이디어: SQL 수정·엔진 수정·스키마 변경·네트워크 단절의 네 상황을 표로 만듭니다. 각각 캐시 항목을 재사용할지, 재분석할지, 오류가 날지를 Session.#analyze() 분기와 연결하세요. 오프라인에서 성공했다는 사실이 최신 스키마로 검증되었다는 뜻인지는 별도로 적습니다.
추론 테스트의 좋은 질문은 “기대한 문자열이 나왔나”에 더해 “이 타입을 깨는 실제 값이 나올 수 있나”입니다. oracle/src/main.rs는 쿼리를 분석한 뒤, 경계값을 넣은 데이터에서 실제로 실행해 결과 값·행 수가 분석과 충돌하는지 검사합니다. 기대 출력 비교와 실행 기반 확인을 결합합니다.
기본 실행 제한 시간은 5,000ms입니다. 모든 시도에서 실패하거나 시간 제한에 걸린 SQL은 검증한 것으로 세지 않고 UNCHECKED로 드러냅니다. --strict-coverage도 “모든 SQL이 반드시 성공”이라는 옵션은 아닙니다. 실행되지 않은 문장이 있는데 사례의 unchecked.txt로 관리되지 않으면 실패시키는 조건이 있습니다.
실습 아이디어: 한 사례의 setup.sql·질의·expected.txt·unchecked.txt를 묶어 읽습니다. 테스트가 있다고 말하는 것보다, 어떤 입력에서 무엇을 관찰했고 무엇은 관찰하지 못했는지 설명하는 연습이 중요합니다.
“유일 키로 조회하면 최대 한 행이다”라는 사례를 골라 SQL 조건 → Rust 행 수 분석 → Analysis → 가상 Query 타입 → materialize() 순서로 설명해 보세요. 여러 언어를 따로 공부하는 것보다 경계를 건너는 정보 하나를 추적하는 편이 이 저장소를 이해하는 데 효과적입니다.
README와 빌드 설정에는 최소 CPU 코어 수·RAM 수치가 명시되어 있지 않습니다. 아래 표는 명시된 소프트웨어 조건과 작업별 추가 준비 사항을 정리한 것입니다. 모델 학습이나 GPU 추론을 하는 도구가 아니므로 이 기능을 위해 별도 GPU를 요구하는 구성은 없습니다.
| 하려는 일 | 확인한 조건 | 추가 부담 |
|---|---|---|
| 소스 읽기 | 텍스트 편집기와 파일을 읽는 도구 | Node·DB·Rust 설치 없이도 구조와 회귀 사례를 읽을 수 있음 |
| 이용 프로젝트 타입 검사 | README 기준 Node 24+, TypeScript 7.1, 해당 DB 엔진 패키지 | 처음 분석할 개발용 DB 또는 SQLite 파일 필요. 기존 캐시 사용은 조건부 |
| Postgres 사용 | DB별 문서 기준 14~18 | 해당 드라이버와 연결 설정 필요. 실제 CI 기준은 Postgres 18 |
| MySQL 사용 | README·DB별 문서 기준 8.0.22+ | MySQL 드라이버 필요. 실제 CI 기준은 MySQL 8.4 |
| SQLite 사용 | DB 파일, 선택한 SQLite 드라이버 | 엔진 내 SQLite와 드라이버의 SQLite 버전 차이를 고려 |
| 엔진 수정·재빌드 | Rust 도구체인, wasm32-wasip1, wasi-sdk 34, C 빌드 도구 | 준비 단계의 외부 소스 다운로드와 C 파서 빌드가 추가됨 |
| 문서 사이트 개발 | site/의 별도 의존성 설치·빌드 | 문서 타입 캡처에는 별도 개발용 DB도 사용 |
위 지원 버전은 README와 DB별 문서 기준이며, CI의 실행 환경은 .github/workflows/test.yml 기준입니다. 지원 범위와 CI에서 사용하는 버전은 서로 다르므로 이용할 DB·드라이버 조합을 함께 살펴보세요.
먼저 작은 SQLite·TypeScript 예제로 시작하고, 엔진 개발 단계에서 DB 서버와 Rust 빌드 도구를 추가하세요. 메모리와 CPU 부담은 DB 데이터 크기, 함께 실행하는 프로세스, 빌드 병렬도에 따라 달라집니다.
엔진 개발은 npm 패키지 이용보다 준비 비용이 큽니다. engine/build.sh --prepare는 SQLite 소스와 패치할 파서 소스를 준비하고, Cargo 워크스페이스는 준비된 경로를 참조합니다. 일반 이용자는 커밋된 WASM 산출물을 받으므로 이 C·Rust 빌드 과정을 반복하지 않습니다.
| 엔진 파일 | 파일 크기 | 해석 |
|---|---|---|
packages/postgres/engine.wasm | 3,192,461바이트, 약 3.04MiB | Postgres 엔진 파일 자체의 크기 |
packages/sqlite/engine.wasm | 2,457,764바이트, 약 2.34MiB | SQLite 엔진 파일 자체의 크기 |
packages/mysql/engine.wasm | 1,739,400바이트, 약 1.66MiB | MySQL 엔진 파일 자체의 크기 |
압축된 npm 다운로드 크기나 실행 중 최대 메모리는 따로 측정해야 합니다. engine/build.sh의 -zstack-size=8388608은 WASM의 스택 영역 설정인 8MiB를 가리킵니다. Node·WASM 메모리·파서·캐시·DB를 합친 전체 RAM이 8MiB라는 뜻이 아닙니다.
타입 검사는 쿼리의 prepare와 메타데이터 조회를 사용하지만, oracle은 사례용 DB를 만들고 SQL을 실제 실행합니다. oracle/src/main.rs는 Postgres·MySQL 사례마다 DB를 생성·삭제하므로, oracle 실습에는 폐기 가능한 학습용 서버를 사용하세요.
다음 다섯 과제는 읽기에서 실행으로 이어지는 학습안입니다. 실행 과제에는 학습용 프로젝트와 폐기 가능한 DB를 준비하세요. 앞의 두 과제는 소스와 설정 파일을 읽는 것만으로 시작할 수 있습니다.
목표: TypeScript 문법보다 먼저 데이터의 모양을 이해합니다. examples/node/queries/get_user.sql과 examples/node/src/main.ts를 나란히 읽습니다.
src/index.ts에서 그 반환 형태를 정하는 분기를 찾습니다.완료 기준: “객체가 NULL”인 상황과 “객체 속 이메일이 NULL”인 상황을 데이터 예시 두 개로 설명하고, 근거 함수로 materialize()를 제시할 수 있습니다.
목표: SQL 가져오기에 필요한 두 종류의 설정을 구분합니다. examples/node/package.json과 examples/node/tsconfig.json을 읽고 다음 빈칸을 채웁니다.
.sql을 이해하게 하는 설정: contentMappers.--runExternalCode.--import reylo/register.완료 기준: “편집기에서 타입이 보여도 런타임 연결 설정이 필요하다”는 이유를 설명합니다. Vite 예제에서는 등록 훅 대신 어느 플러그인을 연결하는지 찾아 비교합니다.
아래는 README와 Node 예제에서 가져온 핵심 형태입니다. 이미 TypeScript 프로젝트와 드라이버를 준비했다는 전제이며, 이 블록만 복사하면 프로젝트 전체가 설정되는 것은 아닙니다. 이용할 릴리스의 reylo와 엔진 패키지는 같은 버전으로 맞추고 해당 버전의 문서를 확인하세요.
// tsconfig.json의 최상위 항목에 추가하는 형태
{
"contentMappers": [
{
"package": "reylo",
"extensions": [".sql", ".reylo.yaml", ".reylo.yml"]
}
]
}
# 환경변수 또는 .env가 학습용 DB를 가리키도록 준비한 뒤 사용
# 타입 검사 시 Reylo는 환경변수 → .env.local → .env 순서로 값을 찾음
npx tsc --noEmit --runExternalCode
# Node 실행은 .env를 읽고 SQL 등록 훅을 불러오는 예시
node --env-file=.env --import reylo/register main.ts
타입 검사 시 Reylo는 src/project.ts에서 연결 설정을 읽습니다. 연결 URL은 .reylo.yaml의 direct_url이 우선이며, 없으면 DATABASE_URL을 사용합니다. 환경변수의 기본 조회 순서는 프로세스 환경변수 → .env.local → .env이며, 설정의 env로 파일 목록을 바꿀 수 있습니다. 반면 위 Node 명령의 --env-file=.env는 애플리케이션의 process.env에 값을 넣습니다. Reylo의 파일 조회가 애플리케이션 환경변수까지 설정해 주는 것은 아닙니다.
목표: 한 번에 한 조건만 바꾸며 타입 변화의 원인을 찾습니다. 1절의 users 스키마를 학습용 DB에 준비하고, 각 질의를 별도 파일로 작성합니다.
-- A: 여러 행 가능
select id, name, email from users;
-- B: 최대 한 행
select id, name, email from users where id = :id::int;
-- C: 행 수는 여러 행 가능, email의 NULL은 제거
select id, name, email from users where email is not null;
-- D: GROUP BY 없는 count, int로 변환해 number 예시로 통일
select count(*)::int as total from users;
예상: A와 C는 배열, B는 객체 또는 NULL, D는 한 객체입니다. C에서는 이메일의 NULL 가능성 변화도 관찰합니다. 실제 결과가 예상과 다르면 스키마·질의·분석 결과를 기록하고 추론 규칙의 지원 범위를 확인하세요.
완료 기준: 네 질의 각각에 대해 행 수 범주와 열의 NULL 가능성을 별도 열로 기록합니다. count(*) 원형은 Postgres에서 64비트 정수이므로 캐스트를 빼면 bigint 표현도 함께 관찰할 수 있습니다.
목표: “같은 SQL이면 같은 결과”라는 가정을 깨 봅니다. 타입 검사를 한 번 수행한 뒤 .reylo/queries/의 항목에서 schema·analysis·at를 확인합니다.
완료 기준: 온라인 캐시 적중·스키마 변경 후 재분석·오프라인 기존 캐시·오프라인 캐시 없음의 네 상태를 구분하고, Session.#analyze()의 해당 분기를 표시합니다. 장기 실행 세션에서는 TTL 때문에 관찰 시점도 달라질 수 있음을 기록합니다.
목표: 구체적인 변경 주제를 테스트의 입력과 출력으로 읽습니다. HEAD 메시지는 “스키마 이름으로 한정한 테이블을 읽는 파생 테이블”에 대한 테스트 보강입니다. 다음 두 질의와 기대값을 확인하세요.
-- oracle/cases/derived_tables/schema_qualified.sql
select s.id, s.score from (select id, score from public.bar) s
-- oracle/cases/derived_tables/with_schema_qualified.sql
with b as (select id, score from public.bar) select id, score from b
oracle/cases/derived_tables/setup.sql의 bar.id는 bigint primary key, score는 int not null입니다. oracle/cases/derived_tables/expected.txt의 두 줄 모두 many () -> { id: int8, score: int4 }를 기대합니다. 이 출력은 oracle의 SQL 타입 표기이며 TypeScript 선언 그 자체는 아닙니다.
완료 기준: 파생 테이블과 CTE를 지나도 원래 열의 타입·NULL 정보가 왜 유지되어야 하는지 설명합니다. 원한다면 이후 별도 개발 복사본에서 외부 조인 사례와 기대값을 추가하고 oracle로 확인할 수 있습니다. 기대값의 변화가 의도한 추론 규칙과 일치하는지도 함께 살펴보세요.
이 회귀 사례의 파일은 고정 커밋의 derived_tables 사례 모음에서 함께 볼 수 있습니다. 두 질의가 보존해야 할 타입과 NULL 가능성은 해당 사례의 입력·기대값에 명시되어 있습니다.
하루에 모든 파일을 읽는 방식보다, 같은 예제를 여러 관점에서 다시 보는 편이 좋습니다. 아래는 SQL 기본 문법과 TypeScript 함수 호출을 아는 초보자를 가정한 학습안입니다. 주차는 일정의 예시이며 실제 소요 시간은 선행 지식에 따라 달라집니다.
| 주차 | 배울 기술 | 저장소 읽기 | 남길 결과물 |
|---|---|---|---|
| 1주차 | SELECT·키·NULL·JOIN | examples/node/queries/, oracle/cases/derived_tables/ | 행 수와 열의 NULL 가능성을 분리한 예상 결과표 |
| 2주차 | 제네릭·조건부 타입·구조적 타이핑 | src/index.ts, src/execute.ts의 클라이언트 타입 | 카디널리티별 타입과 런타임 반환값 대응표 |
| 3주차 | JSON-RPC·가상 모듈·소스 위치 | src/rpc.ts, src/mapper.ts, src/module-types.ts | SQL 오류가 편집기 진단이 되기까지의 순서도 |
| 4주차 | Rust trait·enum·Option·Result | engine/core/src/database.rs, engine/core/src/host.rs, engine/core/src/api.rs | 엔진 요청·응답 상태도와 가짜 DB 응답 시나리오 |
| 5주차 | AST·카탈로그·보수적 추론 | engine/postgres/src/infer.rs, engine/postgres/src/cardinality.rs | count·coalesce·외부 조인의 타입 규칙 카드 |
| 6주차 | 캐시 무효화·WASM 경계·실행 기반 검증 | src/analyze.ts, src/engine.ts, oracle/src/main.rs | 캐시 상태표와 회귀 사례 하나에 대한 분석 보고서 |
Materialize의 타입 변수에 구체적인 값을 대입해 삼항 조건을 한 단계씩 지웁니다.Host가 답 목록을 소유하는지 빌리는지만 표시하고 전체 문법 해석은 뒤로 미룹니다.infer::plan()과 resolve() 사이에 어떤 정보가 전달되는지 찾습니다.setup.sql·쿼리·기대값 세 파일부터 읽습니다.공부의 완료 기준은 파일을 전부 읽는 것이 아니라, SQL 한 줄의 변화가 타입과 실행값에 미치는 영향을 근거와 함께 설명하는 것입니다. 각 주의 결과물에 추측과 확인된 사실을 따로 표시하면, 이후 다른 라이브러리를 분석할 때도 재사용할 수 있습니다.
처음에는 자동차의 페달과 핸들을 익히고, 다음에는 동력 전달을 보고, 마지막에 엔진 내부를 봅니다. reylo도 먼저 .run()의 입력과 결과를 이해한 뒤, 분석 세션과 Rust 규칙으로 들어가세요. 처음부터 파서의 모든 분기를 외울 필요는 없습니다.
컴파일러 도구에 관심이 생겼다면 콘텐츠 매퍼의 원본·가상 위치 매핑을 더 공부하세요. 데이터 엔지니어링 쪽이라면 NULL과 키 제약이 분석에 주는 정보를 파고들 수 있습니다. 시스템 설계가 재미있다면 Need·Answer 왕복 프로토콜과 캐시 키 설계를 작은 다른 문제에 적용해 보세요.
예를 들어 로컬 설정 파일 검사기를 만들 때도 “순수한 검사 규칙”과 “파일을 읽는 호스트”를 나누고, 파일 내용의 지문을 캐시에 넣을 수 있습니다. 이때 목표는 reylo를 복제하는 것이 아니라, 어떤 입력이 결과를 결정하는지 드러내는 설계 습관을 가져가는 것입니다.
| 용어 | 쉬운 설명 |
|---|---|
| 타입 추론 | 직접 적지 않은 타입을 문장·스키마·분석 규칙에서 알아내는 일. 여기서는 매개변수·결과 열·행 수를 함께 다룹니다. |
| 콘텐츠 매퍼 | TypeScript가 모르는 파일을 가상 TypeScript로 바꾸고, 원본과 변환 결과의 위치 관계를 알려주는 외부 도구입니다. |
| 가상 모듈 | 컴파일러나 빌드 도구에 텍스트로 제공하는 모듈입니다. 사용자가 관리하는 생성 파일과 구분됩니다. |
| 카디널리티 | 여기서는 결과 행 수의 범주입니다. 한 행·최대 한 행·여러 행 가능·반환 없음에 따라 API의 반환 형태가 달라집니다. |
| NULL 가능성 | 값이 없음을 나타내는 NULL이 결과에 올 수 있는지입니다. 열의 가능성과 행 자체의 부재는 별개입니다. |
| prepare / describe | 문장을 실행하기 전에 파싱·검사하고 입력·출력 메타데이터를 얻는 과정입니다. DB별 구체적 경로는 다릅니다. |
| 시스템 카탈로그 | 테이블·열·타입·제약 같은 DB 구조를 DB 자체가 보관하는 정보입니다. 사용자 데이터 행과 구분됩니다. |
| AST | 문장을 문법 단위의 트리로 표현한 것입니다. 이름·연산·조인의 관계를 정규식보다 구조적으로 읽게 합니다. |
| 출처 정보 | 결과 열이 어느 테이블의 어느 열에서 왔는지입니다. NULL 제약 등을 추적하는 근거가 됩니다. |
| 파생 테이블 | FROM (SELECT ...)처럼 쿼리 결과를 다시 테이블처럼 쓰는 구조입니다. |
| CTE | WITH name AS (...)로 한 문장 안에서 중간 결과에 이름을 붙이는 문법입니다. |
| 드라이버 | 애플리케이션이 DB와 통신할 때 사용하는 라이브러리입니다. reylo는 호출자가 넘긴 드라이버 클라이언트를 활용합니다. |
| 바인딩 | SQL 문장의 매개변수 자리에 값을 전달하는 일입니다. 문자열을 직접 이어 붙이는 방식과 구분해야 합니다. |
| 디코딩 | 드라이버가 반환한 값을 약속한 JavaScript 표현으로 바꾸는 과정입니다. 큰 정수·날짜·JSON 처리가 대표적입니다. |
| 구조적 타이핑 | 타입의 이름보다 속성과 메서드의 형태로 호환성을 판단하는 방식입니다. |
| trait | Rust에서 타입이 제공해야 할 동작을 정하는 계약입니다. Database가 DB별 엔진의 공통 경계를 만듭니다. |
| 정적 디스패치 | 구체적인 타입에 따른 호출 대상을 컴파일 시 정하는 방식입니다. DB별 WASM 진입점에서 사용합니다. |
| WASM / WASI | 이식 가능한 바이너리 형식과 그 호스트 인터페이스입니다. 이 저장소는 DB별 엔진을 WASI 모듈로 배포합니다. |
| ABI | 바이너리 경계에서 함수를 부르고 데이터를 주고받는 약속입니다. 포인터·길이·메모리 해제 규칙이 포함됩니다. |
| JSON-RPC | 메서드 이름·매개변수·응답 식별자를 JSON으로 표현하는 요청·응답 규약입니다. 컴파일러와 매퍼 사이에 사용합니다. |
| 지문 | 입력이 바뀌었는지 비교하기 위한 요약값입니다. 엔진 소스의 지문과 DB 스키마 지문은 대상이 다릅니다. |
| TTL | 캐시 값을 다시 확인하기 전까지 신뢰하는 시간입니다. 최신성을 확인하는 빈도와 조회 비용 사이의 선택입니다. |
| peer 의존성 | 호스트 프로젝트가 함께 제공하도록 선언하는 패키지 관계입니다. reylo의 DB 엔진은 선택적 peer이며 버전 일치를 요구합니다. |
| oracle | 여기서는 분석한 타입을 실제 쿼리 결과와 비교하는 검증 도구의 이름입니다. 모든 입력에 대한 완전한 수학적 증명을 뜻하지 않습니다. |
| 회귀 테스트 | 특정 동작이 이후 변경으로 다시 깨지지 않도록 고정한 사례입니다. 입력과 기대값을 함께 읽어야 의도를 알 수 있습니다. |
용어를 외우기보다 하나씩 파일에 연결해 보세요. “지문은 engine/core/src/fingerprint.rs와 Session에서 서로 다른 대상을 다룬다”, “카디널리티는 Rust 분석과 Materialize 양쪽에 나온다”처럼 사용되는 경계까지 기억하면 코드 탐색이 빨라집니다.
외부 자료는 각 기술의 원리를 더 읽기 위한 링크입니다. reylo의 동작·설정값은 커밋 d93ee3a의 소스 기준입니다. 경쟁 도구 비교와 콘텐츠 매퍼 규약의 출처는 위 공식 페이지이며, 비교 범위는 타입 갱신 흐름입니다.
| 항목 | 확인한 값 | 출처·범위 |
|---|---|---|
| 저장소 | colinhacks/reylo | 공식 GitHub 저장소 |
| 스타 / 포크 | 776 / 7 | 2026-10-23 GitHub 기준 |
| 주 언어 | Rust | 2026-10-23 GitHub 기준 |
| 생성일 | 2026-10-21 | 공개 저장소 생성일. 전체 개발 기간과 구분 |
| 최근 push | 2026-10-23 | 2026-10-23 GitHub 기준 |
| 최신 릴리스 | v0.3.0 · 2026-10-22 | 2026-10-23 GitHub 기준. 소스의 패키지 선언과 구분 |
| 소스 패키지 버전 | reylo와 DB 엔진 패키지 0.4.0 | 커밋 d93ee3a의 루트·엔진 패키지 선언 버전 |
| 분석 HEAD | d93ee3a | Git 커밋 식별자 |
| HEAD 작성 시각 | 2026-10-22 22:45:29 −07:00 | Git author 시각. 한국 시각으로는 2026-10-23 14:45:29 |
| HEAD 제목 | test(postgres): cover derived tables over schema-qualified tables (#63) | 스키마 이름으로 한정한 테이블을 읽는 파생 테이블의 테스트 보강 |
| 코드 규모 | Rust 약 36,152줄, TS·TSX 약 22,564줄 | 5절의 외부·복사 영역 제외 기준. 주석·테스트 포함 |
| 회귀 사례 | oracle 사례 폴더 110개 | oracle/cases/ 바로 아래 디렉토리 수 |
| 라이선스 | MIT | 2026-10-23 GitHub 기준 및 LICENSE·패키지 선언. 외부 복사 코드에는 각 라이선스도 존재 |
최신 릴리스의 버전, 소스의 패키지 버전, 설명의 기준 커밋을 구분해야 나중에도 같은 설명을 검증할 수 있습니다. 본문의 동작 설명은 패키지 버전 0.4.0을 선언한 커밋 d93ee3a 기준이며, v0.3.0 릴리스와 구분해야 합니다.
컴파일러 의존: README 기준 TypeScript 7.1 베타의 콘텐츠 매퍼를 전제로 합니다. 예제·에디터·런타임 설정이 모두 맞아야 하며, 기존 TypeScript 프로젝트에 패키지 하나만 더한다고 전체 경로가 완성되는 것은 아닙니다.
추론의 범위: 파서가 문장을 읽고 DB가 받아 준다고 해서 모든 표현식의 NULL·JSON 모양·행 수를 가장 좁게 추론한다는 뜻은 아닙니다. 모르는 부분은 넓게 남을 수 있고, 드라이버·확장·DB 버전별 차이도 있습니다.
캐시와 문서: FAQ와 오프라인 캐시 분기가 어긋나는 사례가 확인됩니다. 캐시가 있어 빌드가 진행되어도 현재 DB 스키마를 새로 검사했다는 보장은 없습니다. 버전이 달라지면 이 설명도 다시 확인해야 합니다.
수치의 범위: 줄 수는 주석·테스트를 포함한 물리적 줄 수이고, WASM 크기는 압축 전 파일 크기입니다. 이 수치에서 실행 성능, 메모리 요구량, 테스트 통과율을 추정할 수는 없습니다.
데이터베이스 경계: reylo는 마이그레이션 도구가 아니며 스키마 변경은 별도로 관리합니다. oracle은 실제 SQL과 DB 생성·삭제를 수행하므로 일반 타입 검사와 취급이 다릅니다. 학습용 DB와 애플리케이션의 DB를 구분해 설정하세요.