Cascade

A code-to-column change-impact knowledge graph for AI coding agents.

View the Project on GitHub alexsoft-hq/Cascade

English 한국어

TypeScript 레인(NestJS, Prisma, TypeORM) 설정

TypeScript 레인은 NestJS 백엔드를 읽습니다. 컨트롤러가 여는 라우트, 메서드끼리의 호출, 그리고 Prisma나 TypeORM 호출 하나하나를 SQL 문장으로 읽습니다. 그 문장이 건드리는 테이블·컬럼은 schema.prisma나 TypeORM 엔티티가 선언한 것입니다. Spring 애플리케이션에서 Java 레인이 하는 일과 같은 왕복입니다 (endpoint → handler → service → statement → table → column). 그래서 웹 레인이 읽은 프런트엔드 호출이 Spring 라우트를 만나듯 Nest 라우트도 그대로 만납니다.

모르는 것을 아는 것처럼 적지 않습니다. 주소가 변수에 들어 있는 라우트는 만들지 않습니다. 읽지 못한 제외 패턴이 이름을 댈 수도 있는 라우트는 만들되 HEURISTIC(추정) 등급을 붙입니다. 이 레인이 읽지 못한 Prisma·TypeORM 인자는 문장에 이름을 남깁니다. 이런 빈틈은 모두 알립니다. 실행의 진단으로 알리거나, 엣지의 근거에 적습니다.

무엇이 필요한가

Node, 그것뿐입니다. 웹 레인이 동봉한 파서 (adapters/web/vendor/babel-parser.cjs)로 읽기 때문에 TypeScript 컴파일러도, npm install도, 빌드도 필요 없습니다. 소스를 읽기만 하고 실행하지는 않습니다.

실행

cascade init --root .        # NestJS 애플리케이션을 찾아 프로필에 적는다
cascade analyze --root .     # 플래그 없이 그대로 읽는다

cascade init은 부트스트랩(애플리케이션을 띄우는 코드)으로 애플리케이션을 찾습니다. @nestjs/core에 의존하는 패키지 아래에서 NestFactory.create를 부르는 .ts 파일입니다. 테스트 폴더는 뺍니다. 그 파일의 폴더가 profile.tsBackend.app이 되고, frameworkPacks에 nestjs가 들어갑니다.

둘을 찾으면 아무것도 적지 않고 둘 다 알려 줍니다. pack 하나는 애플리케이션 하나만 읽습니다. 엔드포인트는 메서드와 경로만으로 구분하므로, 두 애플리케이션의 라우트가 섞이면 하나로 합쳐져 버립니다. 애플리케이션마다 프로젝트를 따로 두면 됩니다.

--ts-src <dir>로 이번 실행에서만 애플리케이션을 지정할 수 있고, --no-ts로 이 레인을 끌 수 있습니다. 프런트엔드 루트가 백엔드까지 품고 있는 경우도 있습니다. ghostfolio 같은 Nx 워크스페이스는 package.json 하나로 둘을 함께 관리합니다. 이때 웹 레인은 백엔드 파일을 빼고 읽습니다. 그 파일은 이 레인의 몫입니다. 이 레인은 애플리케이션 루트를 읽고, import가 닿는 분석 루트 안의 다른 파일도 읽습니다(아래 “공유 라이브러리” 절).

레인 요약 줄은 이렇게 나옵니다.

TypeScript lane: 310 file(s), 118 route(s) from 34 registered controller(s), 1368 call(s) linked, 1001 into packages, 1777 on a receiver not typed here; Prisma: 149 statement(s) from 149 client call(s)

실행에서 더 찾은 것이 있으면 줄이 길어집니다. import를 따라가다 애플리케이션 밖에서 읽은 파일 수, 연결한 호출 가운데 그 파일로 들어간 수, 프로젝트 클래스가 상속·구현하는 타입을 거친 호출 수가 붙습니다. 마지막 것에는 모듈 바인딩으로 확정된 수와 HEURISTIC 등급을 받은 수도 함께 나옵니다. TypeORM 애플리케이션이면 Prisma 부분 뒤에 TypeORM 문장 수, 엔티티 수, 네이밍 전략(naming strategy, 이름을 만드는 규칙)이 붙습니다. schema.prisma를 읽었으면 TypeScript lane: schema.prisma:로 시작하는 둘째 줄이 나옵니다. schema가 선언한 테이블·컬럼 수(SQL 카탈로그가 있으면 뒷받침한 수와 불일치 수), relation에서 나온 조인 수, relation을 따라간 문장 수, $extends로 만든 클라이언트를 거친 문장 수를 알려 줍니다.

라우트

컨트롤러 클래스가 있다고 라우트가 되지는 않습니다. 애플리케이션이 불러오는 모듈이 그 컨트롤러를 등록해야 서비스됩니다. 부트스트랩이 NestFactory.create에 넘긴 모듈에서 시작해 각 모듈의 imports를 따라갑니다. 모듈 클래스, X.forRoot(...), forwardRef(() => X)를 모두 따라갑니다. 어느 모듈도 등록하지 않은 컨트롤러는 meta.laneStats.ts.unregisteredControllers에 이름을 남기고, 라우트는 만들지 않습니다.

클래스는 Nest가 읽는 방식대로 읽습니다. 데코레이터는 패키지가 붙인 이름으로 봅니다. import { Controller as Ctl } from '@nestjs/common'의 Ctl도, import * as common을 거친 common.Controller도 Controller입니다. 프로젝트가 직접 만든 Controller라는 데코레이터는 Nest의 것이 아닙니다. 컨트롤러는 자기 메서드뿐 아니라 프로젝트 안의 부모 클래스에서 물려받은 라우트 메서드도 서비스합니다. Nest가 프로토타입 체인을 훑는 것과 같습니다. 그런 라우트의 핸들러는 그 메서드를 선언한 클래스의 메서드입니다.

애플리케이션은 부트스트랩이 listen까지 하는 쪽입니다. 설정만 읽으려고 만들었다가 닫는 애플리케이션은 해당하지 않습니다. 설정은 그 애플리케이션에 대고 부르는 호출입니다. 부트스트랩 안에서도 찾고, 애플리케이션을 넘겨받는 프로젝트 함수 안에서도 찾습니다. configure(app)이면 그 함수의 매개변수 이름으로 따라갑니다. 주소 규칙은 Nest가 적용하는 방식 그대로 적용합니다.

