ES /docs

ActiveRecord::StatementInvalid: Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds t

RCA: Mysql2::Error SQL syntax error (Compass Text-to-SQL {facility_id}} placeholder)

Overview#

What Happened#

cupixworks-mysql2 (= tesla 의 mysql2 APM adapter span, 실제 앱은 tesla cupixworks-api) 에서 Mysql2::Error: You have an error in your SQL syntax 가 집계되었다. 클러스터의 Representative Error 는 facility_repository.rbdefault_joins SELECT 문(facilities.*, workspaces.name AS workspace_name, teams.qa_preference AS team_qa_preference)을 가리키지만, 이 변형은 최근 14일 창에서 0건이며 STALE 하다. last_seen 시점의 실제 발생은 전혀 다른 경로 — Compass Text-to-SQL 기능(POST /api/v1/mcp/search, Api::V1::CompassSearchController#search)에서 Gemini LLM 이 생성한 SQL 에 치환되지 않은 템플릿 placeholder {facility_id}} 가 남아 MySQL EXPLAIN 파싱이 실패한 케이스다. 이 실패는 이미 Cupix::Errors::Parameter (ARG20006) → HTTP 400 으로 정상 매핑된다.

Quick Facts#

Field Value
exception.class Cupix::Errors::Parameter (wrapping ActiveRecord::StatementInvalid / Mysql2::Error)
exception.message SQL validation failed: Mysql2::Error: You have an error in your SQL syntax; ... near '{facility_id}} AND f.trashed_at IS NULL AND (f.id IN (SELECT facility_id FROM ' at line 8
error.code ARG20006 (develop tree 상 동형 코드 = ARG20003, query_executor.rb:62)
top_frame app/services/cupix/compass/db_search/query_executor.rb:59 (explain_validate!EXPLAIN)
endpoint POST /api/v1/mcp/search (Api::V1::CompassSearchController#search)
http.status 400
deploy production-us-west-2-20260723t0838z0-7d0d9d21-cupixworks
env dev + production (us-west-2)

Affected Teams#

Team / Domain Error Count (14d) Impact
cupix (internal) 29 Compass Text-to-SQL 검색 요청이 400 반환 (기능 개발/테스트 트래픽)
updatedemo 7 동일 400
nexus 4 동일 400
built 3 동일 400
secc / admin 2 동일 400

env 분포: dev 30 / production 15. 모두 Api::V1::CompassSearchController#search, 모두 HTTP 400.

Timeline#

  1. 2026-03-18 19:17 KST — 클러스터 first_seen (ET 가 SQL syntax error 이슈로 최초 그룹화, 당시 Representative = facility_repository.rb default_joins 변형).
  2. 2026-07-24 13:47 KST — 최근 14일 창 내 Compass {facility_id}} placeholder 400 최초 관측 (production, team secc).
  3. 2026-08-06 05:54 KST — 클러스터 last_seen. 동일 시각(20:54:34Z) Compass {facility_id}} 400 로그 확인.
  4. 2026-08-06 — RCA 수행. Representative(default_joins) 변형은 14일 창 0건으로 STALE 확인.

Error Log#

Datadog Logs

text
Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '*,
      workspaces.name AS workspace_name,
      teams.qa_preference AS team_qa' at line 2

주의: 위 Representative Error 는 STALE 하다. 실제 최근 발생 메시지는 아래 Log Evidence 의 {facility_id}} 변형이다.

Impact#

  • Service: cupixworks-mysql2 (APM adapter span; 실제 앱 = tesla cupixworks-api)
  • 발생 횟수: 11 (ET 집계); 최근 14일 실측 SQL syntax error = 45건, 전부 Compass 경로
  • 최초 발생: 2026-03-18 19:17 KST
  • 최근 발생: 2026-08-06 05:54 KST

Root Cause Summary#

