@aidra/pmx-io 0.0.0-stage → 9.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +69 -2
  2. package/bin/pmx-io-node.js +143 -0
  3. package/index.d.ts +381 -0
  4. package/index.js +27 -0
  5. package/index.mjs +47 -0
  6. package/lib/actions.js +120 -0
  7. package/lib/activate.js +402 -0
  8. package/lib/application.js +106 -0
  9. package/lib/bun.js +155 -0
  10. package/lib/config.js +67 -0
  11. package/lib/constants.js +17 -0
  12. package/lib/debug.js +48 -0
  13. package/lib/esm-hooks.mjs +111 -0
  14. package/lib/exit.js +79 -0
  15. package/lib/features/metrics.js +409 -0
  16. package/lib/features/notify.js +358 -0
  17. package/lib/features/profiling.js +208 -0
  18. package/lib/hooks.js +151 -0
  19. package/lib/inbound.js +174 -0
  20. package/lib/integrations.js +372 -0
  21. package/lib/itg/amqplib.js +169 -0
  22. package/lib/itg/bullmq.js +141 -0
  23. package/lib/itg/common.js +657 -0
  24. package/lib/itg/dbcomment.js +114 -0
  25. package/lib/itg/index.js +103 -0
  26. package/lib/itg/ioredis.js +212 -0
  27. package/lib/itg/kafkajs.js +205 -0
  28. package/lib/itg/mongodb.js +322 -0
  29. package/lib/itg/mysql2.js +205 -0
  30. package/lib/itg/nats.js +531 -0
  31. package/lib/itg/noderedis.js +236 -0
  32. package/lib/itg/pg.js +253 -0
  33. package/lib/itg/pgqueue.js +305 -0
  34. package/lib/itg/postgres.js +252 -0
  35. package/lib/itg/redis-common.js +265 -0
  36. package/lib/itg/sql.js +302 -0
  37. package/lib/metrics/service.js +286 -0
  38. package/lib/metrics/types.js +342 -0
  39. package/lib/modules.js +260 -0
  40. package/lib/node-options.js +62 -0
  41. package/lib/otel/bridge.js +797 -0
  42. package/lib/otel/env.js +114 -0
  43. package/lib/outbound.js +362 -0
  44. package/lib/pmx.js +757 -0
  45. package/lib/relay/hub.js +279 -0
  46. package/lib/relay/transport.js +253 -0
  47. package/lib/standalone/activate.js +307 -0
  48. package/lib/standalone/capture.js +55 -0
  49. package/lib/standalone/container.js +909 -0
  50. package/lib/standalone/transport.js +1023 -0
  51. package/lib/tracing/context.js +90 -0
  52. package/lib/tracing/control.js +124 -0
  53. package/lib/tracing/exporter.js +115 -0
  54. package/lib/tracing/index.js +242 -0
  55. package/lib/tracing/logs.js +176 -0
  56. package/lib/tracing/options.js +105 -0
  57. package/lib/tracing/paths.js +34 -0
  58. package/lib/tracing/tracer.js +850 -0
  59. package/lib/tracing/w3c.js +415 -0
  60. package/lib/transport/ipc.js +97 -0
  61. package/lib/transport/ws.js +511 -0
  62. package/lib/util.js +109 -0
  63. package/package.json +48 -4
  64. package/preload.cjs +26 -0
  65. package/preload.mjs +25 -0
  66. package/standalone.cjs +16 -0
  67. package/standalone.mjs +17 -0
