services/metrics/storage.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 #ifndef MERGED_COMPARTMENT
michael@0 8
michael@0 9 this.EXPORTED_SYMBOLS = [
michael@0 10 "DailyValues",
michael@0 11 "MetricsStorageBackend",
michael@0 12 "dateToDays",
michael@0 13 "daysToDate",
michael@0 14 ];
michael@0 15
michael@0 16 const {utils: Cu} = Components;
michael@0 17
michael@0 18 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
michael@0 19
michael@0 20 #endif
michael@0 21
michael@0 22 Cu.import("resource://gre/modules/Promise.jsm");
michael@0 23 Cu.import("resource://gre/modules/Sqlite.jsm");
michael@0 24 Cu.import("resource://gre/modules/Task.jsm");
michael@0 25 Cu.import("resource://gre/modules/Log.jsm");
michael@0 26 Cu.import("resource://services-common/utils.js");
michael@0 27
michael@0 28
michael@0 29 // These do not account for leap seconds. Meh.
michael@0 30 function dateToDays(date) {
michael@0 31 return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
michael@0 32 }
michael@0 33
michael@0 34 function daysToDate(days) {
michael@0 35 return new Date(days * MILLISECONDS_PER_DAY);
michael@0 36 }
michael@0 37
michael@0 38 /**
michael@0 39 * Represents a collection of per-day values.
michael@0 40 *
michael@0 41 * This is a proxy around a Map which can transparently round Date instances to
michael@0 42 * their appropriate key.
michael@0 43 *
michael@0 44 * This emulates Map by providing .size and iterator support. Note that keys
michael@0 45 * from the iterator are Date instances corresponding to midnight of the start
michael@0 46 * of the day. get(), has(), and set() are modeled as getDay(), hasDay(), and
michael@0 47 * setDay(), respectively.
michael@0 48 *
michael@0 49 * All days are defined in terms of UTC (as opposed to local time).
michael@0 50 */
michael@0 51 this.DailyValues = function () {
michael@0 52 this._days = new Map();
michael@0 53 };
michael@0 54
michael@0 55 DailyValues.prototype = Object.freeze({
michael@0 56 __iterator__: function () {
michael@0 57 for (let [k, v] of this._days) {
michael@0 58 yield [daysToDate(k), v];
michael@0 59 }
michael@0 60 },
michael@0 61
michael@0 62 get size() {
michael@0 63 return this._days.size;
michael@0 64 },
michael@0 65
michael@0 66 hasDay: function (date) {
michael@0 67 return this._days.has(dateToDays(date));
michael@0 68 },
michael@0 69
michael@0 70 getDay: function (date) {
michael@0 71 return this._days.get(dateToDays(date));
michael@0 72 },
michael@0 73
michael@0 74 setDay: function (date, value) {
michael@0 75 this._days.set(dateToDays(date), value);
michael@0 76 },
michael@0 77
michael@0 78 appendValue: function (date, value) {
michael@0 79 let key = dateToDays(date);
michael@0 80
michael@0 81 if (this._days.has(key)) {
michael@0 82 return this._days.get(key).push(value);
michael@0 83 }
michael@0 84
michael@0 85 this._days.set(key, [value]);
michael@0 86 },
michael@0 87 });
michael@0 88
michael@0 89
michael@0 90 /**
michael@0 91 * DATABASE INFO
michael@0 92 * =============
michael@0 93 *
michael@0 94 * We use a SQLite database as the backend for persistent storage of metrics
michael@0 95 * data.
michael@0 96 *
michael@0 97 * Every piece of recorded data is associated with a measurement. A measurement
michael@0 98 * is an entity with a name and version. Each measurement is associated with a
michael@0 99 * named provider.
michael@0 100 *
michael@0 101 * When the metrics system is initialized, we ask providers (the entities that
michael@0 102 * emit data) to configure the database for storage of their data. They tell
michael@0 103 * storage what their requirements are. For example, they'll register
michael@0 104 * named daily counters associated with specific measurements.
michael@0 105 *
michael@0 106 * Recorded data is stored differently depending on the requirements for
michael@0 107 * storing it. We have facilities for storing the following classes of data:
michael@0 108 *
michael@0 109 * 1) Counts of event/field occurrences aggregated by day.
michael@0 110 * 2) Discrete values of fields aggregated by day.
michael@0 111 * 3) Discrete values of fields aggregated by day max 1 per day (last write
michael@0 112 * wins).
michael@0 113 * 4) Discrete values of fields max 1 (last write wins).
michael@0 114 *
michael@0 115 * Most data is aggregated per day mainly for privacy reasons. This does throw
michael@0 116 * away potentially useful data. But, it's not currently used, so there is no
michael@0 117 * need to keep the granular information.
michael@0 118 *
michael@0 119 * Database Schema
michael@0 120 * ---------------
michael@0 121 *
michael@0 122 * This database contains the following tables:
michael@0 123 *
michael@0 124 * providers -- Maps provider string name to an internal ID.
michael@0 125 * provider_state -- Holds opaque persisted state for providers.
michael@0 126 * measurements -- Holds the set of known measurements (name, version,
michael@0 127 * provider tuples).
michael@0 128 * types -- The data types that can be stored in measurements/fields.
michael@0 129 * fields -- Describes entities that occur within measurements.
michael@0 130 * daily_counters -- Holds daily-aggregated counts of events. Each row is
michael@0 131 * associated with a field and a day.
michael@0 132 * daily_discrete_numeric -- Holds numeric values for fields grouped by day.
michael@0 133 * Each row contains a discrete value associated with a field that occurred
michael@0 134 * on a specific day. There can be multiple rows per field per day.
michael@0 135 * daily_discrete_text -- Holds text values for fields grouped by day. Each
michael@0 136 * row contains a discrete value associated with a field that occurred on a
michael@0 137 * specific day.
michael@0 138 * daily_last_numeric -- Holds numeric values where the last encountered
michael@0 139 * value for a given day is retained.
michael@0 140 * daily_last_text -- Like daily_last_numeric except for text values.
michael@0 141 * last_numeric -- Holds the most recent value for a numeric field.
michael@0 142 * last_text -- Like last_numeric except for text fields.
michael@0 143 *
michael@0 144 * Notes
michael@0 145 * -----
michael@0 146 *
michael@0 147 * It is tempting to use SQLite's julianday() function to store days that
michael@0 148 * things happened. However, a Julian Day begins at *noon* in 4714 B.C. This
michael@0 149 * results in weird half day offsets from UNIX time. So, we instead store
michael@0 150 * number of days since UNIX epoch, not Julian.
michael@0 151 */
michael@0 152
michael@0 153 /**
michael@0 154 * All of our SQL statements are stored in a central mapping so they can easily
michael@0 155 * be audited for security, perf, etc.
michael@0 156 */
michael@0 157 const SQL = {
michael@0 158 // Create the providers table.
michael@0 159 createProvidersTable: "\
michael@0 160 CREATE TABLE providers (\
michael@0 161 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 162 name TEXT, \
michael@0 163 UNIQUE (name) \
michael@0 164 )",
michael@0 165
michael@0 166 createProviderStateTable: "\
michael@0 167 CREATE TABLE provider_state (\
michael@0 168 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 169 provider_id INTEGER, \
michael@0 170 name TEXT, \
michael@0 171 VALUE TEXT, \
michael@0 172 UNIQUE (provider_id, name), \
michael@0 173 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE\
michael@0 174 )",
michael@0 175
michael@0 176 createProviderStateProviderIndex: "\
michael@0 177 CREATE INDEX i_provider_state_provider_id ON provider_state (provider_id)",
michael@0 178
michael@0 179 createMeasurementsTable: "\
michael@0 180 CREATE TABLE measurements (\
michael@0 181 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 182 provider_id INTEGER, \
michael@0 183 name TEXT, \
michael@0 184 version INTEGER, \
michael@0 185 UNIQUE (provider_id, name, version), \
michael@0 186 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE\
michael@0 187 )",
michael@0 188
michael@0 189 createMeasurementsProviderIndex: "\
michael@0 190 CREATE INDEX i_measurements_provider_id ON measurements (provider_id)",
michael@0 191
michael@0 192 createMeasurementsView: "\
michael@0 193 CREATE VIEW v_measurements AS \
michael@0 194 SELECT \
michael@0 195 providers.id AS provider_id, \
michael@0 196 providers.name AS provider_name, \
michael@0 197 measurements.id AS measurement_id, \
michael@0 198 measurements.name AS measurement_name, \
michael@0 199 measurements.version AS measurement_version \
michael@0 200 FROM providers, measurements \
michael@0 201 WHERE \
michael@0 202 measurements.provider_id = providers.id",
michael@0 203
michael@0 204 createTypesTable: "\
michael@0 205 CREATE TABLE types (\
michael@0 206 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 207 name TEXT, \
michael@0 208 UNIQUE (name)\
michael@0 209 )",
michael@0 210
michael@0 211 createFieldsTable: "\
michael@0 212 CREATE TABLE fields (\
michael@0 213 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 214 measurement_id INTEGER, \
michael@0 215 name TEXT, \
michael@0 216 value_type INTEGER , \
michael@0 217 UNIQUE (measurement_id, name), \
michael@0 218 FOREIGN KEY (measurement_id) REFERENCES measurements(id) ON DELETE CASCADE \
michael@0 219 FOREIGN KEY (value_type) REFERENCES types(id) ON DELETE CASCADE \
michael@0 220 )",
michael@0 221
michael@0 222 createFieldsMeasurementIndex: "\
michael@0 223 CREATE INDEX i_fields_measurement_id ON fields (measurement_id)",
michael@0 224
michael@0 225 createFieldsView: "\
michael@0 226 CREATE VIEW v_fields AS \
michael@0 227 SELECT \
michael@0 228 providers.id AS provider_id, \
michael@0 229 providers.name AS provider_name, \
michael@0 230 measurements.id AS measurement_id, \
michael@0 231 measurements.name AS measurement_name, \
michael@0 232 measurements.version AS measurement_version, \
michael@0 233 fields.id AS field_id, \
michael@0 234 fields.name AS field_name, \
michael@0 235 types.id AS type_id, \
michael@0 236 types.name AS type_name \
michael@0 237 FROM providers, measurements, fields, types \
michael@0 238 WHERE \
michael@0 239 fields.measurement_id = measurements.id \
michael@0 240 AND measurements.provider_id = providers.id \
michael@0 241 AND fields.value_type = types.id",
michael@0 242
michael@0 243 createDailyCountersTable: "\
michael@0 244 CREATE TABLE daily_counters (\
michael@0 245 field_id INTEGER, \
michael@0 246 day INTEGER, \
michael@0 247 value INTEGER, \
michael@0 248 UNIQUE(field_id, day), \
michael@0 249 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 250 )",
michael@0 251
michael@0 252 createDailyCountersFieldIndex: "\
michael@0 253 CREATE INDEX i_daily_counters_field_id ON daily_counters (field_id)",
michael@0 254
michael@0 255 createDailyCountersDayIndex: "\
michael@0 256 CREATE INDEX i_daily_counters_day ON daily_counters (day)",
michael@0 257
michael@0 258 createDailyCountersView: "\
michael@0 259 CREATE VIEW v_daily_counters AS SELECT \
michael@0 260 providers.id AS provider_id, \
michael@0 261 providers.name AS provider_name, \
michael@0 262 measurements.id AS measurement_id, \
michael@0 263 measurements.name AS measurement_name, \
michael@0 264 measurements.version AS measurement_version, \
michael@0 265 fields.id AS field_id, \
michael@0 266 fields.name AS field_name, \
michael@0 267 daily_counters.day AS day, \
michael@0 268 daily_counters.value AS value \
michael@0 269 FROM providers, measurements, fields, daily_counters \
michael@0 270 WHERE \
michael@0 271 daily_counters.field_id = fields.id \
michael@0 272 AND fields.measurement_id = measurements.id \
michael@0 273 AND measurements.provider_id = providers.id",
michael@0 274
michael@0 275 createDailyDiscreteNumericsTable: "\
michael@0 276 CREATE TABLE daily_discrete_numeric (\
michael@0 277 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 278 field_id INTEGER, \
michael@0 279 day INTEGER, \
michael@0 280 value INTEGER, \
michael@0 281 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 282 )",
michael@0 283
michael@0 284 createDailyDiscreteNumericsFieldIndex: "\
michael@0 285 CREATE INDEX i_daily_discrete_numeric_field_id \
michael@0 286 ON daily_discrete_numeric (field_id)",
michael@0 287
michael@0 288 createDailyDiscreteNumericsDayIndex: "\
michael@0 289 CREATE INDEX i_daily_discrete_numeric_day \
michael@0 290 ON daily_discrete_numeric (day)",
michael@0 291
michael@0 292 createDailyDiscreteTextTable: "\
michael@0 293 CREATE TABLE daily_discrete_text (\
michael@0 294 id INTEGER PRIMARY KEY AUTOINCREMENT, \
michael@0 295 field_id INTEGER, \
michael@0 296 day INTEGER, \
michael@0 297 value TEXT, \
michael@0 298 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 299 )",
michael@0 300
michael@0 301 createDailyDiscreteTextFieldIndex: "\
michael@0 302 CREATE INDEX i_daily_discrete_text_field_id \
michael@0 303 ON daily_discrete_text (field_id)",
michael@0 304
michael@0 305 createDailyDiscreteTextDayIndex: "\
michael@0 306 CREATE INDEX i_daily_discrete_text_day \
michael@0 307 ON daily_discrete_text (day)",
michael@0 308
michael@0 309 createDailyDiscreteView: "\
michael@0 310 CREATE VIEW v_daily_discrete AS \
michael@0 311 SELECT \
michael@0 312 providers.id AS provider_id, \
michael@0 313 providers.name AS provider_name, \
michael@0 314 measurements.id AS measurement_id, \
michael@0 315 measurements.name AS measurement_name, \
michael@0 316 measurements.version AS measurement_version, \
michael@0 317 fields.id AS field_id, \
michael@0 318 fields.name AS field_name, \
michael@0 319 daily_discrete_numeric.id AS value_id, \
michael@0 320 daily_discrete_numeric.day AS day, \
michael@0 321 daily_discrete_numeric.value AS value, \
michael@0 322 \"numeric\" AS value_type \
michael@0 323 FROM providers, measurements, fields, daily_discrete_numeric \
michael@0 324 WHERE \
michael@0 325 daily_discrete_numeric.field_id = fields.id \
michael@0 326 AND fields.measurement_id = measurements.id \
michael@0 327 AND measurements.provider_id = providers.id \
michael@0 328 UNION ALL \
michael@0 329 SELECT \
michael@0 330 providers.id AS provider_id, \
michael@0 331 providers.name AS provider_name, \
michael@0 332 measurements.id AS measurement_id, \
michael@0 333 measurements.name AS measurement_name, \
michael@0 334 measurements.version AS measurement_version, \
michael@0 335 fields.id AS field_id, \
michael@0 336 fields.name AS field_name, \
michael@0 337 daily_discrete_text.id AS value_id, \
michael@0 338 daily_discrete_text.day AS day, \
michael@0 339 daily_discrete_text.value AS value, \
michael@0 340 \"text\" AS value_type \
michael@0 341 FROM providers, measurements, fields, daily_discrete_text \
michael@0 342 WHERE \
michael@0 343 daily_discrete_text.field_id = fields.id \
michael@0 344 AND fields.measurement_id = measurements.id \
michael@0 345 AND measurements.provider_id = providers.id \
michael@0 346 ORDER BY day ASC, value_id ASC",
michael@0 347
michael@0 348 createLastNumericTable: "\
michael@0 349 CREATE TABLE last_numeric (\
michael@0 350 field_id INTEGER PRIMARY KEY, \
michael@0 351 day INTEGER, \
michael@0 352 value NUMERIC, \
michael@0 353 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 354 )",
michael@0 355
michael@0 356 createLastTextTable: "\
michael@0 357 CREATE TABLE last_text (\
michael@0 358 field_id INTEGER PRIMARY KEY, \
michael@0 359 day INTEGER, \
michael@0 360 value TEXT, \
michael@0 361 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 362 )",
michael@0 363
michael@0 364 createLastView: "\
michael@0 365 CREATE VIEW v_last AS \
michael@0 366 SELECT \
michael@0 367 providers.id AS provider_id, \
michael@0 368 providers.name AS provider_name, \
michael@0 369 measurements.id AS measurement_id, \
michael@0 370 measurements.name AS measurement_name, \
michael@0 371 measurements.version AS measurement_version, \
michael@0 372 fields.id AS field_id, \
michael@0 373 fields.name AS field_name, \
michael@0 374 last_numeric.day AS day, \
michael@0 375 last_numeric.value AS value, \
michael@0 376 \"numeric\" AS value_type \
michael@0 377 FROM providers, measurements, fields, last_numeric \
michael@0 378 WHERE \
michael@0 379 last_numeric.field_id = fields.id \
michael@0 380 AND fields.measurement_id = measurements.id \
michael@0 381 AND measurements.provider_id = providers.id \
michael@0 382 UNION ALL \
michael@0 383 SELECT \
michael@0 384 providers.id AS provider_id, \
michael@0 385 providers.name AS provider_name, \
michael@0 386 measurements.id AS measurement_id, \
michael@0 387 measurements.name AS measurement_name, \
michael@0 388 measurements.version AS measurement_version, \
michael@0 389 fields.id AS field_id, \
michael@0 390 fields.name AS field_name, \
michael@0 391 last_text.day AS day, \
michael@0 392 last_text.value AS value, \
michael@0 393 \"text\" AS value_type \
michael@0 394 FROM providers, measurements, fields, last_text \
michael@0 395 WHERE \
michael@0 396 last_text.field_id = fields.id \
michael@0 397 AND fields.measurement_id = measurements.id \
michael@0 398 AND measurements.provider_id = providers.id",
michael@0 399
michael@0 400 createDailyLastNumericTable: "\
michael@0 401 CREATE TABLE daily_last_numeric (\
michael@0 402 field_id INTEGER, \
michael@0 403 day INTEGER, \
michael@0 404 value NUMERIC, \
michael@0 405 UNIQUE (field_id, day) \
michael@0 406 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 407 )",
michael@0 408
michael@0 409 createDailyLastNumericFieldIndex: "\
michael@0 410 CREATE INDEX i_daily_last_numeric_field_id ON daily_last_numeric (field_id)",
michael@0 411
michael@0 412 createDailyLastNumericDayIndex: "\
michael@0 413 CREATE INDEX i_daily_last_numeric_day ON daily_last_numeric (day)",
michael@0 414
michael@0 415 createDailyLastTextTable: "\
michael@0 416 CREATE TABLE daily_last_text (\
michael@0 417 field_id INTEGER, \
michael@0 418 day INTEGER, \
michael@0 419 value TEXT, \
michael@0 420 UNIQUE (field_id, day) \
michael@0 421 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
michael@0 422 )",
michael@0 423
michael@0 424 createDailyLastTextFieldIndex: "\
michael@0 425 CREATE INDEX i_daily_last_text_field_id ON daily_last_text (field_id)",
michael@0 426
michael@0 427 createDailyLastTextDayIndex: "\
michael@0 428 CREATE INDEX i_daily_last_text_day ON daily_last_text (day)",
michael@0 429
michael@0 430 createDailyLastView: "\
michael@0 431 CREATE VIEW v_daily_last AS \
michael@0 432 SELECT \
michael@0 433 providers.id AS provider_id, \
michael@0 434 providers.name AS provider_name, \
michael@0 435 measurements.id AS measurement_id, \
michael@0 436 measurements.name AS measurement_name, \
michael@0 437 measurements.version AS measurement_version, \
michael@0 438 fields.id AS field_id, \
michael@0 439 fields.name AS field_name, \
michael@0 440 daily_last_numeric.day AS day, \
michael@0 441 daily_last_numeric.value AS value, \
michael@0 442 \"numeric\" as value_type \
michael@0 443 FROM providers, measurements, fields, daily_last_numeric \
michael@0 444 WHERE \
michael@0 445 daily_last_numeric.field_id = fields.id \
michael@0 446 AND fields.measurement_id = measurements.id \
michael@0 447 AND measurements.provider_id = providers.id \
michael@0 448 UNION ALL \
michael@0 449 SELECT \
michael@0 450 providers.id AS provider_id, \
michael@0 451 providers.name AS provider_name, \
michael@0 452 measurements.id AS measurement_id, \
michael@0 453 measurements.name AS measurement_name, \
michael@0 454 measurements.version AS measurement_version, \
michael@0 455 fields.id AS field_id, \
michael@0 456 fields.name AS field_name, \
michael@0 457 daily_last_text.day AS day, \
michael@0 458 daily_last_text.value AS value, \
michael@0 459 \"text\" as value_type \
michael@0 460 FROM providers, measurements, fields, daily_last_text \
michael@0 461 WHERE \
michael@0 462 daily_last_text.field_id = fields.id \
michael@0 463 AND fields.measurement_id = measurements.id \
michael@0 464 AND measurements.provider_id = providers.id",
michael@0 465
michael@0 466 // Mutation.
michael@0 467
michael@0 468 addProvider: "INSERT INTO providers (name) VALUES (:provider)",
michael@0 469
michael@0 470 setProviderState: "\
michael@0 471 INSERT OR REPLACE INTO provider_state \
michael@0 472 (provider_id, name, value) \
michael@0 473 VALUES (:provider_id, :name, :value)",
michael@0 474
michael@0 475 addMeasurement: "\
michael@0 476 INSERT INTO measurements (provider_id, name, version) \
michael@0 477 VALUES (:provider_id, :measurement, :version)",
michael@0 478
michael@0 479 addType: "INSERT INTO types (name) VALUES (:name)",
michael@0 480
michael@0 481 addField: "\
michael@0 482 INSERT INTO fields (measurement_id, name, value_type) \
michael@0 483 VALUES (:measurement_id, :field, :value_type)",
michael@0 484
michael@0 485 incrementDailyCounterFromFieldID: "\
michael@0 486 INSERT OR REPLACE INTO daily_counters VALUES (\
michael@0 487 :field_id, \
michael@0 488 :days, \
michael@0 489 COALESCE(\
michael@0 490 (SELECT value FROM daily_counters WHERE \
michael@0 491 field_id = :field_id AND day = :days \
michael@0 492 ), \
michael@0 493 0\
michael@0 494 ) + :by)",
michael@0 495
michael@0 496 deleteLastNumericFromFieldID: "\
michael@0 497 DELETE FROM last_numeric WHERE field_id = :field_id",
michael@0 498
michael@0 499 deleteLastTextFromFieldID: "\
michael@0 500 DELETE FROM last_text WHERE field_id = :field_id",
michael@0 501
michael@0 502 setLastNumeric: "\
michael@0 503 INSERT OR REPLACE INTO last_numeric VALUES (:field_id, :days, :value)",
michael@0 504
michael@0 505 setLastText: "\
michael@0 506 INSERT OR REPLACE INTO last_text VALUES (:field_id, :days, :value)",
michael@0 507
michael@0 508 setDailyLastNumeric: "\
michael@0 509 INSERT OR REPLACE INTO daily_last_numeric VALUES (:field_id, :days, :value)",
michael@0 510
michael@0 511 setDailyLastText: "\
michael@0 512 INSERT OR REPLACE INTO daily_last_text VALUES (:field_id, :days, :value)",
michael@0 513
michael@0 514 addDailyDiscreteNumeric: "\
michael@0 515 INSERT INTO daily_discrete_numeric \
michael@0 516 (field_id, day, value) VALUES (:field_id, :days, :value)",
michael@0 517
michael@0 518 addDailyDiscreteText: "\
michael@0 519 INSERT INTO daily_discrete_text \
michael@0 520 (field_id, day, value) VALUES (:field_id, :days, :value)",
michael@0 521
michael@0 522 pruneOldDailyCounters: "DELETE FROM daily_counters WHERE day < :days",
michael@0 523 pruneOldDailyDiscreteNumeric: "DELETE FROM daily_discrete_numeric WHERE day < :days",
michael@0 524 pruneOldDailyDiscreteText: "DELETE FROM daily_discrete_text WHERE day < :days",
michael@0 525 pruneOldDailyLastNumeric: "DELETE FROM daily_last_numeric WHERE day < :days",
michael@0 526 pruneOldDailyLastText: "DELETE FROM daily_last_text WHERE day < :days",
michael@0 527 pruneOldLastNumeric: "DELETE FROM last_numeric WHERE day < :days",
michael@0 528 pruneOldLastText: "DELETE FROM last_text WHERE day < :days",
michael@0 529
michael@0 530 // Retrieval.
michael@0 531
michael@0 532 getProviderID: "SELECT id FROM providers WHERE name = :provider",
michael@0 533
michael@0 534 getProviders: "SELECT id, name FROM providers",
michael@0 535
michael@0 536 getProviderStateWithName: "\
michael@0 537 SELECT value FROM provider_state \
michael@0 538 WHERE provider_id = :provider_id \
michael@0 539 AND name = :name",
michael@0 540
michael@0 541 getMeasurements: "SELECT * FROM v_measurements",
michael@0 542
michael@0 543 getMeasurementID: "\
michael@0 544 SELECT id FROM measurements \
michael@0 545 WHERE provider_id = :provider_id \
michael@0 546 AND name = :measurement \
michael@0 547 AND version = :version",
michael@0 548
michael@0 549 getFieldID: "\
michael@0 550 SELECT id FROM fields \
michael@0 551 WHERE measurement_id = :measurement_id \
michael@0 552 AND name = :field \
michael@0 553 AND value_type = :value_type \
michael@0 554 ",
michael@0 555
michael@0 556 getTypes: "SELECT * FROM types",
michael@0 557
michael@0 558 getTypeID: "SELECT id FROM types WHERE name = :name",
michael@0 559
michael@0 560 getDailyCounterCountsFromFieldID: "\
michael@0 561 SELECT day, value FROM daily_counters \
michael@0 562 WHERE field_id = :field_id \
michael@0 563 ORDER BY day ASC",
michael@0 564
michael@0 565 getDailyCounterCountFromFieldID: "\
michael@0 566 SELECT value FROM daily_counters \
michael@0 567 WHERE field_id = :field_id \
michael@0 568 AND day = :days",
michael@0 569
michael@0 570 getMeasurementDailyCounters: "\
michael@0 571 SELECT field_name, day, value FROM v_daily_counters \
michael@0 572 WHERE measurement_id = :measurement_id",
michael@0 573
michael@0 574 getFieldInfo: "SELECT * FROM v_fields",
michael@0 575
michael@0 576 getLastNumericFromFieldID: "\
michael@0 577 SELECT day, value FROM last_numeric WHERE field_id = :field_id",
michael@0 578
michael@0 579 getLastTextFromFieldID: "\
michael@0 580 SELECT day, value FROM last_text WHERE field_id = :field_id",
michael@0 581
michael@0 582 getMeasurementLastValues: "\
michael@0 583 SELECT field_name, day, value FROM v_last \
michael@0 584 WHERE measurement_id = :measurement_id",
michael@0 585
michael@0 586 getDailyDiscreteNumericFromFieldID: "\
michael@0 587 SELECT day, value FROM daily_discrete_numeric \
michael@0 588 WHERE field_id = :field_id \
michael@0 589 ORDER BY day ASC, id ASC",
michael@0 590
michael@0 591 getDailyDiscreteNumericFromFieldIDAndDay: "\
michael@0 592 SELECT day, value FROM daily_discrete_numeric \
michael@0 593 WHERE field_id = :field_id AND day = :days \
michael@0 594 ORDER BY id ASC",
michael@0 595
michael@0 596 getDailyDiscreteTextFromFieldID: "\
michael@0 597 SELECT day, value FROM daily_discrete_text \
michael@0 598 WHERE field_id = :field_id \
michael@0 599 ORDER BY day ASC, id ASC",
michael@0 600
michael@0 601 getDailyDiscreteTextFromFieldIDAndDay: "\
michael@0 602 SELECT day, value FROM daily_discrete_text \
michael@0 603 WHERE field_id = :field_id AND day = :days \
michael@0 604 ORDER BY id ASC",
michael@0 605
michael@0 606 getMeasurementDailyDiscreteValues: "\
michael@0 607 SELECT field_name, day, value_id, value FROM v_daily_discrete \
michael@0 608 WHERE measurement_id = :measurement_id \
michael@0 609 ORDER BY day ASC, value_id ASC",
michael@0 610
michael@0 611 getDailyLastNumericFromFieldID: "\
michael@0 612 SELECT day, value FROM daily_last_numeric \
michael@0 613 WHERE field_id = :field_id \
michael@0 614 ORDER BY day ASC",
michael@0 615
michael@0 616 getDailyLastNumericFromFieldIDAndDay: "\
michael@0 617 SELECT day, value FROM daily_last_numeric \
michael@0 618 WHERE field_id = :field_id AND day = :days",
michael@0 619
michael@0 620 getDailyLastTextFromFieldID: "\
michael@0 621 SELECT day, value FROM daily_last_text \
michael@0 622 WHERE field_id = :field_id \
michael@0 623 ORDER BY day ASC",
michael@0 624
michael@0 625 getDailyLastTextFromFieldIDAndDay: "\
michael@0 626 SELECT day, value FROM daily_last_text \
michael@0 627 WHERE field_id = :field_id AND day = :days",
michael@0 628
michael@0 629 getMeasurementDailyLastValues: "\
michael@0 630 SELECT field_name, day, value FROM v_daily_last \
michael@0 631 WHERE measurement_id = :measurement_id",
michael@0 632 };
michael@0 633
michael@0 634
michael@0 635 function dailyKeyFromDate(date) {
michael@0 636 let year = String(date.getUTCFullYear());
michael@0 637 let month = String(date.getUTCMonth() + 1);
michael@0 638 let day = String(date.getUTCDate());
michael@0 639
michael@0 640 if (month.length < 2) {
michael@0 641 month = "0" + month;
michael@0 642 }
michael@0 643
michael@0 644 if (day.length < 2) {
michael@0 645 day = "0" + day;
michael@0 646 }
michael@0 647
michael@0 648 return year + "-" + month + "-" + day;
michael@0 649 }
michael@0 650
michael@0 651
michael@0 652 /**
michael@0 653 * Create a new backend instance bound to a SQLite database at the given path.
michael@0 654 *
michael@0 655 * This returns a promise that will resolve to a `MetricsStorageSqliteBackend`
michael@0 656 * instance. The resolved instance will be initialized and ready for use.
michael@0 657 *
michael@0 658 * Very few consumers have a need to call this. Instead, a higher-level entity
michael@0 659 * likely calls this and sets up the database connection for a service or
michael@0 660 * singleton.
michael@0 661 */
michael@0 662 this.MetricsStorageBackend = function (path) {
michael@0 663 return Task.spawn(function initTask() {
michael@0 664 let connection = yield Sqlite.openConnection({
michael@0 665 path: path,
michael@0 666
michael@0 667 // There should only be one connection per database, so we disable this
michael@0 668 // for perf reasons.
michael@0 669 sharedMemoryCache: false,
michael@0 670 });
michael@0 671
michael@0 672 // If we fail initializing the storage object, we need to close the
michael@0 673 // database connection or else Storage will assert on shutdown.
michael@0 674 let storage;
michael@0 675 try {
michael@0 676 storage = new MetricsStorageSqliteBackend(connection);
michael@0 677 yield storage._init();
michael@0 678 } catch (ex) {
michael@0 679 yield connection.close();
michael@0 680 throw ex;
michael@0 681 }
michael@0 682
michael@0 683 throw new Task.Result(storage);
michael@0 684 });
michael@0 685 };
michael@0 686
michael@0 687
michael@0 688 /**
michael@0 689 * Manages storage of metrics data in a SQLite database.
michael@0 690 *
michael@0 691 * This is the main type used for interfacing with the database.
michael@0 692 *
michael@0 693 * Instances of this should be obtained by calling MetricsStorageConnection().
michael@0 694 *
michael@0 695 * The current implementation will not work if the database is mutated by
michael@0 696 * multiple connections because of the way we cache primary keys.
michael@0 697 *
michael@0 698 * FUTURE enforce 1 read/write connection per database limit.
michael@0 699 */
michael@0 700 function MetricsStorageSqliteBackend(connection) {
michael@0 701 this._log = Log.repository.getLogger("Services.Metrics.MetricsStorage");
michael@0 702
michael@0 703 this._connection = connection;
michael@0 704 this._enabledWALCheckpointPages = null;
michael@0 705
michael@0 706 // Integer IDs to string name.
michael@0 707 this._typesByID = new Map();
michael@0 708
michael@0 709 // String name to integer IDs.
michael@0 710 this._typesByName = new Map();
michael@0 711
michael@0 712 // Maps provider names to integer IDs.
michael@0 713 this._providerIDs = new Map();
michael@0 714
michael@0 715 // Maps :-delimited strings of [provider name, name, version] to integer IDs.
michael@0 716 this._measurementsByInfo = new Map();
michael@0 717
michael@0 718 // Integer IDs to Arrays of [provider name, name, version].
michael@0 719 this._measurementsByID = new Map();
michael@0 720
michael@0 721 // Integer IDs to Arrays of [measurement id, field name, value name]
michael@0 722 this._fieldsByID = new Map();
michael@0 723
michael@0 724 // Maps :-delimited strings of [measurement id, field name] to integer ID.
michael@0 725 this._fieldsByInfo = new Map();
michael@0 726
michael@0 727 // Maps measurement ID to sets of field IDs.
michael@0 728 this._fieldsByMeasurement = new Map();
michael@0 729
michael@0 730 this._queuedOperations = [];
michael@0 731 this._queuedInProgress = false;
michael@0 732 }
michael@0 733
michael@0 734 MetricsStorageSqliteBackend.prototype = Object.freeze({
michael@0 735 // Max size (in kibibytes) the WAL log is allowed to grow to before it is
michael@0 736 // checkpointed.
michael@0 737 //
michael@0 738 // This was first deployed in bug 848136. We want a value large enough
michael@0 739 // that we aren't checkpointing all the time. However, we want it
michael@0 740 // small enough so we don't have to read so much when we open the
michael@0 741 // database.
michael@0 742 MAX_WAL_SIZE_KB: 512,
michael@0 743
michael@0 744 FIELD_DAILY_COUNTER: "daily-counter",
michael@0 745 FIELD_DAILY_DISCRETE_NUMERIC: "daily-discrete-numeric",
michael@0 746 FIELD_DAILY_DISCRETE_TEXT: "daily-discrete-text",
michael@0 747 FIELD_DAILY_LAST_NUMERIC: "daily-last-numeric",
michael@0 748 FIELD_DAILY_LAST_TEXT: "daily-last-text",
michael@0 749 FIELD_LAST_NUMERIC: "last-numeric",
michael@0 750 FIELD_LAST_TEXT: "last-text",
michael@0 751
michael@0 752 _BUILTIN_TYPES: [
michael@0 753 "FIELD_DAILY_COUNTER",
michael@0 754 "FIELD_DAILY_DISCRETE_NUMERIC",
michael@0 755 "FIELD_DAILY_DISCRETE_TEXT",
michael@0 756 "FIELD_DAILY_LAST_NUMERIC",
michael@0 757 "FIELD_DAILY_LAST_TEXT",
michael@0 758 "FIELD_LAST_NUMERIC",
michael@0 759 "FIELD_LAST_TEXT",
michael@0 760 ],
michael@0 761
michael@0 762 // Statements that are used to create the initial DB schema.
michael@0 763 _SCHEMA_STATEMENTS: [
michael@0 764 "createProvidersTable",
michael@0 765 "createProviderStateTable",
michael@0 766 "createProviderStateProviderIndex",
michael@0 767 "createMeasurementsTable",
michael@0 768 "createMeasurementsProviderIndex",
michael@0 769 "createMeasurementsView",
michael@0 770 "createTypesTable",
michael@0 771 "createFieldsTable",
michael@0 772 "createFieldsMeasurementIndex",
michael@0 773 "createFieldsView",
michael@0 774 "createDailyCountersTable",
michael@0 775 "createDailyCountersFieldIndex",
michael@0 776 "createDailyCountersDayIndex",
michael@0 777 "createDailyCountersView",
michael@0 778 "createDailyDiscreteNumericsTable",
michael@0 779 "createDailyDiscreteNumericsFieldIndex",
michael@0 780 "createDailyDiscreteNumericsDayIndex",
michael@0 781 "createDailyDiscreteTextTable",
michael@0 782 "createDailyDiscreteTextFieldIndex",
michael@0 783 "createDailyDiscreteTextDayIndex",
michael@0 784 "createDailyDiscreteView",
michael@0 785 "createDailyLastNumericTable",
michael@0 786 "createDailyLastNumericFieldIndex",
michael@0 787 "createDailyLastNumericDayIndex",
michael@0 788 "createDailyLastTextTable",
michael@0 789 "createDailyLastTextFieldIndex",
michael@0 790 "createDailyLastTextDayIndex",
michael@0 791 "createDailyLastView",
michael@0 792 "createLastNumericTable",
michael@0 793 "createLastTextTable",
michael@0 794 "createLastView",
michael@0 795 ],
michael@0 796
michael@0 797 // Statements that are used to prune old data.
michael@0 798 _PRUNE_STATEMENTS: [
michael@0 799 "pruneOldDailyCounters",
michael@0 800 "pruneOldDailyDiscreteNumeric",
michael@0 801 "pruneOldDailyDiscreteText",
michael@0 802 "pruneOldDailyLastNumeric",
michael@0 803 "pruneOldDailyLastText",
michael@0 804 "pruneOldLastNumeric",
michael@0 805 "pruneOldLastText",
michael@0 806 ],
michael@0 807
michael@0 808 /**
michael@0 809 * Close the database connection.
michael@0 810 *
michael@0 811 * This should be called on all instances or the SQLite layer may complain
michael@0 812 * loudly. After this has been called, the connection cannot be used.
michael@0 813 *
michael@0 814 * @return Promise<>
michael@0 815 */
michael@0 816 close: function () {
michael@0 817 return Task.spawn(function doClose() {
michael@0 818 // There is some light magic involved here. First, we enqueue an
michael@0 819 // operation to ensure that all pending operations have the opportunity
michael@0 820 // to execute. We additionally execute a SQL operation. Due to the FIFO
michael@0 821 // execution order of issued statements, this will cause us to wait on
michael@0 822 // any outstanding statements before closing.
michael@0 823 try {
michael@0 824 yield this.enqueueOperation(function dummyOperation() {
michael@0 825 return this._connection.execute("SELECT 1");
michael@0 826 }.bind(this));
michael@0 827 } catch (ex) {}
michael@0 828
michael@0 829 try {
michael@0 830 yield this._connection.close();
michael@0 831 } finally {
michael@0 832 this._connection = null;
michael@0 833 }
michael@0 834 }.bind(this));
michael@0 835 },
michael@0 836
michael@0 837 /**
michael@0 838 * Whether a provider is known to exist.
michael@0 839 *
michael@0 840 * @param provider
michael@0 841 * (string) Name of the provider.
michael@0 842 */
michael@0 843 hasProvider: function (provider) {
michael@0 844 return this._providerIDs.has(provider);
michael@0 845 },
michael@0 846
michael@0 847 /**
michael@0 848 * Whether a measurement is known to exist.
michael@0 849 *
michael@0 850 * @param provider
michael@0 851 * (string) Name of the provider.
michael@0 852 * @param name
michael@0 853 * (string) Name of the measurement.
michael@0 854 * @param version
michael@0 855 * (Number) Integer measurement version.
michael@0 856 */
michael@0 857 hasMeasurement: function (provider, name, version) {
michael@0 858 return this._measurementsByInfo.has([provider, name, version].join(":"));
michael@0 859 },
michael@0 860
michael@0 861 /**
michael@0 862 * Whether a named field exists in a measurement.
michael@0 863 *
michael@0 864 * @param measurementID
michael@0 865 * (Number) The integer primary key of the measurement.
michael@0 866 * @param field
michael@0 867 * (string) The name of the field to look for.
michael@0 868 */
michael@0 869 hasFieldFromMeasurement: function (measurementID, field) {
michael@0 870 return this._fieldsByInfo.has([measurementID, field].join(":"));
michael@0 871 },
michael@0 872
michael@0 873 /**
michael@0 874 * Whether a field is known.
michael@0 875 *
michael@0 876 * @param provider
michael@0 877 * (string) Name of the provider having the field.
michael@0 878 * @param measurement
michael@0 879 * (string) Name of the measurement in the provider having the field.
michael@0 880 * @param field
michael@0 881 * (string) Name of the field in the measurement.
michael@0 882 */
michael@0 883 hasField: function (provider, measurement, version, field) {
michael@0 884 let key = [provider, measurement, version].join(":");
michael@0 885 let measurementID = this._measurementsByInfo.get(key);
michael@0 886 if (!measurementID) {
michael@0 887 return false;
michael@0 888 }
michael@0 889
michael@0 890 return this.hasFieldFromMeasurement(measurementID, field);
michael@0 891 },
michael@0 892
michael@0 893 /**
michael@0 894 * Look up the integer primary key of a provider.
michael@0 895 *
michael@0 896 * @param provider
michael@0 897 * (string) Name of the provider.
michael@0 898 */
michael@0 899 providerID: function (provider) {
michael@0 900 return this._providerIDs.get(provider);
michael@0 901 },
michael@0 902
michael@0 903 /**
michael@0 904 * Look up the integer primary key of a measurement.
michael@0 905 *
michael@0 906 * @param provider
michael@0 907 * (string) Name of the provider.
michael@0 908 * @param measurement
michael@0 909 * (string) Name of the measurement.
michael@0 910 * @param version
michael@0 911 * (Number) Integer version of the measurement.
michael@0 912 */
michael@0 913 measurementID: function (provider, measurement, version) {
michael@0 914 return this._measurementsByInfo.get([provider, measurement, version].join(":"));
michael@0 915 },
michael@0 916
michael@0 917 fieldIDFromMeasurement: function (measurementID, field) {
michael@0 918 return this._fieldsByInfo.get([measurementID, field].join(":"));
michael@0 919 },
michael@0 920
michael@0 921 fieldID: function (provider, measurement, version, field) {
michael@0 922 let measurementID = this.measurementID(provider, measurement, version);
michael@0 923 if (!measurementID) {
michael@0 924 return null;
michael@0 925 }
michael@0 926
michael@0 927 return this.fieldIDFromMeasurement(measurementID, field);
michael@0 928 },
michael@0 929
michael@0 930 measurementHasAnyDailyCounterFields: function (measurementID) {
michael@0 931 return this.measurementHasAnyFieldsOfTypes(measurementID,
michael@0 932 [this.FIELD_DAILY_COUNTER]);
michael@0 933 },
michael@0 934
michael@0 935 measurementHasAnyLastFields: function (measurementID) {
michael@0 936 return this.measurementHasAnyFieldsOfTypes(measurementID,
michael@0 937 [this.FIELD_LAST_NUMERIC,
michael@0 938 this.FIELD_LAST_TEXT]);
michael@0 939 },
michael@0 940
michael@0 941 measurementHasAnyDailyLastFields: function (measurementID) {
michael@0 942 return this.measurementHasAnyFieldsOfTypes(measurementID,
michael@0 943 [this.FIELD_DAILY_LAST_NUMERIC,
michael@0 944 this.FIELD_DAILY_LAST_TEXT]);
michael@0 945 },
michael@0 946
michael@0 947 measurementHasAnyDailyDiscreteFields: function (measurementID) {
michael@0 948 return this.measurementHasAnyFieldsOfTypes(measurementID,
michael@0 949 [this.FIELD_DAILY_DISCRETE_NUMERIC,
michael@0 950 this.FIELD_DAILY_DISCRETE_TEXT]);
michael@0 951 },
michael@0 952
michael@0 953 measurementHasAnyFieldsOfTypes: function (measurementID, types) {
michael@0 954 if (!this._fieldsByMeasurement.has(measurementID)) {
michael@0 955 return false;
michael@0 956 }
michael@0 957
michael@0 958 let fieldIDs = this._fieldsByMeasurement.get(measurementID);
michael@0 959 for (let fieldID of fieldIDs) {
michael@0 960 let fieldType = this._fieldsByID.get(fieldID)[2];
michael@0 961 if (types.indexOf(fieldType) != -1) {
michael@0 962 return true;
michael@0 963 }
michael@0 964 }
michael@0 965
michael@0 966 return false;
michael@0 967 },
michael@0 968
michael@0 969 /**
michael@0 970 * Register a measurement with the backend.
michael@0 971 *
michael@0 972 * Measurements must be registered before storage can be allocated to them.
michael@0 973 *
michael@0 974 * A measurement consists of a string name and integer version attached
michael@0 975 * to a named provider.
michael@0 976 *
michael@0 977 * This returns a promise that resolves to the storage ID for this
michael@0 978 * measurement.
michael@0 979 *
michael@0 980 * If the measurement is not known to exist, it is registered with storage.
michael@0 981 * If the measurement has already been registered, this is effectively a
michael@0 982 * no-op (that still returns a promise resolving to the storage ID).
michael@0 983 *
michael@0 984 * @param provider
michael@0 985 * (string) Name of the provider this measurement belongs to.
michael@0 986 * @param name
michael@0 987 * (string) Name of this measurement.
michael@0 988 * @param version
michael@0 989 * (Number) Integer version of this measurement.
michael@0 990 */
michael@0 991 registerMeasurement: function (provider, name, version) {
michael@0 992 if (this.hasMeasurement(provider, name, version)) {
michael@0 993 return CommonUtils.laterTickResolvingPromise(
michael@0 994 this.measurementID(provider, name, version));
michael@0 995 }
michael@0 996
michael@0 997 // Registrations might not be safe to perform in parallel with provider
michael@0 998 // operations. So, we queue them.
michael@0 999 let self = this;
michael@0 1000 return this.enqueueOperation(function createMeasurementOperation() {
michael@0 1001 return Task.spawn(function createMeasurement() {
michael@0 1002 let providerID = self._providerIDs.get(provider);
michael@0 1003
michael@0 1004 if (!providerID) {
michael@0 1005 yield self._connection.executeCached(SQL.addProvider, {provider: provider});
michael@0 1006 let rows = yield self._connection.executeCached(SQL.getProviderID,
michael@0 1007 {provider: provider});
michael@0 1008
michael@0 1009 providerID = rows[0].getResultByIndex(0);
michael@0 1010
michael@0 1011 self._providerIDs.set(provider, providerID);
michael@0 1012 }
michael@0 1013
michael@0 1014 let params = {
michael@0 1015 provider_id: providerID,
michael@0 1016 measurement: name,
michael@0 1017 version: version,
michael@0 1018 };
michael@0 1019
michael@0 1020 yield self._connection.executeCached(SQL.addMeasurement, params);
michael@0 1021 let rows = yield self._connection.executeCached(SQL.getMeasurementID, params);
michael@0 1022
michael@0 1023 let measurementID = rows[0].getResultByIndex(0);
michael@0 1024
michael@0 1025 self._measurementsByInfo.set([provider, name, version].join(":"), measurementID);
michael@0 1026 self._measurementsByID.set(measurementID, [provider, name, version]);
michael@0 1027 self._fieldsByMeasurement.set(measurementID, new Set());
michael@0 1028
michael@0 1029 throw new Task.Result(measurementID);
michael@0 1030 });
michael@0 1031 });
michael@0 1032 },
michael@0 1033
michael@0 1034 /**
michael@0 1035 * Register a field with the backend.
michael@0 1036 *
michael@0 1037 * Fields are what recorded pieces of data are primarily associated with.
michael@0 1038 *
michael@0 1039 * Fields are associated with measurements. Measurements must be registered
michael@0 1040 * via `registerMeasurement` before fields can be registered. This is
michael@0 1041 * enforced by this function requiring the database primary key of the
michael@0 1042 * measurement as an argument.
michael@0 1043 *
michael@0 1044 * @param measurementID
michael@0 1045 * (Number) Integer primary key of measurement this field belongs to.
michael@0 1046 * @param field
michael@0 1047 * (string) Name of this field.
michael@0 1048 * @param valueType
michael@0 1049 * (string) Type name of this field. Must be a registered type. Is
michael@0 1050 * likely one of the FIELD_ constants on this type.
michael@0 1051 *
michael@0 1052 * @return Promise<integer>
michael@0 1053 */
michael@0 1054 registerField: function (measurementID, field, valueType) {
michael@0 1055 if (!valueType) {
michael@0 1056 throw new Error("Value type must be defined.");
michael@0 1057 }
michael@0 1058
michael@0 1059 if (!this._measurementsByID.has(measurementID)) {
michael@0 1060 throw new Error("Measurement not known: " + measurementID);
michael@0 1061 }
michael@0 1062
michael@0 1063 if (!this._typesByName.has(valueType)) {
michael@0 1064 throw new Error("Unknown value type: " + valueType);
michael@0 1065 }
michael@0 1066
michael@0 1067 let typeID = this._typesByName.get(valueType);
michael@0 1068
michael@0 1069 if (!typeID) {
michael@0 1070 throw new Error("Undefined type: " + valueType);
michael@0 1071 }
michael@0 1072
michael@0 1073 if (this.hasFieldFromMeasurement(measurementID, field)) {
michael@0 1074 let id = this.fieldIDFromMeasurement(measurementID, field);
michael@0 1075 let existingType = this._fieldsByID.get(id)[2];
michael@0 1076
michael@0 1077 if (valueType != existingType) {
michael@0 1078 throw new Error("Field already defined with different type: " + existingType);
michael@0 1079 }
michael@0 1080
michael@0 1081 return CommonUtils.laterTickResolvingPromise(
michael@0 1082 this.fieldIDFromMeasurement(measurementID, field));
michael@0 1083 }
michael@0 1084
michael@0 1085 let self = this;
michael@0 1086 return Task.spawn(function createField() {
michael@0 1087 let params = {
michael@0 1088 measurement_id: measurementID,
michael@0 1089 field: field,
michael@0 1090 value_type: typeID,
michael@0 1091 };
michael@0 1092
michael@0 1093 yield self._connection.executeCached(SQL.addField, params);
michael@0 1094
michael@0 1095 let rows = yield self._connection.executeCached(SQL.getFieldID, params);
michael@0 1096
michael@0 1097 let fieldID = rows[0].getResultByIndex(0);
michael@0 1098
michael@0 1099 self._fieldsByID.set(fieldID, [measurementID, field, valueType]);
michael@0 1100 self._fieldsByInfo.set([measurementID, field].join(":"), fieldID);
michael@0 1101 self._fieldsByMeasurement.get(measurementID).add(fieldID);
michael@0 1102
michael@0 1103 throw new Task.Result(fieldID);
michael@0 1104 });
michael@0 1105 },
michael@0 1106
michael@0 1107 /**
michael@0 1108 * Initializes this instance with the database.
michael@0 1109 *
michael@0 1110 * This performs 2 major roles:
michael@0 1111 *
michael@0 1112 * 1) Set up database schema (creates tables).
michael@0 1113 * 2) Synchronize database with local instance.
michael@0 1114 */
michael@0 1115 _init: function() {
michael@0 1116 let self = this;
michael@0 1117 return Task.spawn(function initTask() {
michael@0 1118 // 0. Database file and connection configuration.
michael@0 1119
michael@0 1120 // This should never fail. But, we assume the default of 1024 in case it
michael@0 1121 // does.
michael@0 1122 let rows = yield self._connection.execute("PRAGMA page_size");
michael@0 1123 let pageSize = 1024;
michael@0 1124 if (rows.length) {
michael@0 1125 pageSize = rows[0].getResultByIndex(0);
michael@0 1126 }
michael@0 1127
michael@0 1128 self._log.debug("Page size is " + pageSize);
michael@0 1129
michael@0 1130 // Ensure temp tables are stored in memory, not on disk.
michael@0 1131 yield self._connection.execute("PRAGMA temp_store=MEMORY");
michael@0 1132
michael@0 1133 let journalMode;
michael@0 1134 rows = yield self._connection.execute("PRAGMA journal_mode=WAL");
michael@0 1135 if (rows.length) {
michael@0 1136 journalMode = rows[0].getResultByIndex(0);
michael@0 1137 }
michael@0 1138
michael@0 1139 self._log.info("Journal mode is " + journalMode);
michael@0 1140
michael@0 1141 if (journalMode == "wal") {
michael@0 1142 self._enabledWALCheckpointPages =
michael@0 1143 Math.ceil(self.MAX_WAL_SIZE_KB * 1024 / pageSize);
michael@0 1144
michael@0 1145 self._log.info("WAL auto checkpoint pages: " +
michael@0 1146 self._enabledWALCheckpointPages);
michael@0 1147
michael@0 1148 // We disable auto checkpoint during initialization to make it
michael@0 1149 // quicker.
michael@0 1150 yield self.setAutoCheckpoint(0);
michael@0 1151 } else {
michael@0 1152 if (journalMode != "truncate") {
michael@0 1153 // Fall back to truncate (which is faster than delete).
michael@0 1154 yield self._connection.execute("PRAGMA journal_mode=TRUNCATE");
michael@0 1155 }
michael@0 1156
michael@0 1157 // And always use full synchronous mode to reduce possibility for data
michael@0 1158 // loss.
michael@0 1159 yield self._connection.execute("PRAGMA synchronous=FULL");
michael@0 1160 }
michael@0 1161
michael@0 1162 let doCheckpoint = false;
michael@0 1163
michael@0 1164 // 1. Create the schema.
michael@0 1165 yield self._connection.executeTransaction(function ensureSchema(conn) {
michael@0 1166 let schema = yield conn.getSchemaVersion();
michael@0 1167
michael@0 1168 if (schema == 0) {
michael@0 1169 self._log.info("Creating database schema.");
michael@0 1170
michael@0 1171 for (let k of self._SCHEMA_STATEMENTS) {
michael@0 1172 yield self._connection.execute(SQL[k]);
michael@0 1173 }
michael@0 1174
michael@0 1175 yield self._connection.setSchemaVersion(1);
michael@0 1176 doCheckpoint = true;
michael@0 1177 } else if (schema != 1) {
michael@0 1178 throw new Error("Unknown database schema: " + schema);
michael@0 1179 } else {
michael@0 1180 self._log.debug("Database schema up to date.");
michael@0 1181 }
michael@0 1182 });
michael@0 1183
michael@0 1184 // 2. Retrieve existing types.
michael@0 1185 yield self._connection.execute(SQL.getTypes, null, function onRow(row) {
michael@0 1186 let id = row.getResultByName("id");
michael@0 1187 let name = row.getResultByName("name");
michael@0 1188
michael@0 1189 self._typesByID.set(id, name);
michael@0 1190 self._typesByName.set(name, id);
michael@0 1191 });
michael@0 1192
michael@0 1193 // 3. Populate built-in types with database.
michael@0 1194 let missingTypes = [];
michael@0 1195 for (let type of self._BUILTIN_TYPES) {
michael@0 1196 type = self[type];
michael@0 1197 if (self._typesByName.has(type)) {
michael@0 1198 continue;
michael@0 1199 }
michael@0 1200
michael@0 1201 missingTypes.push(type);
michael@0 1202 }
michael@0 1203
michael@0 1204 // Don't perform DB transaction unless there is work to do.
michael@0 1205 if (missingTypes.length) {
michael@0 1206 yield self._connection.executeTransaction(function populateBuiltinTypes() {
michael@0 1207 for (let type of missingTypes) {
michael@0 1208 let params = {name: type};
michael@0 1209 yield self._connection.executeCached(SQL.addType, params);
michael@0 1210 let rows = yield self._connection.executeCached(SQL.getTypeID, params);
michael@0 1211 let id = rows[0].getResultByIndex(0);
michael@0 1212
michael@0 1213 self._typesByID.set(id, type);
michael@0 1214 self._typesByName.set(type, id);
michael@0 1215 }
michael@0 1216 });
michael@0 1217
michael@0 1218 doCheckpoint = true;
michael@0 1219 }
michael@0 1220
michael@0 1221 // 4. Obtain measurement info.
michael@0 1222 yield self._connection.execute(SQL.getMeasurements, null, function onRow(row) {
michael@0 1223 let providerID = row.getResultByName("provider_id");
michael@0 1224 let providerName = row.getResultByName("provider_name");
michael@0 1225 let measurementID = row.getResultByName("measurement_id");
michael@0 1226 let measurementName = row.getResultByName("measurement_name");
michael@0 1227 let measurementVersion = row.getResultByName("measurement_version");
michael@0 1228
michael@0 1229 self._providerIDs.set(providerName, providerID);
michael@0 1230
michael@0 1231 let info = [providerName, measurementName, measurementVersion].join(":");
michael@0 1232
michael@0 1233 self._measurementsByInfo.set(info, measurementID);
michael@0 1234 self._measurementsByID.set(measurementID, info);
michael@0 1235 self._fieldsByMeasurement.set(measurementID, new Set());
michael@0 1236 });
michael@0 1237
michael@0 1238 // 5. Obtain field info.
michael@0 1239 yield self._connection.execute(SQL.getFieldInfo, null, function onRow(row) {
michael@0 1240 let measurementID = row.getResultByName("measurement_id");
michael@0 1241 let fieldID = row.getResultByName("field_id");
michael@0 1242 let fieldName = row.getResultByName("field_name");
michael@0 1243 let typeName = row.getResultByName("type_name");
michael@0 1244
michael@0 1245 self._fieldsByID.set(fieldID, [measurementID, fieldName, typeName]);
michael@0 1246 self._fieldsByInfo.set([measurementID, fieldName].join(":"), fieldID);
michael@0 1247 self._fieldsByMeasurement.get(measurementID).add(fieldID);
michael@0 1248 });
michael@0 1249
michael@0 1250 // Perform a checkpoint after initialization (if needed) and
michael@0 1251 // enable auto checkpoint during regular operation.
michael@0 1252 if (doCheckpoint) {
michael@0 1253 yield self.checkpoint();
michael@0 1254 }
michael@0 1255
michael@0 1256 yield self.setAutoCheckpoint(1);
michael@0 1257 });
michael@0 1258 },
michael@0 1259
michael@0 1260 /**
michael@0 1261 * Prune all data from earlier than the specified date.
michael@0 1262 *
michael@0 1263 * Data stored on days before the specified Date will be permanently
michael@0 1264 * deleted.
michael@0 1265 *
michael@0 1266 * This returns a promise that will be resolved when data has been deleted.
michael@0 1267 *
michael@0 1268 * @param date
michael@0 1269 * (Date) Old data threshold.
michael@0 1270 * @return Promise<>
michael@0 1271 */
michael@0 1272 pruneDataBefore: function (date) {
michael@0 1273 let statements = this._PRUNE_STATEMENTS;
michael@0 1274
michael@0 1275 let self = this;
michael@0 1276 return this.enqueueOperation(function doPrune() {
michael@0 1277 return self._connection.executeTransaction(function prune(conn) {
michael@0 1278 let days = dateToDays(date);
michael@0 1279
michael@0 1280 let params = {days: days};
michael@0 1281 for (let name of statements) {
michael@0 1282 yield conn.execute(SQL[name], params);
michael@0 1283 }
michael@0 1284 });
michael@0 1285 });
michael@0 1286 },
michael@0 1287
michael@0 1288 /**
michael@0 1289 * Reduce memory usage as much as possible.
michael@0 1290 *
michael@0 1291 * This returns a promise that will be resolved on completion.
michael@0 1292 *
michael@0 1293 * @return Promise<>
michael@0 1294 */
michael@0 1295 compact: function () {
michael@0 1296 let self = this;
michael@0 1297 return this.enqueueOperation(function doCompact() {
michael@0 1298 self._connection.discardCachedStatements();
michael@0 1299 return self._connection.shrinkMemory();
michael@0 1300 });
michael@0 1301 },
michael@0 1302
michael@0 1303 /**
michael@0 1304 * Checkpoint writes requiring flush to disk.
michael@0 1305 *
michael@0 1306 * This is called to persist queued and non-flushed writes to disk.
michael@0 1307 * It will force an fsync, so it is expensive and should be used
michael@0 1308 * sparingly.
michael@0 1309 */
michael@0 1310 checkpoint: function () {
michael@0 1311 if (!this._enabledWALCheckpointPages) {
michael@0 1312 return CommonUtils.laterTickResolvingPromise();
michael@0 1313 }
michael@0 1314
michael@0 1315 return this.enqueueOperation(function checkpoint() {
michael@0 1316 this._log.info("Performing manual WAL checkpoint.");
michael@0 1317 return this._connection.execute("PRAGMA wal_checkpoint");
michael@0 1318 }.bind(this));
michael@0 1319 },
michael@0 1320
michael@0 1321 setAutoCheckpoint: function (on) {
michael@0 1322 // If we aren't in WAL mode, wal_autocheckpoint won't do anything so
michael@0 1323 // we no-op.
michael@0 1324 if (!this._enabledWALCheckpointPages) {
michael@0 1325 return CommonUtils.laterTickResolvingPromise();
michael@0 1326 }
michael@0 1327
michael@0 1328 let val = on ? this._enabledWALCheckpointPages : 0;
michael@0 1329
michael@0 1330 return this.enqueueOperation(function setWALCheckpoint() {
michael@0 1331 this._log.info("Setting WAL auto checkpoint to " + val);
michael@0 1332 return this._connection.execute("PRAGMA wal_autocheckpoint=" + val);
michael@0 1333 }.bind(this));
michael@0 1334 },
michael@0 1335
michael@0 1336 /**
michael@0 1337 * Ensure a field ID matches a specified type.
michael@0 1338 *
michael@0 1339 * This is called internally as part of adding values to ensure that
michael@0 1340 * the type of a field matches the operation being performed.
michael@0 1341 */
michael@0 1342 _ensureFieldType: function (id, type) {
michael@0 1343 let info = this._fieldsByID.get(id);
michael@0 1344
michael@0 1345 if (!info || !Array.isArray(info)) {
michael@0 1346 throw new Error("Unknown field ID: " + id);
michael@0 1347 }
michael@0 1348
michael@0 1349 if (type != info[2]) {
michael@0 1350 throw new Error("Field type does not match the expected for this " +
michael@0 1351 "operation. Actual: " + info[2] + "; Expected: " +
michael@0 1352 type);
michael@0 1353 }
michael@0 1354 },
michael@0 1355
michael@0 1356 /**
michael@0 1357 * Enqueue a storage operation to be performed when the database is ready.
michael@0 1358 *
michael@0 1359 * The primary use case of this function is to prevent potentially
michael@0 1360 * conflicting storage operations from being performed in parallel. By
michael@0 1361 * calling this function, passed storage operations will be serially
michael@0 1362 * executed, avoiding potential order of operation issues.
michael@0 1363 *
michael@0 1364 * The passed argument is a function that will perform storage operations.
michael@0 1365 * The function should return a promise that will be resolved when all
michael@0 1366 * storage operations have been completed.
michael@0 1367 *
michael@0 1368 * The passed function may be executed immediately. If there are already
michael@0 1369 * queued operations, it will be appended to the queue and executed after all
michael@0 1370 * before it have finished.
michael@0 1371 *
michael@0 1372 * This function returns a promise that will be resolved or rejected with
michael@0 1373 * the same value that the function's promise was resolved or rejected with.
michael@0 1374 *
michael@0 1375 * @param func
michael@0 1376 * (function) Function performing storage interactions.
michael@0 1377 * @return Promise<>
michael@0 1378 */
michael@0 1379 enqueueOperation: function (func) {
michael@0 1380 if (typeof(func) != "function") {
michael@0 1381 throw new Error("enqueueOperation expects a function. Got: " + typeof(func));
michael@0 1382 }
michael@0 1383
michael@0 1384 this._log.trace("Enqueueing operation.");
michael@0 1385 let deferred = Promise.defer();
michael@0 1386
michael@0 1387 this._queuedOperations.push([func, deferred]);
michael@0 1388
michael@0 1389 if (this._queuedOperations.length == 1) {
michael@0 1390 this._popAndPerformQueuedOperation();
michael@0 1391 }
michael@0 1392
michael@0 1393 return deferred.promise;
michael@0 1394 },
michael@0 1395
michael@0 1396 /**
michael@0 1397 * Enqueue a function to be performed as a transaction.
michael@0 1398 *
michael@0 1399 * The passed function should be a generator suitable for calling with
michael@0 1400 * `executeTransaction` from the SQLite connection.
michael@0 1401 */
michael@0 1402 enqueueTransaction: function (func, type) {
michael@0 1403 return this.enqueueOperation(
michael@0 1404 this._connection.executeTransaction.bind(this._connection, func, type)
michael@0 1405 );
michael@0 1406 },
michael@0 1407
michael@0 1408 _popAndPerformQueuedOperation: function () {
michael@0 1409 if (!this._queuedOperations.length || this._queuedInProgress) {
michael@0 1410 return;
michael@0 1411 }
michael@0 1412
michael@0 1413 this._log.trace("Performing queued operation.");
michael@0 1414 let [func, deferred] = this._queuedOperations.shift();
michael@0 1415 let promise;
michael@0 1416
michael@0 1417 try {
michael@0 1418 this._queuedInProgress = true;
michael@0 1419 promise = func();
michael@0 1420 } catch (ex) {
michael@0 1421 this._log.warn("Queued operation threw during execution: " +
michael@0 1422 CommonUtils.exceptionStr(ex));
michael@0 1423 this._queuedInProgress = false;
michael@0 1424 deferred.reject(ex);
michael@0 1425 this._popAndPerformQueuedOperation();
michael@0 1426 return;
michael@0 1427 }
michael@0 1428
michael@0 1429 if (!promise || typeof(promise.then) != "function") {
michael@0 1430 let msg = "Queued operation did not return a promise: " + func;
michael@0 1431 this._log.warn(msg);
michael@0 1432
michael@0 1433 this._queuedInProgress = false;
michael@0 1434 deferred.reject(new Error(msg));
michael@0 1435 this._popAndPerformQueuedOperation();
michael@0 1436 return;
michael@0 1437 }
michael@0 1438
michael@0 1439 promise.then(
michael@0 1440 function onSuccess(result) {
michael@0 1441 this._log.trace("Queued operation completed.");
michael@0 1442 this._queuedInProgress = false;
michael@0 1443 deferred.resolve(result);
michael@0 1444 this._popAndPerformQueuedOperation();
michael@0 1445 }.bind(this),
michael@0 1446 function onError(error) {
michael@0 1447 this._log.warn("Failure when performing queued operation: " +
michael@0 1448 CommonUtils.exceptionStr(error));
michael@0 1449 this._queuedInProgress = false;
michael@0 1450 deferred.reject(error);
michael@0 1451 this._popAndPerformQueuedOperation();
michael@0 1452 }.bind(this)
michael@0 1453 );
michael@0 1454 },
michael@0 1455
michael@0 1456 /**
michael@0 1457 * Obtain all values associated with a measurement.
michael@0 1458 *
michael@0 1459 * This returns a promise that resolves to an object. The keys of the object
michael@0 1460 * are:
michael@0 1461 *
michael@0 1462 * days -- DailyValues where the values are Maps of field name to data
michael@0 1463 * structures. The data structures could be simple (string or number) or
michael@0 1464 * Arrays if the field type allows multiple values per day.
michael@0 1465 *
michael@0 1466 * singular -- Map of field names to values. This holds all fields that
michael@0 1467 * don't have a temporal component.
michael@0 1468 *
michael@0 1469 * @param id
michael@0 1470 * (Number) Primary key of measurement whose values to retrieve.
michael@0 1471 */
michael@0 1472 getMeasurementValues: function (id) {
michael@0 1473 let deferred = Promise.defer();
michael@0 1474 let days = new DailyValues();
michael@0 1475 let singular = new Map();
michael@0 1476
michael@0 1477 let self = this;
michael@0 1478 this.enqueueOperation(function enqueuedGetMeasurementValues() {
michael@0 1479 return Task.spawn(function fetchMeasurementValues() {
michael@0 1480 function handleResult(data) {
michael@0 1481 for (let [field, values] of data) {
michael@0 1482 for (let [day, value] of Iterator(values)) {
michael@0 1483 if (!days.hasDay(day)) {
michael@0 1484 days.setDay(day, new Map());
michael@0 1485 }
michael@0 1486
michael@0 1487 days.getDay(day).set(field, value);
michael@0 1488 }
michael@0 1489 }
michael@0 1490 }
michael@0 1491
michael@0 1492 if (self.measurementHasAnyDailyCounterFields(id)) {
michael@0 1493 let counters = yield self.getMeasurementDailyCountersFromMeasurementID(id);
michael@0 1494 handleResult(counters);
michael@0 1495 }
michael@0 1496
michael@0 1497 if (self.measurementHasAnyDailyLastFields(id)) {
michael@0 1498 let dailyLast = yield self.getMeasurementDailyLastValuesFromMeasurementID(id);
michael@0 1499 handleResult(dailyLast);
michael@0 1500 }
michael@0 1501
michael@0 1502 if (self.measurementHasAnyDailyDiscreteFields(id)) {
michael@0 1503 let dailyDiscrete = yield self.getMeasurementDailyDiscreteValuesFromMeasurementID(id);
michael@0 1504 handleResult(dailyDiscrete);
michael@0 1505 }
michael@0 1506
michael@0 1507 if (self.measurementHasAnyLastFields(id)) {
michael@0 1508 let last = yield self.getMeasurementLastValuesFromMeasurementID(id);
michael@0 1509
michael@0 1510 for (let [field, value] of last) {
michael@0 1511 singular.set(field, value);
michael@0 1512 }
michael@0 1513 }
michael@0 1514
michael@0 1515 });
michael@0 1516 }).then(function onSuccess() {
michael@0 1517 deferred.resolve({singular: singular, days: days});
michael@0 1518 }, function onError(error) {
michael@0 1519 deferred.reject(error);
michael@0 1520 });
michael@0 1521
michael@0 1522 return deferred.promise;
michael@0 1523 },
michael@0 1524
michael@0 1525 //---------------------------------------------------------------------------
michael@0 1526 // Low-level storage operations
michael@0 1527 //
michael@0 1528 // These will be performed immediately (or at least as soon as the underlying
michael@0 1529 // connection allows them to be.) It is recommended to call these from within
michael@0 1530 // a function added via `enqueueOperation()` or they may inadvertently be
michael@0 1531 // performed during another enqueued operation, which may be a transaction
michael@0 1532 // that is rolled back.
michael@0 1533 // ---------------------------------------------------------------------------
michael@0 1534
michael@0 1535 /**
michael@0 1536 * Set state for a provider.
michael@0 1537 *
michael@0 1538 * Providers have the ability to register persistent state with the backend.
michael@0 1539 * Persistent state doesn't expire. The format of the data is completely up
michael@0 1540 * to the provider beyond the requirement that values be UTF-8 strings.
michael@0 1541 *
michael@0 1542 * This returns a promise that will be resolved when the underlying database
michael@0 1543 * operation has completed.
michael@0 1544 *
michael@0 1545 * @param provider
michael@0 1546 * (string) Name of the provider.
michael@0 1547 * @param key
michael@0 1548 * (string) Key under which to store this state.
michael@0 1549 * @param value
michael@0 1550 * (string) Value for this state.
michael@0 1551 * @return Promise<>
michael@0 1552 */
michael@0 1553 setProviderState: function (provider, key, value) {
michael@0 1554 if (typeof(key) != "string") {
michael@0 1555 throw new Error("State key must be a string. Got: " + key);
michael@0 1556 }
michael@0 1557
michael@0 1558 if (typeof(value) != "string") {
michael@0 1559 throw new Error("State value must be a string. Got: " + value);
michael@0 1560 }
michael@0 1561
michael@0 1562 let id = this.providerID(provider);
michael@0 1563 if (!id) {
michael@0 1564 throw new Error("Unknown provider: " + provider);
michael@0 1565 }
michael@0 1566
michael@0 1567 return this._connection.executeCached(SQL.setProviderState, {
michael@0 1568 provider_id: id,
michael@0 1569 name: key,
michael@0 1570 value: value,
michael@0 1571 });
michael@0 1572 },
michael@0 1573
michael@0 1574 /**
michael@0 1575 * Obtain named state for a provider.
michael@0 1576 *
michael@0 1577 *
michael@0 1578 * The returned promise will resolve to the state from the database or null
michael@0 1579 * if the key is not stored.
michael@0 1580 *
michael@0 1581 * @param provider
michael@0 1582 * (string) The name of the provider whose state to obtain.
michael@0 1583 * @param key
michael@0 1584 * (string) The state's key to retrieve.
michael@0 1585 *
michael@0 1586 * @return Promise<data>
michael@0 1587 */
michael@0 1588 getProviderState: function (provider, key) {
michael@0 1589 let id = this.providerID(provider);
michael@0 1590 if (!id) {
michael@0 1591 throw new Error("Unknown provider: " + provider);
michael@0 1592 }
michael@0 1593
michael@0 1594 let conn = this._connection;
michael@0 1595 return Task.spawn(function queryDB() {
michael@0 1596 let rows = yield conn.executeCached(SQL.getProviderStateWithName, {
michael@0 1597 provider_id: id,
michael@0 1598 name: key,
michael@0 1599 });
michael@0 1600
michael@0 1601 if (!rows.length) {
michael@0 1602 throw new Task.Result(null);
michael@0 1603 }
michael@0 1604
michael@0 1605 throw new Task.Result(rows[0].getResultByIndex(0));
michael@0 1606 });
michael@0 1607 },
michael@0 1608
michael@0 1609 /**
michael@0 1610 * Increment a daily counter from a numeric field id.
michael@0 1611 *
michael@0 1612 * @param id
michael@0 1613 * (integer) Primary key of field to increment.
michael@0 1614 * @param date
michael@0 1615 * (Date) When the increment occurred. This is typically "now" but can
michael@0 1616 * be explicitly defined for events that occurred in the past.
michael@0 1617 * @param by
michael@0 1618 * (integer) How much to increment the value by. Defaults to 1.
michael@0 1619 */
michael@0 1620 incrementDailyCounterFromFieldID: function (id, date=new Date(), by=1) {
michael@0 1621 this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
michael@0 1622
michael@0 1623 let params = {
michael@0 1624 field_id: id,
michael@0 1625 days: dateToDays(date),
michael@0 1626 by: by,
michael@0 1627 };
michael@0 1628
michael@0 1629 return this._connection.executeCached(SQL.incrementDailyCounterFromFieldID,
michael@0 1630 params);
michael@0 1631 },
michael@0 1632
michael@0 1633 /**
michael@0 1634 * Obtain all counts for a specific daily counter.
michael@0 1635 *
michael@0 1636 * @param id
michael@0 1637 * (integer) The ID of the field being retrieved.
michael@0 1638 */
michael@0 1639 getDailyCounterCountsFromFieldID: function (id) {
michael@0 1640 this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
michael@0 1641
michael@0 1642 let self = this;
michael@0 1643 return Task.spawn(function fetchCounterDays() {
michael@0 1644 let rows = yield self._connection.executeCached(SQL.getDailyCounterCountsFromFieldID,
michael@0 1645 {field_id: id});
michael@0 1646
michael@0 1647 let result = new DailyValues();
michael@0 1648 for (let row of rows) {
michael@0 1649 let days = row.getResultByIndex(0);
michael@0 1650 let counter = row.getResultByIndex(1);
michael@0 1651
michael@0 1652 let date = daysToDate(days);
michael@0 1653 result.setDay(date, counter);
michael@0 1654 }
michael@0 1655
michael@0 1656 throw new Task.Result(result);
michael@0 1657 });
michael@0 1658 },
michael@0 1659
michael@0 1660 /**
michael@0 1661 * Get the value of a daily counter for a given day.
michael@0 1662 *
michael@0 1663 * @param field
michael@0 1664 * (integer) Field ID to retrieve.
michael@0 1665 * @param date
michael@0 1666 * (Date) Date for day from which to obtain data.
michael@0 1667 */
michael@0 1668 getDailyCounterCountFromFieldID: function (field, date) {
michael@0 1669 this._ensureFieldType(field, this.FIELD_DAILY_COUNTER);
michael@0 1670
michael@0 1671 let params = {
michael@0 1672 field_id: field,
michael@0 1673 days: dateToDays(date),
michael@0 1674 };
michael@0 1675
michael@0 1676 let self = this;
michael@0 1677 return Task.spawn(function fetchCounter() {
michael@0 1678 let rows = yield self._connection.executeCached(SQL.getDailyCounterCountFromFieldID,
michael@0 1679 params);
michael@0 1680 if (!rows.length) {
michael@0 1681 throw new Task.Result(null);
michael@0 1682 }
michael@0 1683
michael@0 1684 throw new Task.Result(rows[0].getResultByIndex(0));
michael@0 1685 });
michael@0 1686 },
michael@0 1687
michael@0 1688 /**
michael@0 1689 * Define the value for a "last numeric" field.
michael@0 1690 *
michael@0 1691 * The previous value (if any) will be replaced by the value passed, even if
michael@0 1692 * the date of the incoming value is older than what's recorded in the
michael@0 1693 * database.
michael@0 1694 *
michael@0 1695 * @param fieldID
michael@0 1696 * (Number) Integer primary key of field to update.
michael@0 1697 * @param value
michael@0 1698 * (Number) Value to record.
michael@0 1699 * @param date
michael@0 1700 * (Date) When this value was produced.
michael@0 1701 */
michael@0 1702 setLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1703 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
michael@0 1704
michael@0 1705 if (typeof(value) != "number") {
michael@0 1706 throw new Error("Value is not a number: " + value);
michael@0 1707 }
michael@0 1708
michael@0 1709 let params = {
michael@0 1710 field_id: fieldID,
michael@0 1711 days: dateToDays(date),
michael@0 1712 value: value,
michael@0 1713 };
michael@0 1714
michael@0 1715 return this._connection.executeCached(SQL.setLastNumeric, params);
michael@0 1716 },
michael@0 1717
michael@0 1718 /**
michael@0 1719 * Define the value of a "last text" field.
michael@0 1720 *
michael@0 1721 * See `setLastNumericFromFieldID` for behavior.
michael@0 1722 */
michael@0 1723 setLastTextFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1724 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
michael@0 1725
michael@0 1726 if (typeof(value) != "string") {
michael@0 1727 throw new Error("Value is not a string: " + value);
michael@0 1728 }
michael@0 1729
michael@0 1730 let params = {
michael@0 1731 field_id: fieldID,
michael@0 1732 days: dateToDays(date),
michael@0 1733 value: value,
michael@0 1734 };
michael@0 1735
michael@0 1736 return this._connection.executeCached(SQL.setLastText, params);
michael@0 1737 },
michael@0 1738
michael@0 1739 /**
michael@0 1740 * Obtain the value of a "last numeric" field.
michael@0 1741 *
michael@0 1742 * This returns a promise that will be resolved with an Array of [date, value]
michael@0 1743 * if a value is known or null if no last value is present.
michael@0 1744 *
michael@0 1745 * @param fieldID
michael@0 1746 * (Number) Integer primary key of field to retrieve.
michael@0 1747 */
michael@0 1748 getLastNumericFromFieldID: function (fieldID) {
michael@0 1749 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
michael@0 1750
michael@0 1751 let self = this;
michael@0 1752 return Task.spawn(function fetchLastField() {
michael@0 1753 let rows = yield self._connection.executeCached(SQL.getLastNumericFromFieldID,
michael@0 1754 {field_id: fieldID});
michael@0 1755
michael@0 1756 if (!rows.length) {
michael@0 1757 throw new Task.Result(null);
michael@0 1758 }
michael@0 1759
michael@0 1760 let row = rows[0];
michael@0 1761 let days = row.getResultByIndex(0);
michael@0 1762 let value = row.getResultByIndex(1);
michael@0 1763
michael@0 1764 throw new Task.Result([daysToDate(days), value]);
michael@0 1765 });
michael@0 1766 },
michael@0 1767
michael@0 1768 /**
michael@0 1769 * Obtain the value of a "last text" field.
michael@0 1770 *
michael@0 1771 * See `getLastNumericFromFieldID` for behavior.
michael@0 1772 */
michael@0 1773 getLastTextFromFieldID: function (fieldID) {
michael@0 1774 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
michael@0 1775
michael@0 1776 let self = this;
michael@0 1777 return Task.spawn(function fetchLastField() {
michael@0 1778 let rows = yield self._connection.executeCached(SQL.getLastTextFromFieldID,
michael@0 1779 {field_id: fieldID});
michael@0 1780
michael@0 1781 if (!rows.length) {
michael@0 1782 throw new Task.Result(null);
michael@0 1783 }
michael@0 1784
michael@0 1785 let row = rows[0];
michael@0 1786 let days = row.getResultByIndex(0);
michael@0 1787 let value = row.getResultByIndex(1);
michael@0 1788
michael@0 1789 throw new Task.Result([daysToDate(days), value]);
michael@0 1790 });
michael@0 1791 },
michael@0 1792
michael@0 1793 /**
michael@0 1794 * Delete the value (if any) in a "last numeric" field.
michael@0 1795 */
michael@0 1796 deleteLastNumericFromFieldID: function (fieldID) {
michael@0 1797 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
michael@0 1798
michael@0 1799 return this._connection.executeCached(SQL.deleteLastNumericFromFieldID,
michael@0 1800 {field_id: fieldID});
michael@0 1801 },
michael@0 1802
michael@0 1803 /**
michael@0 1804 * Delete the value (if any) in a "last text" field.
michael@0 1805 */
michael@0 1806 deleteLastTextFromFieldID: function (fieldID) {
michael@0 1807 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
michael@0 1808
michael@0 1809 return this._connection.executeCached(SQL.deleteLastTextFromFieldID,
michael@0 1810 {field_id: fieldID});
michael@0 1811 },
michael@0 1812
michael@0 1813 /**
michael@0 1814 * Record a value for a "daily last numeric" field.
michael@0 1815 *
michael@0 1816 * The field can hold 1 value per calendar day. If the field already has a
michael@0 1817 * value for the day specified (defaults to now), that value will be
michael@0 1818 * replaced, even if the date specified is older (within the day) than the
michael@0 1819 * previously recorded value.
michael@0 1820 *
michael@0 1821 * @param fieldID
michael@0 1822 * (Number) Integer primary key of field.
michael@0 1823 * @param value
michael@0 1824 * (Number) Value to record.
michael@0 1825 * @param date
michael@0 1826 * (Date) When the value was produced. Defaults to now.
michael@0 1827 */
michael@0 1828 setDailyLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1829 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
michael@0 1830
michael@0 1831 let params = {
michael@0 1832 field_id: fieldID,
michael@0 1833 days: dateToDays(date),
michael@0 1834 value: value,
michael@0 1835 };
michael@0 1836
michael@0 1837 return this._connection.executeCached(SQL.setDailyLastNumeric, params);
michael@0 1838 },
michael@0 1839
michael@0 1840 /**
michael@0 1841 * Record a value for a "daily last text" field.
michael@0 1842 *
michael@0 1843 * See `setDailyLastNumericFromFieldID` for behavior.
michael@0 1844 */
michael@0 1845 setDailyLastTextFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1846 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
michael@0 1847
michael@0 1848 let params = {
michael@0 1849 field_id: fieldID,
michael@0 1850 days: dateToDays(date),
michael@0 1851 value: value,
michael@0 1852 };
michael@0 1853
michael@0 1854 return this._connection.executeCached(SQL.setDailyLastText, params);
michael@0 1855 },
michael@0 1856
michael@0 1857 /**
michael@0 1858 * Obtain value(s) from a "daily last numeric" field.
michael@0 1859 *
michael@0 1860 * This returns a promise that resolves to a DailyValues instance. If `date`
michael@0 1861 * is specified, that instance will have at most 1 entry. If there is no
michael@0 1862 * `date` constraint, then all stored values will be retrieved.
michael@0 1863 *
michael@0 1864 * @param fieldID
michael@0 1865 * (Number) Integer primary key of field to retrieve.
michael@0 1866 * @param date optional
michael@0 1867 * (Date) If specified, only return data for this day.
michael@0 1868 *
michael@0 1869 * @return Promise<DailyValues>
michael@0 1870 */
michael@0 1871 getDailyLastNumericFromFieldID: function (fieldID, date=null) {
michael@0 1872 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
michael@0 1873
michael@0 1874 let params = {field_id: fieldID};
michael@0 1875 let name = "getDailyLastNumericFromFieldID";
michael@0 1876
michael@0 1877 if (date) {
michael@0 1878 params.days = dateToDays(date);
michael@0 1879 name = "getDailyLastNumericFromFieldIDAndDay";
michael@0 1880 }
michael@0 1881
michael@0 1882 return this._getDailyLastFromFieldID(name, params);
michael@0 1883 },
michael@0 1884
michael@0 1885 /**
michael@0 1886 * Obtain value(s) from a "daily last text" field.
michael@0 1887 *
michael@0 1888 * See `getDailyLastNumericFromFieldID` for behavior.
michael@0 1889 */
michael@0 1890 getDailyLastTextFromFieldID: function (fieldID, date=null) {
michael@0 1891 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
michael@0 1892
michael@0 1893 let params = {field_id: fieldID};
michael@0 1894 let name = "getDailyLastTextFromFieldID";
michael@0 1895
michael@0 1896 if (date) {
michael@0 1897 params.days = dateToDays(date);
michael@0 1898 name = "getDailyLastTextFromFieldIDAndDay";
michael@0 1899 }
michael@0 1900
michael@0 1901 return this._getDailyLastFromFieldID(name, params);
michael@0 1902 },
michael@0 1903
michael@0 1904 _getDailyLastFromFieldID: function (name, params) {
michael@0 1905 let self = this;
michael@0 1906 return Task.spawn(function fetchDailyLastForField() {
michael@0 1907 let rows = yield self._connection.executeCached(SQL[name], params);
michael@0 1908
michael@0 1909 let result = new DailyValues();
michael@0 1910 for (let row of rows) {
michael@0 1911 let d = daysToDate(row.getResultByIndex(0));
michael@0 1912 let value = row.getResultByIndex(1);
michael@0 1913
michael@0 1914 result.setDay(d, value);
michael@0 1915 }
michael@0 1916
michael@0 1917 throw new Task.Result(result);
michael@0 1918 });
michael@0 1919 },
michael@0 1920
michael@0 1921 /**
michael@0 1922 * Add a new value for a "daily discrete numeric" field.
michael@0 1923 *
michael@0 1924 * This appends a new value to the list of values for a specific field. All
michael@0 1925 * values are retained. Duplicate values are allowed.
michael@0 1926 *
michael@0 1927 * @param fieldID
michael@0 1928 * (Number) Integer primary key of field.
michael@0 1929 * @param value
michael@0 1930 * (Number) Value to record.
michael@0 1931 * @param date optional
michael@0 1932 * (Date) When this value occurred. Values are bucketed by day.
michael@0 1933 */
michael@0 1934 addDailyDiscreteNumericFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1935 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
michael@0 1936
michael@0 1937 if (typeof(value) != "number") {
michael@0 1938 throw new Error("Number expected. Got: " + value);
michael@0 1939 }
michael@0 1940
michael@0 1941 let params = {
michael@0 1942 field_id: fieldID,
michael@0 1943 days: dateToDays(date),
michael@0 1944 value: value,
michael@0 1945 };
michael@0 1946
michael@0 1947 return this._connection.executeCached(SQL.addDailyDiscreteNumeric, params);
michael@0 1948 },
michael@0 1949
michael@0 1950 /**
michael@0 1951 * Add a new value for a "daily discrete text" field.
michael@0 1952 *
michael@0 1953 * See `addDailyDiscreteNumericFromFieldID` for behavior.
michael@0 1954 */
michael@0 1955 addDailyDiscreteTextFromFieldID: function (fieldID, value, date=new Date()) {
michael@0 1956 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
michael@0 1957
michael@0 1958 if (typeof(value) != "string") {
michael@0 1959 throw new Error("String expected. Got: " + value);
michael@0 1960 }
michael@0 1961
michael@0 1962 let params = {
michael@0 1963 field_id: fieldID,
michael@0 1964 days: dateToDays(date),
michael@0 1965 value: value,
michael@0 1966 };
michael@0 1967
michael@0 1968 return this._connection.executeCached(SQL.addDailyDiscreteText, params);
michael@0 1969 },
michael@0 1970
michael@0 1971 /**
michael@0 1972 * Obtain values for a "daily discrete numeric" field.
michael@0 1973 *
michael@0 1974 * This returns a promise that resolves to a `DailyValues` instance. If
michael@0 1975 * `date` is specified, there will be at most 1 key in that instance. If
michael@0 1976 * not, all data from the database will be retrieved.
michael@0 1977 *
michael@0 1978 * Values in that instance will be arrays of the raw values.
michael@0 1979 *
michael@0 1980 * @param fieldID
michael@0 1981 * (Number) Integer primary key of field to retrieve.
michael@0 1982 * @param date optional
michael@0 1983 * (Date) Day to obtain data for. Date can be any time in the day.
michael@0 1984 */
michael@0 1985 getDailyDiscreteNumericFromFieldID: function (fieldID, date=null) {
michael@0 1986 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
michael@0 1987
michael@0 1988 let params = {field_id: fieldID};
michael@0 1989
michael@0 1990 let name = "getDailyDiscreteNumericFromFieldID";
michael@0 1991
michael@0 1992 if (date) {
michael@0 1993 params.days = dateToDays(date);
michael@0 1994 name = "getDailyDiscreteNumericFromFieldIDAndDay";
michael@0 1995 }
michael@0 1996
michael@0 1997 return this._getDailyDiscreteFromFieldID(name, params);
michael@0 1998 },
michael@0 1999
michael@0 2000 /**
michael@0 2001 * Obtain values for a "daily discrete text" field.
michael@0 2002 *
michael@0 2003 * See `getDailyDiscreteNumericFromFieldID` for behavior.
michael@0 2004 */
michael@0 2005 getDailyDiscreteTextFromFieldID: function (fieldID, date=null) {
michael@0 2006 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
michael@0 2007
michael@0 2008 let params = {field_id: fieldID};
michael@0 2009
michael@0 2010 let name = "getDailyDiscreteTextFromFieldID";
michael@0 2011
michael@0 2012 if (date) {
michael@0 2013 params.days = dateToDays(date);
michael@0 2014 name = "getDailyDiscreteTextFromFieldIDAndDay";
michael@0 2015 }
michael@0 2016
michael@0 2017 return this._getDailyDiscreteFromFieldID(name, params);
michael@0 2018 },
michael@0 2019
michael@0 2020 _getDailyDiscreteFromFieldID: function (name, params) {
michael@0 2021 let self = this;
michael@0 2022 return Task.spawn(function fetchDailyDiscreteValuesForField() {
michael@0 2023 let rows = yield self._connection.executeCached(SQL[name], params);
michael@0 2024
michael@0 2025 let result = new DailyValues();
michael@0 2026 for (let row of rows) {
michael@0 2027 let d = daysToDate(row.getResultByIndex(0));
michael@0 2028 let value = row.getResultByIndex(1);
michael@0 2029
michael@0 2030 result.appendValue(d, value);
michael@0 2031 }
michael@0 2032
michael@0 2033 throw new Task.Result(result);
michael@0 2034 });
michael@0 2035 },
michael@0 2036
michael@0 2037 /**
michael@0 2038 * Obtain the counts of daily counters in a measurement.
michael@0 2039 *
michael@0 2040 * This returns a promise that resolves to a Map of field name strings to
michael@0 2041 * DailyValues that hold per-day counts.
michael@0 2042 *
michael@0 2043 * @param id
michael@0 2044 * (Number) Integer primary key of measurement.
michael@0 2045 *
michael@0 2046 * @return Promise<Map>
michael@0 2047 */
michael@0 2048 getMeasurementDailyCountersFromMeasurementID: function (id) {
michael@0 2049 let self = this;
michael@0 2050 return Task.spawn(function fetchDailyCounters() {
michael@0 2051 let rows = yield self._connection.execute(SQL.getMeasurementDailyCounters,
michael@0 2052 {measurement_id: id});
michael@0 2053
michael@0 2054 let result = new Map();
michael@0 2055 for (let row of rows) {
michael@0 2056 let field = row.getResultByName("field_name");
michael@0 2057 let date = daysToDate(row.getResultByName("day"));
michael@0 2058 let value = row.getResultByName("value");
michael@0 2059
michael@0 2060 if (!result.has(field)) {
michael@0 2061 result.set(field, new DailyValues());
michael@0 2062 }
michael@0 2063
michael@0 2064 result.get(field).setDay(date, value);
michael@0 2065 }
michael@0 2066
michael@0 2067 throw new Task.Result(result);
michael@0 2068 });
michael@0 2069 },
michael@0 2070
michael@0 2071 /**
michael@0 2072 * Obtain the values of "last" fields from a measurement.
michael@0 2073 *
michael@0 2074 * This returns a promise that resolves to a Map of field name to an array
michael@0 2075 * of [date, value].
michael@0 2076 *
michael@0 2077 * @param id
michael@0 2078 * (Number) Integer primary key of measurement whose data to retrieve.
michael@0 2079 *
michael@0 2080 * @return Promise<Map>
michael@0 2081 */
michael@0 2082 getMeasurementLastValuesFromMeasurementID: function (id) {
michael@0 2083 let self = this;
michael@0 2084 return Task.spawn(function fetchMeasurementLastValues() {
michael@0 2085 let rows = yield self._connection.execute(SQL.getMeasurementLastValues,
michael@0 2086 {measurement_id: id});
michael@0 2087
michael@0 2088 let result = new Map();
michael@0 2089 for (let row of rows) {
michael@0 2090 let date = daysToDate(row.getResultByIndex(1));
michael@0 2091 let value = row.getResultByIndex(2);
michael@0 2092 result.set(row.getResultByIndex(0), [date, value]);
michael@0 2093 }
michael@0 2094
michael@0 2095 throw new Task.Result(result);
michael@0 2096 });
michael@0 2097 },
michael@0 2098
michael@0 2099 /**
michael@0 2100 * Obtain the values of "last daily" fields from a measurement.
michael@0 2101 *
michael@0 2102 * This returns a promise that resolves to a Map of field name to DailyValues
michael@0 2103 * instances. Each DailyValues instance has days for which a daily last value
michael@0 2104 * is defined. The values in each DailyValues are the raw last value for that
michael@0 2105 * day.
michael@0 2106 *
michael@0 2107 * @param id
michael@0 2108 * (Number) Integer primary key of measurement whose data to retrieve.
michael@0 2109 *
michael@0 2110 * @return Promise<Map>
michael@0 2111 */
michael@0 2112 getMeasurementDailyLastValuesFromMeasurementID: function (id) {
michael@0 2113 let self = this;
michael@0 2114 return Task.spawn(function fetchMeasurementDailyLastValues() {
michael@0 2115 let rows = yield self._connection.execute(SQL.getMeasurementDailyLastValues,
michael@0 2116 {measurement_id: id});
michael@0 2117
michael@0 2118 let result = new Map();
michael@0 2119 for (let row of rows) {
michael@0 2120 let field = row.getResultByName("field_name");
michael@0 2121 let date = daysToDate(row.getResultByName("day"));
michael@0 2122 let value = row.getResultByName("value");
michael@0 2123
michael@0 2124 if (!result.has(field)) {
michael@0 2125 result.set(field, new DailyValues());
michael@0 2126 }
michael@0 2127
michael@0 2128 result.get(field).setDay(date, value);
michael@0 2129 }
michael@0 2130
michael@0 2131 throw new Task.Result(result);
michael@0 2132 });
michael@0 2133 },
michael@0 2134
michael@0 2135 /**
michael@0 2136 * Obtain the values of "daily discrete" fields from a measurement.
michael@0 2137 *
michael@0 2138 * This obtains all discrete values for all "daily discrete" fields in a
michael@0 2139 * measurement.
michael@0 2140 *
michael@0 2141 * This returns a promise that resolves to a Map. The Map's keys are field
michael@0 2142 * string names. Values are `DailyValues` instances. The values inside
michael@0 2143 * the `DailyValues` are arrays of the raw discrete values.
michael@0 2144 *
michael@0 2145 * @param id
michael@0 2146 * (Number) Integer primary key of measurement.
michael@0 2147 *
michael@0 2148 * @return Promise<Map>
michael@0 2149 */
michael@0 2150 getMeasurementDailyDiscreteValuesFromMeasurementID: function (id) {
michael@0 2151 let deferred = Promise.defer();
michael@0 2152 let result = new Map();
michael@0 2153
michael@0 2154 this._connection.execute(SQL.getMeasurementDailyDiscreteValues,
michael@0 2155 {measurement_id: id}, function onRow(row) {
michael@0 2156 let field = row.getResultByName("field_name");
michael@0 2157 let date = daysToDate(row.getResultByName("day"));
michael@0 2158 let value = row.getResultByName("value");
michael@0 2159
michael@0 2160 if (!result.has(field)) {
michael@0 2161 result.set(field, new DailyValues());
michael@0 2162 }
michael@0 2163
michael@0 2164 result.get(field).appendValue(date, value);
michael@0 2165 }).then(function onComplete() {
michael@0 2166 deferred.resolve(result);
michael@0 2167 }, function onError(error) {
michael@0 2168 deferred.reject(error);
michael@0 2169 });
michael@0 2170
michael@0 2171 return deferred.promise;
michael@0 2172 },
michael@0 2173 });
michael@0 2174
michael@0 2175 // Alias built-in field types to public API.
michael@0 2176 for (let property of MetricsStorageSqliteBackend.prototype._BUILTIN_TYPES) {
michael@0 2177 this.MetricsStorageBackend[property] = MetricsStorageSqliteBackend.prototype[property];
michael@0 2178 }
michael@0 2179

mercurial