Revision 2 note (release cadence): 담당자에 따르면 이 placeholder 미치환 문제에 대한 수정버전이 이미 존재하며 현재 QA 까지만 배포된 상태이고, prod 로도 올릴 예정이다. 즉 최근 창의 production 발생분(env dist 15/45)은 코드 결함이 아니라 fix 가 아직 prod 에 미배포된 release-pending 상황으로 설명된다. RCA 조사 시점의 tesla develop/master 브랜치에는 placeholder guard 커밋이 확인되지 않으므로(아래 Revision History Revision 2 근거 참조), 해당 수정버전은 내가 접근 가능한 브랜치에 아직 병합되지 않은 변경으로 판단한다. 어느 쪽이든 근본 원인 판정(placeholder → EXPLAIN 실패 → 400, noise)과 무관하다.

Revision 3 note (prod 배포 완료): 담당자가 "Prod 배포 완료" 를 회신했다 (Slack ts 1785994882, 2026-08-06 06:21 UTC). 로그 실측상 production 의 {facility_id}} placeholder 400 은 2026-08-05T21:50:37Z (deploy production-us-west-2-20260805t1546z0-c1c2bbd4-cupixworks) 를 마지막으로 배포 완료 회신 시점(2026-08-06 06:21 UTC) 이후 재발하지 않는다 — 배포로 prod 발생이 그친 것과 일관된다(단 관측 창이 ~8.5h 로 짧아 완전 소멸 단정은 유보). 다만 revision 2 와 동일하게, 배포되었다는 placeholder 수정 자체는 tesla develop/master 트리에서 여전히 확인되지 않는다 — 최근 병합된 compass 커밋(7d0d9d211 TSLA-13611 permission_filter.rb semi-join 재작성, TSLA-13592 source_table)은 모두 placeholder guard 와 무관하며 query_validator.rb·gemini_operation.rb 프롬프트에 placeholder 방어 코드는 0건이다. 근본 원인 판정(placeholder → EXPLAIN 실패 → 400, noise)은 불변.

Compass DB Search 는 자연어 질문을 Gemini LLM(gemini-3-flash-preview, temperature 0.0)으로 SQL 로 변환하는 실험적 Text-to-SQL 기능이다. LLM 이 간헐적으로 구체적인 값 대신 Python 스타일 템플릿 placeholder {facility_id} (여기에 stray 중괄호가 더해져 {facility_id}})를 SQL 에 그대로 출력한다. 이 SQL 은 QueryValidator/PermissionFilter 를 통과한 뒤 QueryExecutor.executeexplain_validate! (EXPLAIN #{sql}, query_executor.rb:59) 단계에서 MySQL 파서가 {facility_id}} 토큰을 이해하지 못해 Mysql2::Error: You have an error in your SQL syntax 를 raise 한다. 이는 rescue ActiveRecord::StatementInvalid 로 잡혀 Cupix::Errors::Parameter (ARG20006/ARG20003) → HTTP 400 으로 정상 매핑된다. 즉, 잘못된(placeholder 미치환) LLM SQL 이 실행 전 검증 계층에서 거부되는 정상 방어 동작이며, 서버 코드 결함·데이터 손상·crash 가 아니다. MAX_CORRECTION_ATTEMPTS=2 자동 교정 루프가 있으나 LLM 이 동일 placeholder 를 반복 생성해 결국 400 으로 표면화된다. 클러스터의 Representative(facility_repository.rb default_joins)는 별개 옛 변형으로 최근 창에서 0건이며 ET 가 두 변형을 한 이슈로 묶은 결과 STALE 하게 고정된 것이다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/compass_search_controller.rb:7-16
app/controllers/api/v1/compass_search_controller.rb:7-16ruby
def search
  question = params.require(:question)

  result = Cupix::Compass::DbSearch::Pipeline.run(
    question: question,
    user_id: @current_user.id
  )

  render json: { result: result }
end
  • Text-to-SQL 생성 + 자동 교정 루프: app/services/cupix/compass/db_search/pipeline.rb:64-96. Gemini 가 SQL 을 생성하고, 검증 실패 시 error_context 를 넣어 MAX_CORRECTION_ATTEMPTS(=2)까지 재시도한다.
app/services/cupix/compass/db_search/pipeline.rb:81-94ruby
begin
  validated_sql = Cupix::Compass::DbSearch::QueryValidator.validate!(generated_sql)
  filtered_sql = user_id ? Cupix::Compass::DbSearch::PermissionFilter.apply(validated_sql, user_id) : validated_sql
  return { sql: generated_sql, filtered_sql: filtered_sql, token_usage: gemini_result[:token_usage] }
rescue Cupix::Errors::Parameter, Cupix::Errors::InvalidState => e
  attempts += 1
  raise if attempts >= MAX_CORRECTION_ATTEMPTS
  # ... error_context = e.reason 후 재생성
end
  • LLM 이 raw SQL 을 그대로 반환 (gemini_operation.rb:129-150 extract_sql). 프롬프트는 "Return ONLY the raw SQL" 를 요구하지만 값 치환은 강제하지 못한다 — placeholder {facility_id} 가 텍스트로 새어 나올 수 있다.

  • Failure point: app/services/cupix/compass/db_search/query_executor.rb:58-65EXPLAIN #{sql}{facility_id}} 때문에 MySQL 파싱 실패.

app/services/cupix/compass/db_search/query_executor.rb:58-65ruby
def explain_validate!(connection, sql)
  connection.exec_query("EXPLAIN #{sql}")
rescue ActiveRecord::StatementInvalid => e
  raise Cupix::Errors::Parameter.new(
    code: 'ARG20003',
    reason: "SQL validation failed: #{e.message}"
  )
end
  • Status-code 매핑: Cupix::Errors::Parameterclient_400_error → HTTP 400.
app/controllers/concerns/client_error_controller.rb:10-14ruby
rescue_from Cupix::Errors::Parameter,
            # ...
            Cupix::Errors::Siteinsights, with: :client_400_error
  • 기대 동작 vs 실제 동작: 기대 — LLM 이 항상 실행 가능한 완전한 SELECT SQL 을 생성. 실제 — LLM 이 값을 치환하지 않은 채 {facility_id}} 템플릿을 출력 → EXPLAIN 실패 → 400. 앱은 이를 실행 전에 안전하게 거부하므로 DB 에 잘못된 쿼리가 실행되지 않는다.

  • error.code=ARG20006 (prod) vs ARG20003 (develop tree): 조사 시점의 소스 트리(develop, 790e093bb)는 query_executor.rb:62 에서 ARG20003 을 쓴다. production 배포(20260723t0838z0)는 ARG20006 을 반환 — deploy skew(코드 넘버 표기 차이)이며 근본 원인(placeholder 미치환 → 400)은 동일하다. (uncertain — 정확한 코드 넘버 정의 소스는 배포 커밋에 따라 다를 수 있으나 root cause 판단에는 영향 없음.)