소스 라우트
app.setGlobalPrefix('api') 모든 라우트 앞에 /api/...
그 옵션의 exclude: ['health'] /health는 prefix 없이 서비스
exclude: [{ path: 'health', method: RequestMethod.GET }] GET /health만 prefix 없이 서비스
exclude: ['docs{/*rest}', 'users/:id'] Nest 11 방식으로 맞춰 봄. :name은 한 구간, *name은 나머지 전부, {...}는 있어도 되고 없어도 됨, 대소문자 무시
app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' }) /api/v1/...
@Controller({ path: 'users', version: ['1', '2'] }) 버전마다 라우트 하나씩
메서드의 @Version('2') 컨트롤러 설정과 상관없이 그 라우트는 v2
@Version(VERSION_NEUTRAL) 버전 구간 없음
RouterModule.register([{ path: 'admin', module: AdminModule }]) AdminModule이 선언한 컨트롤러의 라우트 앞에 /admin
@Get(':id') {id}. 다른 레인이 경로 변수를 쓰는 방식과 같습니다

prefix 제외 목록은 같은 파일이나 import한 리터럴 상수로 만든 제한된 표현식도 읽습니다. 템플릿 문자열, 정수 리터럴을 인자로 받는 문자열 substring, 순서를 유지하는 배열 펼치기, 원시 값 배열에 대한 매개변수 하나의 순수한 map을 지원합니다. 애플리케이션을 실행하지 않고 문법을 읽습니다. 배열은 읽은 프로젝트 전체에서 참조 사용처를 확인할 수 있어야 합니다. 변경, 안전성을 확인할 수 없는 외부 전달, 모호한 import, 지원하지 않는 콜백은 여전히 모름으로 남깁니다. eval이나 Function처럼 코드를 동적으로 만드는 경우에도 참조를 모두 확인했다고 보지 않습니다. 이 한계에 걸리면 확인한 목록을 tsBackend.globalPrefixExclude로 선언할 수 있습니다. 실행 환경에서 정하는 설정은 실제 배포 값을 프로필에 선언해야 합니다.

모르면 라우트를 만들지 않습니다. 설정은 확실할 때만 압니다. 실행되지 않을 수 있는 호출(조건문 안, 반복문 안, 콜백 안), 서로 다른 값을 넣는 두 호출, 애플리케이션이 아닌 대상에 같은 이름으로 부르는 호출, 이 엔진이 읽지 못하는 코드에 애플리케이션을 넘기는 호출은 모두 그 설정을 “모름”으로 만듭니다. 아래는 모두 실행 결과에 진단으로 남습니다.

진단 소스가 한 일 결과
TS_APP_NOT_FOUND NestFactory.create가 없거나, 여러 개인데 listen하는 쪽이 하나로 정해지지 않음 라우트 없음
TS_PREFIX_UNREAD 전역 prefix를 설정에서 읽음(configService.get('app.apiPrefix')), 조건 안에서 정함, 서로 다른 값으로 두 번 정함, 읽지 못하는 코드에 애플리케이션을 넘김 tsBackend.globalPrefix에 배포 값을 적기 전까지 라우트 없음
TS_VERSIONING_UNREAD 같은 이유로 버전 옵션을 모름 라우트 없음
TS_ROUTER_MODULE_UNREAD RouterModule.register 인자가 리터럴이 아님 라우트 없음
TS_ROUTE_PATH_UNREAD, TS_ROUTE_VERSION_UNREAD 라우트 하나의 경로나 버전이 변수에 있거나, 펼치기가 바꿀 수 있는 옵션에 있음(@Controller({ ...base })) 그 라우트만 만들지 않음
TS_MODULE_UNREAD 모듈 옵션을 통째로 읽을 수 없음(변수, 목록을 덮어쓸 수 있는 펼치기) 그 모듈이 import하거나 등록한 것은 서비스하지 않음
TS_MODULE_IMPORT_UNREAD 모듈 목록에 변수(isDocumentDb ? A : B)나 펼치기가 있음 그 안의 모듈은 따라가지 않음
TS_CONTROLLER_UNREAD 모듈의 controllers 가 이 엔진이 읽지 않은 클래스(패키지의 것, 이번 실행이 뺀 파일의 것)를 가리키거나, 클래스 이름이 아닌 것을 담음 그 컨트롤러의 라우트는 여기서 서빙하지 않음
TS_ROUTES_WITHOUT_CONTROLLER 메서드가 라우트를 선언했는데 Nest 컨트롤러 데코레이터가 붙지 않은 클래스(Controller를 감싼 프로젝트 자체 데코레이터) 그 라우트는 서비스하지 않음
TS_PREFIX_EXCLUDE_UNREAD 이 엔진이 확정하지 못하는 exclude 항목: 실행 시 정하는 값, 안전성을 확인할 수 없는 배열 사용, 지원하지 않는 표현식이나 패턴 prefix 아래 모든 라우트를 prefix 붙은 주소로 만들고 HEURISTIC 등급을 붙임. 그 항목이 이름을 댈 수도 있기 때문. tsBackend.globalPrefixExclude에 목록을 적으면 EXACT가 됨
TS_PREFIX_DECLARED 프로필의 tsBackend.globalPrefix가 부트스트랩의 리터럴과 다름 프로필 값을 씀

무언가를 모르는데도 만드는 주소는 딱 하나, 읽지 못한 exclude가 바꿀 수 있는 주소입니다. 라우트는 분명히 있고 핸들러도 확실합니다. 그래서 그 항목이 이름을 대지 않았다면 갖게 될 주소로 만들고, HANDLES 엣지를 HEURISTIC으로 매겨 근거에 이유를 적습니다. 기본 conservative 모드의 걷기는 이 엣지를 지나지 않고, heuristic 모드는 지나갑니다.

패키지에서 가져온 모듈(ConfigModule.forRoot(), JwtModule)은 그 패키지의 것이라 빈틈으로 치지 않습니다. 패키지 코드는 읽지 않으니, 그 모듈이 여는 라우트도 읽지 않습니다.

호출

메서드가 부르는 대상을 이 레인이 타입으로 알 수 있으면, SOUND_SET 등급의 MAY_CALL 엣지로 잇습니다.

규칙(evidence.rule) 호출
ts-this-method this.m(): 같은 클래스나 상속한 클래스의 메서드
ts-injected-field this.svc.m(): 생성자로 주입한 필드의 타입 클래스의 메서드
ts-this-dispatch 프로젝트 클래스가 상속하는 클래스 안의 this.m(): 이 클래스와 하위 클래스 각각에서 가장 가까운 m 선언
ts-field-dispatch 필드 타입이 추상 클래스, 인터페이스, 또는 프로젝트가 상속하는 클래스인 this.svc.m(): 그 필드에 들어갈 수 있는 클래스 각각에서 가장 가까운 m 선언
ts-function f(): 파일이 선언하거나 이름으로 import한 함수
ts-static-method Cls.m(): 파일이 가리키는 클래스의 static 메서드

