services/metrics/storage.jsm

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

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

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

     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);
  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.
  1037    * Fields are what recorded pieces of data are primarily associated with.
  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.
  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.
  1052    * @return Promise<integer>
  1053    */
  1054   registerField: function (measurementID, field, valueType) {
  1055     if (!valueType) {
  1056       throw new Error("Value type must be defined.");
  1059     if (!this._measurementsByID.has(measurementID)) {
  1060       throw new Error("Measurement not known: " + measurementID);
  1063     if (!this._typesByName.has(valueType)) {
  1064       throw new Error("Unknown value type: " + valueType);
  1067     let typeID = this._typesByName.get(valueType);
  1069     if (!typeID) {
  1070       throw new Error("Undefined type: " + valueType);
  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);
  1081       return CommonUtils.laterTickResolvingPromise(
  1082         this.fieldIDFromMeasurement(measurementID, field));
  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.
  1110    * This performs 2 major roles:
  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);
  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);
  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");
  1157         // And always use full synchronous mode to reduce possibility for data
  1158         // loss.
  1159         yield self._connection.execute("PRAGMA synchronous=FULL");
  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]);
  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.");
  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;
  1201         missingTypes.push(type);
  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);
  1216         });
  1218         doCheckpoint = true;
  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();
  1256       yield self.setAutoCheckpoint(1);
  1257     });
  1258   },
  1260   /**
  1261    * Prune all data from earlier than the specified date.
  1263    * Data stored on days before the specified Date will be permanently
  1264    * deleted.
  1266    * This returns a promise that will be resolved when data has been deleted.
  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);
  1284       });
  1285     });
  1286   },
  1288   /**
  1289    * Reduce memory usage as much as possible.
  1291    * This returns a promise that will be resolved on completion.
  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.
  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();
  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();
  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.
  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);
  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);
  1354   },
  1356   /**
  1357    * Enqueue a storage operation to be performed when the database is ready.
  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.
  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.
  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.
  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.
  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));
  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();
  1393     return deferred.promise;
  1394   },
  1396   /**
  1397    * Enqueue a function to be performed as a transaction.
  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;
  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;
  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;
  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.
  1459    * This returns a promise that resolves to an object. The keys of the object
  1460    * are:
  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.
  1466    *   singular -- Map of field names to values. This holds all fields that
  1467    *     don't have a temporal component.
  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());
  1487               days.getDay(day).set(field, value);
  1492         if (self.measurementHasAnyDailyCounterFields(id)) {
  1493           let counters = yield self.getMeasurementDailyCountersFromMeasurementID(id);
  1494           handleResult(counters);
  1497         if (self.measurementHasAnyDailyLastFields(id)) {
  1498           let dailyLast = yield self.getMeasurementDailyLastValuesFromMeasurementID(id);
  1499           handleResult(dailyLast);
  1502         if (self.measurementHasAnyDailyDiscreteFields(id)) {
  1503           let dailyDiscrete = yield self.getMeasurementDailyDiscreteValuesFromMeasurementID(id);
  1504           handleResult(dailyDiscrete);
  1507         if (self.measurementHasAnyLastFields(id)) {
  1508           let last = yield self.getMeasurementLastValuesFromMeasurementID(id);
  1510           for (let [field, value] of last) {
  1511             singular.set(field, value);
  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.
  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.
  1542    * This returns a promise that will be resolved when the underlying database
  1543    * operation has completed.
  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);
  1558     if (typeof(value) != "string") {
  1559       throw new Error("State value must be a string. Got: " + value);
  1562     let id = this.providerID(provider);
  1563     if (!id) {
  1564       throw new Error("Unknown provider: " + provider);
  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.
  1578    * The returned promise will resolve to the state from the database or null
  1579    * if the key is not stored.
  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.
  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);
  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);
  1605       throw new Task.Result(rows[0].getResultByIndex(0));
  1606     });
  1607   },
  1609   /**
  1610    * Increment a daily counter from a numeric field id.
  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.
  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);
  1656       throw new Task.Result(result);
  1657     });
  1658   },
  1660   /**
  1661    * Get the value of a daily counter for a given day.
  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);
  1684       throw new Task.Result(rows[0].getResultByIndex(0));
  1685     });
  1686   },
  1688   /**
  1689    * Define the value for a "last numeric" field.
  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.
  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);
  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.
  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);
  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.
  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.
  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);
  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.
  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);
  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.
  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.
  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.
  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.
  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.
  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.
  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";
  1882     return this._getDailyLastFromFieldID(name, params);
  1883   },
  1885   /**
  1886    * Obtain value(s) from a "daily last text" field.
  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";
  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);
  1917       throw new Task.Result(result);
  1918     });
  1919   },
  1921   /**
  1922    * Add a new value for a "daily discrete numeric" field.
  1924    * This appends a new value to the list of values for a specific field. All
  1925    * values are retained. Duplicate values are allowed.
  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);
  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.
  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);
  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.
  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.
  1978    * Values in that instance will be arrays of the raw values.
  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";
  1997     return this._getDailyDiscreteFromFieldID(name, params);
  1998   },
  2000   /**
  2001    * Obtain values for a "daily discrete text" field.
  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";
  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);
  2033       throw new Task.Result(result);
  2034     });
  2035   },
  2037   /**
  2038    * Obtain the counts of daily counters in a measurement.
  2040    * This returns a promise that resolves to a Map of field name strings to
  2041    * DailyValues that hold per-day counts.
  2043    * @param id
  2044    *        (Number) Integer primary key of measurement.
  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());
  2064         result.get(field).setDay(date, value);
  2067       throw new Task.Result(result);
  2068     });
  2069   },
  2071   /**
  2072    * Obtain the values of "last" fields from a measurement.
  2074    * This returns a promise that resolves to a Map of field name to an array
  2075    * of [date, value].
  2077    * @param id
  2078    *        (Number) Integer primary key of measurement whose data to retrieve.
  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]);
  2095       throw new Task.Result(result);
  2096     });
  2097   },
  2099   /**
  2100    * Obtain the values of "last daily" fields from a measurement.
  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.
  2107    * @param id
  2108    *        (Number) Integer primary key of measurement whose data to retrieve.
  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());
  2128         result.get(field).setDay(date, value);
  2131       throw new Task.Result(result);
  2132     });
  2133   },
  2135   /**
  2136    * Obtain the values of "daily discrete" fields from a measurement.
  2138    * This obtains all discrete values for all "daily discrete" fields in a
  2139    * measurement.
  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.
  2145    * @param id
  2146    *        (Number) Integer primary key of measurement.
  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());
  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];

mercurial