네이티브 SQL을 사용하여 Firebase SQL Connect 작업 구현

GraphQL 대신 SQL로 Firebase SQL Connect 작업을 작성하는 가이드입니다. page_type: guide announcement: > 네이티브 SQL은 기능 미리보기로 제공되므로 SLA 또는 지원 중단 정책이 적용되지 않으며 이전 버전과 호환되지 않는 방식으로 변경될 수 있습니다. 동적 SQL을 실행하는 저장 프로시저 또는 함수와 함께 이 기능을 사용하는 경우 이 페이지 하단에 설명된 보안 권장사항을 따르세요.

Firebase SQL ConnectCloud SQL 데이터베이스와 상호작용하는 여러 방법을 제공합니다.

  • 네이티브 GraphQL: schema.gql에서 유형을 정의하면 GraphQL 작업이 SQL로 변환됩니다.SQL Connect 이는 강력한 타이핑과 스키마 적용 구조를 제공하는 표준 접근 방식입니다. 이 페이지를 제외한 대부분의 SQL Connect 문서에서는 이 옵션을 설명합니다. 가능한 경우 이 메서드를 사용하여 완전한 유형 안전성과 도구 지원을 활용해야 합니다.
  • @view 지시어: 맞춤 SELECT SQL 문으로 지원되는 schema.gql의 GraphQL 유형을 정의합니다. 이는 복잡한 SQL 논리를 기반으로 강력한 유형의 읽기 전용 뷰를 만드는 데 유용합니다. 이러한 유형은 일반 유형처럼 쿼리할 수 있습니다. @view을 참고하세요.
  • 네이티브 SQL: 특수 루트 필드를 사용하여 .gql 파일의 명명된 작업에 SQL 문을 직접 삽입합니다. 이를 통해 특히 표준 GraphQL에서 지원되지 않는 작업, 데이터베이스별 기능 활용, PostgreSQL 확장 프로그램 사용 시 최대한의 유연성과 직접 제어가 가능합니다. GraphQL 및 @view 지시어와 달리 기본 SQL은 강력한 유형의 출력을 제공하지 않습니다.

이 가이드에서는 네이티브 SQL 옵션을 중점적으로 다룹니다.

네이티브 SQL의 일반적인 사용 사례

네이티브 GraphQL은 완전한 유형 안전성을 제공하고 @view 지시어는 읽기 전용 SQL 보고서에 강력한 유형의 결과를 제공하지만 네이티브 SQL은 다음을 위해 필요한 유연성을 제공합니다.

  • PostgreSQL 확장 프로그램: GraphQL 스키마에서 복잡한 유형을 매핑할 필요 없이 설치된 PostgreSQL 확장 프로그램 (예: 지리 공간 데이터용 PostGIS)을 직접 쿼리하고 사용할 수 있습니다.
  • 복잡한 쿼리: 조인, 하위 쿼리, 집계, 윈도우 함수, 저장 프로시저를 사용하여 복잡한 SQL을 실행합니다.
  • 데이터 조작 (DML): INSERT, UPDATE, DELETE 작업을 직접 실행합니다. (하지만 데이터 정의 언어 (DDL) 명령어에는 네이티브 SQL을 사용하지 마세요. 백엔드와 생성된 SDK를 동기화 상태로 유지하려면 GraphQL을 사용하여 스키마 수준 변경사항을 계속 적용해야 합니다.)
  • 데이터베이스별 기능: PostgreSQL에 고유한 함수, 연산자 또는 데이터 유형을 활용합니다.
  • 성능 최적화: 중요한 경로에 맞게 SQL 문을 수동으로 조정합니다.

기본 SQL 루트 필드

SQL로 작업을 작성하려면 query 또는 mutation 유형의 다음 루트 필드 중 하나를 사용하세요.

필드 query

필드 설명
_select

0개 이상의 행을 반환하는 SQL 쿼리를 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.
  • params: 자리표시자에 바인딩할 값의 정렬된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 배열 ([Any])

_selectFirst

0개 또는 1개의 행을 반환할 것으로 예상되는 SQL 쿼리를 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.
  • params: 자리표시자에 바인딩할 값의 정렬된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 객체 (Any) 또는 null

필드 mutation

필드 설명
_execute

