1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["ESEDBReader"]; /* exported ESEDBReader */
8
9ChromeUtils.import("resource://gre/modules/ctypes.jsm");
10ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
11ChromeUtils.import("resource://gre/modules/Services.jsm");
12XPCOMUtils.defineLazyGetter(this, "log", () => {
13  let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
14  let consoleOptions = {
15    maxLogLevelPref: "browser.esedbreader.loglevel",
16    prefix: "ESEDBReader",
17  };
18  return new ConsoleAPI(consoleOptions);
19});
20
21ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
22
23// We have a globally unique identifier for ESE instances. A new one
24// is used for each different database opened.
25let gESEInstanceCounter = 0;
26
27// We limit the length of strings that we read from databases.
28const MAX_STR_LENGTH = 64 * 1024;
29
30// Kernel-related types:
31const KERNEL = {};
32KERNEL.FILETIME = new ctypes.StructType("FILETIME", [
33  {dwLowDateTime: ctypes.uint32_t},
34  {dwHighDateTime: ctypes.uint32_t},
35]);
36KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
37  {wYear: ctypes.uint16_t},
38  {wMonth: ctypes.uint16_t},
39  {wDayOfWeek: ctypes.uint16_t},
40  {wDay: ctypes.uint16_t},
41  {wHour: ctypes.uint16_t},
42  {wMinute: ctypes.uint16_t},
43  {wSecond: ctypes.uint16_t},
44  {wMilliseconds: ctypes.uint16_t},
45]);
46
47// DB column types, cribbed from the ESE header
48var COLUMN_TYPES = {
49  JET_coltypBit:           1, /* True, False, or NULL */
50  JET_coltypUnsignedByte:  2, /* 1-byte integer, unsigned */
51  JET_coltypShort:         3, /* 2-byte integer, signed */
52  JET_coltypLong:          4, /* 4-byte integer, signed */
53  JET_coltypCurrency:      5, /* 8 byte integer, signed */
54  JET_coltypIEEESingle:    6, /* 4-byte IEEE single precision */
55  JET_coltypIEEEDouble:    7, /* 8-byte IEEE double precision */
56  JET_coltypDateTime:      8, /* Integral date, fractional time */
57  JET_coltypBinary:        9, /* Binary data, < 255 bytes */
58  JET_coltypText:         10, /* ANSI text, case insensitive, < 255 bytes */
59  JET_coltypLongBinary:   11, /* Binary data, long value */
60  JET_coltypLongText:     12, /* ANSI text, long value */
61
62  JET_coltypUnsignedLong: 14, /* 4-byte unsigned integer */
63  JET_coltypLongLong:     15, /* 8-byte signed integer */
64  JET_coltypGUID:         16, /* 16-byte globally unique identifier */
65};
66
67// Not very efficient, but only used for error messages
68function getColTypeName(numericValue) {
69  return Object.keys(COLUMN_TYPES).find(t => COLUMN_TYPES[t] == numericValue) || "unknown";
70}
71
72// All type constants and method wrappers go on this object:
73const ESE = {};
74ESE.JET_ERR = ctypes.long;
75ESE.JET_PCWSTR = ctypes.char16_t.ptr;
76// The ESE header calls this JET_API_PTR, but because it isn't ever used as a
77// pointer and because OS.File code implies that the name you give a type
78// matters, I opted for a different name.
79// Note that this is defined differently on 32 vs. 64-bit in the header.
80ESE.JET_API_ITEM = ctypes.voidptr_t.size == 4 ? ctypes.unsigned_long : ctypes.uint64_t;
81ESE.JET_INSTANCE = ESE.JET_API_ITEM;
82ESE.JET_SESID = ESE.JET_API_ITEM;
83ESE.JET_TABLEID = ESE.JET_API_ITEM;
84ESE.JET_COLUMNID = ctypes.unsigned_long;
85ESE.JET_GRBIT = ctypes.unsigned_long;
86ESE.JET_COLTYP = ctypes.unsigned_long;
87ESE.JET_DBID = ctypes.unsigned_long;
88
89ESE.JET_COLUMNDEF = new ctypes.StructType("JET_COLUMNDEF", [
90  {"cbStruct": ctypes.unsigned_long },
91  {"columnid": ESE.JET_COLUMNID },
92  {"coltyp": ESE.JET_COLTYP },
93  {"wCountry": ctypes.unsigned_short }, // sepcifies the country/region for the column definition
94  {"langid": ctypes.unsigned_short },
95  {"cp": ctypes.unsigned_short },
96  {"wCollate": ctypes.unsigned_short }, /* Must be 0 */
97  {"cbMax": ctypes.unsigned_long },
98  {"grbit": ESE.JET_GRBIT },
99]);
100
101// Track open databases
102let gOpenDBs = new Map();
103
104// Track open libraries
105let gLibs = {};
106this.ESE = ESE; // Required for tests.
107this.KERNEL = KERNEL; // ditto
108this.gLibs = gLibs; // ditto
109
110function convertESEError(errorCode) {
111  switch (errorCode) {
112    case -1213 /* JET_errPageSizeMismatch */:
113    case -1002 /* JET_errInvalidName*/:
114    case -1507 /* JET_errColumnNotFound */:
115      // The DB format has changed and we haven't updated this migration code:
116      return "The database format has changed, error code: " + errorCode;
117    case -1032 /* JET_errFileAccessDenied */:
118    case -1207 /* JET_errDatabaseLocked */:
119    case -1302 /* JET_errTableLocked */:
120      return "The database or table is locked, error code: " + errorCode;
121    case -1809 /* JET_errPermissionDenied*/:
122    case -1907 /* JET_errAccessDenied */:
123      return "Access or permission denied, error code: " + errorCode;
124    case -1044 /* JET_errInvalidFilename */:
125      return "Invalid file name";
126    case -1811 /* JET_errFileNotFound */:
127      return "File not found";
128    case -550 /* JET_errDatabaseDirtyShutdown */:
129      return "Database in dirty shutdown state (without the requisite logs?)";
130    case -514 /* JET_errBadLogVersion */:
131      return "Database log version does not match the version of ESE in use.";
132    default:
133      return "Unknown error: " + errorCode;
134  }
135}
136
137function handleESEError(method, methodName, shouldThrow = true, errorLog = true) {
138  return function() {
139    let rv;
140    try {
141      rv = method.apply(null, arguments);
142    } catch (ex) {
143      log.error("Error calling into ctypes method", methodName, ex);
144      throw ex;
145    }
146    let resultCode = parseInt(rv.toString(10), 10);
147    if (resultCode < 0) {
148      if (errorLog) {
149        log.error("Got error " + resultCode + " calling " + methodName);
150      }
151      if (shouldThrow) {
152        throw new Error(convertESEError(rv));
153      }
154    } else if (resultCode > 0 && errorLog) {
155      log.warn("Got warning " + resultCode + " calling " + methodName);
156    }
157    return resultCode;
158  };
159}
160
161
162function declareESEFunction(methodName, ...args) {
163  let declaration = ["Jet" + methodName, ctypes.winapi_abi, ESE.JET_ERR].concat(args);
164  let ctypeMethod = gLibs.ese.declare.apply(gLibs.ese, declaration);
165  ESE[methodName] = handleESEError(ctypeMethod, methodName);
166  ESE["FailSafe" + methodName] = handleESEError(ctypeMethod, methodName, false);
167  ESE["Manual" + methodName] = handleESEError(ctypeMethod, methodName, false, false);
168}
169
170function declareESEFunctions() {
171  declareESEFunction("GetDatabaseFileInfoW", ESE.JET_PCWSTR, ctypes.voidptr_t,
172                     ctypes.unsigned_long, ctypes.unsigned_long);
173
174  declareESEFunction("GetSystemParameterW", ESE.JET_INSTANCE, ESE.JET_SESID,
175                     ctypes.unsigned_long, ESE.JET_API_ITEM.ptr,
176                     ESE.JET_PCWSTR, ctypes.unsigned_long);
177  declareESEFunction("SetSystemParameterW", ESE.JET_INSTANCE.ptr,
178                     ESE.JET_SESID, ctypes.unsigned_long, ESE.JET_API_ITEM,
179                     ESE.JET_PCWSTR);
180  declareESEFunction("CreateInstanceW", ESE.JET_INSTANCE.ptr, ESE.JET_PCWSTR);
181  declareESEFunction("Init", ESE.JET_INSTANCE.ptr);
182
183  declareESEFunction("BeginSessionW", ESE.JET_INSTANCE, ESE.JET_SESID.ptr,
184                     ESE.JET_PCWSTR, ESE.JET_PCWSTR);
185  declareESEFunction("AttachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
186                     ESE.JET_GRBIT);
187  declareESEFunction("DetachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR);
188  declareESEFunction("OpenDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR,
189                     ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT);
190  declareESEFunction("OpenTableW", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_PCWSTR,
191                     ctypes.voidptr_t, ctypes.unsigned_long, ESE.JET_GRBIT,
192                     ESE.JET_TABLEID.ptr);
193
194  declareESEFunction("GetColumnInfoW", ESE.JET_SESID, ESE.JET_DBID,
195                     ESE.JET_PCWSTR, ESE.JET_PCWSTR, ctypes.voidptr_t,
196                     ctypes.unsigned_long, ctypes.unsigned_long);
197
198  declareESEFunction("Move", ESE.JET_SESID, ESE.JET_TABLEID, ctypes.long,
199                     ESE.JET_GRBIT);
200
201  declareESEFunction("RetrieveColumn", ESE.JET_SESID, ESE.JET_TABLEID,
202                     ESE.JET_COLUMNID, ctypes.voidptr_t, ctypes.unsigned_long,
203                     ctypes.unsigned_long.ptr, ESE.JET_GRBIT, ctypes.voidptr_t);
204
205  declareESEFunction("CloseTable", ESE.JET_SESID, ESE.JET_TABLEID);
206  declareESEFunction("CloseDatabase", ESE.JET_SESID, ESE.JET_DBID,
207                     ESE.JET_GRBIT);
208
209  declareESEFunction("EndSession", ESE.JET_SESID, ESE.JET_GRBIT);
210
211  declareESEFunction("Term", ESE.JET_INSTANCE);
212}
213
214function unloadLibraries() {
215  log.debug("Unloading");
216  if (gOpenDBs.size) {
217    log.error("Shouldn't unload libraries before DBs are closed!");
218    for (let db of gOpenDBs.values()) {
219      db._close();
220    }
221  }
222  for (let k of Object.keys(ESE)) {
223    delete ESE[k];
224  }
225  gLibs.ese.close();
226  gLibs.kernel.close();
227  delete gLibs.ese;
228  delete gLibs.kernel;
229}
230
231function loadLibraries() {
232  Services.obs.addObserver(unloadLibraries, "xpcom-shutdown");
233  gLibs.ese = ctypes.open("esent.dll");
234  gLibs.kernel = ctypes.open("kernel32.dll");
235  KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
236    ctypes.winapi_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr);
237
238  declareESEFunctions();
239}
240
241function ESEDB(rootPath, dbPath, logPath) {
242  log.info("Created db");
243  this.rootPath = rootPath;
244  this.dbPath = dbPath;
245  this.logPath = logPath;
246  this._references = 0;
247  this._init();
248}
249
250ESEDB.prototype = {
251  rootPath: null,
252  dbPath: null,
253  logPath: null,
254  _opened: false,
255  _attached: false,
256  _sessionCreated: false,
257  _instanceCreated: false,
258  _dbId: null,
259  _sessionId: null,
260  _instanceId: null,
261
262  _init() {
263    if (!gLibs.ese) {
264      loadLibraries();
265    }
266    this.incrementReferenceCounter();
267    this._internalOpen();
268  },
269
270  _internalOpen() {
271    try {
272      let dbinfo = new ctypes.unsigned_long();
273      ESE.GetDatabaseFileInfoW(this.dbPath, dbinfo.address(),
274                               ctypes.unsigned_long.size, 17);
275
276      let pageSize = ctypes.UInt64.lo(dbinfo.value);
277      ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/,
278                              pageSize, null);
279
280      this._instanceId = new ESE.JET_INSTANCE();
281      ESE.CreateInstanceW(this._instanceId.address(),
282                          "firefox-dbreader-" + (gESEInstanceCounter++));
283      this._instanceCreated = true;
284
285      ESE.SetSystemParameterW(this._instanceId.address(), 0,
286                              0 /* JET_paramSystemPath*/, 0, this.rootPath);
287      ESE.SetSystemParameterW(this._instanceId.address(), 0,
288                              1 /* JET_paramTempPath */, 0, this.rootPath);
289      ESE.SetSystemParameterW(this._instanceId.address(), 0,
290                              2 /* JET_paramLogFilePath*/, 0, this.logPath);
291
292      // Shouldn't try to call JetTerm if the following call fails.
293      this._instanceCreated = false;
294      ESE.Init(this._instanceId.address());
295      this._instanceCreated = true;
296      this._sessionId = new ESE.JET_SESID();
297      ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null,
298                        null);
299      this._sessionCreated = true;
300
301      const JET_bitDbReadOnly = 1;
302      ESE.AttachDatabaseW(this._sessionId, this.dbPath, JET_bitDbReadOnly);
303      this._attached = true;
304      this._dbId = new ESE.JET_DBID();
305      ESE.OpenDatabaseW(this._sessionId, this.dbPath, null,
306                        this._dbId.address(), JET_bitDbReadOnly);
307      this._opened = true;
308    } catch (ex) {
309      try {
310        this._close();
311      } catch (innerException) {
312        Cu.reportError(innerException);
313      }
314      // Make sure caller knows we failed.
315      throw ex;
316    }
317    gOpenDBs.set(this.dbPath, this);
318  },
319
320  checkForColumn(tableName, columnName) {
321    if (!this._opened) {
322      throw new Error("The database was closed!");
323    }
324
325    let columnInfo;
326    try {
327      columnInfo = this._getColumnInfo(tableName, [{name: columnName}]);
328    } catch (ex) {
329      return null;
330    }
331    return columnInfo[0];
332  },
333
334  tableExists(tableName) {
335    if (!this._opened) {
336      throw new Error("The database was closed!");
337    }
338
339    let tableId = new ESE.JET_TABLEID();
340    let rv = ESE.ManualOpenTableW(this._sessionId, this._dbId, tableName, null,
341                                  0, 4 /* JET_bitTableReadOnly */,
342                                  tableId.address());
343    if (rv == -1305 /* JET_errObjectNotFound */) {
344      return false;
345    }
346    if (rv < 0) {
347      log.error("Got error " + rv + " calling OpenTableW");
348      throw new Error(convertESEError(rv));
349    }
350
351    if (rv > 0) {
352      log.error("Got warning " + rv + " calling OpenTableW");
353    }
354    ESE.FailSafeCloseTable(this._sessionId, tableId);
355    return true;
356  },
357
358  * tableItems(tableName, columns) {
359    if (!this._opened) {
360      throw new Error("The database was closed!");
361    }
362
363    let tableOpened = false;
364    let tableId;
365    try {
366      tableId = this._openTable(tableName);
367      tableOpened = true;
368
369      let columnInfo = this._getColumnInfo(tableName, columns);
370
371      let rv = ESE.ManualMove(this._sessionId, tableId,
372                              -2147483648 /* JET_MoveFirst */, 0);
373      if (rv == -1603 /* JET_errNoCurrentRecord */) {
374        // There are no rows in the table.
375        this._closeTable(tableId);
376        return;
377      }
378      if (rv != 0) {
379        throw new Error(convertESEError(rv));
380      }
381
382      do {
383        let rowContents = {};
384        for (let column of columnInfo) {
385          let [buffer, bufferSize] = this._getBufferForColumn(column);
386          // We handle errors manually so we accurately deal with NULL values.
387          let err = ESE.ManualRetrieveColumn(this._sessionId, tableId,
388                                             column.id, buffer.address(),
389                                             bufferSize, null, 0, null);
390          rowContents[column.name] = this._convertResult(column, buffer, err);
391        }
392        yield rowContents;
393      } while (ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 0) === 0);
394    } catch (ex) {
395      if (tableOpened) {
396        this._closeTable(tableId);
397      }
398      throw ex;
399    }
400    this._closeTable(tableId);
401  },
402
403  _openTable(tableName) {
404    let tableId = new ESE.JET_TABLEID();
405    ESE.OpenTableW(this._sessionId, this._dbId, tableName, null,
406                   0, 4 /* JET_bitTableReadOnly */, tableId.address());
407    return tableId;
408  },
409
410  _getBufferForColumn(column) {
411    let buffer;
412    if (column.type == "string") {
413      let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
414      // size on the column is in bytes, 2 bytes to a wchar, so:
415      let charCount = column.dbSize >> 1;
416      buffer = new wchar_tArray(charCount);
417    } else if (column.type == "boolean") {
418      buffer = new ctypes.uint8_t();
419    } else if (column.type == "date") {
420      buffer = new KERNEL.FILETIME();
421    } else if (column.type == "guid") {
422      let byteArray = ctypes.ArrayType(ctypes.uint8_t);
423      buffer = new byteArray(column.dbSize);
424    } else {
425      throw new Error("Unknown type " + column.type);
426    }
427    return [buffer, buffer.constructor.size];
428  },
429
430  _convertResult(column, buffer, err) {
431    if (err != 0) {
432      if (err == 1004) {
433        // Deal with null values:
434        buffer = null;
435      } else {
436        Cu.reportError("Unexpected JET error: " + err + "; retrieving value for column " + column.name);
437        throw new Error(convertESEError(err));
438      }
439    }
440    if (column.type == "string") {
441      return buffer ? buffer.readString() : "";
442    }
443    if (column.type == "boolean") {
444      return buffer ? (buffer.value == 255) : false;
445    }
446    if (column.type == "guid") {
447      if (buffer.length != 16) {
448        Cu.reportError("Buffer size for guid field " + column.id + " should have been 16!");
449        return "";
450      }
451      let rv = "{";
452      for (let i = 0; i < 16; i++) {
453        if (i == 4 || i == 6 || i == 8 || i == 10) {
454          rv += "-";
455        }
456        let byteValue = buffer.addressOfElement(i).contents;
457        // Ensure there's a leading 0
458        rv += ("0" + byteValue.toString(16)).substr(-2);
459      }
460      return rv + "}";
461    }
462    if (column.type == "date") {
463      if (!buffer) {
464        return null;
465      }
466      let systemTime = new KERNEL.SYSTEMTIME();
467      let result = KERNEL.FileTimeToSystemTime(buffer.address(), systemTime.address());
468      if (result == 0) {
469        throw new Error(ctypes.winLastError);
470      }
471
472      // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
473      // then divide by 1000 to get seconds, and round down:
474      return new Date(Date.UTC(systemTime.wYear,
475                                 systemTime.wMonth - 1,
476                                 systemTime.wDay,
477                                 systemTime.wHour,
478                                 systemTime.wMinute,
479                                 systemTime.wSecond,
480                                 systemTime.wMilliseconds));
481    }
482    return undefined;
483  },
484
485  _getColumnInfo(tableName, columns) {
486    let rv = [];
487    for (let column of columns) {
488      let columnInfoFromDB = new ESE.JET_COLUMNDEF();
489      ESE.GetColumnInfoW(this._sessionId, this._dbId, tableName, column.name,
490                         columnInfoFromDB.address(), ESE.JET_COLUMNDEF.size, 0 /* JET_ColInfo */);
491      let dbType = parseInt(columnInfoFromDB.coltyp.toString(10), 10);
492      let dbSize = parseInt(columnInfoFromDB.cbMax.toString(10), 10);
493      if (column.type == "string") {
494        if (dbType != COLUMN_TYPES.JET_coltypLongText &&
495            dbType != COLUMN_TYPES.JET_coltypText) {
496          throw new Error("Invalid column type for column " + column.name +
497                          "; expected text type, got type " + getColTypeName(dbType));
498        }
499        if (dbSize > MAX_STR_LENGTH) {
500          throw new Error("Column " + column.name + " has more than 64k data in it. This API is not designed to handle data that large.");
501        }
502      } else if (column.type == "boolean") {
503        if (dbType != COLUMN_TYPES.JET_coltypBit) {
504          throw new Error("Invalid column type for column " + column.name +
505                          "; expected bit type, got type " + getColTypeName(dbType));
506        }
507      } else if (column.type == "date") {
508        if (dbType != COLUMN_TYPES.JET_coltypLongLong) {
509          throw new Error("Invalid column type for column " + column.name +
510                          "; expected long long type, got type " + getColTypeName(dbType));
511        }
512      } else if (column.type == "guid") {
513        if (dbType != COLUMN_TYPES.JET_coltypGUID) {
514          throw new Error("Invalid column type for column " + column.name +
515                          "; expected guid type, got type " + getColTypeName(dbType));
516        }
517      } else if (column.type) {
518        throw new Error("Unknown column type " + column.type + " requested for column " +
519                        column.name + ", don't know what to do.");
520      }
521
522      rv.push({name: column.name, id: columnInfoFromDB.columnid, type: column.type, dbSize, dbType});
523    }
524    return rv;
525  },
526
527  _closeTable(tableId) {
528    ESE.FailSafeCloseTable(this._sessionId, tableId);
529  },
530
531  _close() {
532    this._internalClose();
533    gOpenDBs.delete(this.dbPath);
534  },
535
536  _internalClose() {
537    if (this._opened) {
538      log.debug("close db");
539      ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
540      log.debug("finished close db");
541      this._opened = false;
542    }
543    if (this._attached) {
544      log.debug("detach db");
545      ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath);
546      this._attached = false;
547    }
548    if (this._sessionCreated) {
549      log.debug("end session");
550      ESE.FailSafeEndSession(this._sessionId, 0);
551      this._sessionCreated = false;
552    }
553    if (this._instanceCreated) {
554      log.debug("term");
555      ESE.FailSafeTerm(this._instanceId);
556      this._instanceCreated = false;
557    }
558  },
559
560  incrementReferenceCounter() {
561    this._references++;
562  },
563
564  decrementReferenceCounter() {
565    this._references--;
566    if (this._references <= 0) {
567      this._close();
568    }
569  },
570};
571
572let ESEDBReader = {
573  openDB(rootDir, dbFile, logDir) {
574    let dbFilePath = dbFile.path;
575    if (gOpenDBs.has(dbFilePath)) {
576      let db = gOpenDBs.get(dbFilePath);
577      db.incrementReferenceCounter();
578      return db;
579    }
580    // ESE is really picky about the trailing slashes according to the docs,
581    // so we do as we're told and ensure those are there:
582    return new ESEDB(rootDir.path + "\\", dbFilePath, logDir.path + "\\");
583  },
584
585  async dbLocked(dbFile) {
586    let options = {winShare: OS.Constants.Win.FILE_SHARE_READ};
587    let locked = true;
588    await OS.File.open(dbFile.path, {read: true}, options).then(fileHandle => {
589      locked = false;
590      // Return the close promise so we wait for the file to be closed again.
591      // Otherwise the file might still be kept open by this handle by the time
592      // that we try to use the ESE APIs to access it.
593      return fileHandle.close();
594    }, () => {
595      Cu.reportError("ESE DB at " + dbFile.path + " is locked.");
596    });
597    return locked;
598  },
599
600  closeDB(db) {
601    db.decrementReferenceCounter();
602  },
603
604  COLUMN_TYPES,
605};
606
607