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