등급이 SOUND_SET인 이유가 있습니다. 하위 클래스가 메서드를 덮어쓸 수 있고, 필드 타입에 실제로 묶이는 provider가 다른 클래스일 수도 있습니다. 프로젝트 클래스가 그 타입을 실제로 상속하거나 구현하면, 호출을 그 클래스 각각으로 잇습니다(아래). 패키지로 들어가는 호출은 따로 셉니다. 이 레인이 타입을 모르는 대상(지역 변수, 매개변수, 필드를 거친 체인)에 대한 호출과는 다릅니다. 앞의 것은 프로젝트 연결의 빈틈이 아니고, 뒤의 것은 빈틈입니다. 데코레이터는 메서드가 부르는 호출이 아닙니다. 클래스를 정의할 때 한 번 실행될 뿐입니다.

추측인 호출이 두 가지 더 있습니다. 파일이 이름을 다시 쓰는 함수(let f = ...; f = other) 는 호출할 때 다른 함수일 수 있습니다. 엣지는 HEURISTIC 이고 evidence.incomplete 가 그렇다고 말합니다. 그리고 배포될 수 있는 패키지 안에서는 대상이 하나뿐인 this.m() 도 ts-this-dispatch, HEURISTIC 입니다. 트리 밖의 클래스가 상속할 수 있기 때문입니다.

추상 클래스나 인터페이스를 거친 호출

추상 클래스로 타입을 적은 필드에 this.users.findById(id)를 부르면, 실제로 도는 것은 Nest가 주입한 객체의 findById입니다. 본문 없는 추상 선언은 돌지 않습니다. 인터페이스는 아예 본문이 없습니다. 부모 클래스 안의 this.m()도 this가 하위 클래스 인스턴스면 하위 클래스가 덮어쓴 메서드가 돕니다. 그래서 프로젝트 클래스가 상속하거나 구현하는 타입을 거친 호출은, 그 타입 자신과 그런 클래스 각각(직접이든 다른 클래스·인터페이스를 거쳐서든) 에서 가장 가까운 메서드 선언으로 이어집니다. 추상 메서드는 대상이 되지 않습니다. evidence.dispatch에 타입과 후보 메서드 수(candidates)가 남습니다. 프로젝트 안에서 아무 클래스도 상속·구현하지 않는 타입은, 이 엔진이 읽지 못한 무엇도 그 집합에 후보를 더할 수 없을 때만 예전처럼 잇습니다(아래).

this.m()이 무엇을 부르는지는 세 가지가 더 정합니다.

후보 집합은 프로젝트 소스의 extends, implements 절만 보고 만듭니다. 그런데 TypeScript는 둘 다 적지 않은 객체도 모양만 맞으면 그 타입으로 받아 줍니다. 그래서 무언가가 집합을 확정해 줄 때만 완전한 집합으로 봅니다.

집합이 완전하면 엣지는 SOUND_SET입니다. 빠진 것이 있을 수 있으면 HEURISTIC이고, 이유를 evidence.dispatch.incomplete에 적습니다.

TS_DISPATCH_INCOMPLETE는 HEURISTIC이 된 호출 수와 가장 흔한 이유를 알려 줍니다. TS_BINDING_NOT_READ는 이 엔진이 읽지 못하는 방식으로 묶인 타입을 하나씩 알려 줍니다.

공유 라이브러리

모노레포의 애플리케이션은 자기 루트 밖의 코드를 import합니다. tsconfig의 paths 별칭, baseUrl, 상대 경로로 닿는 공유 라이브러리입니다. 이 레인은 애플리케이션 파일을 읽은 다음, 그 파일들의 import와 re-export가 닿는 파일을 분석 루트 안에서 한 바퀴씩 더 읽습니다. 새 파일이 나오지 않을 때까지 되풀이합니다. import 경로는 브리지와 똑같은 방식으로 풉니다. 그래서 import 하나를 두고 이 단계가 읽는 파일과 브리지가 그 import의 뜻으로 보는 파일이 같습니다. node_modules, 분석 루트 밖 파일, 바이트가 루트 밖에 있는 파일은 읽지 않습니다. 루트 밖으로 이어지는 링크(libs/shared -> ../../elsewhere)는 경로가 어떻게 생겼든 읽지 않고, 그렇다고 알립니다(TS_FILES_LEFT_OUT). 이번 실행이 읽는 라이브러리에서 가져온 이름은 애플리케이션 자신의 코드처럼 잇고, 패키지로 세지 않습니다. 이번 실행이 뺀 파일에서 가져온 이름도 패키지의 것으로 세지 않습니다. 여기서는 아무것도 가리키지 않습니다.

테스트 지원 파일(test support)은 이번 실행이 읽는 파일이 import 하지 않으면 뺍니다. 애플리케이션 루트 안이든 공유 라이브러리 안이든 같습니다. spec, mock, stub, story 파일, 그리고 __mocks__, testing, e2e 같은 폴더 아래 파일입니다. 애플리케이션은 import 한 것을 실행합니다. 그래서 앱 모듈이 import 하는 testing 기능 모듈은 읽고, 그 컨트롤러도 서빙합니다. barrel 의 export * from './x.mock' 하나만으로는 mock 이 애플리케이션의 것이 되지 않습니다. 어떤 이름이 테스트 지원 파일인지는 typescript.test-support 룰(src/core/rules/packs/typescript.json, 종류 ts.test-support)이 정하고, 워커는 따로 목록을 갖지 않습니다. mock을 애플리케이션 코드로 읽으면, 추상 타입을 거친 호출이 닿을 클래스가 하나 더 생깁니다. 라이브러리의 barrel(re-export를 모은 파일)이 mock까지 내보내기도 합니다. 뺀 것은 알립니다. TS_FILES_LEFT_OUT 이 읽는 파일 어디서도 import 하지 않는 테스트 지원 파일과 바이트가 루트 밖에 있는 파일을 세고, 그중 하나를 import 하거나 다시 export 하는 읽은 파일을 짚습니다. ghostfolio 에서는 492 개를 읽고, 테스트 지원 파일 58 개를 빼고 알립니다.

애플리케이션 밖에서 읽은 파일은 meta.laneStats.ts.reached에 남고, 레인 요약 줄에도 개수가 나옵니다. 이 파일도 다른 파일처럼 분석 입력이라, 하나라도 HEAD와 다르면 pack이 dirty로 표시됩니다.