DML 문 (INSERT, UPDATE, DELETE)을 실행합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요.

    이 필드는 행 수만 반환하므로 데이터 수정 공통 테이블 표현식 (예: WITH new_row AS (INSERT...))을 사용할 수 있습니다. _execute만 CTE를 지원합니다.

  • params: 자리표시자에 바인딩할 값의 정렬된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: Int (영향을 받는 행 수)을 반환합니다.

결과에서 RETURNING 절은 무시됩니다.

_executeReturning

RETURNING 절이 있는 DML 문을 실행하여 0개 이상의 행을 반환합니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요. 데이터 수정 공통 테이블 표현식은 지원되지 않습니다.
  • params: 자리표시자에 바인딩할 값의 정렬된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 배열 ([Any])

_executeReturningFirst

RETURNING 절이 있는 DML 문을 실행합니다. 0개 또는 1개의 행이 반환될 것으로 예상됩니다.

인수:

  • sql: SQL 문 문자열 리터럴입니다. SQL 삽입을 방지하려면 매개변수 값에 위치 자리표시자 ($1, $2 등)를 사용하세요. 데이터 수정 공통 테이블 표현식은 지원되지 않습니다.
  • params: 자리표시자에 바인딩할 값의 정렬된 목록입니다. 여기에는 리터럴, GraphQL 변수, {_expr: "auth.uid"} (인증된 사용자의 ID)와 같은 특수 서버 삽입 컨텍스트 맵이 포함될 수 있습니다.

반환: JSON 객체 (Any) 또는 null

참고:

  • 작업은 SQL Connect 서비스 계정에 부여된 권한을 사용하여 실행됩니다.

문법 규칙 및 제한사항

네이티브 SQL은 보안을 보장하고 SQL 삽입을 방지하기 위해 엄격한 파싱 규칙을 적용합니다. 다음 제약 조건에 유의하세요.

  • 주석: 블록 주석 (/* ... */)을 사용합니다. 쿼리 연결 중에 후속 절 (예: 보안 필터)이 잘릴 수 있으므로 줄 주석 (--)은 금지됩니다.
  • 매개변수: params 배열 순서와 일치하는 위치 매개변수 ($1, $2)를 사용합니다. 이름이 지정된 매개변수 ($id, :name)는 지원되지 않습니다.
  • 문자열: 확장 문자열 리터럴 (E'...') 및 달러로 인용된 문자열($$...$$)이 지원됩니다. PostgreSQL 유니코드 이스케이프 (U&'...')는 지원되지 않습니다.

댓글의 매개변수

파서는 블록 주석 내의 모든 항목을 무시합니다. 매개변수 (예: /* WHERE id = $1 */)가 포함된 행을 주석 처리하는 경우 params 목록에서도 해당 매개변수를 삭제해야 합니다. 그렇지 않으면 unused parameter: $1 오류와 함께 작업이 실패합니다.

이름 지정 규칙

네이티브 SQL을 작성할 때는 PostgreSQL 데이터베이스와 직접 상호작용하므로 테이블과 열에 실제 데이터베이스 이름을 사용해야 합니다. 기본적으로 SQL Connect은 GraphQL 스키마의 이름을 @table(name)@col(name) 지시어를 사용하여 PostgreSQL 식별자를 명시적으로 맞춤설정하지 않는 한 데이터베이스의 스네이크 케이스에 자동으로 매핑합니다.

지시문 없이 유형을 정의하면 GraphQL 테이블 및 필드 이름이 기본 snake_case PostgreSQL 식별자에 매핑됩니다.

schema.gql queries.gql
type UserProfile {
  userId: ID!
  displayName: String
}
query GetUserProfileDefault($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT user_id, display_name
      FROM user_profile
      WHERE user_id = $1
    """,
    params: [$id]
  )
}

PostgreSQL 식별자는 기본적으로 대소문자를 구분하지 않습니다. @table 또는 @col와 같은 지시어를 사용하여 대문자 또는 혼합 대소문자가 포함된 이름을 지정하는 경우 SQL 문에서 해당 식별자를 큰따옴표로 묶어야 합니다.

다음 예에서는 테이블 이름에 "UserProfiles"을 사용하고 userId 열에 "profileId"을 사용해야 합니다. displayName 필드는 display_name으로의 기본 변환을 따릅니다.

schema.gql queries.gql
type UserProfileCustom @table(name: "UserProfiles") {
  userId: ID! @col(name: "profileId")
  displayName: String
}
query GetUserProfileCustom($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT "profileId", display_name
      FROM "UserProfiles"
      WHERE "profileId" = $1
    """,
    params: [$id]
  )
}

