Query Gateway
เส้นทางหลักที่หน้าเว็บใช้อ่าน/เขียนฐานข้อมูลบริษัทด้วย SQL ตรง ๆ: client ส่ง SQL ดิบ, backend ตรวจรูปแบบ แล้วรันกับ Company Database ใน token ภายใต้ transaction/timeout ที่ควบคุมไว้. ADR: ADR-0005 Query Gateway เริ่มแบบอ่านอย่างเดียว + สัญญา wire ใหม่, ADR-0006 /query/exec รับเฉพาะ DML + ตารางระบบต้อง level>=9 (ถูกแทนด้วย ADR-0011), ADR-0008 /query/batch รัน SELECT หลายตัวในคำขอเดียว, ADR-0009 /query/execBatch เขียนหลาย statement ใน transaction เดียว (atomic), ADR-0011 ล็อก SQL เขียนจาก browser: ตารางเงิน/สต๊อก/GL ผ่าน /api/process เท่านั้น.
Endpoint (ทุกตัวต้อง Authorization: Bearer — router.go:90-102)
| Endpoint | body | ทำอะไร |
|---|---|---|
POST /query/list |
{"sql"} |
SELECT/WITH เดียว คืนทุกแถว (ไม่เกิน 50,000) |
POST /query/limit |
{"sql","offset","limit","includeTotal"} |
แบ่งหน้า + นับรวมถ้า includeTotal → {"totalRecords","rows"} |
POST /query/batch |
{"sql":[...]} |
SELECT หลายคำสั่งใน read-only tx เดียว (ไม่เกิน 20) คืนอาร์เรย์ของ rows ต่อคำสั่ง |
POST /query/exec |
{"sql"} |
INSERT/UPDATE/DELETE เดียว (รองรับ RETURNING) → {"rowsAffected","rows"}; ต้องผ่านสิทธิ์เมนู |
POST /query/execBatch |
{"sql":[...]} |
DML หลายคำสั่งใน transaction เดียว (≤1000 คำสั่ง, ≤2MB) |
ชื่อฐานข้อมูลมาจาก claim databaseName ของ token เสมอ (ไม่รับจาก body) — handler.go:119,179,242 (JWT session token และการเพิกถอน (JWT + revocation store)).
ทำงานอย่างไร (อ่าน)
- handler ดึง claims แล้วเรียก use case (
handler.go:103). domainquery.NewStatement(domain/query/query.go:290) — ผ่านscanSQL(ตัวสแกน lexer ที่ข้าม string literal,"identifier", dollar-quote, คอมเมนต์; ตัด;ท้ายได้ 1 ตัว; ปฏิเสธ quote ไม่ปิดหรือมีคำสั่งหลัง;) แล้วต้องขึ้นต้นด้วยSELECTหรือWITH, ห้ามมีINSERT|UPDATE|DELETE(dmlKeyword),CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|COPY|MERGE(ddlOrAdminKeyword), และINTO|pg_sleep|pg_read_file|pg_read_binary_file|pg_ls_dir|lo_import|lo_export|lo_get|lo_put|set_config|dblink|copy(forbiddenSelectPattern) (query.go:50-59). ผิด → 400 ข้อความErrNotSelectOnly.Limit:NewPaginationบีบoffset>=0,limitอยู่ใน (0, 1000] (query.go:454-466), ห่อเป็นSELECT * FROM (<sql>) AS _q OFFSET n LIMIT m(:468); นับรวมด้วยSELECT count(*) FROM (<sql>) AS _query_count(:318) — ไม่ต้องเขียน count เอง.dynamicsql.Gateway.Query(outbound/dynamicsql/gateway.go:92): เปิด tx แบบReadOnly+RepeatableReadใช้QueryExecModeSimpleProtocol, สแกนไม่เกินMaxListRows= 50,000 (เกิน → error), ค่าNUMERICแปลงเป็นสตริงทศนิยมตรงตัว (NormalizeValue,:26) กันเพี้ยนแบบ float (ตัวจัดการ connection pool ต่อฐานข้อมูล (PoolManager)).- ทุก use case ครอบด้วย timeout 30 วินาที (
service.go:16) และResolveConnection(provider)ก่อนเรียก gateway (ทะเบียน Provider แบบไฟล์ JSON (Provider registry)).
ทำงานอย่างไร (เขียน)
Exec(service.go:95):NewWriteStatementตรวจเป็น DML เดียว (หรือWITHที่มี DML) →ValidateUserLevel(ตารางsml_user_list,sml_database_list*,erp_optionต้อง level ≥ 9, ไม่งั้น 403) → ถ้าClientRequestต่อด้วยValidateClientPolicy(นโยบายล็อกการเขียนผ่าน Query Gateway (Query write policy / lockdown)) → รันด้วยpool.Query(ไม่มี tx ครอบ ต่างจาก batch;gateway.go:170-190).ExecBatch(service.go:137):NewWriteBatchStatements(≤1000 คำสั่งและรวม ≤ 2MB,query.go:75-81,397-426) → ตรวจนโยบายทีละคำสั่ง (ข้อความ error ระบุstatement i) →tx.Beginรันตามลำดับ ล้มคำสั่งใดก็ rollback ทั้งหมด (gateway.go:197-240).- ผู้เรียกภายใน (handler ของโมดูล) ใช้
ExecInput.AllowDDL=trueข้ามตัวตรวจ DML-only ทั้งหมด และClientRequest=falseข้ามนโยบายล็อก; เฉพาะquery/handler.go:245,299ที่ตั้งClientRequest: true(สถาปัตยกรรม Hexagonal และ composition root ของ Go backend (Hexagonal Architecture + DI)).
Logging / ข้อควรระวัง
- handler log SQL แบบ mask string literal (
maskSQLLiterals), hash SHA-256, คำกริยาแรก และตารางเป้าหมาย; ถ้าเกิน 1 วินาทีจะ log เป็น slow query (handler.go:28-82,135,200) — ตัว log ไม่เก็บค่าใน literal. - error จาก DB ทุกชนิดถูกห่อเป็น 400 พร้อมข้อความของ Postgres (รูปแบบ error และ response ของ API (apperr + response helper)).
/query/execผ่านpool.Query+ SimpleProtocol จึงไม่ parameterize — ความปลอดภัยพึ่งตัวตรวจ lexer/regex และสิทธิ์ ไม่ใช่ prepared statement; ห้ามประกอบ SQL จากข้อมูลผู้ใช้โดยไม่ escape (ดูsqllitใน เครื่องมือกลาง platform: docno, sqllit, search, security (Platform utilities))./query/batchอยู่นอกqueryWriteGuardเพราะอ่านอย่างเดียว (router.go:102).- เอกสาร ADR-0005 บอกว่ายังไม่มี exec — โค้ดปัจจุบันมี exec/execBatch แล้ว และชื่อ alias นับรวมคือ
_query_countให้ยึดโค้ด.
ที่มาในโค้ด
backend/internal/application/query/service.go:16,32-217— use case ทั้งหมด.backend/internal/domain/query/query.go:50-81,284-326,397-470— regex, limit/caps,Statement, batch, pagination.backend/internal/adapter/outbound/dynamicsql/gateway.go:26-234— pool, tx, สแกนแถว.backend/internal/adapter/inbound/http/query/handler.go:98-380— DTO และ handler.backend/internal/adapter/inbound/http/router.go:90-103— route.
ดูเพิ่ม
ภาพรวม Backend Go ของ SML Ai Account (Backend overview) · นโยบายล็อกการเขียนผ่าน Query Gateway (Query write policy / lockdown) · สิทธิ์เมนูและ middleware RequireRight (Menu rights / permission) · ตัวจัดการ connection pool ต่อฐานข้อมูล (PoolManager) · ADR-0005 Query Gateway เริ่มแบบอ่านอย่างเดียว + สัญญา wire ใหม่ · สารบัญ ADR ของ backend (Architecture Decision Records)