웹 레인도 읽는 파일(프런트엔드와 백엔드가 같이 쓰는 라이브러리)은 레인마다 shard(파일별 캐시 조각)를 따로 둡니다. 그래서 한 레인 때문에 다른 레인이 그 파일을 다시 읽는 일이 없습니다. 두 레인이 같이 만든 함수는 코드가 하나이므로 노드도 하나이고, lanes에 두 레인이 모두 적힙니다. TS_SYMBOL_SHARED_WITH_WEB이 그런 함수를 알려 줍니다. 웹 레인은 그 함수가 보내는 요청의 주소를 프런트엔드 규칙으로 정합니다. 그래서 백엔드 라우트에서 출발한 걷기가 그 함수를 지나면, 그 요청도 프런트엔드가 쓸 주소로 따라갑니다.

Prisma

필드가 Prisma 클라이언트인 경우는 셋입니다. @prisma/client의 PrismaClient로 타입을 적은 경우, 그걸 상속한 프로젝트 클래스(흔히 쓰는 PrismaService)로 적은 경우, nestjs-prisma의 PrismaService로 적은 경우입니다. 마지막은 라이브러리의 타입이라 연결을 SOUND_SET으로 매기고, 근거에 라이브러리를 적습니다. 규칙 팩 (src/core/rules/packs/prisma.json)에 그렇게 정해 두었습니다.

호출한 자리마다 문장 하나입니다. statement:prisma:<file>#<Class.method>/<n>은 그 메서드의 n번째 클라이언트 호출입니다. 같은 모델을 두 번 부르면서 select가 다르면 문장도 둘입니다. 그래서 한쪽이 읽는 컬럼이 다른 쪽 호출자에게 번지지 않습니다. 모델은 델리게이트 이름으로 찾습니다(this.prisma.userProfile이면 UserProfile). 테이블은 @@map 또는 모델 이름이고, 스키마는 @@schema가 정한 것입니다(Prisma의 멀티 스키마). 컬럼은 @map 또는 필드 이름입니다.

대화형 트랜잭션 this.prisma.$transaction(async (tx) => { ... })은 콜백에 클라이언트를 넘깁니다. 콜백 안에서 tx로 부른 호출은 this.prisma로 부른 것과 똑같이 읽고, 같은 메서드의 순서에 넣으며, 근거에 transaction: "$transaction"을 남깁니다. 배열 형태 $transaction([this.prisma.a.update(...), ...])은 호출 하나하나로 읽습니다.

무엇을 읽고 쓰는지는 인자에서 나옵니다. prisma.operation 규칙이 select, where, orderBy, data 같은 키마다 역할을 정해 두고 그대로 읽습니다. 필터의 AND, OR, NOT 안으로도 들어갑니다. 복합 unique·id 키는 schema가 그 이름으로 묶은 필드를 읽습니다(@@unique([a, b], name: "pair"), 이름이 없으면 a_b). 키를 _로 쪼개서 필드를 짐작하지 않습니다. 호출이 이름을 댄 relation은 그 relation이 닿는 모델까지 따라갑니다(아래 “relation 따라가기” 절). 규칙이 따라가지 못한 것은 문장에 남깁니다.

select가 없고 인자를 통째로 읽었으면, 모델의 스칼라 컬럼을 모두 읽는 것으로 봅니다.

schema에 있는 모델과 연산 이름으로 부르는데 이 레인이 클라이언트라고 알지 못하는 대상에 부른 호출(매개변수로 넘겨받은 클라이언트, 다시 대입한 지역 변수)은 문장을 만들지 않습니다. 대신 TS_PRISMA_CALL_UNREAD로 개수와 몇 곳의 위치를 알려 주고, 이유를 알면 이유도 적습니다.

schema.prisma는 애플리케이션 루트나 그 위의 prisma/schema.prisma에서 찾습니다. Prisma가 가장 먼저 보는 자리입니다. 다른 곳에 있다면 패키지의 "prisma": { "schema": ... }나 프로필의 tsBackend.prismaSchema로 지정합니다. 이 파일은 실행할 때마다 다시 읽고, 경로와 sha256, provider를 meta.laneStats.ts에 남깁니다.

schema.prisma가 카탈로그입니다

schema.prisma의 모델은 모두 테이블이 되고, 스칼라 필드는 모두 컬럼이 됩니다. 호출이 이름을 댔는지와 상관없습니다. view 블록도 똑같이 읽습니다. 노드 모양은 SQL 레인이 DDL로 만드는 것과 같습니다. 컬럼에는 schema에 적힌 그대로의 타입(String, String[], enum 이름, @db.X 속성이 있으면 nativeType)이 붙습니다. ?에서 nullable을, @id나 @@id에서 pk를 읽습니다. 노드마다 declaredBy: "prisma"가 붙습니다. relation 하나는 두 테이블 사이의 JOINS 엣지 하나입니다. fields와 references가 가리키는 컬럼으로 잇고, schema가 직접 말하는 것이라 EXACT입니다.

암묵적 다대다(implicit many-to-many)는 양쪽이 목록이고 어느 쪽에도 fields가 없는 relation입니다. 이 경우 Prisma가 만드는 테이블이 됩니다. 이름은 _에 relation 이름을 붙인 것이고, 이름이 없으면 두 모델 이름을 알파벳 순으로 To로 이은 것입니다 (_OrderToTag). 컬럼 A와 B는 첫째 모델과 둘째 모델의 id를 가리키고, 두 모델과 각각 조인됩니다. (A, B)가 기본키인지 unique 인덱스인지는 Prisma 버전마다 달라서, 어느 컬럼도 키라고 적지 않습니다. 목록 컬럼이 NULL을 허용하는지는 schema에 없어서, 목록의 nullable은 null입니다. DDL이 없으면 pack의 meta.catalog는 {source: "prisma", path, sha256}이고, catalog 축은 axes.catalog.sources에 schema.prisma를 적고 shipped가 됩니다. 이름이 어디서 왔는지는 답을 제한하는 것이 아니므로 note로 달지 않습니다.

SQL 카탈로그(DDL로 지정한 마이그레이션이나 스냅샷)도 함께 읽으면, 그 카탈로그가 선언한 테이블을 두 번 선언하지 않습니다. 노드는 SQL 레인 것으로 두고, schema가 그 노드를 뒷받침합니다(prismaModel, prismaField). 둘이 다르게 말하는 것은 불일치로 셉니다. 한쪽에만 있는 테이블이나 컬럼(table-not-in-catalog, column-not-in-catalog, table-not-in-schema, column-not-in-schema), 기본키 (pk-differs), null 허용 여부(nullable-differs)입니다.