@@ -0,0 +1,265 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Shared by the ioredis and node-redis integrations (82 §6, §10): command statements (ITG-032),
5
+ // error codes (ITG-123), the XADD field carrier and the parsing of XREAD/XREADGROUP replies for
6
+ // Redis streams (ITG-210). Same results as pmx-io's _redis.py.
7
+
8
+ const c = require('./common')
9
+
10
+ const MAX_PLACEHOLDERS = 10
11
+ const MAX_ARG = 100
12
+ const MAX_STATEMENT = 2000
13
+ const MAX_PIPELINE_LINES = 20
14
+ const STREAM_COMMANDS = new Set(['XADD', 'XREAD', 'XREADGROUP'])
15
+
16
+ function text (v) {
17
+ if (typeof v === 'string') return v
18
+ if (v instanceof Uint8Array) return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('utf8')
19
+ return String(v)
20
+ }
21
+
22
+ function secretIndexes (name, args) {
23
+ const out = new Set()
24
+ if (name === 'AUTH') {
25
+ for (let i = 0; i < args.length; i++) out.add(i)
26
+ return out
27
+ }
28
+ const up = args.map((a) => (typeof a === 'string' || a instanceof Uint8Array ? text(a).toUpperCase() : ''))
29
+ if (name === 'HELLO') {
30
+ up.forEach((t, i) => { if (t === 'AUTH') { out.add(i + 1); out.add(i + 2) } })
31
+ } else if (name === 'MIGRATE') {
32
+ up.forEach((t, i) => {
33
+ if (t === 'AUTH') out.add(i + 1)
34
+ else if (t === 'AUTH2') { out.add(i + 1); out.add(i + 2) }
35
+ })
36
+ } else if (name === 'CONFIG' && up[0] === 'SET') {
37
+ for (let i = 2; i < args.length; i += 2) out.add(i)
38
+ } else if (name === 'ACL' && up[0] === 'SETUSER') {
39
+ // the rules carry passwords (`>pw`, `<pw`) and hashes (`#…`, `!…`)
40
+ for (let i = 2; i < args.length; i++) out.add(i)
41
+ }
42
+ return out
43
+ }
44
+
45
+ /** ITG-032: `SET ? ?` (sanitized), the arguments as text (raw, secrets as `?`), or undefined (off). */
46
+ function statement (name, args, mode) {
47
+ if (mode === 'off') return undefined
48
+ args = args || []
49
+ if (mode !== 'raw') {
50
+ const n = args.length
51
+ return name + ' ?'.repeat(Math.min(n, MAX_PLACEHOLDERS)) + (n > MAX_PLACEHOLDERS ? ' …' : '')
52
+ }
53
+ const secret = secretIndexes(name, args)
54
+ const parts = [name]
55
+ for (let i = 0; i < args.length; i++) {
56
+ if (secret.has(i)) {
57
+ parts.push('?')
58
+ continue
59
+ }
60
+ let t = text(args[i])
61
+ if (t.length > MAX_ARG) t = t.slice(0, MAX_ARG) + '…'
62
+ parts.push(t)
63
+ }
64
+ let s = parts.join(' ')
65
+ if (s.length > MAX_STATEMENT) s = s.slice(0, MAX_STATEMENT) + '…'
66
+ return s
67
+ }
68
+
69
+ /** ITG-032 for a pipeline/transaction: commands = [[name, args]]. */
70
+ function pipelineStatement (commands, mode) {
71
+ if (mode === 'off') return undefined
72
+ const lines = []
73
+ for (const [name, args] of commands.slice(0, MAX_PIPELINE_LINES)) lines.push(statement(name, args, mode) || '')
74
+ if (commands.length > MAX_PIPELINE_LINES) lines.push('… (' + (commands.length - MAX_PIPELINE_LINES) + ' more)')
75
+ let s = lines.join('\n')
76
+ if (s.length > MAX_STATEMENT) s = s.slice(0, MAX_STATEMENT) + '…'
77
+ return s
78
+ }
79
+
80
+ /** ITG-123: the first word of a Redis error reply (WRONGTYPE, ERR, …), or undefined. */
81
+ function errorCode (err) {
82
+ if (!err || typeof err.message !== 'string') return undefined
83
+ // ioredis ReplyError; node-redis ErrorReply and its subclasses (SimpleError, BlobError)
84
+ let reply = /Reply/.test(String(err.name || ''))
85
+ for (let p = Object.getPrototypeOf(err); !reply && p && p !== Error.prototype; p = Object.getPrototypeOf(p)) {
86
+ if (p.constructor && /Reply/.test(p.constructor.name)) reply = true
87
+ }
88
+ if (!reply) return undefined
89
+ const first = err.message.split(/\s/)[0]
90
+ return /^[A-Z][A-Z0-9_]*$/.test(first) ? first : undefined
91
+ }
92
+
93
+ const XADD_FLAGS = { NOMKSTREAM: 0, KEEPREF: 0, DELREF: 0, ACKED: 0, IDMPAUTO: 1, IDMP: 2, LIMIT: 1 }
94
+
95
+ /** Index in `args` (the arguments after XADD, key first) of the first field, or -1. */
96
+ function xaddFieldsStart (args) {
97
+ let i = 1
98
+ const n = args.length
99
+ while (i < n) {
100
+ const t = text(args[i]).toUpperCase()
101
+ if (Object.prototype.hasOwnProperty.call(XADD_FLAGS, t)) {
102
+ i += 1 + XADD_FLAGS[t]
103
+ continue
104
+ }
105
+ if (t === 'MAXLEN' || t === 'MINID') {
106
+ i++
107
+ if (i < n && (text(args[i]) === '=' || text(args[i]) === '~')) i++
108
+ i++
109
+ continue
110
+ }
111
+ const start = i + 1
112
+ if (start >= n || (n - start) % 2) return -1
113
+ return start
114
+ }
115
+ return -1
116
+ }
117
+
118
+ /** ITG-210 with `redisStreamFields`: the trace fields the entry lacks, as a flat [f, v, …]. */
119
+ function xaddExtraFields (args, span) {
120
+ const start = xaddFieldsStart(args)
121
+ if (start < 0) return []
122
+ const present = new Set()
123
+ for (let j = start; j < args.length; j += 2) present.add(text(args[j]).toLowerCase())
124
+ const extra = []
125
+ for (const [k, v] of c.traceHeaders(span)) if (!present.has(k)) extra.push(k, v)
126
+ return extra
127
+ }
128
+
129
+ /** (stream names, group) of an XREAD / XREADGROUP argument list. */
130
+ function streamNames (name, args) {
131
+ const up = args.map((a) => (typeof a === 'string' || a instanceof Uint8Array ? text(a).toUpperCase() : ''))
132
+ let group
133
+ if (name === 'XREADGROUP') {
134
+ const g = up.indexOf('GROUP')
135
+ if (g !== -1 && g + 1 < args.length) group = text(args[g + 1])
136
+ }
137
+ const s = up.indexOf('STREAMS')
138
+ if (s === -1) return { names: [], group }
139
+ const rest = args.slice(s + 1)
140
+ const half = Math.floor(rest.length / 2)
141
+ return { names: rest.slice(0, half).map(text), group }
142
+ }
143
+
144
+ /** Each entry's fields of an XREAD/XREADGROUP reply, as objects or flat arrays. */
145
+ function * streamEntries (resp, depth) {
146
+ depth = depth || 0
147
+ if (depth > 5 || resp === null || resp === undefined) return
148
+ if (Array.isArray(resp)) {
149
+ // an entry: [id, [f, v, …]] or [id, {f: v}]
150
+ if (resp.length === 2 && (typeof resp[0] === 'string' || resp[0] instanceof Uint8Array) &&
151
+ (Array.isArray(resp[1]) || (resp[1] && typeof resp[1] === 'object' && !(resp[1] instanceof Uint8Array))) &&
152
+ isEntryId(resp[0])) {
153
+ yield resp[1]
154
+ return
155
+ }
156
+ for (const v of resp) if (v && typeof v === 'object') yield * streamEntries(v, depth + 1)
157
+ return
158
+ }
159
+ if (resp instanceof Map) {
160
+ for (const v of resp.values()) yield * streamEntries(v, depth + 1)
161
+ return
162
+ }
163
+ if (typeof resp === 'object') {
164
+ // node-redis: { name, messages: [{ id, message: {f: v} }] } or { id, message }
165
+ if (resp.message && typeof resp.message === 'object' && resp.id !== undefined) {
166
+ yield resp.message
167
+ return
168
+ }
169
+ if (Array.isArray(resp.messages)) {
170
+ yield * streamEntries(resp.messages, depth + 1)
171
+ return
172
+ }
173
+ for (const k of Object.keys(resp)) yield * streamEntries(resp[k], depth + 1)
174
+ }
175
+ }
176
+
177
+ function isEntryId (v) {
178
+ return /^\d+-\d+$/.test(text(v))
179
+ }
180
+
181
+ function field (fields, name) {
182
+ if (Array.isArray(fields)) {
183
+ for (let i = 0; i + 1 < fields.length; i += 2) if (text(fields[i]).toLowerCase() === name) return text(fields[i + 1])
184
+ return undefined
185
+ }
186
+ if (fields instanceof Map) {
187
+ for (const [k, v] of fields) if (text(k).toLowerCase() === name) return text(v)
188
+ return undefined
189
+ }
190
+ for (const k of Object.keys(fields)) if (k.toLowerCase() === name) return text(fields[k])
191
+ return undefined
192
+ }
193
+
194
+ /** { contexts, count } of the entries of an XREAD/XREADGROUP reply. */
195
+ function streamContexts (resp) {
196
+ const contexts = []
197
+ let count = 0
198
+ for (const fields of streamEntries(resp)) {
199
+ count++
200
+ const tp = field(fields, 'traceparent')
201
+ if (tp === undefined) continue
202
+ const carrier = { traceparent: tp }
203
+ const ts = field(fields, 'tracestate')
204
+ if (ts) carrier.tracestate = ts
205
+ const bg = field(fields, 'baggage')
206
+ if (bg) carrier.baggage = bg
207
+ const ctx = c.extract(carrier)
208
+ if (ctx) contexts.push(ctx)
209
+ }
210
+ return { contexts, count }
211
+ }
212
+
213
+ /**
214
+ * The messaging span of a stream command (ITG-210), or null when messaging is off / no span:
215
+ * { span, extra (fields to append to XADD), done(result, err) }.
216
+ */
217
+ function streamCall (name, args, peer) {
218
+ if (!c.msgTracer()) return null
219
+ if (name === 'XADD') {
220
+ const dest = args.length ? text(args[0]) : undefined
221
+ const span = c.producer('redis', dest, peer)
222
+ if (!span) return { span: null, extra: [], done () {} }
223
+ const t = c.msgTracer()
224
+ const extra = t && t.messaging.redisStreamFields ? xaddExtraFields(args, span) : []
225
+ return {
226
+ span,
227
+ extra,
228
+ done (result, err) {
229
+ if (!err && result !== null && result !== undefined) span.setAttribute('messaging.message.id', text(result))
230
+ c.end(span, err)
231
+ }
232
+ }
233
+ }
234
+ const { names, group } = streamNames(name, args)
235
+ const tags = Object.assign({}, peer)
236
+ if (group) tags['messaging.consumer.group.name'] = group
237
+ // created now (the span covers only the call), kept only when entries came back
238
+ const span = c.batchConsumer('redis', names.length === 1 ? names[0] : undefined, null, tags, 'receive')
239
+ return {
240
+ span,
241
+ extra: [],
242
+ done (result, err) {
243
+ if (!span) return
244
+ if (err) return c.end(span, err)
245
+ const { contexts, count } = streamContexts(result)
246
+ if (count === 0) return // nothing returned: no span (ITG-023)
247
+ if (count >= 2) span.setAttribute('messaging.batch.message_count', String(count))
248
+ for (const ctx of contexts) span.addLink(ctx)
249
+ c.end(span)
250
+ }
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ statement,
256
+ pipelineStatement,
257
+ errorCode,
258
+ xaddFieldsStart,
259
+ xaddExtraFields,
260
+ streamNames,
261
+ streamContexts,
262
+ streamCall,
263
+ text,
264
+ STREAM_COMMANDS
265
+ }
package/lib/itg/sql.js ADDED
@@ -0,0 +1,302 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Statement helpers for database spans (80 PYI-122/PYI-123, 82 ITG-030/ITG-031; same results as
5
+ // pmx-io's _sql.py): the operation keyword and the sanitised statement text (literals → `?`, bind
6
+ // placeholders kept, comments removed, whitespace collapsed, 2000 characters). One pass over the
7
+ // text; results cached per statement (bounded).
8
+
9
+ const MAX_STATEMENT = 2000
10
+ const CACHE_MAX = 512
11
+ // statements longer than this are not cached: a cache of 512 multi-megabyte INSERT texts (bulk
12
+ // loads with inline values, all distinct) would keep them all alive
13
+ const CACHE_KEY_MAX = 8192
14
+
15
+ const isWs = (c) => c === ' ' || c === '\t' || c === '\r' || c === '\n' || c === '\f' || c === '\v'
16
+ const isIdentStart = (c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
17
+ const isDigit = (c) => c >= '0' && c <= '9'
18
+ const isIdent = (c) => isIdentStart(c) || isDigit(c) || c === '$'
19
+ const isHexD = (c) => isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || c === '_'
20
+ const isAlpha = (c) => /\p{L}/u.test(c)
21
+
22
+ const opCache = new Map()
23
+ const pgCache = new Map()
24
+ const myCache = new Map()
25
+
26
+ function cached (map, key, compute) {
27
+ if (key.length > CACHE_KEY_MAX) return compute(key)
28
+ let v = map.get(key)
29
+ if (v !== undefined) return v
30
+ v = compute(key)
31
+ if (map.size >= CACHE_MAX) map.clear()
32
+ map.set(key, v)
33
+ return v
34
+ }
35
+
36
+ function blockCommentEnd (s, i) {
37
+ // PostgreSQL block comments nest
38
+ let depth = 0
39
+ const n = s.length
40
+ while (i < n) {
41
+ if (s.startsWith('/*', i)) {
42
+ depth++
43
+ i += 2
44
+ } else if (s.startsWith('*/', i)) {
45
+ depth--
46
+ i += 2
47
+ if (depth === 0) return i
48
+ } else {
49
+ i++
50
+ }
51
+ }
52
+ return n
53
+ }
54
+
55
+ function skipWsComments (s, i) {
56
+ const n = s.length
57
+ while (i < n) {
58
+ const c = s[i]
59
+ if (isWs(c) || c === '(') i++
60
+ else if (c === '-' && s.startsWith('--', i)) {
61
+ const j = s.indexOf('\n', i)
62
+ i = j < 0 ? n : j + 1
63
+ } else if (c === '/' && s.startsWith('/*', i)) {
64
+ i = blockCommentEnd(s, i)
65
+ } else break
66
+ }
67
+ return i
68
+ }
69
+
70
+ /** PYI-122: the first keyword after leading whitespace and comments, uppercased, ≤ 16 chars. */
71
+ function operation (statement) {
72
+ if (typeof statement !== 'string') return ''
73
+ return cached(opCache, statement, (s) => {
74
+ const i = skipWsComments(s, 0)
75
+ let j = i
76
+ while (j < s.length && isAlpha(s[j]) && j - i < 16) j++
77
+ return s.slice(i, j).toUpperCase()
78
+ })
79
+ }
80
+
81
+ function truncate (text) {
82
+ if (text.length > MAX_STATEMENT) return text.slice(0, MAX_STATEMENT - 1) + '…'
83
+ return text
84
+ }
85
+
86
+ function quotedEnd (s, i, quote, backslash) {
87
+ const n = s.length
88
+ i++
89
+ while (i < n) {
90
+ const c = s[i]
91
+ if (backslash && c === '\\') {
92
+ i += 2
93
+ continue
94
+ }
95
+ if (c === quote) {
96
+ if (i + 1 < n && s[i + 1] === quote) {
97
+ i += 2
98
+ continue
99
+ }
100
+ return i + 1
101
+ }
102
+ i++
103
+ }
104
+ return n
105
+ }
106
+
107
+ function dollarTag (s, i) {
108
+ const n = s.length
109
+ let j = i + 1
110
+ if (j < n && s[j] === '$') return '$$'
111
+ if (j < n && isIdentStart(s[j])) {
112
+ j++
113
+ while (j < n && isIdent(s[j]) && s[j] !== '$') j++
114
+ if (j < n && s[j] === '$') return s.slice(i, j + 1)
115
+ }
116
+ return ''
117
+ }
118
+
119
+ function numberEnd (s, i) {
120
+ const n = s.length
121
+ if (s[i] === '0' && i + 1 < n && 'xXoObB'.includes(s[i + 1])) {
122
+ let j = i + 2
123
+ while (j < n && isHexD(s[j])) j++
124
+ return j
125
+ }
126
+ let j = i
127
+ while (j < n && (isDigit(s[j]) || s[j] === '_')) j++
128
+ if (j < n && s[j] === '.') {
129
+ j++
130
+ while (j < n && (isDigit(s[j]) || s[j] === '_')) j++
131
+ }
132
+ if (j < n && (s[j] === 'e' || s[j] === 'E')) {
133
+ let k = j + 1
134
+ if (k < n && (s[k] === '+' || s[k] === '-')) k++
135
+ if (k < n && isDigit(s[k])) {
136
+ j = k
137
+ while (j < n && isDigit(s[j])) j++
138
+ }
139
+ }
140
+ return j
141
+ }
142
+
143
+ class Out {
144
+ constructor () {
145
+ this.parts = []
146
+ this.pending = false
147
+ this.len = 0
148
+ }
149
+
150
+ emit (text) {
151
+ if (this.pending && this.parts.length) {
152
+ this.parts.push(' ')
153
+ this.len++
154
+ }
155
+ this.pending = false
156
+ this.parts.push(text)
157
+ this.len += text.length
158
+ }
159
+
160
+ last () {
161
+ const p = this.parts[this.parts.length - 1]
162
+ return p ? p[p.length - 1] : ''
163
+ }
164
+
165
+ /** True once the text is past the statement limit (the rest of the input is not read). */
166
+ full () {
167
+ return this.len > MAX_STATEMENT
168
+ }
169
+
170
+ text () {
171
+ return truncate(this.parts.join(''))
172
+ }
173
+ }
174
+
175
+ const PG_PREFIXES = new Set(['e', 'E', 'b', 'B', 'x', 'X', 'n', 'N'])
176
+
177
+ function sanitizePg (s) {
178
+ const out = new Out()
179
+ const n = s.length
180
+ let i = 0
181
+ while (i < n && !out.full()) {
182
+ const c = s[i]
183
+ if (isWs(c)) {
184
+ out.pending = true
185
+ i++
186
+ } else if (c === '-' && s.startsWith('--', i)) {
187
+ const j = s.indexOf('\n', i)
188
+ i = j < 0 ? n : j
189
+ out.pending = true
190
+ } else if (c === '/' && s.startsWith('/*', i)) {
191
+ i = blockCommentEnd(s, i)
192
+ out.pending = true
193
+ } else if (c === "'") {
194
+ i = quotedEnd(s, i, "'", false)
195
+ out.emit('?')
196
+ } else if (c === '"') {
197
+ const j = quotedEnd(s, i, '"', false)
198
+ out.emit(s.slice(i, j))
199
+ i = j
200
+ } else if (c === '$') {
201
+ const tag = dollarTag(s, i)
202
+ if (tag && !(isIdent(out.last()) && !out.pending)) {
203
+ const end = s.indexOf(tag, i + tag.length)
204
+ i = end < 0 ? n : end + tag.length
205
+ out.emit('?')
206
+ } else {
207
+ let j = i + 1
208
+ while (j < n && isDigit(s[j])) j++
209
+ out.emit(s.slice(i, j)) // bind placeholder $1 (or a lone '$')
210
+ i = j
211
+ }
212
+ } else if (isIdentStart(c)) {
213
+ let j = i + 1
214
+ while (j < n && isIdent(s[j])) j++
215
+ const word = s.slice(i, j)
216
+ if (j < n && s[j] === "'" && PG_PREFIXES.has(word)) {
217
+ i = quotedEnd(s, j, "'", word === 'e' || word === 'E')
218
+ out.emit('?')
219
+ } else if (j + 1 < n && s[j] === '&' && s[j + 1] === "'" && (word === 'u' || word === 'U')) {
220
+ i = quotedEnd(s, j + 1, "'", false)
221
+ out.emit('?')
222
+ } else {
223
+ out.emit(word)
224
+ i = j
225
+ }
226
+ } else if (isDigit(c) || (c === '.' && i + 1 < n && isDigit(s[i + 1]))) {
227
+ i = numberEnd(s, i)
228
+ out.emit('?')
229
+ } else {
230
+ out.emit(c)
231
+ i++
232
+ }
233
+ }
234
+ return out.text()
235
+ }
236
+
237
+ const MY_PREFIXES = new Set(['x', 'X', 'b', 'B', 'n', 'N'])
238
+
239
+ function sanitizeMysql (s) {
240
+ const out = new Out()
241
+ const n = s.length
242
+ let i = 0
243
+ while (i < n && !out.full()) {
244
+ const c = s[i]
245
+ if (isWs(c)) {
246
+ out.pending = true
247
+ i++
248
+ } else if (c === '#' || (c === '-' && s.startsWith('--', i) && (i + 2 >= n || isWs(s[i + 2])))) {
249
+ const j = s.indexOf('\n', i)
250
+ i = j < 0 ? n : j
251
+ out.pending = true
252
+ } else if (c === '/' && s.startsWith('/*', i)) {
253
+ const j = s.indexOf('*/', i + 2) // MySQL comments do not nest
254
+ i = j < 0 ? n : j + 2
255
+ out.pending = true
256
+ } else if (c === "'" || c === '"') {
257
+ i = quotedEnd(s, i, c, true)
258
+ out.emit('?')
259
+ } else if (c === '`') {
260
+ const j = quotedEnd(s, i, '`', false)
261
+ out.emit(s.slice(i, j))
262
+ i = j
263
+ } else if (isIdentStart(c)) {
264
+ let j = i + 1
265
+ while (j < n && isIdent(s[j])) j++
266
+ const word = s.slice(i, j)
267
+ if (j < n && s[j] === "'" && (MY_PREFIXES.has(word) || word.startsWith('_'))) {
268
+ i = quotedEnd(s, j, "'", !['x', 'X', 'b', 'B'].includes(word))
269
+ out.emit('?')
270
+ } else {
271
+ out.emit(word)
272
+ i = j
273
+ }
274
+ } else if (isDigit(c) || (c === '.' && i + 1 < n && isDigit(s[i + 1]))) {
275
+ i = numberEnd(s, i)
276
+ out.emit('?')
277
+ } else {
278
+ out.emit(c)
279
+ i++
280
+ }
281
+ }
282
+ return out.text()
283
+ }
284
+
285
+ /** PYI-123 `sanitized` mode for PostgreSQL (cached). */
286
+ function sanitize (statement) {
287
+ return cached(pgCache, statement, sanitizePg)
288
+ }
289
+
290
+ /** 82 ITG-031 `sanitized` mode for MySQL (cached). */
291
+ function sanitizeMySQL (statement) {
292
+ return cached(myCache, statement, sanitizeMysql)
293
+ }
294
+
295
+ /** The `db.statement` tag for `mode` (`sanitized` | `raw` | `off`), or undefined. */
296
+ function statementTag (statement, mode, mysql) {
297
+ if (mode === 'off' || typeof statement !== 'string') return undefined
298
+ if (mode === 'raw') return truncate(statement)
299
+ return mysql ? sanitizeMySQL(statement) : sanitize(statement)
300
+ }
301
+
302
+ module.exports = { operation, sanitize, sanitizeMySQL, statementTag, truncate, MAX_STATEMENT }