import { describe, expectTypeOf, it } from "vitest"; import { type AvailableColumns, type ExtractColumnType, type LeftJoinedMarker, type ParseSelectObject, } from "./puri.types"; // ============================================================================ // 테스트용 Mock 스키마 // ============================================================================ type MockSchema = { users: { id: number; name: string; email: string; department_id: number | null; }; departments: { id: number; name: string; company_id: number; }; companies: { id: number; name: string; }; employees: { id: number; employee_number: string; salary: string | null; // nullable 필드 user_id: number; department_id: number | null; }; }; // ============================================================================ // ExtractColumnType 테스트 // ============================================================================ describe("ExtractColumnType", () => { describe("단일 테이블", () => { it("기본 컬럼 타입을 추출한다", () => { type Tables = { users: MockSchema["users"] }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); it("nullable 컬럼 타입을 추출한다", () => { type Tables = { users: MockSchema["users"] }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); it("단일 테이블에서는 테이블명 없이 컬럼명만으로 추출 가능하다", () => { type Tables = { users: MockSchema["users"] }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); }); describe("innerJoin된 테이블", () => { it("innerJoin 테이블의 컬럼은 non-null이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"]; // innerJoin }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); }); describe("leftJoin된 테이블", () => { it("leftJoin 테이블의 컬럼은 nullable이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"] & LeftJoinedMarker; // leftJoin }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); it("leftJoin 테이블의 원래 nullable 컬럼도 nullable이다", () => { type Tables = { users: MockSchema["users"]; employee: MockSchema["employees"] & LeftJoinedMarker; // leftJoin }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); }); describe("non-null FK로 leftJoin된 테이블", () => { it("non-null FK로 leftJoin된 테이블의 컬럼은 non-null이다 (마커 없음)", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"] & LeftJoinedMarker; // nullable FK company: MockSchema["companies"]; // non-null FK → 마커 없음 }; type Result = ExtractColumnType; const result = {} as Result; expectTypeOf(result).toEqualTypeOf(); }); }); }); // ============================================================================ // ParseSelectObject 테스트 // ============================================================================ describe("ParseSelectObject", () => { describe("단일 테이블 (flat select)", () => { it("기본 필드를 파싱한다", () => { type Tables = { users: MockSchema["users"] }; type Select = { id: "users.id"; name: "users.name"; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; name: string; }>(); }); it("nullable 필드를 파싱한다", () => { type Tables = { users: MockSchema["users"] }; type Select = { id: "users.id"; department_id: "users.department_id"; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; department_id: number | null; }>(); }); }); describe("innerJoin + flat select", () => { it("innerJoin 테이블의 필드는 non-null이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"]; // innerJoin }; type Select = { id: "users.id"; dept_id: "department.id"; dept_name: "department.name"; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; dept_id: number; dept_name: string; }>(); }); }); describe("leftJoin + flat select", () => { it("leftJoin 테이블의 필드는 nullable이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"] & LeftJoinedMarker; // leftJoin }; type Select = { id: "users.id"; dept_id: "department.id"; dept_name: "department.name"; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; dept_id: number | null; dept_name: string | null; }>(); }); }); // ============================================================================ // 핵심: 입체적 select 구조 // ============================================================================ describe("innerJoin + nested select (입체적 구조)", () => { it("innerJoin 테이블의 중첩 객체는 non-null이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"]; // innerJoin }; type Select = { id: "users.id"; department: { id: "department.id"; name: "department.name"; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; department: { id: number; name: string; }; }>(); }); }); describe("leftJoin + nested select (입체적 구조)", () => { it("leftJoin 테이블의 중첩 객체는 nullable이다 (필드는 non-null)", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"] & LeftJoinedMarker; // leftJoin }; type Select = { id: "users.id"; department: { id: "department.id"; name: "department.name"; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; department: { id: number; name: string; } | null; // 객체 단위로 nullable }>(); }); it("leftJoin 테이블 내의 원래 nullable 필드도 non-null이다", () => { type Tables = { users: MockSchema["users"]; employee: MockSchema["employees"] & LeftJoinedMarker; // leftJoin }; type Select = { id: "users.id"; employee: { id: "employee.id"; salary: "employee.salary"; // 스키마상 nullable이지만... }; }; type Result = ParseSelectObject; const result = {} as Result; // employee 객체가 null이 아닐 때만 접근하므로, salary의 원래 nullability 유지 expectTypeOf(result).toEqualTypeOf<{ id: number; employee: { id: number; salary: string | null; // 스키마의 원래 nullability 유지 } | null; }>(); }); }); describe("non-null FK leftJoin + nested select (입체적 구조)", () => { it("non-null FK로 leftJoin된 테이블의 중첩 객체는 non-null이다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"] & LeftJoinedMarker; // nullable FK → 마커 있음 department__company: MockSchema["companies"]; // non-null FK → 마커 없음 }; type Select = { id: "users.id"; department: { id: "department.id"; name: "department.name"; company: { name: "department__company.name"; }; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; department: { id: number; name: string; company: { name: string; }; // non-null! (non-null FK로 조인되어 마커 없음) } | null; }>(); }); it("깊은 중첩에서도 non-null FK leftJoin은 non-null이다", () => { type Tables = { employees: MockSchema["employees"]; user: MockSchema["users"]; // innerJoin → 마커 없음 user__employee: MockSchema["employees"] & LeftJoinedMarker; // nullable FK user__employee__department: MockSchema["departments"]; // non-null FK → 마커 없음 }; type Select = { id: "employees.id"; user: { id: "user.id"; employee: { id: "user__employee.id"; department: { id: "user__employee__department.id"; }; }; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; user: { // user는 innerJoin이므로 무조건 존재합니다. id: number; employee: { // user의 employee는 nullable FK leftJoin이므로 null일 수 있습니다. id: number; department: { // employee가 존재한다면 department는 non-null FK leftJoin이므로 무조건 존재합니다. id: number; }; // non-null FK → non-null } | null; // nullable FK leftJoin → nullable }; // innerJoin → non-null }>(); }); }); describe("복합 케이스", () => { it("innerJoin + nullable FK leftJoin + non-null FK leftJoin 조합", () => { type Tables = { employees: MockSchema["employees"]; user: MockSchema["users"]; // innerJoin (non-null FK) department: MockSchema["departments"] & LeftJoinedMarker; // nullable FK leftJoin department__company: MockSchema["companies"]; // non-null FK leftJoin → 마커 없음 }; type Select = { id: "employees.id"; employee_number: "employees.employee_number"; salary: "employees.salary"; user: { id: "user.id"; username: "user.name"; }; department: { id: "department.id"; name: "department.name"; company: { name: "department__company.name"; }; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; employee_number: string; salary: string | null; // 스키마상 nullable user: { id: number; username: string; }; // innerJoin → non-null department: { id: number; name: string; company: { name: string; }; // non-null FK → non-null } | null; // nullable FK leftJoin → nullable }>(); }); it("여러 leftJoin 관계", () => { type Tables = { employees: MockSchema["employees"]; department: MockSchema["departments"] & LeftJoinedMarker; manager: MockSchema["users"] & LeftJoinedMarker; }; type Select = { id: "employees.id"; department: { name: "department.name"; }; manager: { name: "manager.name"; }; }; type Result = ParseSelectObject; const result = {} as Result; expectTypeOf(result).toEqualTypeOf<{ id: number; department: { name: string } | null; manager: { name: string } | null; }>(); }); }); }); // ============================================================================ // AvailableColumns 테스트 // ============================================================================ describe("AvailableColumns", () => { it("단일 테이블에서 사용 가능한 컬럼을 추출한다", () => { type Tables = { users: MockSchema["users"] }; type Result = AvailableColumns; // "users.id" | "users.name" | "users.email" | "users.department_id" | "id" | "name" | "email" | "department_id" const valid1: Result = "users.id"; const valid2: Result = "id"; // 단일 테이블이면 테이블명 생략 가능 expectTypeOf(valid1).toExtend(); expectTypeOf(valid2).toExtend(); }); it("여러 테이블에서 사용 가능한 컬럼을 추출한다", () => { type Tables = { users: MockSchema["users"]; department: MockSchema["departments"]; }; type Result = AvailableColumns; const valid1: Result = "users.id"; const valid2: Result = "department.name"; expectTypeOf(valid1).toExtend(); expectTypeOf(valid2).toExtend(); }); });