어느 쪽이 더 새것인지는 정하지 않습니다. 마이그레이션이 schema보다 늦을 수도 있고, schema가 DB보다 늦을 수도 있기 때문입니다. 그래서 키나 null 허용 여부가 서로 다른 컬럼은 두 값을 출처별로 노드에 함께 남깁니다. declarationsDiffer: { pk: { catalog, prisma }, nullable: { catalog, prisma } } 입니다. 노드 자신의 pk는 노드와 마찬가지로 SQL 카탈로그 쪽 값입니다. 불일치는 모두 meta.laneStats.ts.prisma.catalog에 세고, PRISMA_CATALOG_DISAGREES 진단 하나로 알리고, catalog 축에 note로 답니다. schema에만 있는 컬럼은 schema 것으로 더합니다. 클라이언트가 그 컬럼을 보내기 때문입니다. SQL 카탈로그에만 있는 컬럼은 알리기만 합니다. 어떤 Prisma 호출도 그 컬럼을 이름으로 댈 수 없기 때문입니다. 타입은 비교하지 않습니다. Prisma의 String과 DB의 TEXT는 어휘가 다를 뿐 불일치가 아닙니다.

relation 따라가기

호출이 이름을 댄 relation은 그 relation이 닿는 모델까지 따라갑니다. include나 select 안(true면 그 행 전체, 객체면 그 자리에서 따로 하는 find 하나), 필터 (some, every, none, is, isNot, 또는 to-one 필터를 바로 쓴 것), orderBy, _count, 그리고 중첩 쓰기(nested write: create, createMany, connect, connectOrCreate, set, disconnect, update, updateMany, upsert, delete, deleteMany)가 여기 해당합니다. 그러면 문장은 관련 테이블과 컬럼을 읽거나 쓰거나 지우고, 양쪽에서 조인이 맞추는 컬럼을 읽습니다. 연결을 걸거나 끊는 쓰기는 연결을 담은 컬럼을 씁니다. 그 컬럼이 어느 쪽에 있든 마찬가지이고, 암묵적 테이블이면 그 행을 넣거나 지웁니다. 이런 엣지는 모두 evidence.relation에 relation 이름을 남깁니다. 이름 목록은 팩 데이터입니다(prisma.json: relationFilters, relationNullFilters, relationCount, nestedWrites).

data 가 자기 모델의 필드는 하나도 적지 않고 relation 쓰기만 담은 update 는, Prisma 6.19 가 보내는 대로 자기 행을 읽기만 하고 아무것도 쓰지 않습니다. 그래서 자기 테이블의 EXECUTES 는 읽기입니다. 소스에 적히지 않은 data(변수)는 필드를 적을 수 있으므로 쓰기는 SOUND_SET 입니다.

relation은 따로 문장이 되지 않고, 그 호출의 문장에 들어갑니다. Prisma가 relation에 보내는 SQL은 호출 자리만 봐서는 정해지지 않기 때문입니다. relationLoadStrategy: "join"이면 조인 쿼리 하나이고, "query"(relationJoins preview 기능이 없을 때의 기본값)면 relation 단계마다 쿼리가 하나씩 늡니다. 중첩 쓰기는 트랜잭션 하나 안의 여러 문장입니다. 이 모두가 공유하는 것은 호출 자리이고, 메서드가 구현하는 것도 호출 자리입니다.

호출 하나는 테이블과 접근 종류(읽기, 쓰기, 지우기)마다 EXECUTES 엣지를 하나씩 둡니다. SQL 레인과 같은 방식입니다. 그래서 한 테이블을 읽고 쓰는 호출은 두 질문 모두에 걸립니다. 엣지마다 그 접근에 대해 어떤 경로가 준 가장 강한 등급을 받습니다. 컬럼은 종류마다 엣지 하나이고, 역시 가장 강한 등급을 받습니다.

실행 중에만 알 수 있는 relation 값(include: { posts: flag }, data: { account })은 일어날 수도 있는(MAY) 것으로 따라갑니다. 그 엣지는 SOUND_SET이고, 문장은 그 키를 columnsRuntimeOnly에 적습니다. schema만으로 이 레인이 자리를 정할 수 없는 relation은 relation-not-followed로 남기고 이유를 적습니다. 반대쪽이 없거나 후보가 하나가 아닌 경우, fields와 references가 맞지 않는 경우, schema가 자리를 정해 주지 않는 암묵적 다대다인 경우입니다. 마지막은 모델이 자기 자신과 맺은 경우(어느 쪽이 A인지 schema가 말하지 않습니다), 필드 하나짜리 id가 없는 모델 사이, 서로 다른 @@schema 블록에 걸친 경우입니다.

Prisma가 먼저 찾는 것. 중첩 쓰기는 관련 행을 바꾸기 전에 그 행을 먼저 찾아야 할 때가 있습니다. 그때 Prisma는 SELECT를 보내고, 이 읽기도 그립니다. 값이 이름을 댄 행을 찾기도 하고(named: connect의 SELECT Tag.id ... WHERE Tag.id = ?), 매달린 행에 연결된 행을 찾기도 합니다(linked: delete의 SELECT Post.id, Post.authorId ... WHERE Post.authorId IN (?)). 이런 조회(lookup)는 관련 테이블, 그 키, 조인이 맞추는 컬럼을 읽습니다. 다대다에서 연결된 행을 찾을 때는 암묵적 테이블(_TagToUser)도 읽습니다. 어느 쓰기가 어느 행을 찾는지는 relation 종류마다 다릅니다. 일대다의 목록 쪽과 to-one 쪽, 키가 이쪽 또는 저쪽에 있는 일대일, 다대다입니다. 팩이 중첩 쓰기마다 이를 적어 둡니다(prisma.json: lookup). Prisma 6.19.0을 SQLite 위에서 돌려 잰 결과입니다. 호출이 지금 만드는 행에는 연결된 것이 아직 없습니다. 그래서 create 아래에서는 연결된 행 조회를 그리지 않습니다. 호출 자신의 create, upsert의 create, 중첩 create가 해당합니다(팩의 argumentRows와 nests). ghostfolio에서는 암묵적 테이블 읽기가 두 개 늘었습니다. tags: { set } 아래의 _OrderToTag, watchlist: { disconnect } 아래의 _UserWatchlist입니다.

리터럴 값이 바꾸는 것. null로 거르는 relation(author: null, { is: null }, { isNot: null }. 어느 키인지는 팩의 relationNullFilters에 있습니다)은 연결만 확인합니다. 외래키가 거르는 모델 자신의 테이블에 있으면, 문장은 그 키 컬럼만 읽고 상대 테이블은 읽지 않습니다. Prisma 엔진이 그렇게 검사하기 때문입니다. 키가 상대 테이블에 있으면 다른 필터처럼 그 테이블을 따라갑니다.

