แปลง 4GL → SQL Query
แปลง Logic ของโปรแกรมเป็น SQL เพื่อดึงข้อมูลไปพัฒนา API
แปลง 4GL → SQL
แปลง SELECT ของ 4GL เป็น Standard SQL ที่ใช้ได้ทุก Database
สร้าง View
รวม Query ทั้งหมดเป็น View เพื่อใช้เป็น Data Source
ต่อ API
ดึงจาก View ด้วย REST API (Python, Node.js, etc.)
ดึงข้อมูล COA Header + Business Partner
รวมข้อมูลหัว COA, ลูกค้า, ที่อยู่ ใน Query เดียว
select sccoa003.*
from sccoa003
where sccoa003.coan >= {:coan.f}
and sccoa003.coan <= {:coan.t}
and sccoa003.ofbp >= {:bpid.f}
and sccoa003.ofbp <= {:bpid.t}
selectdo
| ดึง BP แยก
select tccom100.*
from tccom100
where tccom100.bpid = {:sccoa003.stbp}
selectdo
endselect
| ดึง Address แยก
select tccom130.*
from tccom130
where tccom130.cadr = {:tccom100.cadr}
selectdo
endselect
endselect
SELECT
h.t$coan AS coa_number,
h.t$ofbp AS ordered_by_bp,
h.t$stbp AS ship_to_bp,
h.t$item AS item_code,
h.t$cwar AS warehouse,
h.t$loca AS location,
h.t$qnty AS quantity,
h.t$cuni AS unit,
h.t$mfgd AS mfg_date_utc,
h.t$expd AS exp_date_utc,
h.t$tbvr AS out_of_spec_flag,
bp.t$nama AS bp_name,
addr.t$ln01 AS address_line1,
addr.t$ln02 AS address_line2,
addr.t$ccty AS country_code,
CASE addr.t$ccty
WHEN 'THA' THEN 'Local'
ELSE 'Export'
END AS coa_type
FROM sccoa003 h
LEFT JOIN tccom100 bp ON bp.t$bpid = h.t$stbp
LEFT JOIN tccom130 addr ON addr.t$cadr = bp.t$cadr
WHERE h.t$coan BETWEEN :coan_from AND :coan_to
AND h.t$ofbp BETWEEN :bpid_from AND :bpid_to
ORDER BY h.t$coan;
t$ เช่น t$coan, t$bpid —
ใช้ LEFT JOIN แทน nested SELECT เพื่อให้ได้ข้อมูลแม้ว่าจะไม่มี BP หรือ Address
ดึงรายละเอียดผลทดสอบ (COA Detail)
รวม Detail + Characteristic + Specification ใน Query เดียว
select sccoa004.*, qmptc001.*
from sccoa004
JOIN qmptc001
ON qmptc001.char = sccoa004.char
where sccoa004.coan = {:sccoa003.coan}
selectdo
| จัดรูปแบบตาม ctyp
if qmptc001.ctyp = option then
| ดึง option description
select qmptc014.dsca ...
else
| format ตัวเลข
rpt.result = edit$(sccoa004.mval, ...)
endif
| คำนวณ Spec
if prto = min then
rpt.spec = "Min. " & edit$(llmt, ...)
endif
endselect
SELECT
d.t$coan AS coa_number,
d.t$prts AS sort_key,
d.t$char AS char_code,
c.t$dsca AS char_description,
c.t$ctyp AS char_type,
d.t$mval AS measured_value,
d.t$mopt AS measured_option,
opt.t$dsca AS option_description,
c.t$cuni AS unit,
d.t$prto AS spec_print_option,
d.t$llmt AS lower_limit,
d.t$ulmt AS upper_limit,
d.t$fxsp AS fixed_spec_text,
CASE d.t$prto
WHEN 1 THEN 'Min. ' || CAST(d.t$llmt AS VARCHAR)
WHEN 2 THEN 'Max. ' || CAST(d.t$ulmt AS VARCHAR)
WHEN 3 THEN CAST(d.t$llmt AS VARCHAR)
|| ' - ' || CAST(d.t$ulmt AS VARCHAR)
WHEN 6 THEN d.t$fxsp
ELSE ''
END AS specification,
CASE c.t$ctyp
WHEN 1 THEN opt.t$dsca
ELSE CAST(d.t$mval AS VARCHAR)
END AS result_display
FROM sccoa004 d
JOIN qmptc001 c ON c.t$char = d.t$char
LEFT JOIN qmptc014 opt ON opt.t$oset = d.t$oset
AND opt.t$optn = d.t$mopt
WHERE d.t$coan = :coa_number
ORDER BY d.t$prts;
CASE แทน if...else ใน SQL —
จะได้ Specification และ Result ที่ Format แล้วในระดับ SQL เลย
รวมทุกอย่างเป็น VIEW
สร้าง Database View เพื่อใช้เป็น Data Source สำหรับ API
CREATE VIEW vw_coa_report AS
SELECT
-- Header
h.t$coan AS coa_number,
h.t$item AS item_code,
item.t$dsca AS item_description,
h.t$stbp AS ship_to_bp,
bp.t$nama AS bp_name,
CONCAT(addr.t$ln01, ' ', addr.t$ln02) AS bp_address,
addr.t$ccty AS country,
CASE addr.t$ccty
WHEN 'THA' THEN 'Local' ELSE 'Export'
END AS coa_type,
h.t$cwar AS warehouse,
h.t$loca AS location,
h.t$qnty AS quantity,
u.t$dsca AS unit_desc,
h.t$mfgd AS mfg_date,
h.t$expd AS exp_date,
-- Detail
d.t$prts AS sort_key,
d.t$char AS char_code,
c.t$dsca AS char_name,
c.t$ctyp AS char_type,
d.t$mval AS measured_value,
COALESCE(opt.t$dsca, CAST(d.t$mval AS VARCHAR))
AS result_display,
d.t$llmt AS lower_limit,
d.t$ulmt AS upper_limit,
d.t$fxsp AS fixed_spec,
d.t$prto AS spec_type,
c.t$cuni AS char_unit
FROM sccoa003 h
JOIN sccoa004 d ON d.t$coan = h.t$coan
JOIN qmptc001 c ON c.t$char = d.t$char
LEFT JOIN tccom100 bp ON bp.t$bpid = h.t$stbp
LEFT JOIN tccom130 addr ON addr.t$cadr = bp.t$cadr
LEFT JOIN tcibd001 item ON item.t$item = h.t$item
LEFT JOIN tcmcs001 u ON u.t$cuni = h.t$cuni
LEFT JOIN qmptc014 opt ON opt.t$oset = d.t$oset
AND opt.t$optn = d.t$mopt
WHERE h.t$tbvr = 0 -- เฉพาะ COA ที่ผ่าน QC
ORDER BY h.t$coan, d.t$prts;
แผนภาพความสัมพันธ์ตาราง (Entity Relationship Diagram)
แสดงตารางทั้งหมดที่ใช้ในโปรแกรม COA.4gl พร้อมเส้นเชื่อมโยง Foreign Key
อธิบายความสัมพันธ์ (Relationships)
1 : N(หนึ่งต่อกลุ่ม): ฝั่งซ้าย 1 รายการ มีข้อมูลฝั่งขวาได้หลายรายการ (เช่น COA 1 ใบ มีผลทดสอบ หลายรายการ)N : 1(กลุ่มต่อหนึ่ง): ฝั่งซ้ายหลายรายการ เป็นของข้อมูลฝั่งขวาเดียวกัน (เช่น COA หลายใบ เป็นของ ลูกค้าคนเดียวกัน)JOIN: ข้อมูลต้องมีตรงกันทั้ง 2 ฝั่งเท่านั้น ถึงจะถูกนำมาแสดง (ป้องกันข้อมูลไม่สมบูรณ์ เช่น COA ต้องมีผลทดสอบเสมอ)LEFT JOIN: ยึดฝั่งซ้ายเป็นหลัก ข้อมูลจะถูกดึงมาเสมอแม้ฝั่งขวาจะแหว่งไป (เช่น ดึง COA ออกมาเสมอ แม้ลูกค้าจะถูกลบออกจากระบบไปแล้วก็ตาม)
COA 1 ใบ มีผลทดสอบ หลายรายการ (เช่น Color, FFA, Moisture, ...) เชื่อมด้วย coan
JOIN sccoa004 d ON d.t$coan = h.t$coanดึงชื่อลูกค้าจาก BP Master — ใช้ stbp = bpid — LEFT JOIN เพราะ BP อาจถูกลบ
LEFT JOIN tccom100 bp ON bp.t$bpid = h.t$stbpดึงที่อยู่ของ BP — ใช้ cadr — สำคัญเพราะ ccty ใช้แยก Local/Export
LEFT JOIN tccom130 addr ON addr.t$cadr = bp.t$cadrดึงชื่อ + ประเภทของ Characteristic — ctyp สำคัญมาก ใช้ตัดสินวิธี Format ผลทดสอบ
JOIN qmptc001 c ON c.t$char = d.t$charดึงคำอธิบาย Option — ใช้ เมื่อ ctyp = 1 (Option) เท่านั้น เช่น "Clear & Bright"
LEFT JOIN qmptc014 opt ON opt.t$oset = d.t$oset AND opt.t$optn = d.t$moptดึงชื่อสินค้า — ใช้ item — แสดงในหัว COA เช่น "Soybean Oil RBD"
LEFT JOIN tcibd001 item ON item.t$item = h.t$itemลำดับการดึงข้อมูลในโปรแกรม
WHERE coan BETWEEN :f AND :t
ใช้แยก Local / Export
Format ตาม ctyp + prto
Item+BP → Item → Header
คำอธิบายตาราง (Table Dictionary)
รายการตารางทั้งหมดที่เกี่ยวข้องในโปรแกรม COA พร้อมคำอธิบาย
ตัวอย่าง API Response (JSON)
ข้อมูลที่ได้จาก View สามารถส่งผ่าน REST API ในรูปแบบ JSON
from flask import Flask, jsonify
import pyodbc
app = Flask(__name__)
@app.route('/api/coa/<coa_number>')
def get_coa(coa_number):
conn = pyodbc.connect(LN_CONNECTION)
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM vw_coa_report
WHERE coa_number = ?
""", coa_number)
columns = [col[0] for col in cursor.description]
rows = [dict(zip(columns, row))
for row in cursor.fetchall()]
return jsonify({
"coa_number": coa_number,
"details": rows
})
{
"coa_number": "COA-2026-001",
"item_code": "03020",
"item_description": "Soybean Oil RBD",
"bp_name": "Customer ABC Co.,Ltd.",
"coa_type": "Export",
"mfg_date": "2026-01-15",
"details": [
{
"char_name": "Color",
"result": "3.0",
"spec": "Max. 4.0",
"unit": "Lovibond"
},
{
"char_name": "FFA",
"result": "0.04",
"spec": "Max. 0.10",
"unit": "% as Oleic"
},
{
"char_name": "Appearance",
"result": "Clear & Bright",
"spec": "Clear & Bright",
"unit": ""
}
]
}
Business Logic ที่ต้อง Migrate ไปด้วย!
SQL ดึงได้แค่ข้อมูลดิบ (Raw Data) — Logic เหล่านี้ต้องย้ายไปอยู่ใน Application Layer (API Code)
SQL / View
ดึงข้อมูลดิบ + JOIN + Filter
- JOIN ตาราง
- WHERE filter
- CASE WHEN (simple)
Application Logic
แปลง + จัดรูปแบบ + ตรรกะ
- Format ตัวเลข
- แปลงวันที่
- คำนวณ Spec
- Remark fallback
API Response
จัดโครงสร้าง JSON + Validate
- Grouping (Header/Detail)
- Error handling
- Pagination
8 เงื่อนไขสำคัญจากโปรแกรมเดิม
จัดรูปแบบผลทดสอบตาม Characteristic Type
ต้องย้ายไป APIโปรแกรมเดิมใช้ qmptc001.ctyp เพื่อตัดสินว่าผลทดสอบจะแสดงอย่างไร:
if ctyp = option then
| แสดงชื่อ Option เช่น "Clear & Bright"
rpt.result = qmptc014.dsca
else if ctyp = fraction then
| ทศนิยม เช่น "0.04"
rpt.result = edit$(mval, "ZZZZZ9VD99")
else if ctyp = integer then
| จำนวนเต็ม เช่น "185"
rpt.result = edit$(mval, "ZZZZZZZZZ9")
endif
def format_result(row):
if row['char_type'] == 1: # Option
return row['option_description']
elif row['char_type'] == 2: # Fraction
val = row['measured_value']
# ⚠️ ตรวจ High-Precision chars
if row['char_code'] in HIGH_PREC:
return f"{val:.4f}"
return f"{val:.2f}"
else: # Integer
return str(int(row['measured_value']))
ทศนิยมพิเศษ (High-Precision) สำหรับบาง Characteristic
ต้องย้ายไป APIบาง Characteristic ต้องแสดง 4 ทศนิยม แทน 2 — ถ้าไม่ทำ ค่าจะผิด!
| รายการ Characteristic ที่ต้องแสดง 4 ทศนิยม
if trim$(sccoa004.char) = "SP_GRAV2"
or trim$(sccoa004.char) = "RE_DEN25"
or trim$(sccoa004.char) = "RI_25C" then
rpt.result = edit$(mval, "ZZZ9VD9999") | 4 ตำแหน่ง
else
rpt.result = edit$(mval, "ZZZZZ9VD99") | 2 ตำแหน่ง
endif
HIGH_PRECISION_CHARS = ["SP_GRAV2", "RE_DEN25", "RI_25C"]คำนวณ Specification (Min/Max/Range/Fixed)
ทำใน SQL ได้บางส่วนSpecification มี 6 ประเภท — ทำใน SQL ได้แค่ Simple CASE, แต่ Format ตัวเลขต้องทำใน API:
| prto | ประเภท | ตัวอย่างผลลัพธ์ | ทำใน SQL? | ทำใน API? |
|---|---|---|---|---|
| 1 | Min | Min. 0.05 | ✅ CASE | ⚠️ Format decimal |
| 2 | Max | Max. 4.0 | ✅ CASE | ⚠️ Format decimal |
| 3 | Range | 0.05 - 0.10 | ✅ CASE | ⚠️ Format both |
| 4 | Min Value | Min. 185 | ✅ CASE | ⚠️ Format integer |
| 5 | Max Value | Max. 195 | ✅ CASE | ⚠️ Format integer |
| 6 | Fixed | Clear & Bright | ✅ ดึง fxsp | ไม่ต้อง |
llmt, ulmt, prto, fxsp เป็น Raw Data จาก SQL แล้วไป Format ใน API —
เพราะ Format ตัวเลขใน SQL มีข้อจำกัด (ลบ trailing zero ยาก)แปลงวันที่ UTC → DD/MM/YYYY
ต้องย้ายไป APILN เก็บวันที่เป็น UTC integer ไม่ใช่ DATE type — ต้องแปลงเอง:
function convert.utc.to.date()
date.l = 0
utc.to.date(utc.i, date.l)
date.to.num(date.l, num.i)
endfunction
| ใช้: rpt.mfgd = edit$(num.i)
| format: DD/MM/YYYY
from datetime import datetime, timedelta
# LN UTC = seconds since 1970-01-01
def utc_to_date(utc_val):
if utc_val == 0:
return None
dt = datetime(1970, 1, 1) + timedelta(
seconds=utc_val
)
return dt.strftime("%d/%m/%Y")
Remark Fallback Logic (ลำดับการค้นหาหมายเหตุ)
ต้องย้ายไป APIโปรแกรมดึง Remark แบบมี Fallback 3 ระดับ — SQL อย่างเดียวทำไม่ได้:
sccoa009 เก็บ Remark เป็น Text Object —
ต้องเรียก dal.get.text() ดึงข้อความหลายบรรทัด ซึ่ง SQL ธรรมดาเข้าถึง Text Object ไม่ได้Logic เฉพาะลูกค้า (Hardcoded BP)
ย้ายไป Configมี Logic พิเศษสำหรับลูกค้าเฉพาะ (เช่น Nestlé = BP 110472) — ควรย้ายไปเป็น Config Table:
| Vendor Site Info สำหรับ Nestlé เท่านั้น
if sccoa003.stbp(1;6) = "110472" then
Vender_No = "VenderSiteNo: 100786789"
Vender_Manu = "VenderSiteCity: SamutPrakan TH"
endif
| Packaging สำหรับ Item 03020 เท่านั้น
if trim$(tcibd001.item) = "03020" then
rpt.packaging = "IBC"
endif
{"110472": {"vendor_no": "100786789", "vendor_city": "SamutPrakan TH"}}
เพื่อจะได้แก้ Config ได้โดยไม่ต้องแก้ Codeตรวจสอบ QC ผ่านหรือไม่ (Out-of-Spec Flag)
ทำใน SQL ได้โปรแกรมเดิมไม่พิมพ์ COA ถ้า tbvr = 1 (Out of Spec) — อันนี้ทำใน SQL ได้เลย:
WHERE h.t$tbvr = 0 -- 0 = ผ่าน QC, 1 = ไม่ผ่าน
AND h.t$coan BETWEEN :from AND :to
แยก Local / Export (ตามประเทศลูกค้า)
ทำใน SQL ได้โปรแกรมเดิมใช้แยกรูปแบบรายงาน — ใน API ใช้แยกเงื่อนไขการแสดงผลได้:
CASE addr.t$ccty
WHEN 'THA' THEN 'Local' -- ในประเทศ
ELSE 'Export' -- ส่งออก
END AS coa_type
coa_type เพื่อเลือก Template ที่ต่างกัน —
เช่น Export อาจต้องแสดง Vendor Info เพิ่ม หรือใช้ภาษาอังกฤษเท่านั้นสรุป: อะไรอยู่ไหน?
| Logic | ทำใน SQL? | ทำใน API? | เหตุผล |
|---|---|---|---|
| JOIN ตาราง | ✅ ใช่ | — | SQL ถนัด JOIN |
| Filter QC Pass/Fail | ✅ ใช่ | — | WHERE clause |
| Local vs Export | ✅ ใช่ | — | CASE WHEN |
| Format ตัวเลข (ทศนิยม/จำนวนเต็ม) | — | ⚠️ ใช่ | SQL format ตัวเลขลำบาก |
| High-Precision (4 ทศนิยม) | — | ⚠️ ใช่ | ต้องมี Config list |
| คำนวณ Specification | ✅ บางส่วน | ⚠️ Format | CASE logic + API format |
| แปลงวันที่ UTC | — | ⚠️ ใช่ | UTC integer → Date |
| Remark Fallback | — | 🔴 ต้องทำ | ต้องเรียก Text Object |
| Hardcoded BP/Item | — | 🔴 Config | ย้ายไป Config file |
ตัวอย่าง API ที่รวม Logic ครบ
import pyodbc
from flask import Flask, jsonify
from datetime import datetime, timedelta
# ===== Config (ย้ายมาจาก Hardcode ในโปรแกรม) =====
HIGH_PRECISION_CHARS = ["SP_GRAV2", "RE_DEN25", "RI_25C"]
BP_CONFIG = {
"110472": {
"vendor_no": "VenderSiteNo: 100786789",
"vendor_city": "VenderSiteCity: SamutPrakan TH"
}
}
ITEM_PACKAGING = {"03020": "IBC"}
# ===== Business Logic Functions =====
def utc_to_date(utc_val):
"""Logic จากบรรทัด 675-703 ของ COA.4gl"""
if not utc_val or utc_val == 0:
return None
dt = datetime(1970, 1, 1) + timedelta(seconds=utc_val)
return dt.strftime("%d/%m/%Y")
def format_result(char_code, char_type, mval, opt_desc):
"""Logic จากบรรทัด 290-360 ของ COA.4gl"""
if char_type == 1: # Option
return opt_desc or ""
elif char_type == 2: # Fraction
decimals = 4 if char_code.strip() in HIGH_PRECISION_CHARS else 2
return f"{mval:.{decimals}f}"
else: # Integer
return str(int(mval)) if mval else "0"
def format_spec(prto, llmt, ulmt, fxsp, char_code, char_type):
"""Logic จากบรรทัด 409-572 ของ COA.4gl"""
fmt = lambda v: format_result(char_code, char_type, v, None)
specs = {
1: lambda: f"Min. {fmt(llmt)}",
2: lambda: f"Max. {fmt(ulmt)}",
3: lambda: f"{fmt(llmt)} - {fmt(ulmt)}",
4: lambda: f"Min. {fmt(llmt)}",
5: lambda: f"Max. {fmt(ulmt)}",
6: lambda: fxsp or "",
}
return specs.get(prto, lambda: "")()
@app.route('/api/coa/<coa_number>')
def get_coa(coa_number):
cursor.execute("SELECT * FROM vw_coa_report WHERE coa_number = ?", coa_number)
rows = cursor.fetchall()
if not rows:
return jsonify({"error": "COA not found"}), 404
header = rows[0]
bp_extra = BP_CONFIG.get(header.ship_to_bp.strip()[:6], {})
return jsonify({
"coa_number": coa_number,
"coa_type": header.coa_type,
"mfg_date": utc_to_date(header.mfg_date), # ← Logic #4
"exp_date": utc_to_date(header.exp_date),
"vendor_info": bp_extra, # ← Logic #6
"packaging": ITEM_PACKAGING.get(header.item_code.strip(), ""),
"details": [{
"name": r.char_name,
"result": format_result( # ← Logic #1, #2
r.char_code, r.char_type, r.measured_value, r.option_desc),
"spec": format_spec( # ← Logic #3
r.spec_type, r.lower_limit, r.upper_limit,
r.fixed_spec, r.char_code, r.char_type),
"unit": r.char_unit
} for r in rows]
})
Interactive API API Mockup
จำลองการเรียกใช้งาน REST API ดูผลลัพธ์ข้อมูลจริงๆ จากโครงสร้างที่ได้ออกแบบไว้ข้างต้น (จำลอง)
ข้อมูลการค้นหา (Request Parameters)
// กด "Send Request" เพื่อดูผลลัพธ์
// (สามารถเปลี่ยน COA Number เป็นอย่างอื่นเพื่อดูการตอบสนองที่เปลี่ยนไป)
เปรียบเทียบ 4GL vs SQL
| หัวข้อ | 4GL ในโปรแกรม | Standard SQL |
|---|---|---|
| การ Join ตาราง | select ซ้อน select (N+1 query) |
JOIN ... ON (1 query) |
| Conditional Logic | if...else...endif ซ้อนกัน |
CASE WHEN...THEN...END |
| Format ตัวเลข | edit$(..., "ZZZ9VD99") |
FORMAT() หรือ CAST() |
| String Concat | trim$() & strip$() |
CONCAT() หรือ || |
| Host Variable | {:variable} |
:param หรือ ? |
| No Data Found | selectempty |
LEFT JOIN + COALESCE() |
| End of Query | selecteos |
ไม่จำเป็น (Process หลัง query) |