import each from 'lodash-es/each.js'
import get from 'lodash-es/get.js'
import isDate from 'lodash-es/isDate.js'
import isarr from 'wsemi/src/isarr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isstr from 'wsemi/src/isstr.mjs'
//產生送交Jet引擎執行之SQL文字
//
//本模組僅產生六種固定形狀之語句: 全表查詢、依主鍵查詢、插入、依主鍵更新、依主鍵刪除、依主鍵清單刪除。
//複雜查詢條件($in、$nin、$regex等)一律不送Jet, 而由Jet取回全部數據後於記憶體sqlite內過濾(見queryMemory.mjs),
//因Access之SQL方言不支援該些運算子。故此處不需要通用之WHERE產生器。
//
//註: 舊版係以sequelize之logging回調反推SQL文字, 已不採用。其有二問題:
// 一為getInsert實際會對記憶體sqlite執行INSERT(產生副作用, 同一主鍵重試即撞唯一約束),
// 二為值置換未對字串做跳脫(值內含單引號即產出壞SQL)。
/**
* 轉為Jet之識別字(資料表名、欄位名)
*
* 註: Access以中括號引用識別字,其內不支援跳脫,故一律移除中括號字元
*
* @param {String} s 輸入識別字字串
* @returns {String} 回傳Jet識別字字串
*/
function escId(s) {
let t = String(s).replace(/[[\]]/g, '')
return `[${t}]`
}
/**
* 轉為Jet之值常值
*
* 註: 字串以單引號包覆,內部單引號以連續兩個單引號跳脫(Jet之標準跳脫方式)
* 註: 不可用N'...'前綴,該為SQL Server語法,Jet不支援
* 註: 日期物件以#...#表示,字串則維持引號由Jet自行隱式轉換
*
* @param {*} v 輸入值
* @returns {String} 回傳Jet值常值字串
*/
function escVal(v) {
//null
if (v === null || v === undefined) {
return 'NULL'
}
//boolean
if (isbol(v)) {
return v ? 'True' : 'False'
}
//number, 以typeof判定而非isnum, 因isnum對NaN為false會令其落入字串分支而產出'NaN'
if (typeof v === 'number') {
if (!isFinite(v)) {
return 'NULL' //NaN與Infinity於Jet無對應表示
}
return String(v)
}
//date
if (isDate(v)) {
let p = (n, w = 2) => {
return String(n).padStart(w, '0')
}
let s = `${v.getFullYear()}-${p(v.getMonth() + 1)}-${p(v.getDate())} ${p(v.getHours())}:${p(v.getMinutes())}:${p(v.getSeconds())}`
return `#${s}#`
}
//string與其餘型別
let s = isstr(v) ? v : String(v)
return `'${s.replace(/'/g, `''`)}'`
}
/**
* 取數據內屬於資料表欄位之鍵值對
*
* 註: 濾除非欄位之鍵,避免產出Jet不認得之欄位名
* 註: 值為undefined者不納入語句,令該欄位保留資料庫端之現值
*
* @ignore
* @param {Array} cols 輸入資料表欄位名陣列
* @param {Object} data 輸入數據物件
* @returns {Array} 回傳[{k,v}]陣列
*/
function pickCols(cols, data) {
let rs = []
each(cols, (k) => {
let v = get(data, k)
if (v === undefined) {
return
}
rs.push({ k, v })
})
return rs
}
/**
* 產生全表查詢語句
*
* @param {String} tableName 輸入資料表名稱字串
* @returns {String} 回傳SQL字串
*/
function getSelectAll(tableName) {
return `SELECT * FROM ${escId(tableName)}`
}
/**
* 產生依主鍵查詢語句
*
* @param {String} tableName 輸入資料表名稱字串
* @param {String} pk 輸入主鍵欄位名字串
* @param {String|Number} pkv 輸入主鍵值
* @returns {String} 回傳SQL字串
*/
function getSelectByPk(tableName, pk, pkv) {
return `SELECT * FROM ${escId(tableName)} WHERE ${escId(pk)} = ${escVal(pkv)}`
}
/**
* 產生依主鍵清單查詢語句,僅取主鍵欄位
*
* 註: 供insert於偵測到重複鍵錯誤後,核對該些主鍵是否確實已存在,
* 藉以區辨[主鍵已存在]與[撞及其他唯一索引],因Jet之錯誤碼3022涵蓋兩者
*
* @param {String} tableName 輸入資料表名稱字串
* @param {String} pk 輸入主鍵欄位名字串
* @param {Array} pkvs 輸入主鍵值陣列
* @returns {String} 回傳SQL字串,清單為空時回傳null
*/
function getSelectByPks(tableName, pk, pkvs) {
//check
if (!isarr(pkvs) || pkvs.length === 0) {
return null
}
//vs
let vs = pkvs.map((v) => {
return escVal(v)
})
return `SELECT ${escId(pk)} FROM ${escId(tableName)} WHERE ${escId(pk)} IN (${vs.join(', ')})`
}
/**
* 產生插入語句
*
* 註: Access不支援一次插入多組VALUES,故一次僅處理一筆
*
* @param {String} tableName 輸入資料表名稱字串
* @param {Array} cols 輸入資料表欄位名陣列
* @param {Object} data 輸入數據物件
* @returns {String} 回傳SQL字串
*/
function getInsert(tableName, cols, data) {
//kvs
let kvs = pickCols(cols, data)
//check
if (kvs.length === 0) {
return null
}
//ks, vs
let ks = kvs.map((v) => {
return escId(v.k)
})
let vs = kvs.map((v) => {
return escVal(v.v)
})
return `INSERT INTO ${escId(tableName)} (${ks.join(', ')}) VALUES (${vs.join(', ')})`
}
/**
* 產生依主鍵更新語句
*
* 註: 主鍵欄位不納入SET,避免更新時改動主鍵本身
*
* @param {String} tableName 輸入資料表名稱字串
* @param {Array} cols 輸入資料表欄位名陣列
* @param {Object} data 輸入數據物件
* @param {String} pk 輸入主鍵欄位名字串
* @param {String|Number} pkv 輸入主鍵值
* @returns {String} 回傳SQL字串,無可更新欄位時回傳null
*/
function getUpdate(tableName, cols, data, pk, pkv) {
//kvs, 排除主鍵欄位
let kvs = pickCols(cols, data)
.filter((v) => {
return v.k !== pk
})
//check
if (kvs.length === 0) {
return null
}
//sets
let sets = kvs.map((v) => {
return `${escId(v.k)} = ${escVal(v.v)}`
})
return `UPDATE ${escId(tableName)} SET ${sets.join(', ')} WHERE ${escId(pk)} = ${escVal(pkv)}`
}
/**
* 產生依主鍵刪除語句
*
* @param {String} tableName 輸入資料表名稱字串
* @param {String} pk 輸入主鍵欄位名字串
* @param {String|Number} pkv 輸入主鍵值
* @returns {String} 回傳SQL字串
*/
function getDeleteByPk(tableName, pk, pkv) {
return `DELETE FROM ${escId(tableName)} WHERE ${escId(pk)} = ${escVal(pkv)}`
}
/**
* 產生依主鍵清單刪除語句
*
* @param {String} tableName 輸入資料表名稱字串
* @param {String} pk 輸入主鍵欄位名字串
* @param {Array} pkvs 輸入主鍵值陣列
* @returns {String} 回傳SQL字串,清單為空時回傳null
*/
function getDeleteByPks(tableName, pk, pkvs) {
//check
if (!isarr(pkvs) || pkvs.length === 0) {
return null
}
//vs
let vs = pkvs.map((v) => {
return escVal(v)
})
return `DELETE FROM ${escId(tableName)} WHERE ${escId(pk)} IN (${vs.join(', ')})`
}
/**
* 產生清空全表語句
*
* @param {String} tableName 輸入資料表名稱字串
* @returns {String} 回傳SQL字串
*/
function getDeleteAll(tableName) {
return `DELETE FROM ${escId(tableName)}`
}
export {
escId,
escVal,
getSelectAll,
getSelectByPk,
getSelectByPks,
getInsert,
getUpdate,
getDeleteByPk,
getDeleteByPks,
getDeleteAll
}