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