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/. */
7 #ifndef MERGED_COMPARTMENT
9 this.EXPORTED_SYMBOLS = [
11 "MetricsStorageBackend",
16 const {utils: Cu} = Components;
18 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
22 Cu.import("resource://gre/modules/Promise.jsm");
23 Cu.import("resource://gre/modules/Sqlite.jsm");
24 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
25 Cu.import("resource://gre/modules/Task.jsm");
26 Cu.import("resource://gre/modules/Log.jsm");
27 Cu.import("resource://services-common/utils.js");
30 // These do not account for leap seconds. Meh.
31 function dateToDays(date) {
32 return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
35 function daysToDate(days) {
36 return new Date(days * MILLISECONDS_PER_DAY);
40 * Represents a collection of per-day values.
42 * This is a proxy around a Map which can transparently round Date instances to
43 * their appropriate key.
45 * This emulates Map by providing .size and iterator support. Note that keys
46 * from the iterator are Date instances corresponding to midnight of the start
47 * of the day. get(), has(), and set() are modeled as getDay(), hasDay(), and
48 * setDay(), respectively.
50 * All days are defined in terms of UTC (as opposed to local time).
52 this.DailyValues = function () {
53 this._days = new Map();
56 DailyValues.prototype = Object.freeze({
57 __iterator__: function () {
58 for (let [k, v] of this._days) {
59 yield [daysToDate(k), v];
64 return this._days.size;
67 hasDay: function (date) {
68 return this._days.has(dateToDays(date));
71 getDay: function (date) {
72 return this._days.get(dateToDays(date));
75 setDay: function (date, value) {
76 this._days.set(dateToDays(date), value);
79 appendValue: function (date, value) {
80 let key = dateToDays(date);
82 if (this._days.has(key)) {
83 return this._days.get(key).push(value);
86 this._days.set(key, [value]);
95 * We use a SQLite database as the backend for persistent storage of metrics
98 * Every piece of recorded data is associated with a measurement. A measurement
99 * is an entity with a name and version. Each measurement is associated with a
102 * When the metrics system is initialized, we ask providers (the entities that
103 * emit data) to configure the database for storage of their data. They tell
104 * storage what their requirements are. For example, they'll register
105 * named daily counters associated with specific measurements.
107 * Recorded data is stored differently depending on the requirements for
108 * storing it. We have facilities for storing the following classes of data:
110 * 1) Counts of event/field occurrences aggregated by day.
111 * 2) Discrete values of fields aggregated by day.
112 * 3) Discrete values of fields aggregated by day max 1 per day (last write
114 * 4) Discrete values of fields max 1 (last write wins).
116 * Most data is aggregated per day mainly for privacy reasons. This does throw
117 * away potentially useful data. But, it's not currently used, so there is no
118 * need to keep the granular information.
123 * This database contains the following tables:
125 * providers -- Maps provider string name to an internal ID.
126 * provider_state -- Holds opaque persisted state for providers.
127 * measurements -- Holds the set of known measurements (name, version,
129 * types -- The data types that can be stored in measurements/fields.
130 * fields -- Describes entities that occur within measurements.
131 * daily_counters -- Holds daily-aggregated counts of events. Each row is
132 * associated with a field and a day.
133 * daily_discrete_numeric -- Holds numeric values for fields grouped by day.
134 * Each row contains a discrete value associated with a field that occurred
135 * on a specific day. There can be multiple rows per field per day.
136 * daily_discrete_text -- Holds text values for fields grouped by day. Each
137 * row contains a discrete value associated with a field that occurred on a
139 * daily_last_numeric -- Holds numeric values where the last encountered
140 * value for a given day is retained.
141 * daily_last_text -- Like daily_last_numeric except for text values.
142 * last_numeric -- Holds the most recent value for a numeric field.
143 * last_text -- Like last_numeric except for text fields.
148 * It is tempting to use SQLite's julianday() function to store days that
149 * things happened. However, a Julian Day begins at *noon* in 4714 B.C. This
150 * results in weird half day offsets from UNIX time. So, we instead store
151 * number of days since UNIX epoch, not Julian.
155 * All of our SQL statements are stored in a central mapping so they can easily
156 * be audited for security, perf, etc.
159 // Create the providers table.
160 createProvidersTable: "\
161 CREATE TABLE providers (\
162 id INTEGER PRIMARY KEY AUTOINCREMENT, \
167 createProviderStateTable: "\
168 CREATE TABLE provider_state (\
169 id INTEGER PRIMARY KEY AUTOINCREMENT, \
170 provider_id INTEGER, \
173 UNIQUE (provider_id, name), \
174 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE\
177 createProviderStateProviderIndex: "\
178 CREATE INDEX i_provider_state_provider_id ON provider_state (provider_id)",
180 createMeasurementsTable: "\
181 CREATE TABLE measurements (\
182 id INTEGER PRIMARY KEY AUTOINCREMENT, \
183 provider_id INTEGER, \
186 UNIQUE (provider_id, name, version), \
187 FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE\
190 createMeasurementsProviderIndex: "\
191 CREATE INDEX i_measurements_provider_id ON measurements (provider_id)",
193 createMeasurementsView: "\
194 CREATE VIEW v_measurements AS \
196 providers.id AS provider_id, \
197 providers.name AS provider_name, \
198 measurements.id AS measurement_id, \
199 measurements.name AS measurement_name, \
200 measurements.version AS measurement_version \
201 FROM providers, measurements \
203 measurements.provider_id = providers.id",
206 CREATE TABLE types (\
207 id INTEGER PRIMARY KEY AUTOINCREMENT, \
212 createFieldsTable: "\
213 CREATE TABLE fields (\
214 id INTEGER PRIMARY KEY AUTOINCREMENT, \
215 measurement_id INTEGER, \
217 value_type INTEGER , \
218 UNIQUE (measurement_id, name), \
219 FOREIGN KEY (measurement_id) REFERENCES measurements(id) ON DELETE CASCADE \
220 FOREIGN KEY (value_type) REFERENCES types(id) ON DELETE CASCADE \
223 createFieldsMeasurementIndex: "\
224 CREATE INDEX i_fields_measurement_id ON fields (measurement_id)",
227 CREATE VIEW v_fields AS \
229 providers.id AS provider_id, \
230 providers.name AS provider_name, \
231 measurements.id AS measurement_id, \
232 measurements.name AS measurement_name, \
233 measurements.version AS measurement_version, \
234 fields.id AS field_id, \
235 fields.name AS field_name, \
236 types.id AS type_id, \
237 types.name AS type_name \
238 FROM providers, measurements, fields, types \
240 fields.measurement_id = measurements.id \
241 AND measurements.provider_id = providers.id \
242 AND fields.value_type = types.id",
244 createDailyCountersTable: "\
245 CREATE TABLE daily_counters (\
249 UNIQUE(field_id, day), \
250 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
253 createDailyCountersFieldIndex: "\
254 CREATE INDEX i_daily_counters_field_id ON daily_counters (field_id)",
256 createDailyCountersDayIndex: "\
257 CREATE INDEX i_daily_counters_day ON daily_counters (day)",
259 createDailyCountersView: "\
260 CREATE VIEW v_daily_counters AS SELECT \
261 providers.id AS provider_id, \
262 providers.name AS provider_name, \
263 measurements.id AS measurement_id, \
264 measurements.name AS measurement_name, \
265 measurements.version AS measurement_version, \
266 fields.id AS field_id, \
267 fields.name AS field_name, \
268 daily_counters.day AS day, \
269 daily_counters.value AS value \
270 FROM providers, measurements, fields, daily_counters \
272 daily_counters.field_id = fields.id \
273 AND fields.measurement_id = measurements.id \
274 AND measurements.provider_id = providers.id",
276 createDailyDiscreteNumericsTable: "\
277 CREATE TABLE daily_discrete_numeric (\
278 id INTEGER PRIMARY KEY AUTOINCREMENT, \
282 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
285 createDailyDiscreteNumericsFieldIndex: "\
286 CREATE INDEX i_daily_discrete_numeric_field_id \
287 ON daily_discrete_numeric (field_id)",
289 createDailyDiscreteNumericsDayIndex: "\
290 CREATE INDEX i_daily_discrete_numeric_day \
291 ON daily_discrete_numeric (day)",
293 createDailyDiscreteTextTable: "\
294 CREATE TABLE daily_discrete_text (\
295 id INTEGER PRIMARY KEY AUTOINCREMENT, \
299 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
302 createDailyDiscreteTextFieldIndex: "\
303 CREATE INDEX i_daily_discrete_text_field_id \
304 ON daily_discrete_text (field_id)",
306 createDailyDiscreteTextDayIndex: "\
307 CREATE INDEX i_daily_discrete_text_day \
308 ON daily_discrete_text (day)",
310 createDailyDiscreteView: "\
311 CREATE VIEW v_daily_discrete AS \
313 providers.id AS provider_id, \
314 providers.name AS provider_name, \
315 measurements.id AS measurement_id, \
316 measurements.name AS measurement_name, \
317 measurements.version AS measurement_version, \
318 fields.id AS field_id, \
319 fields.name AS field_name, \
320 daily_discrete_numeric.id AS value_id, \
321 daily_discrete_numeric.day AS day, \
322 daily_discrete_numeric.value AS value, \
323 \"numeric\" AS value_type \
324 FROM providers, measurements, fields, daily_discrete_numeric \
326 daily_discrete_numeric.field_id = fields.id \
327 AND fields.measurement_id = measurements.id \
328 AND measurements.provider_id = providers.id \
331 providers.id AS provider_id, \
332 providers.name AS provider_name, \
333 measurements.id AS measurement_id, \
334 measurements.name AS measurement_name, \
335 measurements.version AS measurement_version, \
336 fields.id AS field_id, \
337 fields.name AS field_name, \
338 daily_discrete_text.id AS value_id, \
339 daily_discrete_text.day AS day, \
340 daily_discrete_text.value AS value, \
341 \"text\" AS value_type \
342 FROM providers, measurements, fields, daily_discrete_text \
344 daily_discrete_text.field_id = fields.id \
345 AND fields.measurement_id = measurements.id \
346 AND measurements.provider_id = providers.id \
347 ORDER BY day ASC, value_id ASC",
349 createLastNumericTable: "\
350 CREATE TABLE last_numeric (\
351 field_id INTEGER PRIMARY KEY, \
354 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
357 createLastTextTable: "\
358 CREATE TABLE last_text (\
359 field_id INTEGER PRIMARY KEY, \
362 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
366 CREATE VIEW v_last AS \
368 providers.id AS provider_id, \
369 providers.name AS provider_name, \
370 measurements.id AS measurement_id, \
371 measurements.name AS measurement_name, \
372 measurements.version AS measurement_version, \
373 fields.id AS field_id, \
374 fields.name AS field_name, \
375 last_numeric.day AS day, \
376 last_numeric.value AS value, \
377 \"numeric\" AS value_type \
378 FROM providers, measurements, fields, last_numeric \
380 last_numeric.field_id = fields.id \
381 AND fields.measurement_id = measurements.id \
382 AND measurements.provider_id = providers.id \
385 providers.id AS provider_id, \
386 providers.name AS provider_name, \
387 measurements.id AS measurement_id, \
388 measurements.name AS measurement_name, \
389 measurements.version AS measurement_version, \
390 fields.id AS field_id, \
391 fields.name AS field_name, \
392 last_text.day AS day, \
393 last_text.value AS value, \
394 \"text\" AS value_type \
395 FROM providers, measurements, fields, last_text \
397 last_text.field_id = fields.id \
398 AND fields.measurement_id = measurements.id \
399 AND measurements.provider_id = providers.id",
401 createDailyLastNumericTable: "\
402 CREATE TABLE daily_last_numeric (\
406 UNIQUE (field_id, day) \
407 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
410 createDailyLastNumericFieldIndex: "\
411 CREATE INDEX i_daily_last_numeric_field_id ON daily_last_numeric (field_id)",
413 createDailyLastNumericDayIndex: "\
414 CREATE INDEX i_daily_last_numeric_day ON daily_last_numeric (day)",
416 createDailyLastTextTable: "\
417 CREATE TABLE daily_last_text (\
421 UNIQUE (field_id, day) \
422 FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE\
425 createDailyLastTextFieldIndex: "\
426 CREATE INDEX i_daily_last_text_field_id ON daily_last_text (field_id)",
428 createDailyLastTextDayIndex: "\
429 CREATE INDEX i_daily_last_text_day ON daily_last_text (day)",
431 createDailyLastView: "\
432 CREATE VIEW v_daily_last AS \
434 providers.id AS provider_id, \
435 providers.name AS provider_name, \
436 measurements.id AS measurement_id, \
437 measurements.name AS measurement_name, \
438 measurements.version AS measurement_version, \
439 fields.id AS field_id, \
440 fields.name AS field_name, \
441 daily_last_numeric.day AS day, \
442 daily_last_numeric.value AS value, \
443 \"numeric\" as value_type \
444 FROM providers, measurements, fields, daily_last_numeric \
446 daily_last_numeric.field_id = fields.id \
447 AND fields.measurement_id = measurements.id \
448 AND measurements.provider_id = providers.id \
451 providers.id AS provider_id, \
452 providers.name AS provider_name, \
453 measurements.id AS measurement_id, \
454 measurements.name AS measurement_name, \
455 measurements.version AS measurement_version, \
456 fields.id AS field_id, \
457 fields.name AS field_name, \
458 daily_last_text.day AS day, \
459 daily_last_text.value AS value, \
460 \"text\" as value_type \
461 FROM providers, measurements, fields, daily_last_text \
463 daily_last_text.field_id = fields.id \
464 AND fields.measurement_id = measurements.id \
465 AND measurements.provider_id = providers.id",
469 addProvider: "INSERT INTO providers (name) VALUES (:provider)",
472 INSERT OR REPLACE INTO provider_state \
473 (provider_id, name, value) \
474 VALUES (:provider_id, :name, :value)",
477 INSERT INTO measurements (provider_id, name, version) \
478 VALUES (:provider_id, :measurement, :version)",
480 addType: "INSERT INTO types (name) VALUES (:name)",
483 INSERT INTO fields (measurement_id, name, value_type) \
484 VALUES (:measurement_id, :field, :value_type)",
486 incrementDailyCounterFromFieldID: "\
487 INSERT OR REPLACE INTO daily_counters VALUES (\
491 (SELECT value FROM daily_counters WHERE \
492 field_id = :field_id AND day = :days \
497 deleteLastNumericFromFieldID: "\
498 DELETE FROM last_numeric WHERE field_id = :field_id",
500 deleteLastTextFromFieldID: "\
501 DELETE FROM last_text WHERE field_id = :field_id",
504 INSERT OR REPLACE INTO last_numeric VALUES (:field_id, :days, :value)",
507 INSERT OR REPLACE INTO last_text VALUES (:field_id, :days, :value)",
509 setDailyLastNumeric: "\
510 INSERT OR REPLACE INTO daily_last_numeric VALUES (:field_id, :days, :value)",
513 INSERT OR REPLACE INTO daily_last_text VALUES (:field_id, :days, :value)",
515 addDailyDiscreteNumeric: "\
516 INSERT INTO daily_discrete_numeric \
517 (field_id, day, value) VALUES (:field_id, :days, :value)",
519 addDailyDiscreteText: "\
520 INSERT INTO daily_discrete_text \
521 (field_id, day, value) VALUES (:field_id, :days, :value)",
523 pruneOldDailyCounters: "DELETE FROM daily_counters WHERE day < :days",
524 pruneOldDailyDiscreteNumeric: "DELETE FROM daily_discrete_numeric WHERE day < :days",
525 pruneOldDailyDiscreteText: "DELETE FROM daily_discrete_text WHERE day < :days",
526 pruneOldDailyLastNumeric: "DELETE FROM daily_last_numeric WHERE day < :days",
527 pruneOldDailyLastText: "DELETE FROM daily_last_text WHERE day < :days",
528 pruneOldLastNumeric: "DELETE FROM last_numeric WHERE day < :days",
529 pruneOldLastText: "DELETE FROM last_text WHERE day < :days",
533 getProviderID: "SELECT id FROM providers WHERE name = :provider",
535 getProviders: "SELECT id, name FROM providers",
537 getProviderStateWithName: "\
538 SELECT value FROM provider_state \
539 WHERE provider_id = :provider_id \
542 getMeasurements: "SELECT * FROM v_measurements",
545 SELECT id FROM measurements \
546 WHERE provider_id = :provider_id \
547 AND name = :measurement \
548 AND version = :version",
551 SELECT id FROM fields \
552 WHERE measurement_id = :measurement_id \
554 AND value_type = :value_type \
557 getTypes: "SELECT * FROM types",
559 getTypeID: "SELECT id FROM types WHERE name = :name",
561 getDailyCounterCountsFromFieldID: "\
562 SELECT day, value FROM daily_counters \
563 WHERE field_id = :field_id \
566 getDailyCounterCountFromFieldID: "\
567 SELECT value FROM daily_counters \
568 WHERE field_id = :field_id \
571 getMeasurementDailyCounters: "\
572 SELECT field_name, day, value FROM v_daily_counters \
573 WHERE measurement_id = :measurement_id",
575 getFieldInfo: "SELECT * FROM v_fields",
577 getLastNumericFromFieldID: "\
578 SELECT day, value FROM last_numeric WHERE field_id = :field_id",
580 getLastTextFromFieldID: "\
581 SELECT day, value FROM last_text WHERE field_id = :field_id",
583 getMeasurementLastValues: "\
584 SELECT field_name, day, value FROM v_last \
585 WHERE measurement_id = :measurement_id",
587 getDailyDiscreteNumericFromFieldID: "\
588 SELECT day, value FROM daily_discrete_numeric \
589 WHERE field_id = :field_id \
590 ORDER BY day ASC, id ASC",
592 getDailyDiscreteNumericFromFieldIDAndDay: "\
593 SELECT day, value FROM daily_discrete_numeric \
594 WHERE field_id = :field_id AND day = :days \
597 getDailyDiscreteTextFromFieldID: "\
598 SELECT day, value FROM daily_discrete_text \
599 WHERE field_id = :field_id \
600 ORDER BY day ASC, id ASC",
602 getDailyDiscreteTextFromFieldIDAndDay: "\
603 SELECT day, value FROM daily_discrete_text \
604 WHERE field_id = :field_id AND day = :days \
607 getMeasurementDailyDiscreteValues: "\
608 SELECT field_name, day, value_id, value FROM v_daily_discrete \
609 WHERE measurement_id = :measurement_id \
610 ORDER BY day ASC, value_id ASC",
612 getDailyLastNumericFromFieldID: "\
613 SELECT day, value FROM daily_last_numeric \
614 WHERE field_id = :field_id \
617 getDailyLastNumericFromFieldIDAndDay: "\
618 SELECT day, value FROM daily_last_numeric \
619 WHERE field_id = :field_id AND day = :days",
621 getDailyLastTextFromFieldID: "\
622 SELECT day, value FROM daily_last_text \
623 WHERE field_id = :field_id \
626 getDailyLastTextFromFieldIDAndDay: "\
627 SELECT day, value FROM daily_last_text \
628 WHERE field_id = :field_id AND day = :days",
630 getMeasurementDailyLastValues: "\
631 SELECT field_name, day, value FROM v_daily_last \
632 WHERE measurement_id = :measurement_id",
636 function dailyKeyFromDate(date) {
637 let year = String(date.getUTCFullYear());
638 let month = String(date.getUTCMonth() + 1);
639 let day = String(date.getUTCDate());
641 if (month.length < 2) {
645 if (day.length < 2) {
649 return year + "-" + month + "-" + day;
654 * Create a new backend instance bound to a SQLite database at the given path.
656 * This returns a promise that will resolve to a `MetricsStorageSqliteBackend`
657 * instance. The resolved instance will be initialized and ready for use.
659 * Very few consumers have a need to call this. Instead, a higher-level entity
660 * likely calls this and sets up the database connection for a service or
663 this.MetricsStorageBackend = function (path) {
664 return Task.spawn(function initTask() {
665 let connection = yield Sqlite.openConnection({
668 // There should only be one connection per database, so we disable this
670 sharedMemoryCache: false,
673 // If we fail initializing the storage object, we need to close the
674 // database connection or else Storage will assert on shutdown.
677 storage = new MetricsStorageSqliteBackend(connection);
678 yield storage._init();
680 yield connection.close();
684 throw new Task.Result(storage);
688 // Expose an asynchronous barrier `shutdown` that clients may use to
689 // perform last minute cleanup and shutdown requests before this module
691 // See the documentation of AsyncShutdown.Barrier for more details.
692 let shutdown = new AsyncShutdown.Barrier("Metrics Storage Backend");
693 this.MetricsStorageBackend.shutdown = shutdown.client;
694 Sqlite.shutdown.addBlocker("Metrics Storage: Shutting down",
695 () => shutdown.wait());
698 * Manages storage of metrics data in a SQLite database.
700 * This is the main type used for interfacing with the database.
702 * Instances of this should be obtained by calling MetricsStorageConnection().
704 * The current implementation will not work if the database is mutated by
705 * multiple connections because of the way we cache primary keys.
707 * FUTURE enforce 1 read/write connection per database limit.
709 function MetricsStorageSqliteBackend(connection) {
710 this._log = Log.repository.getLogger("Services.Metrics.MetricsStorage");
712 this._connection = connection;
713 this._enabledWALCheckpointPages = null;
715 // Integer IDs to string name.
716 this._typesByID = new Map();
718 // String name to integer IDs.
719 this._typesByName = new Map();
721 // Maps provider names to integer IDs.
722 this._providerIDs = new Map();
724 // Maps :-delimited strings of [provider name, name, version] to integer IDs.
725 this._measurementsByInfo = new Map();
727 // Integer IDs to Arrays of [provider name, name, version].
728 this._measurementsByID = new Map();
730 // Integer IDs to Arrays of [measurement id, field name, value name]
731 this._fieldsByID = new Map();
733 // Maps :-delimited strings of [measurement id, field name] to integer ID.
734 this._fieldsByInfo = new Map();
736 // Maps measurement ID to sets of field IDs.
737 this._fieldsByMeasurement = new Map();
739 this._queuedOperations = [];
740 this._queuedInProgress = false;
743 MetricsStorageSqliteBackend.prototype = Object.freeze({
744 // Max size (in kibibytes) the WAL log is allowed to grow to before it is
747 // This was first deployed in bug 848136. We want a value large enough
748 // that we aren't checkpointing all the time. However, we want it
749 // small enough so we don't have to read so much when we open the
751 MAX_WAL_SIZE_KB: 512,
753 FIELD_DAILY_COUNTER: "daily-counter",
754 FIELD_DAILY_DISCRETE_NUMERIC: "daily-discrete-numeric",
755 FIELD_DAILY_DISCRETE_TEXT: "daily-discrete-text",
756 FIELD_DAILY_LAST_NUMERIC: "daily-last-numeric",
757 FIELD_DAILY_LAST_TEXT: "daily-last-text",
758 FIELD_LAST_NUMERIC: "last-numeric",
759 FIELD_LAST_TEXT: "last-text",
762 "FIELD_DAILY_COUNTER",
763 "FIELD_DAILY_DISCRETE_NUMERIC",
764 "FIELD_DAILY_DISCRETE_TEXT",
765 "FIELD_DAILY_LAST_NUMERIC",
766 "FIELD_DAILY_LAST_TEXT",
767 "FIELD_LAST_NUMERIC",
771 // Statements that are used to create the initial DB schema.
772 _SCHEMA_STATEMENTS: [
773 "createProvidersTable",
774 "createProviderStateTable",
775 "createProviderStateProviderIndex",
776 "createMeasurementsTable",
777 "createMeasurementsProviderIndex",
778 "createMeasurementsView",
781 "createFieldsMeasurementIndex",
783 "createDailyCountersTable",
784 "createDailyCountersFieldIndex",
785 "createDailyCountersDayIndex",
786 "createDailyCountersView",
787 "createDailyDiscreteNumericsTable",
788 "createDailyDiscreteNumericsFieldIndex",
789 "createDailyDiscreteNumericsDayIndex",
790 "createDailyDiscreteTextTable",
791 "createDailyDiscreteTextFieldIndex",
792 "createDailyDiscreteTextDayIndex",
793 "createDailyDiscreteView",
794 "createDailyLastNumericTable",
795 "createDailyLastNumericFieldIndex",
796 "createDailyLastNumericDayIndex",
797 "createDailyLastTextTable",
798 "createDailyLastTextFieldIndex",
799 "createDailyLastTextDayIndex",
800 "createDailyLastView",
801 "createLastNumericTable",
802 "createLastTextTable",
806 // Statements that are used to prune old data.
808 "pruneOldDailyCounters",
809 "pruneOldDailyDiscreteNumeric",
810 "pruneOldDailyDiscreteText",
811 "pruneOldDailyLastNumeric",
812 "pruneOldDailyLastText",
813 "pruneOldLastNumeric",
818 * Close the database connection.
820 * This should be called on all instances or the SQLite layer may complain
821 * loudly. After this has been called, the connection cannot be used.
823 * @return Promise<> A promise fulfilled once the connection is closed.
824 * This promise never rejects.
827 return Task.spawn(function doClose() {
828 // There is some light magic involved here. First, we enqueue an
829 // operation to ensure that all pending operations have the opportunity
830 // to execute. We additionally execute a SQL operation. Due to the FIFO
831 // execution order of issued statements, this will cause us to wait on
832 // any outstanding statements before closing.
834 yield this.enqueueOperation(function dummyOperation() {
835 return this._connection.execute("SELECT 1");
840 yield this._connection.close();
842 this._connection = null;
848 * Whether a provider is known to exist.
851 * (string) Name of the provider.
853 hasProvider: function (provider) {
854 return this._providerIDs.has(provider);
858 * Whether a measurement is known to exist.
861 * (string) Name of the provider.
863 * (string) Name of the measurement.
865 * (Number) Integer measurement version.
867 hasMeasurement: function (provider, name, version) {
868 return this._measurementsByInfo.has([provider, name, version].join(":"));
872 * Whether a named field exists in a measurement.
874 * @param measurementID
875 * (Number) The integer primary key of the measurement.
877 * (string) The name of the field to look for.
879 hasFieldFromMeasurement: function (measurementID, field) {
880 return this._fieldsByInfo.has([measurementID, field].join(":"));
884 * Whether a field is known.
887 * (string) Name of the provider having the field.
889 * (string) Name of the measurement in the provider having the field.
891 * (string) Name of the field in the measurement.
893 hasField: function (provider, measurement, version, field) {
894 let key = [provider, measurement, version].join(":");
895 let measurementID = this._measurementsByInfo.get(key);
896 if (!measurementID) {
900 return this.hasFieldFromMeasurement(measurementID, field);
904 * Look up the integer primary key of a provider.
907 * (string) Name of the provider.
909 providerID: function (provider) {
910 return this._providerIDs.get(provider);
914 * Look up the integer primary key of a measurement.
917 * (string) Name of the provider.
919 * (string) Name of the measurement.
921 * (Number) Integer version of the measurement.
923 measurementID: function (provider, measurement, version) {
924 return this._measurementsByInfo.get([provider, measurement, version].join(":"));
927 fieldIDFromMeasurement: function (measurementID, field) {
928 return this._fieldsByInfo.get([measurementID, field].join(":"));
931 fieldID: function (provider, measurement, version, field) {
932 let measurementID = this.measurementID(provider, measurement, version);
933 if (!measurementID) {
937 return this.fieldIDFromMeasurement(measurementID, field);
940 measurementHasAnyDailyCounterFields: function (measurementID) {
941 return this.measurementHasAnyFieldsOfTypes(measurementID,
942 [this.FIELD_DAILY_COUNTER]);
945 measurementHasAnyLastFields: function (measurementID) {
946 return this.measurementHasAnyFieldsOfTypes(measurementID,
947 [this.FIELD_LAST_NUMERIC,
948 this.FIELD_LAST_TEXT]);
951 measurementHasAnyDailyLastFields: function (measurementID) {
952 return this.measurementHasAnyFieldsOfTypes(measurementID,
953 [this.FIELD_DAILY_LAST_NUMERIC,
954 this.FIELD_DAILY_LAST_TEXT]);
957 measurementHasAnyDailyDiscreteFields: function (measurementID) {
958 return this.measurementHasAnyFieldsOfTypes(measurementID,
959 [this.FIELD_DAILY_DISCRETE_NUMERIC,
960 this.FIELD_DAILY_DISCRETE_TEXT]);
963 measurementHasAnyFieldsOfTypes: function (measurementID, types) {
964 if (!this._fieldsByMeasurement.has(measurementID)) {
968 let fieldIDs = this._fieldsByMeasurement.get(measurementID);
969 for (let fieldID of fieldIDs) {
970 let fieldType = this._fieldsByID.get(fieldID)[2];
971 if (types.indexOf(fieldType) != -1) {
980 * Register a measurement with the backend.
982 * Measurements must be registered before storage can be allocated to them.
984 * A measurement consists of a string name and integer version attached
985 * to a named provider.
987 * This returns a promise that resolves to the storage ID for this
990 * If the measurement is not known to exist, it is registered with storage.
991 * If the measurement has already been registered, this is effectively a
992 * no-op (that still returns a promise resolving to the storage ID).
995 * (string) Name of the provider this measurement belongs to.
997 * (string) Name of this measurement.
999 * (Number) Integer version of this measurement.
1001 registerMeasurement: function (provider, name, version) {
1002 if (this.hasMeasurement(provider, name, version)) {
1003 return CommonUtils.laterTickResolvingPromise(
1004 this.measurementID(provider, name, version));
1007 // Registrations might not be safe to perform in parallel with provider
1008 // operations. So, we queue them.
1010 return this.enqueueOperation(function createMeasurementOperation() {
1011 return Task.spawn(function createMeasurement() {
1012 let providerID = self._providerIDs.get(provider);
1015 yield self._connection.executeCached(SQL.addProvider, {provider: provider});
1016 let rows = yield self._connection.executeCached(SQL.getProviderID,
1017 {provider: provider});
1019 providerID = rows[0].getResultByIndex(0);
1021 self._providerIDs.set(provider, providerID);
1025 provider_id: providerID,
1030 yield self._connection.executeCached(SQL.addMeasurement, params);
1031 let rows = yield self._connection.executeCached(SQL.getMeasurementID, params);
1033 let measurementID = rows[0].getResultByIndex(0);
1035 self._measurementsByInfo.set([provider, name, version].join(":"), measurementID);
1036 self._measurementsByID.set(measurementID, [provider, name, version]);
1037 self._fieldsByMeasurement.set(measurementID, new Set());
1039 throw new Task.Result(measurementID);
1045 * Register a field with the backend.
1047 * Fields are what recorded pieces of data are primarily associated with.
1049 * Fields are associated with measurements. Measurements must be registered
1050 * via `registerMeasurement` before fields can be registered. This is
1051 * enforced by this function requiring the database primary key of the
1052 * measurement as an argument.
1054 * @param measurementID
1055 * (Number) Integer primary key of measurement this field belongs to.
1057 * (string) Name of this field.
1059 * (string) Type name of this field. Must be a registered type. Is
1060 * likely one of the FIELD_ constants on this type.
1062 * @return Promise<integer>
1064 registerField: function (measurementID, field, valueType) {
1066 throw new Error("Value type must be defined.");
1069 if (!this._measurementsByID.has(measurementID)) {
1070 throw new Error("Measurement not known: " + measurementID);
1073 if (!this._typesByName.has(valueType)) {
1074 throw new Error("Unknown value type: " + valueType);
1077 let typeID = this._typesByName.get(valueType);
1080 throw new Error("Undefined type: " + valueType);
1083 if (this.hasFieldFromMeasurement(measurementID, field)) {
1084 let id = this.fieldIDFromMeasurement(measurementID, field);
1085 let existingType = this._fieldsByID.get(id)[2];
1087 if (valueType != existingType) {
1088 throw new Error("Field already defined with different type: " + existingType);
1091 return CommonUtils.laterTickResolvingPromise(
1092 this.fieldIDFromMeasurement(measurementID, field));
1096 return Task.spawn(function createField() {
1098 measurement_id: measurementID,
1103 yield self._connection.executeCached(SQL.addField, params);
1105 let rows = yield self._connection.executeCached(SQL.getFieldID, params);
1107 let fieldID = rows[0].getResultByIndex(0);
1109 self._fieldsByID.set(fieldID, [measurementID, field, valueType]);
1110 self._fieldsByInfo.set([measurementID, field].join(":"), fieldID);
1111 self._fieldsByMeasurement.get(measurementID).add(fieldID);
1113 throw new Task.Result(fieldID);
1118 * Initializes this instance with the database.
1120 * This performs 2 major roles:
1122 * 1) Set up database schema (creates tables).
1123 * 2) Synchronize database with local instance.
1127 return Task.spawn(function initTask() {
1128 // 0. Database file and connection configuration.
1130 // This should never fail. But, we assume the default of 1024 in case it
1132 let rows = yield self._connection.execute("PRAGMA page_size");
1133 let pageSize = 1024;
1135 pageSize = rows[0].getResultByIndex(0);
1138 self._log.debug("Page size is " + pageSize);
1140 // Ensure temp tables are stored in memory, not on disk.
1141 yield self._connection.execute("PRAGMA temp_store=MEMORY");
1144 rows = yield self._connection.execute("PRAGMA journal_mode=WAL");
1146 journalMode = rows[0].getResultByIndex(0);
1149 self._log.info("Journal mode is " + journalMode);
1151 if (journalMode == "wal") {
1152 self._enabledWALCheckpointPages =
1153 Math.ceil(self.MAX_WAL_SIZE_KB * 1024 / pageSize);
1155 self._log.info("WAL auto checkpoint pages: " +
1156 self._enabledWALCheckpointPages);
1158 // We disable auto checkpoint during initialization to make it
1160 yield self.setAutoCheckpoint(0);
1162 if (journalMode != "truncate") {
1163 // Fall back to truncate (which is faster than delete).
1164 yield self._connection.execute("PRAGMA journal_mode=TRUNCATE");
1167 // And always use full synchronous mode to reduce possibility for data
1169 yield self._connection.execute("PRAGMA synchronous=FULL");
1172 let doCheckpoint = false;
1174 // 1. Create the schema.
1175 yield self._connection.executeTransaction(function ensureSchema(conn) {
1176 let schema = yield conn.getSchemaVersion();
1179 self._log.info("Creating database schema.");
1181 for (let k of self._SCHEMA_STATEMENTS) {
1182 yield self._connection.execute(SQL[k]);
1185 yield self._connection.setSchemaVersion(1);
1186 doCheckpoint = true;
1187 } else if (schema != 1) {
1188 throw new Error("Unknown database schema: " + schema);
1190 self._log.debug("Database schema up to date.");
1194 // 2. Retrieve existing types.
1195 yield self._connection.execute(SQL.getTypes, null, function onRow(row) {
1196 let id = row.getResultByName("id");
1197 let name = row.getResultByName("name");
1199 self._typesByID.set(id, name);
1200 self._typesByName.set(name, id);
1203 // 3. Populate built-in types with database.
1204 let missingTypes = [];
1205 for (let type of self._BUILTIN_TYPES) {
1207 if (self._typesByName.has(type)) {
1211 missingTypes.push(type);
1214 // Don't perform DB transaction unless there is work to do.
1215 if (missingTypes.length) {
1216 yield self._connection.executeTransaction(function populateBuiltinTypes() {
1217 for (let type of missingTypes) {
1218 let params = {name: type};
1219 yield self._connection.executeCached(SQL.addType, params);
1220 let rows = yield self._connection.executeCached(SQL.getTypeID, params);
1221 let id = rows[0].getResultByIndex(0);
1223 self._typesByID.set(id, type);
1224 self._typesByName.set(type, id);
1228 doCheckpoint = true;
1231 // 4. Obtain measurement info.
1232 yield self._connection.execute(SQL.getMeasurements, null, function onRow(row) {
1233 let providerID = row.getResultByName("provider_id");
1234 let providerName = row.getResultByName("provider_name");
1235 let measurementID = row.getResultByName("measurement_id");
1236 let measurementName = row.getResultByName("measurement_name");
1237 let measurementVersion = row.getResultByName("measurement_version");
1239 self._providerIDs.set(providerName, providerID);
1241 let info = [providerName, measurementName, measurementVersion].join(":");
1243 self._measurementsByInfo.set(info, measurementID);
1244 self._measurementsByID.set(measurementID, info);
1245 self._fieldsByMeasurement.set(measurementID, new Set());
1248 // 5. Obtain field info.
1249 yield self._connection.execute(SQL.getFieldInfo, null, function onRow(row) {
1250 let measurementID = row.getResultByName("measurement_id");
1251 let fieldID = row.getResultByName("field_id");
1252 let fieldName = row.getResultByName("field_name");
1253 let typeName = row.getResultByName("type_name");
1255 self._fieldsByID.set(fieldID, [measurementID, fieldName, typeName]);
1256 self._fieldsByInfo.set([measurementID, fieldName].join(":"), fieldID);
1257 self._fieldsByMeasurement.get(measurementID).add(fieldID);
1260 // Perform a checkpoint after initialization (if needed) and
1261 // enable auto checkpoint during regular operation.
1263 yield self.checkpoint();
1266 yield self.setAutoCheckpoint(1);
1271 * Prune all data from earlier than the specified date.
1273 * Data stored on days before the specified Date will be permanently
1276 * This returns a promise that will be resolved when data has been deleted.
1279 * (Date) Old data threshold.
1282 pruneDataBefore: function (date) {
1283 let statements = this._PRUNE_STATEMENTS;
1286 return this.enqueueOperation(function doPrune() {
1287 return self._connection.executeTransaction(function prune(conn) {
1288 let days = dateToDays(date);
1290 let params = {days: days};
1291 for (let name of statements) {
1292 yield conn.execute(SQL[name], params);
1299 * Reduce memory usage as much as possible.
1301 * This returns a promise that will be resolved on completion.
1305 compact: function () {
1307 return this.enqueueOperation(function doCompact() {
1308 self._connection.discardCachedStatements();
1309 return self._connection.shrinkMemory();
1314 * Checkpoint writes requiring flush to disk.
1316 * This is called to persist queued and non-flushed writes to disk.
1317 * It will force an fsync, so it is expensive and should be used
1320 checkpoint: function () {
1321 if (!this._enabledWALCheckpointPages) {
1322 return CommonUtils.laterTickResolvingPromise();
1325 return this.enqueueOperation(function checkpoint() {
1326 this._log.info("Performing manual WAL checkpoint.");
1327 return this._connection.execute("PRAGMA wal_checkpoint");
1331 setAutoCheckpoint: function (on) {
1332 // If we aren't in WAL mode, wal_autocheckpoint won't do anything so
1334 if (!this._enabledWALCheckpointPages) {
1335 return CommonUtils.laterTickResolvingPromise();
1338 let val = on ? this._enabledWALCheckpointPages : 0;
1340 return this.enqueueOperation(function setWALCheckpoint() {
1341 this._log.info("Setting WAL auto checkpoint to " + val);
1342 return this._connection.execute("PRAGMA wal_autocheckpoint=" + val);
1347 * Ensure a field ID matches a specified type.
1349 * This is called internally as part of adding values to ensure that
1350 * the type of a field matches the operation being performed.
1352 _ensureFieldType: function (id, type) {
1353 let info = this._fieldsByID.get(id);
1355 if (!info || !Array.isArray(info)) {
1356 throw new Error("Unknown field ID: " + id);
1359 if (type != info[2]) {
1360 throw new Error("Field type does not match the expected for this " +
1361 "operation. Actual: " + info[2] + "; Expected: " +
1367 * Enqueue a storage operation to be performed when the database is ready.
1369 * The primary use case of this function is to prevent potentially
1370 * conflicting storage operations from being performed in parallel. By
1371 * calling this function, passed storage operations will be serially
1372 * executed, avoiding potential order of operation issues.
1374 * The passed argument is a function that will perform storage operations.
1375 * The function should return a promise that will be resolved when all
1376 * storage operations have been completed.
1378 * The passed function may be executed immediately. If there are already
1379 * queued operations, it will be appended to the queue and executed after all
1380 * before it have finished.
1382 * This function returns a promise that will be resolved or rejected with
1383 * the same value that the function's promise was resolved or rejected with.
1386 * (function) Function performing storage interactions.
1389 enqueueOperation: function (func) {
1390 if (typeof(func) != "function") {
1391 throw new Error("enqueueOperation expects a function. Got: " + typeof(func));
1394 this._log.trace("Enqueueing operation.");
1395 let deferred = Promise.defer();
1397 this._queuedOperations.push([func, deferred]);
1399 if (this._queuedOperations.length == 1) {
1400 this._popAndPerformQueuedOperation();
1403 return deferred.promise;
1407 * Enqueue a function to be performed as a transaction.
1409 * The passed function should be a generator suitable for calling with
1410 * `executeTransaction` from the SQLite connection.
1412 enqueueTransaction: function (func, type) {
1413 return this.enqueueOperation(
1414 this._connection.executeTransaction.bind(this._connection, func, type)
1418 _popAndPerformQueuedOperation: function () {
1419 if (!this._queuedOperations.length || this._queuedInProgress) {
1423 this._log.trace("Performing queued operation.");
1424 let [func, deferred] = this._queuedOperations.shift();
1428 this._queuedInProgress = true;
1431 this._log.warn("Queued operation threw during execution: " +
1432 CommonUtils.exceptionStr(ex));
1433 this._queuedInProgress = false;
1434 deferred.reject(ex);
1435 this._popAndPerformQueuedOperation();
1439 if (!promise || typeof(promise.then) != "function") {
1440 let msg = "Queued operation did not return a promise: " + func;
1441 this._log.warn(msg);
1443 this._queuedInProgress = false;
1444 deferred.reject(new Error(msg));
1445 this._popAndPerformQueuedOperation();
1450 function onSuccess(result) {
1451 this._log.trace("Queued operation completed.");
1452 this._queuedInProgress = false;
1453 deferred.resolve(result);
1454 this._popAndPerformQueuedOperation();
1456 function onError(error) {
1457 this._log.warn("Failure when performing queued operation: " +
1458 CommonUtils.exceptionStr(error));
1459 this._queuedInProgress = false;
1460 deferred.reject(error);
1461 this._popAndPerformQueuedOperation();
1467 * Obtain all values associated with a measurement.
1469 * This returns a promise that resolves to an object. The keys of the object
1472 * days -- DailyValues where the values are Maps of field name to data
1473 * structures. The data structures could be simple (string or number) or
1474 * Arrays if the field type allows multiple values per day.
1476 * singular -- Map of field names to values. This holds all fields that
1477 * don't have a temporal component.
1480 * (Number) Primary key of measurement whose values to retrieve.
1482 getMeasurementValues: function (id) {
1483 let deferred = Promise.defer();
1484 let days = new DailyValues();
1485 let singular = new Map();
1488 this.enqueueOperation(function enqueuedGetMeasurementValues() {
1489 return Task.spawn(function fetchMeasurementValues() {
1490 function handleResult(data) {
1491 for (let [field, values] of data) {
1492 for (let [day, value] of Iterator(values)) {
1493 if (!days.hasDay(day)) {
1494 days.setDay(day, new Map());
1497 days.getDay(day).set(field, value);
1502 if (self.measurementHasAnyDailyCounterFields(id)) {
1503 let counters = yield self.getMeasurementDailyCountersFromMeasurementID(id);
1504 handleResult(counters);
1507 if (self.measurementHasAnyDailyLastFields(id)) {
1508 let dailyLast = yield self.getMeasurementDailyLastValuesFromMeasurementID(id);
1509 handleResult(dailyLast);
1512 if (self.measurementHasAnyDailyDiscreteFields(id)) {
1513 let dailyDiscrete = yield self.getMeasurementDailyDiscreteValuesFromMeasurementID(id);
1514 handleResult(dailyDiscrete);
1517 if (self.measurementHasAnyLastFields(id)) {
1518 let last = yield self.getMeasurementLastValuesFromMeasurementID(id);
1520 for (let [field, value] of last) {
1521 singular.set(field, value);
1526 }).then(function onSuccess() {
1527 deferred.resolve({singular: singular, days: days});
1528 }, function onError(error) {
1529 deferred.reject(error);
1532 return deferred.promise;
1535 //---------------------------------------------------------------------------
1536 // Low-level storage operations
1538 // These will be performed immediately (or at least as soon as the underlying
1539 // connection allows them to be.) It is recommended to call these from within
1540 // a function added via `enqueueOperation()` or they may inadvertently be
1541 // performed during another enqueued operation, which may be a transaction
1542 // that is rolled back.
1543 // ---------------------------------------------------------------------------
1546 * Set state for a provider.
1548 * Providers have the ability to register persistent state with the backend.
1549 * Persistent state doesn't expire. The format of the data is completely up
1550 * to the provider beyond the requirement that values be UTF-8 strings.
1552 * This returns a promise that will be resolved when the underlying database
1553 * operation has completed.
1556 * (string) Name of the provider.
1558 * (string) Key under which to store this state.
1560 * (string) Value for this state.
1563 setProviderState: function (provider, key, value) {
1564 if (typeof(key) != "string") {
1565 throw new Error("State key must be a string. Got: " + key);
1568 if (typeof(value) != "string") {
1569 throw new Error("State value must be a string. Got: " + value);
1572 let id = this.providerID(provider);
1574 throw new Error("Unknown provider: " + provider);
1577 return this._connection.executeCached(SQL.setProviderState, {
1585 * Obtain named state for a provider.
1588 * The returned promise will resolve to the state from the database or null
1589 * if the key is not stored.
1592 * (string) The name of the provider whose state to obtain.
1594 * (string) The state's key to retrieve.
1596 * @return Promise<data>
1598 getProviderState: function (provider, key) {
1599 let id = this.providerID(provider);
1601 throw new Error("Unknown provider: " + provider);
1604 let conn = this._connection;
1605 return Task.spawn(function queryDB() {
1606 let rows = yield conn.executeCached(SQL.getProviderStateWithName, {
1612 throw new Task.Result(null);
1615 throw new Task.Result(rows[0].getResultByIndex(0));
1620 * Increment a daily counter from a numeric field id.
1623 * (integer) Primary key of field to increment.
1625 * (Date) When the increment occurred. This is typically "now" but can
1626 * be explicitly defined for events that occurred in the past.
1628 * (integer) How much to increment the value by. Defaults to 1.
1630 incrementDailyCounterFromFieldID: function (id, date=new Date(), by=1) {
1631 this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
1635 days: dateToDays(date),
1639 return this._connection.executeCached(SQL.incrementDailyCounterFromFieldID,
1644 * Obtain all counts for a specific daily counter.
1647 * (integer) The ID of the field being retrieved.
1649 getDailyCounterCountsFromFieldID: function (id) {
1650 this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
1653 return Task.spawn(function fetchCounterDays() {
1654 let rows = yield self._connection.executeCached(SQL.getDailyCounterCountsFromFieldID,
1657 let result = new DailyValues();
1658 for (let row of rows) {
1659 let days = row.getResultByIndex(0);
1660 let counter = row.getResultByIndex(1);
1662 let date = daysToDate(days);
1663 result.setDay(date, counter);
1666 throw new Task.Result(result);
1671 * Get the value of a daily counter for a given day.
1674 * (integer) Field ID to retrieve.
1676 * (Date) Date for day from which to obtain data.
1678 getDailyCounterCountFromFieldID: function (field, date) {
1679 this._ensureFieldType(field, this.FIELD_DAILY_COUNTER);
1683 days: dateToDays(date),
1687 return Task.spawn(function fetchCounter() {
1688 let rows = yield self._connection.executeCached(SQL.getDailyCounterCountFromFieldID,
1691 throw new Task.Result(null);
1694 throw new Task.Result(rows[0].getResultByIndex(0));
1699 * Define the value for a "last numeric" field.
1701 * The previous value (if any) will be replaced by the value passed, even if
1702 * the date of the incoming value is older than what's recorded in the
1706 * (Number) Integer primary key of field to update.
1708 * (Number) Value to record.
1710 * (Date) When this value was produced.
1712 setLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
1713 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
1715 if (typeof(value) != "number") {
1716 throw new Error("Value is not a number: " + value);
1721 days: dateToDays(date),
1725 return this._connection.executeCached(SQL.setLastNumeric, params);
1729 * Define the value of a "last text" field.
1731 * See `setLastNumericFromFieldID` for behavior.
1733 setLastTextFromFieldID: function (fieldID, value, date=new Date()) {
1734 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
1736 if (typeof(value) != "string") {
1737 throw new Error("Value is not a string: " + value);
1742 days: dateToDays(date),
1746 return this._connection.executeCached(SQL.setLastText, params);
1750 * Obtain the value of a "last numeric" field.
1752 * This returns a promise that will be resolved with an Array of [date, value]
1753 * if a value is known or null if no last value is present.
1756 * (Number) Integer primary key of field to retrieve.
1758 getLastNumericFromFieldID: function (fieldID) {
1759 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
1762 return Task.spawn(function fetchLastField() {
1763 let rows = yield self._connection.executeCached(SQL.getLastNumericFromFieldID,
1764 {field_id: fieldID});
1767 throw new Task.Result(null);
1771 let days = row.getResultByIndex(0);
1772 let value = row.getResultByIndex(1);
1774 throw new Task.Result([daysToDate(days), value]);
1779 * Obtain the value of a "last text" field.
1781 * See `getLastNumericFromFieldID` for behavior.
1783 getLastTextFromFieldID: function (fieldID) {
1784 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
1787 return Task.spawn(function fetchLastField() {
1788 let rows = yield self._connection.executeCached(SQL.getLastTextFromFieldID,
1789 {field_id: fieldID});
1792 throw new Task.Result(null);
1796 let days = row.getResultByIndex(0);
1797 let value = row.getResultByIndex(1);
1799 throw new Task.Result([daysToDate(days), value]);
1804 * Delete the value (if any) in a "last numeric" field.
1806 deleteLastNumericFromFieldID: function (fieldID) {
1807 this._ensureFieldType(fieldID, this.FIELD_LAST_NUMERIC);
1809 return this._connection.executeCached(SQL.deleteLastNumericFromFieldID,
1810 {field_id: fieldID});
1814 * Delete the value (if any) in a "last text" field.
1816 deleteLastTextFromFieldID: function (fieldID) {
1817 this._ensureFieldType(fieldID, this.FIELD_LAST_TEXT);
1819 return this._connection.executeCached(SQL.deleteLastTextFromFieldID,
1820 {field_id: fieldID});
1824 * Record a value for a "daily last numeric" field.
1826 * The field can hold 1 value per calendar day. If the field already has a
1827 * value for the day specified (defaults to now), that value will be
1828 * replaced, even if the date specified is older (within the day) than the
1829 * previously recorded value.
1832 * (Number) Integer primary key of field.
1834 * (Number) Value to record.
1836 * (Date) When the value was produced. Defaults to now.
1838 setDailyLastNumericFromFieldID: function (fieldID, value, date=new Date()) {
1839 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
1843 days: dateToDays(date),
1847 return this._connection.executeCached(SQL.setDailyLastNumeric, params);
1851 * Record a value for a "daily last text" field.
1853 * See `setDailyLastNumericFromFieldID` for behavior.
1855 setDailyLastTextFromFieldID: function (fieldID, value, date=new Date()) {
1856 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
1860 days: dateToDays(date),
1864 return this._connection.executeCached(SQL.setDailyLastText, params);
1868 * Obtain value(s) from a "daily last numeric" field.
1870 * This returns a promise that resolves to a DailyValues instance. If `date`
1871 * is specified, that instance will have at most 1 entry. If there is no
1872 * `date` constraint, then all stored values will be retrieved.
1875 * (Number) Integer primary key of field to retrieve.
1876 * @param date optional
1877 * (Date) If specified, only return data for this day.
1879 * @return Promise<DailyValues>
1881 getDailyLastNumericFromFieldID: function (fieldID, date=null) {
1882 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_NUMERIC);
1884 let params = {field_id: fieldID};
1885 let name = "getDailyLastNumericFromFieldID";
1888 params.days = dateToDays(date);
1889 name = "getDailyLastNumericFromFieldIDAndDay";
1892 return this._getDailyLastFromFieldID(name, params);
1896 * Obtain value(s) from a "daily last text" field.
1898 * See `getDailyLastNumericFromFieldID` for behavior.
1900 getDailyLastTextFromFieldID: function (fieldID, date=null) {
1901 this._ensureFieldType(fieldID, this.FIELD_DAILY_LAST_TEXT);
1903 let params = {field_id: fieldID};
1904 let name = "getDailyLastTextFromFieldID";
1907 params.days = dateToDays(date);
1908 name = "getDailyLastTextFromFieldIDAndDay";
1911 return this._getDailyLastFromFieldID(name, params);
1914 _getDailyLastFromFieldID: function (name, params) {
1916 return Task.spawn(function fetchDailyLastForField() {
1917 let rows = yield self._connection.executeCached(SQL[name], params);
1919 let result = new DailyValues();
1920 for (let row of rows) {
1921 let d = daysToDate(row.getResultByIndex(0));
1922 let value = row.getResultByIndex(1);
1924 result.setDay(d, value);
1927 throw new Task.Result(result);
1932 * Add a new value for a "daily discrete numeric" field.
1934 * This appends a new value to the list of values for a specific field. All
1935 * values are retained. Duplicate values are allowed.
1938 * (Number) Integer primary key of field.
1940 * (Number) Value to record.
1941 * @param date optional
1942 * (Date) When this value occurred. Values are bucketed by day.
1944 addDailyDiscreteNumericFromFieldID: function (fieldID, value, date=new Date()) {
1945 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
1947 if (typeof(value) != "number") {
1948 throw new Error("Number expected. Got: " + value);
1953 days: dateToDays(date),
1957 return this._connection.executeCached(SQL.addDailyDiscreteNumeric, params);
1961 * Add a new value for a "daily discrete text" field.
1963 * See `addDailyDiscreteNumericFromFieldID` for behavior.
1965 addDailyDiscreteTextFromFieldID: function (fieldID, value, date=new Date()) {
1966 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
1968 if (typeof(value) != "string") {
1969 throw new Error("String expected. Got: " + value);
1974 days: dateToDays(date),
1978 return this._connection.executeCached(SQL.addDailyDiscreteText, params);
1982 * Obtain values for a "daily discrete numeric" field.
1984 * This returns a promise that resolves to a `DailyValues` instance. If
1985 * `date` is specified, there will be at most 1 key in that instance. If
1986 * not, all data from the database will be retrieved.
1988 * Values in that instance will be arrays of the raw values.
1991 * (Number) Integer primary key of field to retrieve.
1992 * @param date optional
1993 * (Date) Day to obtain data for. Date can be any time in the day.
1995 getDailyDiscreteNumericFromFieldID: function (fieldID, date=null) {
1996 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_NUMERIC);
1998 let params = {field_id: fieldID};
2000 let name = "getDailyDiscreteNumericFromFieldID";
2003 params.days = dateToDays(date);
2004 name = "getDailyDiscreteNumericFromFieldIDAndDay";
2007 return this._getDailyDiscreteFromFieldID(name, params);
2011 * Obtain values for a "daily discrete text" field.
2013 * See `getDailyDiscreteNumericFromFieldID` for behavior.
2015 getDailyDiscreteTextFromFieldID: function (fieldID, date=null) {
2016 this._ensureFieldType(fieldID, this.FIELD_DAILY_DISCRETE_TEXT);
2018 let params = {field_id: fieldID};
2020 let name = "getDailyDiscreteTextFromFieldID";
2023 params.days = dateToDays(date);
2024 name = "getDailyDiscreteTextFromFieldIDAndDay";
2027 return this._getDailyDiscreteFromFieldID(name, params);
2030 _getDailyDiscreteFromFieldID: function (name, params) {
2032 return Task.spawn(function fetchDailyDiscreteValuesForField() {
2033 let rows = yield self._connection.executeCached(SQL[name], params);
2035 let result = new DailyValues();
2036 for (let row of rows) {
2037 let d = daysToDate(row.getResultByIndex(0));
2038 let value = row.getResultByIndex(1);
2040 result.appendValue(d, value);
2043 throw new Task.Result(result);
2048 * Obtain the counts of daily counters in a measurement.
2050 * This returns a promise that resolves to a Map of field name strings to
2051 * DailyValues that hold per-day counts.
2054 * (Number) Integer primary key of measurement.
2056 * @return Promise<Map>
2058 getMeasurementDailyCountersFromMeasurementID: function (id) {
2060 return Task.spawn(function fetchDailyCounters() {
2061 let rows = yield self._connection.execute(SQL.getMeasurementDailyCounters,
2062 {measurement_id: id});
2064 let result = new Map();
2065 for (let row of rows) {
2066 let field = row.getResultByName("field_name");
2067 let date = daysToDate(row.getResultByName("day"));
2068 let value = row.getResultByName("value");
2070 if (!result.has(field)) {
2071 result.set(field, new DailyValues());
2074 result.get(field).setDay(date, value);
2077 throw new Task.Result(result);
2082 * Obtain the values of "last" fields from a measurement.
2084 * This returns a promise that resolves to a Map of field name to an array
2088 * (Number) Integer primary key of measurement whose data to retrieve.
2090 * @return Promise<Map>
2092 getMeasurementLastValuesFromMeasurementID: function (id) {
2094 return Task.spawn(function fetchMeasurementLastValues() {
2095 let rows = yield self._connection.execute(SQL.getMeasurementLastValues,
2096 {measurement_id: id});
2098 let result = new Map();
2099 for (let row of rows) {
2100 let date = daysToDate(row.getResultByIndex(1));
2101 let value = row.getResultByIndex(2);
2102 result.set(row.getResultByIndex(0), [date, value]);
2105 throw new Task.Result(result);
2110 * Obtain the values of "last daily" fields from a measurement.
2112 * This returns a promise that resolves to a Map of field name to DailyValues
2113 * instances. Each DailyValues instance has days for which a daily last value
2114 * is defined. The values in each DailyValues are the raw last value for that
2118 * (Number) Integer primary key of measurement whose data to retrieve.
2120 * @return Promise<Map>
2122 getMeasurementDailyLastValuesFromMeasurementID: function (id) {
2124 return Task.spawn(function fetchMeasurementDailyLastValues() {
2125 let rows = yield self._connection.execute(SQL.getMeasurementDailyLastValues,
2126 {measurement_id: id});
2128 let result = new Map();
2129 for (let row of rows) {
2130 let field = row.getResultByName("field_name");
2131 let date = daysToDate(row.getResultByName("day"));
2132 let value = row.getResultByName("value");
2134 if (!result.has(field)) {
2135 result.set(field, new DailyValues());
2138 result.get(field).setDay(date, value);
2141 throw new Task.Result(result);
2146 * Obtain the values of "daily discrete" fields from a measurement.
2148 * This obtains all discrete values for all "daily discrete" fields in a
2151 * This returns a promise that resolves to a Map. The Map's keys are field
2152 * string names. Values are `DailyValues` instances. The values inside
2153 * the `DailyValues` are arrays of the raw discrete values.
2156 * (Number) Integer primary key of measurement.
2158 * @return Promise<Map>
2160 getMeasurementDailyDiscreteValuesFromMeasurementID: function (id) {
2161 let deferred = Promise.defer();
2162 let result = new Map();
2164 this._connection.execute(SQL.getMeasurementDailyDiscreteValues,
2165 {measurement_id: id}, function onRow(row) {
2166 let field = row.getResultByName("field_name");
2167 let date = daysToDate(row.getResultByName("day"));
2168 let value = row.getResultByName("value");
2170 if (!result.has(field)) {
2171 result.set(field, new DailyValues());
2174 result.get(field).appendValue(date, value);
2175 }).then(function onComplete() {
2176 deferred.resolve(result);
2177 }, function onError(error) {
2178 deferred.reject(error);
2181 return deferred.promise;
2185 // Alias built-in field types to public API.
2186 for (let property of MetricsStorageSqliteBackend.prototype._BUILTIN_TYPES) {
2187 this.MetricsStorageBackend[property] = MetricsStorageSqliteBackend.prototype[property];