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