Log Evidence#

Datadog 쿼리 (실측):

text
service:cupixworks-api "You have an error in your SQL syntax"

14일 창(2026-07-22 ~ 2026-08-05) 결과 45건, 100% Api::V1::CompassSearchController#search, 100% HTTP 400, 100% error.code=ARG20006. 대표 로그 원문:

json
{
  "message": "[400] POST /api/v1/mcp/search (Api::V1::CompassSearchController#search)",
  "status": "info",
  "error": {
    "reason": "SQL validation failed: Mysql2::Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '{facility_id}}\n  AND f.trashed_at IS NULL AND (f.id IN (SELECT facility_id FROM ' at line 8",
    "code": "ARG20006",
    "class": "Cupix::Errors::Parameter"
  },
  "controller": "Api::V1::CompassSearchController",
  "action": "search",
  "user_agent": "python-requests/2.34.2",
  "team": { "domain": "secc", "id": 285 },
  "params": { "facility_key": "l96bq9" },
  "environment": "production"
}

near '<fragment> 히스토그램 (45건):

text
37x  {facility_id}}
 8x  {facility_id}} LIMIT 500' at line 6

STALE 확인 — 옛 Representative 변형(facility_repository.rb default_joins) 쿼리:

text
service:cupixworks-api "workspaces.name AS workspace_name" "teams.qa_preference"

14일 창 결과 = 0건. 즉 Representative 는 더 이상 발생하지 않는 STALE 샘플이다.