아무것도 바꾸지 않게 만드는 리터럴을 받은 중첩 쓰기는, Prisma가 그래도 보내는 것만 그립니다. 중첩 쓰기마다 그런 리터럴을 팩의 idle 항목에 적어 둡니다. 값([]나 false, 값 전체 또는 인자 하나), 적용되는 relation, 그리고 무엇을 빼는지(엣지 전부 또는 쓰기만)입니다. relation에 맞는 첫 항목이 이깁니다.

set은 연결을 바꿔 끼웁니다. 먼저 끊고 다시 걸기 때문에, set: []도 끊는 일은 합니다.

$extends로 만든 클라이언트

클라이언트 필드에 $extends를 불러 만든 것을 담은 지역 변수 (const x = this.prisma.$extends({...}))는, 그 줄부터 그 변수로 부른 호출에 대해 클라이언트입니다. 클래스의 메서드가 돌려준 것을 담은 지역 변수 (const x = this.client())도, 그 메서드의 return이 모두 그런 호출이거나 클라이언트 필드 자체라면 클라이언트입니다. 확장을 이 레인이 통째로 읽었고(그 자리에 적었거나, 같은 메서드의 지역 변수에서 Prisma.defineExtension으로 만든 경우) query 부분이 없으면, 그 호출은 원래 클라이언트만큼 확실합니다. 읽지 못한 확장이나 query 부분이 있는 확장은 호출을 HEURISTIC으로 만듭니다. query는 연산을 가로채서 보내는 내용을 바꿀 수 있기 때문입니다. 문장은 어느 경우인지를 prismaEvidence.extension(메서드, 읽었는지, 들어 있는 부분)에 적고, 클라이언트가 어느 메서드에서 왔는지는 prismaEvidence.returnedBy에 적습니다. 이름 목록은 팩 데이터입니다(prisma.json: extensions).

확장의 result 부분이 계산하는 필드는 컬럼이 아닙니다. 그 필드를 고르면 needs가 true로 둔 필드를 읽습니다. 그 계산 필드가 필요로 하는 다른 계산 필드도 따라가고, 모델 하나에 선언했든 $allModels에 선언했든 똑같습니다. needs가 없는 필드는 더 읽는 것이 없습니다. needs를 이 레인이 읽지 못한 필드는 columnsRuntimeOnly에 이름을 남깁니다. 계산 필드 목록에 spread나 계산된 키가 있어서 필드가 더해지거나 바뀔 수 있으면, 그 목록은 열려 있다고 봅니다. 모델의 객체에 있든 result 부분 자체에 있든 같습니다. 열린 목록의 필드를 고르면 그 needs는 하나도 주장하지 않습니다. 문장에는 실행해야 알 수 있다고 적고, 같은 이름의 진짜 필드가 있으면 그 필드는 그대로 읽습니다.

어느 지역 변수가 클라이언트인가

지역 변수는 이름이 아니라 선언 그 자체로 클라이언트를 담습니다. 워커는 파일마다 언어와 같은 방식으로 변수 범위를 나누고(adapters/ts/tsscope.mjs), 호출을 받는 쪽이 어느 선언인지 적습니다. 안쪽 블록에서 같은 이름을 다시 선언했거나, 안쪽 함수의 매개변수가 트랜잭션 클라이언트의 이름을 가리면, 그건 다른 지역 변수입니다. 그 변수로 부른 호출은 클라이언트 호출이 아닙니다. 범위 안 어디서든 다시 대입되는 지역 변수 (x = other, x++, for...of의 대상)는 호출 시점에 무엇이든 담고 있을 수 있습니다. 그래서 어디서도 클라이언트로 보지 않습니다. 그 변수로 부른 호출은 읽지 않고, TS_PRISMA_CALL_UNREAD가 그 이유와 함께 알려 줍니다.

TypeORM

TypeORM을 쓰는 NestJS 애플리케이션도 Prisma를 쓰는 애플리케이션과 같은 방식으로 읽습니다. 엔티티가 카탈로그이고, SQL을 보내는 호출 하나가 문장 하나입니다. 이 레인이 TypeORM에 대해 아는 것은 typeorm 룰 팩(src/core/rules/packs/typeorm.json)에 있습니다. 종류 넷(typeorm.entity, typeorm.receiver, typeorm.operation, typeorm.query-builder)으로 읽고, cascade rules test가 예제를 돌립니다.

엔티티가 카탈로그입니다

@Entity가 붙은 클래스는 테이블이고, 컬럼 데코레이터(@Column, @PrimaryColumn, @PrimaryGeneratedColumn, @ObjectIdColumn, @CreateDateColumn, @UpdateDateColumn, @VersionColumn, @DeleteDateColumn)가 붙은 속성은 컬럼입니다. 클래스가 상속한 프로젝트 클래스의 컬럼도 자기 컬럼입니다. TypeORM이 프로토타입 사슬 전체를 읽기 때문입니다. @ManyToOne, 그리고 @JoinColumn이 붙은 @OneToOne은 연결을 담는 조인 컬럼을 더합니다. @JoinTable이 붙은 @ManyToMany는 조인 테이블과 그 컬럼 둘을 더합니다. 조인 컬럼을 가진 relation과 조인 테이블은 각각 JOINS 엣지입니다. 호출이 읽든 말든 모든 컬럼이 pack에 들어갑니다. 하위 클래스가 TypeORM 데코레이터 없이 속성을 다시 선언하기만 했으면(declare email: string), 상속한 클래스가 매핑한 컬럼은 하나도 가려지지 않습니다.

엔티티만 선언한 테이블은 declaredBy: "typeorm"인 stub 노드입니다. 엔티티는 애플리케이션이 선언한 것이지, DB에 실제로 있는 것이 아니기 때문입니다. DDL이 선언한 테이블은 같은 노드가 되고, typeormCatalogMatch 표시가 붙습니다. DDL을 읽었는데 엔티티만 선언한 테이블이 DDL의 어느 테이블과도 맞지 않으면 TS_TYPEORM_TABLE_MISSES_DDL로 알립니다. 이름은 같은데 스키마가 다르거나 없는 DDL 테이블도 함께 적습니다. DDL은 보통 스키마 없이 테이블을 적고, TypeORM은 그 앞에 DataSource의 스키마를 붙입니다(users와 public.users). 그 스키마를 프로필의 schema.default로 선언하면, DDL의 스키마 없는 테이블과 TypeORM의 테이블이 같은 테이블이 됩니다.

엔티티가 모두 테이블 상속(@TableInheritance, @ChildEntity)을 쓰는 프로젝트도 있습니다. 이 레인은 테이블 상속을 읽지 않습니다. 그래도 아무것도 내놓지 않는 대신, 레인 요약 줄을 내고 TS_TYPEORM_MAPPING_UNREAD로 그 엔티티들을 알립니다.

