├── .gitignore ├── LICENSE.txt ├── README.org └── lualib └── pgsql ├── util.lua └── pgsql.lua /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016 by Leaf Corcoran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * skynet_pgsql 2 | A pure lua postgres driver for use in [[https://github.com/cloudwu/skynet][skynet]]. 3 | 4 | Modified from [[https://github.com/leafo/pgmoon][leafo/pgmoon]] 5 | 6 | * Install 7 | #+begin_src shell 8 | cp -r skynet_pgsql/lualib/pgsql your_skynet_root/lualib/ 9 | #+end_src 10 | * Simple Example 11 | #+begin_src lua 12 | local pgsql = require "pgsql.pgsql" 13 | local conf = { 14 | host = "127.0.0.1", 15 | port = 5432, 16 | user = "dbadmin", 17 | password = "dbadmin", 18 | database = "exampledb", 19 | } 20 | local db = pgsql.connect(conf) 21 | local sql = "select * from weather;" 22 | local result = db:query(sql) 23 | #+end_src 24 | * Desc 25 | ** Authentication Types 26 | currently supports password based authentication methods: TRUST , MD5, PASSWORD 27 | * LICENSE 28 | 29 | Copyright (C) 2016 by Leaf Corcoran 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy 32 | of this software and associated documentation files (the "Software"), to deal 33 | in the Software without restriction, including without limitation the rights 34 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 35 | copies of the Software, and to permit persons to whom the Software is 36 | furnished to do so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. 48 | -------------------------------------------------------------------------------- /lualib/pgsql/util.lua: -------------------------------------------------------------------------------- 1 | local string = string 2 | local tconcat = table.concat 3 | local tinsert = table.insert 4 | local lpeg = require "lpeg" 5 | 6 | local function escape_identifier(ident) 7 | local v = {'"', tostring(ident):gsub('"', '""'), '"'} 8 | return tconcat(v) 9 | end 10 | 11 | local function default_escape_literal(val) 12 | local t = type(val) 13 | if "number" == t then 14 | return tostring(val) 15 | elseif "string" == t then 16 | local v = {"'", tostring((val:gsub("'", "''"))), "'"} 17 | return tconcat(v) 18 | elseif "boolean" == t then 19 | return val and "TRUE" or "FALSE" 20 | end 21 | return error("don't know how to escape value: " .. tostring(val)) 22 | end 23 | 24 | local util = {} 25 | 26 | function util.encode_int(n, bytes) 27 | if bytes == nil then 28 | bytes = 4 29 | end 30 | if 4 == bytes then 31 | return string.pack(">i4", n) 32 | else 33 | return error("don't know how to encode " .. tostring(bytes) .. " byte(s)") 34 | end 35 | end 36 | 37 | function util.decode_int(str, bytes) 38 | if bytes == nil then 39 | bytes = #str 40 | end 41 | if 4 == bytes then 42 | return string.unpack(">i4", str) 43 | elseif 2 == bytes then 44 | return string.unpack(">i2", str) 45 | else 46 | return error("don't know how to decode " .. tostring(bytes) .. " byte(s)") 47 | end 48 | end 49 | 50 | function util.decode_bytea(str) 51 | if str:sub(1, 2) == '\\x' then 52 | return str:sub(3):gsub('..', function(hex) 53 | return string.char(tonumber(hex, 16)) 54 | end) 55 | else 56 | return str:gsub('\\(%d%d%d)', function(oct) 57 | return string.char(tonumber(oct, 8)) 58 | end) 59 | end 60 | end 61 | 62 | function util.encode_bytea(str) 63 | return string.format("E'\\\\x%s'", str:gsub('.', function(byte) 64 | return string.format('%02x', string.byte(byte)) 65 | end)) 66 | end 67 | 68 | function util.__flatten(t, buffer) 69 | local ttype = type(t) 70 | if "string" == ttype then 71 | buffer[#buffer + 1] = t 72 | elseif "table" == ttype then 73 | for i = 1, #t do 74 | local thing = t[i] 75 | util.__flatten(thing, buffer) 76 | end 77 | end 78 | end 79 | 80 | function util.flatten(t) 81 | local buffer = { } 82 | util.__flatten(t, buffer) 83 | return tconcat(buffer) 84 | end 85 | 86 | function util.cal_len(thing, t) 87 | if t == nil then 88 | t = type(thing) 89 | end 90 | if "string" == t then 91 | return #thing 92 | elseif "table" == t then 93 | local l = 0 94 | for i = 1, #thing do 95 | local inner = thing[i] 96 | local inner_t = type(inner) 97 | if inner_t == "string" then 98 | l = l + #inner 99 | else 100 | l = l + util.cal_len(inner, inner_t) 101 | end 102 | end 103 | return l 104 | else 105 | return error("don't know how to calculate length of " .. tostring(t)) 106 | end 107 | end 108 | 109 | function util.flip(t) 110 | local keys = {} 111 | for k,v in pairs(t) do 112 | tinsert(keys, k) 113 | end 114 | for i=1, #keys do 115 | local k = keys[i] 116 | t[t[k]] = k 117 | end 118 | return t 119 | end 120 | 121 | local function decode_error_whitespace() 122 | return error("got unexpected whitespace") 123 | end 124 | 125 | local decode_array 126 | do 127 | local P, R, S, V, Ct, C, Cs = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.Ct, lpeg.C, lpeg.Cs 128 | local define = { 129 | "array", 130 | array = Ct(V("open") * (V("value") * (P(",") * V("value")) ^ 0) ^ -1 * V("close")), 131 | value = V("invalid_char") + V("string") + V("array") + V("literal"), 132 | string = P('"') * Cs((P([[\\]]) / [[\]] + P([[\"]]) / [["]] + (P(1) - P('"'))) ^ 0) * P('"'), 133 | literal = C((P(1) - S("},")) ^ 1), 134 | invalid_char = S(" \t\r\n") / decode_error_whitespace, 135 | open = P("{"), 136 | delim = P(","), 137 | close = P("}") 138 | } 139 | local g = P(define) 140 | 141 | local convert_values 142 | convert_values = function(array, fn) 143 | for idx, v in ipairs(array) do 144 | if type(v) == "table" then 145 | convert_values(v, fn) 146 | else 147 | array[idx] = fn(v) 148 | end 149 | end 150 | return array 151 | end 152 | 153 | decode_array = function(str, convert_fn) 154 | local out = (assert(g:match(str), "failed to parse postgresql array")) 155 | if convert_fn then 156 | return convert_values(out, convert_fn) 157 | else 158 | return out 159 | end 160 | end 161 | end 162 | 163 | util.decode_array = decode_array 164 | 165 | local encode_array 166 | do 167 | local append_buffer 168 | append_buffer = function(escape_literal, buffer, values) 169 | for i = 1, #values do 170 | local item = values[i] 171 | if type(item) == "table" and not getmetatable(item) then 172 | tinsert(buffer, "[") 173 | append_buffer(escape_literal, buffer, item) 174 | buffer[#buffer] = "]" 175 | tinsert(buffer, ",") 176 | else 177 | tinsert(buffer, escape_literal(item)) 178 | tinsert(buffer, ",") 179 | end 180 | end 181 | return buffer 182 | end 183 | encode_array = function(tbl, escape_literal) 184 | escape_literal = escape_literal or default_escape_literal 185 | local buffer = append_buffer(escape_literal, { 186 | "ARRAY[" 187 | }, tbl) 188 | buffer[#buffer] = "]" 189 | return tconcat(buffer) 190 | end 191 | end 192 | 193 | local decode_hstore 194 | do 195 | local P, R, S, V, Ct, C, Cs, Cg, Cf = lpeg.P, lpeg.R, lpeg.S, lpeg.V, lpeg.Ct, lpeg.C, lpeg.Cs, lpeg.Cg, lpeg.Cf 196 | local define = { 197 | "hstore", 198 | hstore = Cf(Ct("") * (V("pair") * (V("delim") * V("pair")) ^ 0) ^ -1, rawset) * -1, 199 | pair = Cg(V("value") * "=>" * (V("value") + V("null"))), 200 | value = V("invalid_char") + V("string"), 201 | string = P('"') * Cs((P([[\\]]) / [[\]] + P([[\"]]) / [["]] + (P(1) - P('"'))) ^ 0) * P('"'), 202 | null = C('NULL'), 203 | invalid_char = S(" \t\r\n") / decode_error_whitespace, 204 | delim = P(", ") 205 | } 206 | 207 | local g = P(define) 208 | decode_hstore = function(str, convert_fn) 209 | local out = (assert(g:match(str), "failed to parse postgresql hstore")) 210 | return out 211 | end 212 | end 213 | 214 | local encode_hstore 215 | do 216 | encode_hstore = function(tbl, escape_literal) 217 | if not (escape_literal) then 218 | escape_literal = default_escape_literal 219 | end 220 | local buffer = { } 221 | for k, v in pairs(tbl) do 222 | tinsert(buffer, tconcat({'"', k ,'"=>"', v , '"'})) 223 | end 224 | return escape_literal(tconcat(buffer, ", ")) 225 | end 226 | end 227 | 228 | util.encode_array = encode_array 229 | util.decode_array = decode_array 230 | util.encode_hstore = encode_hstore 231 | util.decode_hstore = decode_hstore 232 | util.escape_literal = default_escape_literal 233 | util.escape_identifier = escape_identifier 234 | 235 | return util 236 | -------------------------------------------------------------------------------- /lualib/pgsql/pgsql.lua: -------------------------------------------------------------------------------- 1 | local skynet = require "skynet" 2 | local socket = require "socket" 3 | local socketchannel = require "socketchannel" 4 | local cjson = require "cjson" 5 | local lpeg = require "lpeg" 6 | 7 | cjson.encode_sparse_array(true) 8 | 9 | local util = require "utils.util" 10 | local table = table 11 | local tinsert = table.insert 12 | local tconcat = table.concat 13 | local string = string 14 | local assert = assert 15 | 16 | --TODO 缓存table的表头,避免每次都要解析 17 | local NULL = "\0" 18 | 19 | local pg = {} 20 | local command = {} 21 | 22 | local meta = { 23 | __index = command, 24 | -- DO NOT close channel in __gc 25 | } 26 | 27 | local pgutil = require "pgsql.util" 28 | 29 | local AUTH_TYPE = { 30 | NO_AUTH = 0, 31 | PLAIN_TEXT = 3, 32 | MD5 = 5, 33 | } 34 | 35 | local PG_TYPES = { 36 | [16] = "boolean", 37 | [17] = "bytea", 38 | [20] = "number", 39 | [21] = "number", 40 | [23] = "number", 41 | [700] = "number", 42 | [701] = "number", 43 | [1700] = "number", 44 | [114] = "json", 45 | [3802] = "json", 46 | [1000] = "array_boolean", 47 | [1005] = "array_number", 48 | [1007] = "array_number", 49 | [1016] = "array_number", 50 | [1021] = "array_number", 51 | [1022] = "array_number", 52 | [1231] = "array_number", 53 | [1009] = "array_string", 54 | [1015] = "array_string", 55 | [1002] = "array_string", 56 | [1014] = "array_string", 57 | [2951] = "array_string", 58 | [199] = "array_json", 59 | [3807] = "array_json" 60 | } 61 | 62 | local tobool = function(str) 63 | return str == "t" 64 | end 65 | 66 | local pg_auth_cmd = {} 67 | local read_response = nil 68 | 69 | local MSG_TYPE = { 70 | status = "S", 71 | auth = "R", 72 | backend_key = "K", 73 | ready_for_query = "Z", 74 | query = "Q", 75 | notice = "N", 76 | notification = "A", 77 | password = "p", 78 | row_description = "T", 79 | data_row = "D", 80 | command_complete = "C", 81 | error = "E" 82 | } 83 | local ERROR_TYPES = { 84 | severity = "S", 85 | code = "C", 86 | message = "M", 87 | position = "P", 88 | detail = "D", 89 | schema = "s", 90 | table = "t", 91 | constraint = "n" 92 | } 93 | 94 | MSG_TYPE = pgutil.flip(MSG_TYPE) 95 | ERROR_TYPES = pgutil.flip(ERROR_TYPES) 96 | 97 | local function send_message(so, msg_type, data, len) 98 | if len == nil then 99 | len = pgutil.cal_len(data) 100 | end 101 | len = len + 4 102 | local req_data = {msg_type, pgutil.encode_int(len), data} 103 | local req_msg = pgutil.flatten(req_data) 104 | return so:request(req_msg, read_response) 105 | end 106 | 107 | pg_auth_cmd[AUTH_TYPE.NO_AUTH] = function(fd, data) 108 | return true 109 | end 110 | pg_auth_cmd[AUTH_TYPE.PLAIN_TEXT] = function(so, user, password) 111 | local data = {password, NULL} 112 | send_message(so, MSG_TYPE.password, data) 113 | return true 114 | end 115 | 116 | --TODO md5 password 117 | pg_auth_cmd[AUTH_TYPE.MD5] = function(so, user, password) 118 | end 119 | 120 | function pg_auth_cmd:set_auth_type(auth_type) 121 | self.auth_type = auth_type 122 | end 123 | 124 | function pg_auth_cmd:send_auth_info(so, db_conf) 125 | local auth_type = self.auth_type 126 | local f = self[auth_type] 127 | assert(f, string.format("auth_type func not exist %s", self.auth_type)) 128 | f(so, db_conf.user, db_conf.password) 129 | end 130 | 131 | function pg_auth_cmd:set_ready_for_query() 132 | self.ready_for_query = true 133 | end 134 | 135 | function pg_auth_cmd:wait_ready(so) 136 | while true do 137 | so:response(read_response) 138 | if self.ready_for_query then 139 | break 140 | end 141 | end 142 | end 143 | 144 | setmetatable(pg_auth_cmd, pg_auth_cmd) 145 | 146 | local decode_json = function(json) 147 | return cjson.decode(json) 148 | end 149 | 150 | local type_deserializers = { 151 | json = function(val) 152 | return decode_json(val) 153 | end, 154 | bytea = function(val) 155 | return pgutil.decode_bytea(val) 156 | end, 157 | array_boolean = function(val) 158 | return pgutil.decode_array(val, tobool) 159 | end, 160 | array_number = function(val) 161 | return pgutil.decode_array(val, tonumber) 162 | end, 163 | array_string = function(val) 164 | return pgutil.decode_array(val) 165 | end, 166 | array_json = function(val) 167 | return pgutil.decode_array(val, decode_json) 168 | end, 169 | hstore = function(val) 170 | return pgutil.decode_hstore(val) 171 | end 172 | } 173 | 174 | local function parse_row_desc(row_desc) 175 | local num_fields = pgutil.decode_int(row_desc:sub(1, 2)) 176 | local offset = 3 177 | local fields = {} 178 | 179 | for i = 1, num_fields do 180 | local name = row_desc:match("[^%z]+", offset) 181 | offset = offset + #name + 1 182 | local data_type = pgutil.decode_int(row_desc:sub(offset + 6, offset + 6 + 3)) 183 | data_type = PG_TYPES[data_type] or "string" 184 | local format = pgutil.decode_int(row_desc:sub(offset + 16, offset + 16 + 1)) 185 | assert(0 == format, "don't know how to handle format") 186 | offset = offset + 18 187 | local info = { 188 | name, 189 | data_type 190 | } 191 | tinsert(fields, info) 192 | end 193 | 194 | return fields 195 | end 196 | local function parse_row_data(data_row, fields) 197 | local num_fields = pgutil.decode_int(data_row:sub(1, 2)) 198 | 199 | local out = {} 200 | local offset = 3 201 | for i = 1, num_fields do 202 | local to_continue = false 203 | repeat 204 | local field = fields[i] 205 | if not (field) then 206 | to_continue = true 207 | break 208 | end 209 | local field_name, field_type 210 | field_name, field_type = field[1], field[2] 211 | local len = pgutil.decode_int(data_row:sub(offset, offset + 3)) 212 | offset = offset + 4 213 | if len < 0 then 214 | --TODO null 处理 215 | --if self.convert_null then 216 | -- out[field_name] = NULL 217 | --end 218 | to_continue = true 219 | break 220 | end 221 | local value = data_row:sub(offset, offset + len - 1) 222 | offset = offset + len 223 | if "number" == field_type then 224 | value = tonumber(value) 225 | elseif "boolean" == field_type then 226 | value = value == "t" 227 | elseif "string" == field_type then 228 | value = value 229 | else 230 | local fn = type_deserializers[field_type] 231 | if fn then 232 | value = fn(value, field_type) 233 | end 234 | end 235 | out[field_name] = value 236 | to_continue = true 237 | until true 238 | if not to_continue then 239 | break 240 | end 241 | end 242 | return out 243 | end 244 | 245 | -- pg response 246 | local pg_command = {} 247 | 248 | pg_command[MSG_TYPE.auth] = function(self, data) 249 | local auth_type = pgutil.decode_int(data, 4) 250 | if auth_type ~= AUTH_TYPE.NO_AUTH then 251 | pg_auth_cmd:set_auth_type(auth_type) 252 | end 253 | return true 254 | end 255 | 256 | pg_command[MSG_TYPE.status] = function(self, data) 257 | return true 258 | end 259 | 260 | pg_command[MSG_TYPE.backend_key] = function(self, data) 261 | return true 262 | end 263 | 264 | pg_command[MSG_TYPE.ready_for_query] = function(self, data) 265 | pg_auth_cmd:set_ready_for_query() 266 | return true 267 | end 268 | 269 | pg_command[MSG_TYPE.query] = function(self, data) 270 | end 271 | 272 | pg_command[MSG_TYPE.notice] = function(self, data) 273 | end 274 | 275 | pg_command[MSG_TYPE.notification] = function(self, data) 276 | end 277 | 278 | 279 | pg_command[MSG_TYPE.row_description] = function(self, data) 280 | if data == nil then 281 | self.row_data = {} 282 | return false 283 | else 284 | local fields = parse_row_desc(data) 285 | self.row_fields = fields 286 | self.row_data = {} 287 | return true, data 288 | end 289 | end 290 | 291 | pg_command[MSG_TYPE.data_row] = function(self, data) 292 | local parsed_data = parse_row_data(data, self.row_fields) 293 | tinsert(self.row_data, parsed_data) 294 | return true 295 | end 296 | 297 | pg_command[MSG_TYPE.command_complete] = function(self, msg) 298 | 299 | local command = msg:match("^%w+") 300 | local affected_rows = tonumber(msg:match("(%d+)")) 301 | if affected_rows == 0 then 302 | self.row_data = nil 303 | end 304 | self.command_complete = true 305 | return true 306 | end 307 | 308 | pg_command[MSG_TYPE.error] = function(self, err_msg) 309 | local severity, message, detail, position 310 | local error_data = { } 311 | local offset = 1 312 | while offset <= #err_msg do 313 | local t = err_msg:sub(offset, offset) 314 | local str = err_msg:match("[^%z]+", offset + 1) 315 | if not (str) then 316 | break 317 | end 318 | offset = offset + (2 + #str) 319 | local field = ERROR_TYPES[t] 320 | if field then 321 | error_data[field] = str 322 | end 323 | if ERROR_TYPES.severity == t then 324 | severity = str 325 | elseif ERROR_TYPES.message == t then 326 | message = str 327 | elseif ERROR_TYPES.position == t then 328 | position = str 329 | elseif ERROR_TYPES.detail == t then 330 | detail = str 331 | end 332 | end 333 | local msg = tostring(severity) .. ": " .. tostring(message) 334 | if position then 335 | msg = tostring(msg) .. " (" .. tostring(position) .. ")" 336 | end 337 | if detail then 338 | msg = tostring(msg) .. "\n" .. tostring(detail) 339 | end 340 | return false, msg, error_data 341 | end 342 | 343 | function pg_command:read_response() 344 | local so = self.so 345 | while not self.command_complete do 346 | so:response(read_response) 347 | end 348 | self.command_complete = false 349 | return self.row_data 350 | end 351 | 352 | setmetatable(pg_command, pg_command) 353 | 354 | read_response = function(fd) 355 | local t = fd:read(1) 356 | local len = fd:read(4) 357 | len = pgutil.decode_int(len) 358 | len = len - 4 359 | local msg = fd:read(len) 360 | local f = pg_command[t] 361 | assert(f, string.format("pg response func handle not exist: %s", t)) 362 | return f(pg_command, msg) 363 | end 364 | 365 | 366 | local function pg_login(conf) 367 | return function(so) 368 | local data = { 369 | pgutil.encode_int(196608), 370 | "user", 371 | NULL, 372 | conf.user, 373 | NULL, 374 | "database", 375 | NULL, 376 | conf.database, 377 | NULL, 378 | "application_name", 379 | NULL, 380 | "skynet", 381 | NULL, 382 | NULL 383 | } 384 | local req_msg = pgutil.flatten({pgutil.encode_int(pgutil.cal_len(data)+4), data}) 385 | pg_command.so = so 386 | so:request(req_msg, read_response) 387 | pg_auth_cmd:send_auth_info(so, conf) 388 | pg_auth_cmd:wait_ready(so) 389 | end 390 | end 391 | 392 | function pg.connect(db_conf) 393 | local channel = socketchannel.channel { 394 | host = db_conf.host or "127.0.0.1", 395 | port = db_conf.port or 5432, 396 | auth = pg_login(db_conf), 397 | nodelay = true, 398 | } 399 | -- try connect first only once 400 | channel:connect(true) 401 | return setmetatable( { channel }, meta ) 402 | end 403 | 404 | local compose_message = function(args) 405 | if args ~= nil then 406 | tinsert(args, NULL) 407 | end 408 | return args 409 | end 410 | 411 | function command:disconnect() 412 | self[1]:close() 413 | end 414 | 415 | local command_meta = { 416 | __index = function(t, k) 417 | local cmd = k 418 | local f = function(self, v, ...) 419 | local msg_type = MSG_TYPE[cmd] 420 | local data = compose_message({v}) 421 | local msg = send_message(self[1], msg_type, data) 422 | return pg_command:read_response() 423 | end 424 | t[k] = f 425 | return f 426 | end 427 | } 428 | 429 | setmetatable(command, command_meta) 430 | 431 | pg.escape_identifier = util.escape_identifier 432 | pg.escape_literal = util.escape_literal 433 | 434 | return pg 435 | --------------------------------------------------------------------------------