status: 45/45 [400], status:error 로그는 없음 (400 은 status:info 요청 로그로 기록됨).

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Representative 의 facility_repository.rb default_joins SELECT 문이 malformed SQL 을 만든다 ET Representative 문자열이 facility_repository.rb:274-296 의 SELECT 와 정확히 일치 14일 창 실측 0건 ("workspaces.name AS workspace_name" "teams.qa_preference" = 0); default_joins SELECT 는 문법상 올바름 Rejected (STALE representative)
H2 Compass Text-to-SQL 에서 LLM 이 치환 안 된 placeholder {facility_id}} 를 SQL 에 남겨 EXPLAIN 파싱 실패 → 400 45/45 로그가 CompassSearchController#search, near '{facility_id}}', Cupix::Errors::Parameter ARG20006, HTTP 400; 코드경로 query_executor.rb:59 explain_validate! 확인 Confirmed
H3 SQL injection / 악의적 입력으로 인한 실행 위험 UA python-requests, 외부 IP placeholder 는 EXPLAIN 단계에서 파싱 실패로 즉시 거부되어 실제 실행 안 됨; PermissionFilter 가 user_id 기반 EXISTS 절 주입; 값도 connection.quote 처리 Rejected (실행 전 차단)
H4 서버측 코드 버그로 인한 500 unhandled ActiveRecord::StatementInvalid 발생 rescue ActiveRecord::StatementInvalidCupix::Errors::Parameterclient_400_error = 400 정상 매핑, 45/45 가 400 Rejected

Fix Recommendation#

본 클러스터는 noise 로, tesla 코드 결함이 아니다. 아래는 선택적(feature 품질 개선) 방향이며 자동 code-fix 대상이 아니다.

즉시 조치 (Critical)#

  • 없음. {facility_id}} 미치환 SQL 은 실행 전 EXPLAIN 검증에서 거부되어 400 으로 매핑된다 — 정상 방어 동작.
  • (운영, Revision 2) 담당자가 준비한 placeholder 수정버전을 QA 검증 후 prod 로 배포한다. 배포 완료 시 최근 창 production 발생분(15/45)이 소멸할 것으로 예상되며, 이후 service:cupixworks-api "check the manual that corresponds" "{facility_id}" environment:production 로 재발 여부를 확인한다. prod 발생이 사라지면 ET 이슈를 resolve 처리한다.
  • (운영, Revision 3 — 완료) 담당자가 "Prod 배포 완료" 회신 (2026-08-06 06:21 UTC). 실측상 production {facility_id}} 400 은 2026-08-05T21:50:37Z 이후 재발 없음 → 예상대로 소멸 시작. 후속: 배포 후 24~48h 동안 service:cupixworks-api "check the manual that corresponds" "{facility_id}" environment:production 를 재확인해 0건 지속 시 ET 이슈를 resolve 처리한다 (현재 관측 창이 ~8.5h 로 짧아 최종 확인 필요).

단기 개선 (1주 이내)#

  • (feature 품질, tesla) Cupix::Compass::DbSearch::GeminiOperation.build_prompt (gemini_operation.rb:61-117) 프롬프트에 "Do NOT output template placeholders such as {facility_id}; substitute concrete literal values or omit the predicate" 규칙을 추가해 LLM 이 placeholder 를 남기지 않도록 유도.
  • (feature 품질, tesla) QueryValidator (query_validator.rb) 에 정규식 기반 pre-check(/\{\w+\}/ 등 template placeholder 탐지) 를 추가해 EXPLAIN 이전에 명시적 Cupix::Errors::Parameter 로 조기 반환 — 그러면 자동 교정 루프(pipeline.rb:81-94)가 더 구체적인 error_context 를 받아 재생성 성공률이 오를 수 있다.

장기 개선 (재발 방지)#

  • Compass Text-to-SQL 는 실험적 기능(내부 cupix tenant 29/45, dev 30/45 위주). LLM SQL 생성 신뢰도 지표(생성→검증 성공률)를 별도 대시보드로 추적하고, 사용자에게 노출되는 400 은 "질문을 이해하지 못했습니다" 형태의 friendly 메시지로 매핑. ET 상 이 400 계열은 서버 알람에서 제외(4xx client-side)하도록 필터링.

Monitoring#

Compass Text-to-SQL 검증 실패(400) 추이:

text
service:cupixworks-api "You have an error in your SQL syntax" status:info

Compass 엔드포인트 전체 4xx 볼륨 (검증 실패 비율 파악용):

