mikser-io 9.50.1 → 9.50.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "9.50.1",
3
+ "version": "9.50.2",
4
4
  "description": "<p align=\"center\"> <img src=\"mikser-lockup-stacked.svg\" alt=\"mikser\" width=\"198\" /> </p>",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -104,26 +104,45 @@ let db = null
104
104
  // duplicate detection. Same name twice = the later registration wins
105
105
  // (with a warning). Convention: `<owner>` matching the table prefix
106
106
  // (`catalog`, `manifest`, `vector`, etc.).
107
+ // Comments removed, so nothing downstream ever parses prose as DDL.
108
+ //
109
+ // Done to the WHOLE script before anything splits or counts, because both
110
+ // operations are wrong on a comment: a comma inside one ends a clause, and a
111
+ // bracket inside one unbalances the walk that finds a table body. Stripping
112
+ // per-clause after the split cannot work — by then the damage is done, and the
113
+ // fragment after the comma no longer starts with `--` so it never gets
114
+ // stripped at all. That produced real columns named `and`, `a` and `which` on
115
+ // a live deployment, from the prose of the schema's own comments, while the
116
+ // columns that comment described were silently omitted.
117
+ function stripSqlComments(sqlScript) {
118
+ return String(sqlScript ?? '')
119
+ // Block comments first: one may span the `--` of a line comment.
120
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
121
+ // To end of line, while the newlines are still there to end at.
122
+ .replace(/--[^\n]*/g, '')
123
+ }
124
+
107
125
  // Column names a CREATE TABLE body declares, in order.
108
126
  //
109
127
  // Only the leading identifier of each top-level comma-separated clause, and
110
128
  // only when it is not a table constraint. Good enough for the schemas this
111
129
  // engine registers, and deliberately not a SQL parser.
112
130
  function columnsFrom(sqlScript, table) {
131
+ const clean = stripSqlComments(sqlScript)
113
132
  const re = new RegExp(
114
133
  `CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?[\`"'\\[]?${table}[\`"'\\]]?\\s*\\(`, 'i')
115
- const m = re.exec(sqlScript)
134
+ const m = re.exec(clean)
116
135
  if (!m) return []
117
136
  // Walk to the matching close paren so nested types and CHECK(...) do not
118
137
  // end the body early.
119
138
  let depth = 1
120
139
  let i = m.index + m[0].length
121
140
  const start = i
122
- for (; i < sqlScript.length && depth > 0; i++) {
123
- if (sqlScript[i] === '(') depth++
124
- else if (sqlScript[i] === ')') depth--
141
+ for (; i < clean.length && depth > 0; i++) {
142
+ if (clean[i] === '(') depth++
143
+ else if (clean[i] === ')') depth--
125
144
  }
126
- const body = sqlScript.slice(start, i - 1)
145
+ const body = clean.slice(start, i - 1)
127
146
 
128
147
  const clauses = []
129
148
  let current = ''
@@ -137,9 +156,9 @@ function columnsFrom(sqlScript, table) {
137
156
 
138
157
  const CONSTRAINTS = new Set(['primary', 'unique', 'foreign', 'check', 'constraint'])
139
158
  return clauses
140
- .map(clause => clause.replace(/--[^\n]*/g, '').trim())
159
+ .map(clause => clause.trim())
141
160
  .filter(Boolean)
142
- .map(clause => clause.split(/\s+/)[0].replace(/["`\[\]]/g, ''))
161
+ .map(clause => clause.split(/\s+/)[0].replace(/["\`\[\]]/g, ''))
143
162
  .filter(name => name && !CONSTRAINTS.has(name.toLowerCase()))
144
163
  }
145
164
 
@@ -161,7 +180,8 @@ function migrateDurableColumns(handle, schemas, logger) {
161
180
  existing = new Set(handle.prepare(`PRAGMA table_info("${table}")`).all().map(c => c.name))
162
181
  } catch { continue }
163
182
  if (!existing.size) continue
164
- for (const column of columnsFrom(sql, table)) {
183
+ const declared = columnsFrom(sql, table)
184
+ for (const column of declared) {
165
185
  if (existing.has(column)) continue
166
186
  // Only ever ADD. Dropping or retyping a column in a durable
167
187
  // table would discard data the whole flag exists to keep.
@@ -169,9 +189,28 @@ function migrateDurableColumns(handle, schemas, logger) {
169
189
  handle.exec(`ALTER TABLE "${table}" ADD COLUMN "${column}"`)
170
190
  logger?.info('Durable table %s gained column %s', table, column)
171
191
  } catch (err) {
172
- logger?.warn('Could not add column %s to %s: %s', column, table, err.message)
192
+ logger?.error('Could not add column %s to %s: %s', column, table, err.message)
173
193
  }
174
194
  }
195
+
196
+ // Verify, rather than assume the loop above was enough.
197
+ //
198
+ // Silence was the dangerous half of the last bug here: the
199
+ // migration logged what it ADDED and never what it failed to find,
200
+ // so a parser that quietly omitted a column produced a clean-
201
+ // looking upgrade and a write that failed days later on a
202
+ // deployment. A declared column that is still missing after this
203
+ // runs is a fault in the migration itself, and has to say so here
204
+ // rather than surface as "no such column" at the first write.
205
+ const after = new Set(
206
+ handle.prepare(`PRAGMA table_info("${table}")`).all().map(c => c.name))
207
+ const missing = declared.filter(column => !after.has(column))
208
+ if (missing.length) {
209
+ logger?.error(
210
+ 'Durable table %s is missing declared column(s): %s. Writes naming them will fail — this is a '
211
+ + 'fault in the schema migration, not in the caller.',
212
+ table, missing.join(', '))
213
+ }
175
214
  }
176
215
  }
177
216
  }
@@ -189,9 +228,12 @@ function schemaEntry(value) {
189
228
 
190
229
  function tableNamesFrom(sqlScript) {
191
230
  const names = []
231
+ // Comments stripped for the same reason columnsFrom strips them: a
232
+ // comment that happens to mention CREATE TABLE would otherwise register a
233
+ // table that does not exist.
192
234
  const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"'\[]?([A-Za-z_][\w$]*)/gi
193
235
  let m
194
- while ((m = re.exec(sqlScript))) names.push(m[1])
236
+ while ((m = re.exec(stripSqlComments(sqlScript)))) names.push(m[1])
195
237
  return names
196
238
  }
197
239