사용 예

예 1: 필드 별칭이 있는 기본 SELECT

루트 필드 (예: movies: _select)에 별칭을 지정하여 클라이언트 응답을 더 깔끔하게 만들 수 있습니다 (data._select 대신 data.movies).

queries.gql:

query GetMoviesByGenre($genre: String!, $limit: Int!) @auth(level: PUBLIC) {
  movies: _select(
    sql: """
      SELECT id, title, release_year, rating
      FROM movie
      WHERE genre = $1
      ORDER BY release_year DESC
      LIMIT $2
    """,
    params: [$genre, $limit]
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.movies에 표시됩니다.

예 2: 기본 UPDATE

mutations.gql:

mutation UpdateMovieRating(
  $movieId: UUID!,
  $newRating: Float!
) @auth(level: NO_ACCESS) {
  _execute(
    sql: """
      UPDATE movie
      SET rating = $2
      WHERE id = $1
    """,
    params: [$movieId, $newRating]
  )
}

클라이언트 SDK를 사용하여 변형을 실행하면 영향을 받는 행 수가 data._execute에 표시됩니다.

예 3: 기본 집계

queries.gql:

query GetTotalReviewCount @auth(level: PUBLIC) {
  stats: _selectFirst(
    sql: "SELECT COUNT(*) as total_reviews FROM \"Reviews\""
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.stats.total_reviews에 표시됩니다.

예 4: RANK를 사용한 고급 집계

queries.gql:

query GetMoviesRankedByRating @auth(level: PUBLIC) {
  _select(
    sql: """
      SELECT
        id,
        title,
        rating,
        RANK() OVER (ORDER BY rating DESC) as rank
      FROM movie
      WHERE rating IS NOT NULL
      LIMIT 20
    """,
    params: []
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data._select에 표시됩니다.

예 5: RETURNING 및 인증 컨텍스트를 사용한 UPDATE

mutations.gql:

mutation UpdateMyReviewText(
  $movieId: UUID!,
  $newText: String!
) @auth(level: USER) {
  updatedReview: _executeReturningFirst(
    sql: """
      UPDATE "Reviews"
      SET review_text = $2
      WHERE movie_id = $1 AND user_id = $3
      RETURNING movie_id, user_id, rating, review_text
    """,
    params: [$movieId, $newText, {_expr: "auth.uid"}]
  )
}

클라이언트 SDK를 사용하여 변형을 실행하면 업데이트된 게시물 데이터가 data.updatedReview에 있습니다.

예 6: 업데이트 삽입 (원자적 가져오기 또는 만들기)이 있는 고급 CTE

이 패턴은 단일 데이터베이스 트랜잭션에서 하위 레코드 (예: 리뷰)를 삽입하기 전에 종속 레코드 (예: 사용자 또는 영화)가 있는지 확인하는 데 유용합니다.

mutations.gql:

mutation CreateMovieCTE($movieId: UUID!, $userId: UUID!, $reviewId: UUID!) @auth(level: USER) {
  _execute(
    sql: """
      WITH
      new_user AS (
        INSERT INTO "user" (id, username)
        VALUES ($2, 'Auto-Generated User')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      ),
      movie AS (
        INSERT INTO movie (id, title, image_url, release_year, genre)
        VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
        ON CONFLICT (id) DO NOTHING
        RETURNING id
      )
      INSERT INTO "Reviews" (id, movie_id, user_id, rating, review_text, review_date)
      VALUES (
        $3,
        $1,
        $2,
        5,
        'Good!',
        NOW()
      )
    """,
    params: [$movieId, $userId, $reviewId]
  )
}

_executeReturning_executeReturningFirst은 출력을 JSON으로 포맷하기 위해 상위 CTE로 쿼리를 래핑합니다. PostgreSQL에서는 데이터 수정 CTE를 다른 데이터 수정 문 내에 중첩할 수 없으므로 쿼리가 실패합니다.

예 7: PostgreSQL 확장 프로그램 사용

기본 SQL을 사용하면 복잡한 도형 유형을 GraphQL 스키마에 매핑하거나 기본 테이블을 변경하지 않고도 PostGIS와 같은 PostgreSQL 확장 프로그램을 사용할 수 있습니다.

이 예에서는 레스토랑 앱에 메타데이터 JSON 열 (예: {"latitude": 37.3688, "longitude": -122.0363})에 위치 데이터를 저장하는 테이블이 있다고 가정합니다. PostGIS 확장 프로그램을 사용 설정한 경우 표준 PostgreSQL JSON 연산자 (->>)를 사용하여 이러한 값을 즉석에서 추출하고 PostGIS ST_MakePoint 함수에 전달할 수 있습니다.

query GetNearbyActiveRestaurants(
  $userLong: Float!,
  $userLat: Float!,
  $maxDistanceMeters: Float!
) @auth(level: USER) {
  nearby: _select(
    sql: """
      SELECT
        id,
        name,
        tags,
        ST_Distance(
          ST_MakePoint(
            (metadata->>'longitude')::float,
            (metadata->>'latitude')::float
          )::geography,
          ST_MakePoint($1, $2)::geography
        ) as distance_meters
      FROM restaurant
      WHERE active = true
        AND metadata ? 'longitude' AND metadata ? 'latitude'
        AND ST_DWithin(
          ST_MakePoint(
            (metadata->>'longitude')::float,
            (metadata->>'latitude')::float
          )::geography,
          ST_MakePoint($1, $2)::geography,
          $3
        )
      ORDER BY distance_meters ASC
      LIMIT 10
    """,
    params: [$userLong, $userLat, $maxDistanceMeters]
  )
}

클라이언트 SDK를 사용하여 쿼리를 실행하면 결과가 data.nearby에 표시됩니다.

보안 권장사항: 동적 SQL 및 저장 프로시저

SQL Connect는 GraphQL-데이터베이스 경계에서 모든 입력을 안전하게 매개변수화하여 1차 SQL 삽입으로부터 표준 SQL 쿼리를 완전히 보호합니다. 하지만 SQL을 사용하여 동적 SQL을 실행하는 맞춤 PostgreSQL 저장 프로시저 또는 함수를 호출하는 경우 내부 PL/pgSQL 코드가 이러한 매개변수를 안전하게 처리해야 합니다.

저장 프로시저가 사용자 입력을 EXECUTE 문자열에 직접 연결하면 매개변수화를 우회하고 2차 SQL 삽입 취약점이 생성됩니다.

-- INSECURE: Do not concatenate parameters into dynamic strings!
CREATE OR REPLACE PROCEDURE unsafe_update(user_input TEXT)
LANGUAGE plpgsql AS $$
BEGIN
    -- A malicious user_input (e.g., "val'; DROP TABLE users; --")
    -- will execute as code.
    EXECUTE 'UPDATE target_table SET status = ''' || user_input || '''';
END;
$$;

이를 방지하려면 다음 권장사항을 따르세요.

  • USING 절 사용: 저장 프로시저에서 동적 SQL을 작성할 때는 항상 USING 절을 사용하여 데이터 매개변수를 안전하게 바인딩하세요.
  • 식별자에 format() 사용: 안전한 데이터베이스 식별자 삽입 (예: 테이블 이름)을 위해 %I 플래그와 함께 format()을 사용합니다.
  • 식별자 엄격히 허용: 클라이언트 애플리케이션이 데이터베이스 식별자를 임의로 선택하도록 허용하지 마세요. 프로시저에 동적 식별자가 필요한 경우 실행 전에 PL/pgSQL 로직 내에서 하드 코딩된 허용 목록에 대해 입력을 검증합니다.
-- SECURE: Use format() for identifiers and USING for data values
CREATE OR REPLACE PROCEDURE secure_update(
    target_table TEXT, new_value TEXT, row_id INT
)
LANGUAGE plpgsql AS $$
BEGIN
    -- Validate the dynamic table name against an allowlist
    IF target_table NOT IN ('orders', 'users', 'inventory') THEN
        RAISE EXCEPTION 'Invalid table name';
    END IF;

    -- Execute securely
    EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
    USING new_value, row_id;
END;
$$;