text
service:cupixworks-api @controller:Api::V1::CompassSearchController @http.status_code:400

placeholder 미치환 케이스 격리:

text
service:cupixworks-api "check the manual that corresponds" "{facility_id}"

Risk Assessment#

  • Risk level: low (client-side 400, 실행 전 차단, 사용자 데이터 영향 0)
  • 예상 복잡도: trivial (코드 변경 불필요; 선택적 프롬프트/validator 개선은 standard)
  • Verdict: noise

Revision History#

Revision 1#

Feedback: "감사합니다." (감사 인사, 개정 요구 사항 없음)

판정:

피드백 항목 판정 근거
"감사합니다" — 단순 감사 인사 (correction/reanalysis/deep-investigation/scope-change/detail-request 어디에도 해당 없음) 거부 (해당 없음) 피드백에 사실 정정·재분석·추가 조사·범위 변경·상세화 요청이 전혀 포함되지 않은 순수 acknowledgment 로 해석. 기존 보고서의 핵심 판정(H2 Confirmed, verdict noise)을 반박하거나 수정할 만한 새 증거·질문이 없어 본문 변경 근거가 성립하지 않음. Root Cause Summary 의 query_executor.rb:59 explain_validate!Cupix::Errors::Parameterclient_400_error (client_error_controller.rb:10-14) → HTTP 400 매핑 및 Log Evidence 45/45 = CompassSearchController#search / HTTP 400 / ARG20006 결론은 유지.

변경 사항:

  • 본문 섹션 변경 없음. 피드백이 개정을 요구하지 않는 감사 인사이며, 기존 분석·판정·권장 사항을 반박할 코드/로그 증거가 제시되지 않았다.

추가 조사 내용:

  • 없음. deep-investigation 이 필요한 항목이 없어 신규 레포/로그 탐색을 수행하지 않았다.

Revision 2#

Feedback: "수정버전을 QA까지만 배포해서 그런것 같은데, prod 까지 올려보겠습니다." — placeholder 문제의 수정버전이 QA 까지만 배포되어 있고, prod 까지 배포 예정이라는 운영 컨텍스트 제시. (유형: reanalysis / correction — "코드 결함 없음(noise)" 이라는 기존 판정을, "수정버전은 이미 존재하고 prod 미배포일 뿐"이라는 관점으로 재검토 요청)

판정:

피드백 항목 판정 근거
최근 창 production 발생은 수정버전이 QA 까지만 배포되어(prod 미배포) 생긴 release-pending 상황이다 부분 수용 발생 메커니즘(placeholder → EXPLAIN 실패 → HTTP 400)과 env dist(dev 30 / prod 15)는 기존 Log Evidence 와 일치하며, "prod 코드가 아직 fix 를 받지 못해 계속 발생"이라는 해석은 데이터와 모순되지 않아 운영 컨텍스트로 수용. 단, tesla /home/ec2-user/repos/tesla 조사 결과 해당 수정버전을 내가 접근 가능한 브랜치에서 확인하지 못함 → 특정 commit 을 근거로 검증할 수 없어 "부분" 판정. (1) 현재 develop/master 트리의 query_validator.rb·gemini_operation.rb 에 placeholder guard 없음 (`grep -rn 'placeholder
이 이슈는 코드 결함(bug)으로 재분류되어야 하는가 거부 피드백은 재분류를 요구하지 않으며, 근본 원인은 여전히 LLM 이 placeholder 를 남긴 SQL 을 실행 전 EXPLAIN 검증(query_executor.rb:59 explain_validate!)이 거부해 Cupix::Errors::Parameterclient_400_error (client_error_controller.rb:10-14) → HTTP 400 으로 정상 매핑되는 방어 동작이다. 수정버전은 이 400 을 줄이는 feature 품질 개선(기존 "단기 개선" 권장의 prompt/validator guard 와 동형)이지, crash/data 손상을 막는 bug fix 가 아니다. verdict 는 noise 유지.

변경 사항:

  • Root Cause Summary 상단에 Revision 2 운영 노트 추가: 수정버전이 QA 까지만 배포된 release-pending 상황이며, develop/master 에는 해당 커밋이 확인되지 않는다는 점을 명시. 근본 원인·verdict(noise)는 불변.
  • Fix Recommendation "즉시 조치"에 운영 항목 추가: QA 검증 후 prod 배포 + 배포 후 environment:production 필터로 재발 확인 + 소멸 시 ET resolve.

추가 조사 내용:

  • tesla 레포(/home/ec2-user/repos/tesla) 재조사: git fetch 후 develop(HEAD 790e093bb) 기준으로 compass db_search 커밋 히스토리, origin/master..origin/develop diff, placeholder guard grep, stash/branch 확인 수행. 결론 — 접근 가능한 브랜치에 placeholder 수정버전 부재 (사용자 언급 수정버전은 미병합/미공개 변경으로 추정).

Revision 3#

Feedback: "Prod 배포 완료" + Slack 링크 (ts 1785994882, 2026-08-06 06:21 UTC). — Revision 2 에서 언급된 "prod 배포 예정"이 완료되었다는 운영 상태 업데이트. (유형: correction / scope-change — release-pending 상황이 해소되었음을 확인하고, 그에 따라 "즉시 조치" 운영 항목의 상태를 갱신)

판정:

피드백 항목 판정 근거
placeholder 수정버전의 prod 배포가 완료되었다 부분 수용 (로그 실측) service:cupixworks-api "check the manual that corresponds" "{facility_id}" 14일 창 46건 중 production 발생을 일자·deploy 버전별로 분해한 결과, production 마지막 발생 = 2026-08-05T21:50:37Z, deploy production-us-west-2-20260805t1546z0-c1c2bbd4-cupixworks. 배포 완료 회신 시각(2026-08-06 06:21 UTC) 이후 production {facility_id}} 400 은 0건 → 배포로 prod 발생이 그친 것과 일관돼 운영 상태를 수용. 단 (1) 배포 후 관측 창이 ~8.5h 로 짧아 완전 소멸을 단정할 수 없고, (2) revision 2 와 동일하게 배포되었다는 placeholder 수정 커밋을 tesla 접근 브랜치에서 여전히 확인 불가 → "부분". git fetchgit log --all --since=2026-07-20 -- app/services/cupix/compass/db_search/ 결과 신규 compass 커밋은 7d0d9d211(TSLA-13611 permission_filter.rb semi-join 재작성, develop+master), TSLA-13592(source_table) 뿐이고 placeholder guard 아님. `grep -rniE 'placeholder
이 배포로 이슈를 종결(bug 로 재분류)해야 하는가 거부 배포는 최근 창 400 을 줄이는 feature 품질 개선(Revision 2 판정과 동형)이지, crash/data 손상을 막는 bug fix 가 아니다. 근본 원인은 여전히 LLM placeholder → query_executor.rb:59 explain_validate! EXPLAIN 거부 → Cupix::Errors::Parameterclient_error_controller.rb:10-14 client_400_error → HTTP 400 정상 매핑이다. verdict noise 유지.

변경 사항:

  • Root Cause Summary 상단에 Revision 3 운영 노트 추가: prod 배포 완료 회신, production {facility_id}} 400 이 2026-08-05T21:50:37Z 이후 재발 없음(관측 창 ~8.5h 유보), 배포된 placeholder 수정이 tesla develop/master 에 여전히 미확인이라는 점 명시. 근본 원인·verdict(noise) 불변.
  • Fix Recommendation "즉시 조치"에 Revision 3 완료 항목 추가: 배포 완료 확인 + 24~48h 재확인 후 0건 지속 시 ET resolve.

추가 조사 내용:

  • Datadog 로그 재조사: service:cupixworks-api "check the manual that corresponds" "{facility_id}"... environment:production 을 일자·deploy 버전 태그(version:...)별로 분해 → production 마지막 발생 시각·deploy 버전 확인, 배포 완료 회신 이후 prod 0건 확인.
  • tesla 레포 재조사: git fetch 후 compass db_search 신규 커밋(7d0d9d211 외) 확인, 각 커밋의 branch containment(develop+master) 및 변경 파일(permission_filter.rb/permission_filter_spec.rb) 확인, placeholder guard grep 재실행(0 hits) → 배포된 placeholder 수정은 접근 가능한 브랜치에 여전히 미병합/미공개로 판단.