이름은 TypeORM의 naming strategy, prefix, schema를 따릅니다

데코레이터가 적지 않은 이름은 TypeORM의 메타데이터 빌더가 만드는 방식 그대로 만듭니다. DataSource 옵션의 namingStrategy가 정한 naming strategy(이름을 만드는 규칙)를 따릅니다. 옵션에는 모든 테이블 이름 앞에 붙는 entityPrefix와, schema를 적지 않은 엔티티에 주는 schema도 있습니다. 셋 다 같은 방식으로, 하나씩 따로 읽습니다.

옵션이 실행 시점에 맡긴 값은 프로필의 tsBackend.typeorm에 선언합니다. namingStrategy(팩이 붙인 이름, default나 snake), entityPrefix, schema, 그리고 TypeORM 이 쓰는 이름 그대로의 드라이버 type 입니다. null은 선언하지 않았다는 뜻이고, 앞의 셋에서 ""는 “없음”이라고 선언한 것입니다. 테이블 경로를 정하는 type 값은 팩의 tablePath 가 아는 드라이버 이름(postgres, mysql, mssql, sqlite 등) 입니다. 팩이 모르는 type 은 분석을 멈추지 않고, 그 값이 붙일 이름은 HEURISTIC 으로 남습니다. 선언한 값은 옵션이 말하는 값 대신 쓰고, 옵션에 적힌 값과 다르면 TS_TYPEORM_NAMING_DECLARED로 알립니다. 테이블 이름은 모두 prefix에 기대므로, 옵션이 소스에 없는 애플리케이션은 블록을 선언하기 전까지 기본 conservative 모드에서 어느 테이블에도 닿지 않습니다. 팩이 모르는 전략 이름을 적으면 분석을 멈추고, 아는 이름을 늘어놓은 프로필 오류를 냅니다.

호출한 자리마다 문장 하나입니다

SQL을 보내는 TypeORM 호출은 따로 문장이 됩니다. statement:typeorm:<file>#<Class.method>/<n>은 그 메서드의 n번째 TypeORM 호출이고, 번호는 Prisma 호출처럼 붙입니다. 아무것도 보내지 않는 호출(create, merge)은 번호를 받지 않습니다. 대상은 typeorm.receivers 룰이 아는 객체에 부른 호출입니다.

repository나 manager를 담은 지역 변수(const repo = this.ds.getRepository(User))는 Prisma 클라이언트 변수처럼 선언한 자리로 묶습니다. 안쪽 블록에서 같은 이름으로 선언한 것은 다른 지역 변수입니다. 다시 대입되는 지역 변수는 호출 시점에 무엇을 담고 있을지 모르므로 그 호출을 읽지 않고, TS_TYPEORM_RECEIVER_UNREAD로 알립니다. 다만 두 경우는 읽습니다. 값 없이 선언했다가 한 번만 대입한 지역 변수 (let r; r = this.ds.getRepository(User)), 그리고 repository를 담은 필드나 다른 지역 변수를 받은 지역 변수(const r = this.users)입니다. 조건에 따라 값이 정해지는 지역 변수(flag ? getRepository(Tag) : getRepository(Label))는 둘 중 무엇을 담을지 실행해야 압니다. 그래서 그 호출은 읽지 않고 알리기만 합니다. 구조 분해로 꺼낸 지역 변수 (const { users } = this)는 그 필드가 담은 것을 담습니다. ||, ??, && 로 받은 지역 변수는 두 값 중 어느 쪽이든 담을 수 있으므로, 그 호출도 알리기만 합니다.

연산은 인자마다 맡은 역할로 읽습니다(typeorm.operations).

createQueryBuilder 사슬은 그걸 만든 호출 자리에서 읽습니다. 사슬에 적힌 단계와, 같은 메서드에서 그 사슬을 담은 지역 변수에 나중에 부른 호출까지 함께 읽습니다. 그 지역 변수는 선언한 자리로 찾습니다. alias부터 읽습니다. TypeORM은 쿼리를 실행할 때 SQL을 만들기 때문입니다. 쿼리를 실행할 때마다 그 시점에 고른 것을 돌려주므로, 두 번 실행하면서 고른 것이 다르면 둘 다 남깁니다. 조건 텍스트의 alias.property는 그 alias가 가리키는 엔티티의 컬럼입니다. join은 그 테이블을 더하고, ...AndSelect join은 그 테이블의 행을 돌려줍니다. select는 돌려줄 것을 좁히고, update, delete, insert는 그 종류의 문장으로 만들고, set과 values는 적은 것을 씁니다. update, insert, softDelete, restore는 위 연산처럼 날짜·버전 컬럼을 채웁니다. into에 컬럼 목록을 적은 insert는 그 컬럼만 넣습니다. 삭제 날짜 컬럼이 있는 엔티티의 select 나 join 은 withDeleted 가 먼저 오지 않으면 그 컬럼을 읽습니다. 조건문, 반복문, 콜백 안에 적은 단계는 실행될 수도 있는(MAY) 것이라, 거기서 읽는 것은 SOUND_SET입니다. 그런 자리의 select는, 그대로 남을 수도 있는 원래 선택을 후보로 함께 남깁니다. 이 룰이 자리를 정하지 못한 단계(Brackets로 만든 조건, 팩이 모르는 메서드)는 문장에 builder-step-not-read로 남습니다. 만든 자리에서 쿼리를 실행하는 단계가 하나도 없는 builder는 builder-not-run-here를 남기고, 그 엣지는 SOUND_SET입니다.

builder 는 그 지역 변수를 쓰는 곳이 모두 값을 버리는 단계이거나, 쿼리를 실행하거나 builder 를 하나 더 만드는 단계로 끝나는 사슬일 때만 통째로 읽습니다. 다른 쓰임이 있으면 이 엔진이 보지 못한 단계(행을 좁히는 select, 조건을 바꾸는 where)가 붙을 수 있습니다. 호출이나 new 에 넘기기, 객체나 목록에 넣기, 조건식이나 &&, || 의 한쪽에 두기, 함수에 잡히기, builder 를 돌려주는 단계로 다른 이름에 담기(const q2 = qb.where(...)), 돌려주기가 그렇습니다. 워커는 그 지역 변수의 이런 쓰임을 모두 기록합니다. 그러면 이 builder 가 읽는 것은 SOUND_SET 이고, 문장에 어디로 갔는지(builder-escapes)와 컬럼은 실행해야 안다는 것을 적습니다. column_impact 는 그런 컬럼 목록을 하한으로 주고, 문장 자신의 말로 적습니다. 자기 자신에 단계를 붙이는 let(q = q.andWhere(...))은 builder 하나로 남고, 다른 것을 받으면 builder 가 밖으로 샌 것입니다. clone() 과 subQuery() 는 builder를 하나 더 만듭니다. 복사한 원래 builder는 읽던 것을 그대로 읽고, 복사본의 단계는 읽지 않습니다. 문장에 그렇다고 적습니다(builder-step-not-read).

읽지 못한 것은 알립니다.

진단 소스가 한 일
TS_TYPEORM_CALL_UNREAD raw SQL(query()), 팩이 모르는 연산, 이 엔진이 읽은 엔티티가 없는 호출
TS_TYPEORM_RECEIVER_UNREAD 엔티티를 댄 연산인데, 받는 쪽이 repository나 entity manager인지 모르는 경우. 그런 객체를 담았지만 호출 시점에 다른 값을 담을 수 있는 지역 변수(다시 대입했거나, 조건이 값을 고르는 변수)에 부른 경우도 여기 들어갑니다
TS_TYPEORM_MAPPING_UNREAD 이 엔진이 읽지 않는 매핑. embedded 엔티티(@Column(() => Address)), @ChildEntity, @ViewEntity, @TableInheritance, @Tree가 붙은 클래스, 소스에 적히지 않은 조인 옵션, 대상이 이 엔진이 읽은 엔티티가 아닌 relation
TS_TYPEORM_NAMING_ASSUMED 이 엔진이 옵션을 읽지 못해서 naming strategy, prefix, schema 중 무엇을 모르거나, 드라이버 때문에 테이블 이름이 불확실하고(적혀 있지 않거나 팩이 모르는 드라이버), 그 값에 기대는 이름이 HEURISTIC인 경우. tsBackend.typeorm(namingStrategy, entityPrefix, schema, type)을 선언하면 정해진다고 말합니다
TS_TYPEORM_NAMING_DECLARED 프로필의 tsBackend.typeorm이 옵션에 적힌 것과 다른 값을 선언한 경우. 프로필 값을 씁니다
TS_TYPEORM_TABLE_MISSES_DDL DDL을 읽었는데, 엔티티만 선언한 테이블이 DDL의 어느 테이블과도 맞지 않는 경우. 이름이 같고 스키마가 다른 DDL 테이블을 적고, 둘을 잇는 방법으로 schema.default를 알려 줍니다

where 안의 relation은 문장에 relation-not-followed로 남고, 엔티티에 없는 키는 argument-not-read로 남습니다.

catalog 축은 테이블·컬럼 이름이 모두 데코레이터에 적혀 있거나 이번 실행이 아는 전략을 따를 때 shipped입니다. 이때 axes.catalog.sources에 TypeORM entities (N table(s), M column(s))가 들어갑니다. 이번 실행이 확인하지 못한 규칙으로 이름을 만들어야 했다면(모르는 전략·prefix·schema, TypeORM 버전마다 다른 철자) catalog 축과 column 축은 이유와 함께 degraded입니다. 엔티티 옆에 DDL이 있어도 달라지지 않습니다.

프로필

"frameworkPacks": ["nestjs"],
"tsBackend": { "app": "../apps/api/src", "prismaSchema": null, "globalPrefix": null, "globalPrefixExclude": null,
               "typeorm": { "namingStrategy": null, "entityPrefix": null, "schema": null, "type": null } }

프로필이 가질 수 있는 모든 키와 그 예는 개념 에 모아 두었습니다.

이 키들을 하나도 적지 않은 프로젝트는 키가 생기기 전과 같은 프로필 digest를 유지합니다. 그래서 업그레이드가 보정 게이트에게 “분석 대상이 바뀐 것”으로 보이지 않습니다. typeorm 블록은 다른 키보다 나중에 생겼으므로, 넷이 모두 null이면 블록 하나만 따로 digest에서 뺍니다. 그래서 블록이 생기기 전에 tsBackend를 적어 둔 프로젝트도 digest가 그대로입니다. type 은 그보다도 나중에 생겼으므로 null 인 동안 따로 뺍니다. 그래서 type 이 생기기 전에 블록을 적어 둔 프로젝트도 digest 가 그대로입니다. 반대로 다른 애플리케이션을 읽으면 대상이 바뀐 것으로 봅니다. 읽는 루트가 대상 고정 값(pin)에 들어가기 때문입니다.

cascade init은 schema.prisma의 datasource provider를 sqlDialects.main에도 적습니다. 프로필이 받는 방언(postgresql, mysql)일 때만 적습니다. Prisma를 쓰는 프로젝트는 어느 DB에서 도는지 schema에 직접 적어 두고, 마이그레이션도 그 DB의 SQL로 씁니다.

캐시하는 것과 하지 않는 것

파일마다의 사실은 웹 레인처럼 파일 바이트를 기준으로 캐시합니다. 두 번째 실행부터는 바뀐 파일만 다시 읽습니다. 어떤 파일을 읽을지는 실행마다 다시 정합니다. 애플리케이션의 파일과, 그 import가 분석 루트 안에서 닿는 다른 파일입니다. shard 하나에는 여전히 파일 하나의 레코드만 들어가고, facts 인덱스는 읽은 파일을 웹 레인과 따로 tsFiles 맵에 적습니다. 파일을 넘나드는 판단은 매번 애플리케이션 전체를 놓고 다시 합니다. import가 어느 파일을 가리키는지, 필드 타입이 어느 클래스인지, 어떤 컨트롤러가 등록됐는지, 모듈이 타입에 어느 클래스를 묶는지가 그렇습니다. tsconfig paths, schema.prisma, 그리고 타입의 패키지가 배포될 수 있는지 알려 주는 package.json도 함께 다시 읽습니다. 그래서 캐시된 파일에 다른 파일에 대한 결론이 들어가지 않습니다.

커밋하지 않은 편집(작업 트리 오버레이)

커밋하지 않은 변경에 대한 cascade impact와 MCP changed_impact 도구는, 이 레인이 들어간 pack에도 Java pack과 똑같이 작업 트리 오버레이를 얹습니다. 오버레이는 analyze와 같은 방식으로 이 레인을 훑되, 팩트 캐시에는 아무것도 쓰지 않습니다.

아무것도 고치지 않은 트리에 얹으면 오버레이는 분석한 그래프를 digest까지 똑같이 만들고, 고친 트리에 얹으면 analyze가 그 트리로 만들 그래프를 만듭니다 (test/overlay_equivalence.test.mjs, test/overlay_ts.test.mjs). 이 레인과 웹 레인이 함께 읽은 함수는 여전히 노드 하나이고, lanes에 두 레인이 모두 적힙니다.

거절하는 경우와 알리는 것은 이렇습니다.

이 버전에 없는 것