@mxpicture/tesla-superchargers-mate 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/001_baseline.sql +426 -0
- package/db/schema.sql +1749 -0
- package/dist/store/schema.d.ts +41 -6
- package/dist/store/schema.d.ts.map +1 -1
- package/dist/store/schema.js +100 -7
- package/dist/store/schema.js.map +1 -1
- package/package.json +4 -2
- package/sql/address_clustering.sql +6 -5
- package/sql/reset.sql +12 -3
- package/sql/views-drop.sql +57 -0
- package/sql/{setup.sql → views.sql} +24 -461
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
-- Baseline: the whole stateful half of the pic_* schema as it stood when
|
|
2
|
+
-- migrations were introduced (2026-08-28), taken verbatim from the then
|
|
3
|
+
-- setup.sql. Every statement is idempotent (CREATE ... IF NOT EXISTS, plus the
|
|
4
|
+
-- one pic_pricebooks ALTER and its backfill), so this is both a complete build
|
|
5
|
+
-- on an empty database and a no-op on the production database that already has
|
|
6
|
+
-- these tables -- which is why no manual baselining step was needed there.
|
|
7
|
+
--
|
|
8
|
+
-- Everything after this migration is an ordinary forward migration: add one
|
|
9
|
+
-- with `pnpm run db:new -- <name>` and write the ALTER by hand. Never edit an
|
|
10
|
+
-- applied migration; this file included.
|
|
11
|
+
--
|
|
12
|
+
-- Views and the pic_zip_place function are NOT here -- they are derived, and
|
|
13
|
+
-- live in ../../sql/views.sql, which is dropped and recreated wholesale around
|
|
14
|
+
-- every migration run. See docs/plans/2026-08-27_dbmate-migrations.md.
|
|
15
|
+
--
|
|
16
|
+
-- `cube` and `earthdistance` (needed by ll_to_earth below) come from
|
|
17
|
+
-- TeslaMate's own schema; they are database-level and not created here.
|
|
18
|
+
-- migrate:up
|
|
19
|
+
CREATE TABLE IF NOT EXISTS pic_cluster (
|
|
20
|
+
cluster_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
21
|
+
center_lat DOUBLE PRECISION NOT NULL,
|
|
22
|
+
center_lon DOUBLE PRECISION NOT NULL,
|
|
23
|
+
seed_address_id INTEGER NOT NULL,
|
|
24
|
+
radius_m DOUBLE PRECISION NOT NULL,
|
|
25
|
+
member_count INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
27
|
+
CONSTRAINT pic_cluster_lat_ck CHECK (center_lat BETWEEN -90 AND 90),
|
|
28
|
+
CONSTRAINT pic_cluster_lon_ck CHECK (center_lon BETWEEN -180 AND 180)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS pic_cluster_gix ON pic_cluster USING gist (ll_to_earth (center_lat, center_lon));
|
|
32
|
+
|
|
33
|
+
-- Address -> cluster assignment. No FK to addresses: keeps that table pristine.
|
|
34
|
+
CREATE TABLE IF NOT EXISTS pic_cluster_member (
|
|
35
|
+
address_id INTEGER PRIMARY KEY,
|
|
36
|
+
cluster_id BIGINT NOT NULL REFERENCES pic_cluster (cluster_id) ON DELETE CASCADE,
|
|
37
|
+
distance_m DOUBLE PRECISION NOT NULL,
|
|
38
|
+
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
CREATE INDEX IF NOT EXISTS pic_cluster_member_cluster_idx ON pic_cluster_member (cluster_id);
|
|
42
|
+
|
|
43
|
+
-- postal codes ------------------------------------------------------------------------
|
|
44
|
+
-- Imported from zauberware/postal-codes-json-xml-csv by `import-zip-codes`.
|
|
45
|
+
-- One row per (country, zipcode, place): a zipcode that covers several places
|
|
46
|
+
-- keeps all of them, because pic_zip_place() picks between them by distance.
|
|
47
|
+
CREATE TABLE IF NOT EXISTS pic_zip_country (
|
|
48
|
+
country_code CHAR(2) PRIMARY KEY,
|
|
49
|
+
country_name TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS pic_zip_codes (
|
|
53
|
+
country_code CHAR(2) NOT NULL REFERENCES pic_zip_country (country_code),
|
|
54
|
+
zipcode VARCHAR(20) NOT NULL,
|
|
55
|
+
place TEXT NOT NULL,
|
|
56
|
+
state_code VARCHAR(10),
|
|
57
|
+
state_name TEXT,
|
|
58
|
+
province_code VARCHAR(10),
|
|
59
|
+
province_name TEXT,
|
|
60
|
+
community_code VARCHAR(10),
|
|
61
|
+
community_name TEXT,
|
|
62
|
+
latitude DOUBLE PRECISION NOT NULL,
|
|
63
|
+
longitude DOUBLE PRECISION NOT NULL,
|
|
64
|
+
imported_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
65
|
+
PRIMARY KEY (country_code, zipcode, place)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
-- The resolver's lookup: every candidate place for one zipcode.
|
|
69
|
+
CREATE INDEX IF NOT EXISTS pic_zip_codes_zip_idx ON pic_zip_codes (country_code, zipcode);
|
|
70
|
+
|
|
71
|
+
-- Staging for COPY. UNLOGGED + all-TEXT; the column order MUST match the
|
|
72
|
+
-- dataset's CSV header exactly.
|
|
73
|
+
CREATE UNLOGGED TABLE IF NOT EXISTS pic_zip_import (
|
|
74
|
+
country_code TEXT,
|
|
75
|
+
zipcode TEXT,
|
|
76
|
+
place TEXT,
|
|
77
|
+
state TEXT,
|
|
78
|
+
state_code TEXT,
|
|
79
|
+
province TEXT,
|
|
80
|
+
province_code TEXT,
|
|
81
|
+
community TEXT,
|
|
82
|
+
community_code TEXT,
|
|
83
|
+
latitude TEXT,
|
|
84
|
+
longitude TEXT
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
-- slugs ------------------------------------------------------------------------------
|
|
88
|
+
CREATE TABLE IF NOT EXISTS pic_slugs (
|
|
89
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
90
|
+
fetched_at TIMESTAMPTZ NOT NULL,
|
|
91
|
+
last_fetch_uuid TEXT NOT NULL,
|
|
92
|
+
failure_count SMALLINT NOT NULL,
|
|
93
|
+
--
|
|
94
|
+
latitude DOUBLE PRECISION NOT NULL, -- number
|
|
95
|
+
longitude DOUBLE PRECISION NOT NULL, -- number
|
|
96
|
+
location_types TEXT[] NOT NULL DEFAULT '{}',
|
|
97
|
+
location_url_slug TEXT NOT NULL, -- string
|
|
98
|
+
tesla_uuid TEXT UNIQUE, -- string (uuid), semantic key for syncs
|
|
99
|
+
title TEXT, -- string|null
|
|
100
|
+
access_type TEXT, -- string (supercharger_function.access_type)
|
|
101
|
+
actual_latitude TEXT, -- string (supercharger_function.actual_latitude)
|
|
102
|
+
actual_longitude TEXT, -- string (supercharger_function.actual_longitude)
|
|
103
|
+
charging_accessibility TEXT, -- string (supercharger_function.charging_accessibility)
|
|
104
|
+
coming_soon_latitude TEXT, -- string (supercharger_function.coming_soon_latitude)
|
|
105
|
+
coming_soon_longitude TEXT, -- string (supercharger_function.coming_soon_longitude)
|
|
106
|
+
coming_soon_name TEXT, -- string (supercharger_function.coming_soon_name)
|
|
107
|
+
open_to_non_tesla BOOLEAN, -- boolean (supercharger_function.open_to_non_tesla)
|
|
108
|
+
project_status TEXT, -- string (supercharger_function.project_status)
|
|
109
|
+
show_on_find_us TEXT, -- string (supercharger_function.show_on_find_us)
|
|
110
|
+
site_status TEXT, -- string (supercharger_function.site_status)
|
|
111
|
+
vote_winner_quarter TEXT, -- string (supercharger_function.vote_winner_quarter)
|
|
112
|
+
failure_notified_at TIMESTAMPTZ -- set when a failure notification covered this row; NULL = not yet notified
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE INDEX IF NOT EXISTS pic_slugs_location_types_gin ON pic_slugs USING gin (location_types);
|
|
116
|
+
|
|
117
|
+
CREATE INDEX IF NOT EXISTS pic_slugs_location_slug_idx ON pic_slugs (location_url_slug);
|
|
118
|
+
|
|
119
|
+
CREATE INDEX IF NOT EXISTS pic_slugs_fetched_at_idx ON pic_slugs (fetched_at);
|
|
120
|
+
|
|
121
|
+
CREATE INDEX IF NOT EXISTS pic_slugs_locslug_time_idx ON pic_slugs (location_url_slug, fetched_at DESC);
|
|
122
|
+
|
|
123
|
+
-- chargers ------------------------------------------------------------------------------
|
|
124
|
+
CREATE TABLE IF NOT EXISTS pic_chargers (
|
|
125
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
126
|
+
fetched_at TIMESTAMPTZ NOT NULL,
|
|
127
|
+
last_fetch_uuid TEXT NOT NULL,
|
|
128
|
+
deleted_at TIMESTAMPTZ,
|
|
129
|
+
failure_count SMALLINT NOT NULL,
|
|
130
|
+
pic_slugs_id BIGINT REFERENCES pic_slugs (id) ON DELETE SET NULL,
|
|
131
|
+
location_url_slug TEXT, -- the details slug this row was fetched by; kept here because pic_slugs is hard-deleted with the feed
|
|
132
|
+
--
|
|
133
|
+
access_type TEXT NOT NULL, -- string (accessType)
|
|
134
|
+
location_guid TEXT NOT NULL UNIQUE, -- string (locationGUID), semantic key for syncs
|
|
135
|
+
max_power_kw SMALLINT NOT NULL, -- number (maxPowerKw)
|
|
136
|
+
name TEXT NOT NULL, -- string
|
|
137
|
+
public_stall_count SMALLINT NOT NULL, -- number
|
|
138
|
+
time_zone TEXT NOT NULL, -- string
|
|
139
|
+
entry_point_latitude DOUBLE PRECISION NOT NULL, -- number (entryPoint.latitude)
|
|
140
|
+
entry_point_longitude DOUBLE PRECISION NOT NULL, -- number (entryPoint.longitude)
|
|
141
|
+
centroid_latitude DOUBLE PRECISION NOT NULL, -- number (centroid.latitude)
|
|
142
|
+
centroid_longitude DOUBLE PRECISION NOT NULL, -- number (centroid.longitude)
|
|
143
|
+
brand_name TEXT, -- string|null (brandName)
|
|
144
|
+
common_site_name TEXT NOT NULL, -- string (commonSiteName)
|
|
145
|
+
city TEXT NOT NULL, -- string
|
|
146
|
+
country TEXT NOT NULL, -- string
|
|
147
|
+
country_code TEXT NOT NULL, -- string (countryCode)
|
|
148
|
+
district TEXT, -- string|null
|
|
149
|
+
nav_region TEXT NOT NULL, -- string (navRegion)
|
|
150
|
+
nearest_entrance TEXT, -- string|null (nearestEntrance)
|
|
151
|
+
postal_code TEXT NOT NULL, -- string (postalCode)
|
|
152
|
+
postal_ext TEXT, -- string|null (postalExt)
|
|
153
|
+
state TEXT, -- string|null
|
|
154
|
+
street TEXT NOT NULL, -- string
|
|
155
|
+
street_number TEXT NOT NULL, -- string|null (streetNumber), stored as "" when the feed sends null
|
|
156
|
+
site_id BIGINT NOT NULL, -- number (availabilityProfile.siteId.id)
|
|
157
|
+
site_type TEXT NOT NULL, -- string (availabilityProfile.siteId.siteType)
|
|
158
|
+
trt_id BIGINT NOT NULL, -- number (availabilityProfile.trtId.id)
|
|
159
|
+
trt_type TEXT NOT NULL, -- string (availabilityProfile.trtId.siteType)
|
|
160
|
+
utc_offset TEXT, -- string|null (availabilityProfile.utcOffset)
|
|
161
|
+
failure_notified_at TIMESTAMPTZ, -- set when a failure notification covered this row; NULL = not yet notified
|
|
162
|
+
favourite BOOLEAN NOT NULL DEFAULT FALSE -- user-set flag; not part of the Tesla feed, preserved across re-syncs
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
CREATE INDEX IF NOT EXISTS pic_chargers_location_slug_idx ON pic_chargers (pic_slugs_id);
|
|
166
|
+
|
|
167
|
+
CREATE INDEX IF NOT EXISTS pic_chargers_fetched_at_idx ON pic_chargers (fetched_at);
|
|
168
|
+
|
|
169
|
+
CREATE INDEX IF NOT EXISTS pic_chargers_locslug_time_idx ON pic_chargers (pic_slugs_id, fetched_at DESC);
|
|
170
|
+
|
|
171
|
+
CREATE INDEX IF NOT EXISTS pic_chargers_locurl_slug_idx ON pic_chargers (location_url_slug, fetched_at DESC);
|
|
172
|
+
|
|
173
|
+
-- pricebooks ------------------------------------------------------------------------------
|
|
174
|
+
-- History table: one snapshot ("group") = all rows sharing
|
|
175
|
+
-- (pic_chargers_id, valid_since). valid_since is the group's creation time and
|
|
176
|
+
-- never changes; fetched_at is the last fetch that returned this exact
|
|
177
|
+
-- snapshot, so it advances on an unchanged re-fetch (which also bumps
|
|
178
|
+
-- last_fetch_uuid). fetched_at therefore means the same thing here as in
|
|
179
|
+
-- pic_chargers and pic_slugs: last contact with Tesla.
|
|
180
|
+
CREATE TABLE IF NOT EXISTS pic_pricebooks (
|
|
181
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
182
|
+
valid_since TIMESTAMPTZ NOT NULL,
|
|
183
|
+
fetched_at TIMESTAMPTZ NOT NULL,
|
|
184
|
+
last_fetch_uuid TEXT NOT NULL,
|
|
185
|
+
pic_chargers_id BIGINT NOT NULL REFERENCES pic_chargers (id) ON DELETE CASCADE,
|
|
186
|
+
--
|
|
187
|
+
currency_code TEXT NOT NULL, -- string (currencyCode)
|
|
188
|
+
uom TEXT NOT NULL, -- string (ucom)
|
|
189
|
+
days SMALLINT[] NOT NULL, -- string [0,1,...]
|
|
190
|
+
start_time TIME NOT NULL, -- time (startTime)
|
|
191
|
+
end_time TIME NOT NULL, -- time (endTime)
|
|
192
|
+
vehicle_make_type TEXT NOT NULL, -- string (vehicleMakeType)
|
|
193
|
+
fee_type TEXT NOT NULL, -- string (feeType)
|
|
194
|
+
is_member_pricebook BOOLEAN NOT NULL, -- boolean (isMemberPricebook)
|
|
195
|
+
is_tou BOOLEAN NOT NULL, -- boolean (isTou)
|
|
196
|
+
rate_base NUMERIC(10, 4) NOT NULL, -- number (rateBase)
|
|
197
|
+
rate_tier_1 NUMERIC(10, 4) NOT NULL, -- number (rateTier1)
|
|
198
|
+
rate_tier_2 NUMERIC(10, 4) NOT NULL, -- number (rateTier2)
|
|
199
|
+
-- derived: 0-based dense rank within the (pic_chargers_id, last_fetch_uuid)
|
|
200
|
+
-- group, ordered by start_time (rows sharing a start_time share the value);
|
|
201
|
+
-- NULL when is_tou = false
|
|
202
|
+
sort_order SMALLINT,
|
|
203
|
+
fx_rate_to_eur NUMERIC(18, 8),
|
|
204
|
+
rate_base_eur NUMERIC(10, 4),
|
|
205
|
+
rate_tier_1_eur NUMERIC(10, 4),
|
|
206
|
+
rate_tier_2_eur NUMERIC(10, 4)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
-- upgrade: the single fetched_at was split into valid_since (frozen, the old
|
|
210
|
+
-- column renamed) + fetched_at (last confirming fetch). CREATE TABLE IF NOT
|
|
211
|
+
-- EXISTS is a no-op on an existing database, so the split has to be applied
|
|
212
|
+
-- explicitly here — before the views below are recreated against it.
|
|
213
|
+
-- RENAME COLUMN has no IF NOT EXISTS, hence the guard.
|
|
214
|
+
DO $$ BEGIN
|
|
215
|
+
IF EXISTS (SELECT 1 FROM information_schema.columns
|
|
216
|
+
WHERE table_schema = 'public' AND table_name = 'pic_pricebooks'
|
|
217
|
+
AND column_name = 'fetched_at')
|
|
218
|
+
AND NOT EXISTS (SELECT 1 FROM information_schema.columns
|
|
219
|
+
WHERE table_schema = 'public' AND table_name = 'pic_pricebooks'
|
|
220
|
+
AND column_name = 'valid_since')
|
|
221
|
+
THEN ALTER TABLE pic_pricebooks RENAME COLUMN fetched_at TO valid_since;
|
|
222
|
+
END IF;
|
|
223
|
+
END $$;
|
|
224
|
+
|
|
225
|
+
ALTER TABLE pic_pricebooks
|
|
226
|
+
ADD COLUMN IF NOT EXISTS fetched_at TIMESTAMPTZ;
|
|
227
|
+
|
|
228
|
+
-- backfill: the current group was confirmed by the charger's last fetch; a
|
|
229
|
+
-- superseded group was only ever seen at its own valid_since. The current
|
|
230
|
+
-- group is the one with the max valid_since — resolving it via
|
|
231
|
+
-- c.last_fetch_uuid = p.last_fetch_uuid (as this first did) silently missed
|
|
232
|
+
-- every charger whose uuid had drifted off its pricebooks, and those fell
|
|
233
|
+
-- through to the fallback below: fetched_at = valid_since, which
|
|
234
|
+
-- pic_price_bound_v then reads as "last confirmed the day the price changed"
|
|
235
|
+
-- and truncates the site's history to that single day.
|
|
236
|
+
-- GREATEST over COALESCE keeps fetched_at >= valid_since even if the charger
|
|
237
|
+
-- row somehow predates its own current group (a zero-day interval would drop
|
|
238
|
+
-- the site from the history views entirely), and covers a NULL c.fetched_at
|
|
239
|
+
-- the same way COALESCE did.
|
|
240
|
+
UPDATE pic_pricebooks p
|
|
241
|
+
SET
|
|
242
|
+
fetched_at = GREATEST(c.fetched_at, p.valid_since)
|
|
243
|
+
FROM
|
|
244
|
+
pic_chargers c
|
|
245
|
+
WHERE
|
|
246
|
+
c.id = p.pic_chargers_id
|
|
247
|
+
AND p.fetched_at IS NULL
|
|
248
|
+
AND p.valid_since = (
|
|
249
|
+
SELECT
|
|
250
|
+
max(cur.valid_since)
|
|
251
|
+
FROM
|
|
252
|
+
pic_pricebooks cur
|
|
253
|
+
WHERE
|
|
254
|
+
cur.pic_chargers_id = p.pic_chargers_id
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
UPDATE pic_pricebooks
|
|
258
|
+
SET
|
|
259
|
+
fetched_at = valid_since
|
|
260
|
+
WHERE
|
|
261
|
+
fetched_at IS NULL;
|
|
262
|
+
|
|
263
|
+
-- repair for databases the uuid-joined backfill above already ran on: it left
|
|
264
|
+
-- every drifted charger's current group at fetched_at = valid_since, and the
|
|
265
|
+
-- guard it used (fetched_at IS NULL) no longer holds, so re-running setup
|
|
266
|
+
-- cannot heal them. Raise those to the charger's last contact — the value the
|
|
267
|
+
-- corrected backfill would have written. Idempotent: after this runs
|
|
268
|
+
-- fetched_at > valid_since, so the row no longer matches; a no-op on a fresh
|
|
269
|
+
-- database (no rows) and on one that was migrated correctly (the current group
|
|
270
|
+
-- already carries the charger's fetched_at). A group whose only contact really
|
|
271
|
+
-- was its own creation is raised to the charger's last fetch too, which is at
|
|
272
|
+
-- worst the same claim the backfill makes; the next successful re-fetch
|
|
273
|
+
-- overwrites it with a real confirmation either way.
|
|
274
|
+
UPDATE pic_pricebooks p
|
|
275
|
+
SET
|
|
276
|
+
fetched_at = c.fetched_at
|
|
277
|
+
FROM
|
|
278
|
+
pic_chargers c
|
|
279
|
+
WHERE
|
|
280
|
+
c.id = p.pic_chargers_id
|
|
281
|
+
AND p.fetched_at = p.valid_since
|
|
282
|
+
AND c.fetched_at > p.fetched_at
|
|
283
|
+
AND p.valid_since = (
|
|
284
|
+
SELECT
|
|
285
|
+
max(cur.valid_since)
|
|
286
|
+
FROM
|
|
287
|
+
pic_pricebooks cur
|
|
288
|
+
WHERE
|
|
289
|
+
cur.pic_chargers_id = p.pic_chargers_id
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
CREATE INDEX IF NOT EXISTS pic_pricebooks_loc_idx ON pic_pricebooks (pic_chargers_id);
|
|
293
|
+
|
|
294
|
+
-- group lookups: current group / group at a point in time. Resolved by
|
|
295
|
+
-- validity, not by confirmation; the pre-split index of the same name covered
|
|
296
|
+
-- (pic_chargers_id, fetched_at DESC) and CREATE INDEX IF NOT EXISTS would keep
|
|
297
|
+
-- it, so it is dropped first.
|
|
298
|
+
DROP INDEX IF EXISTS pic_pricebooks_charger_time_idx;
|
|
299
|
+
|
|
300
|
+
CREATE INDEX IF NOT EXISTS pic_pricebooks_charger_time_idx ON pic_pricebooks (pic_chargers_id, valid_since DESC);
|
|
301
|
+
|
|
302
|
+
-- charging process states ------------------------------------------------------------------------------
|
|
303
|
+
CREATE TABLE IF NOT EXISTS pic_charging_process_states (
|
|
304
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
305
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
306
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
307
|
+
--
|
|
308
|
+
charging_processes_id INTEGER NOT NULL, -- no foreign key --> teslamate should not fail because of this extension
|
|
309
|
+
failure_count SMALLINT NOT NULL,
|
|
310
|
+
failure_notified_at TIMESTAMPTZ -- set when a failure notification covered this row; NULL = not yet notified
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
-- the failure upsert conflicts on charging_processes_id, so the index must be
|
|
314
|
+
-- unique; the old non-unique pic_charg_proc_idx is dropped on upgrade
|
|
315
|
+
DROP INDEX IF EXISTS pic_charg_proc_idx;
|
|
316
|
+
|
|
317
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_charg_proc_uq_idx ON pic_charging_process_states (charging_processes_id);
|
|
318
|
+
|
|
319
|
+
-- charging process link ------------------------------------------------------------------------------
|
|
320
|
+
CREATE TABLE IF NOT EXISTS pic_charging_process_links (
|
|
321
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
322
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
323
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
324
|
+
--
|
|
325
|
+
charging_processes_id INTEGER NOT NULL, -- no foreign key --> teslamate should not fail because of this extension
|
|
326
|
+
chargers_id BIGINT REFERENCES pic_chargers (id) ON DELETE SET NULL
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
-- one link row per charge, so the lookup-then-write in updatePrice cannot
|
|
330
|
+
-- create a second one
|
|
331
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_charg_proc_link_uq_idx ON pic_charging_process_links (charging_processes_id);
|
|
332
|
+
|
|
333
|
+
-- invoices (from Tesla App) ------------------------------------------------------------------------------
|
|
334
|
+
CREATE TABLE IF NOT EXISTS pic_invoices (
|
|
335
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
336
|
+
fetched_at TIMESTAMPTZ NOT NULL,
|
|
337
|
+
last_fetch_uuid TEXT NOT NULL,
|
|
338
|
+
charging_processes_id INTEGER, -- no foreign key --> teslamate should not fail because of this extension
|
|
339
|
+
--
|
|
340
|
+
charge_start_date_time TIMESTAMPTZ NOT NULL,
|
|
341
|
+
name TEXT,
|
|
342
|
+
vin TEXT NOT NULL,
|
|
343
|
+
model TEXT,
|
|
344
|
+
country TEXT NOT NULL,
|
|
345
|
+
site_location_name TEXT NOT NULL,
|
|
346
|
+
description TEXT,
|
|
347
|
+
ucom TEXT NOT NULL,
|
|
348
|
+
quantity_base NUMERIC(8, 4) NOT NULL,
|
|
349
|
+
quantity_tier1 NUMERIC(8, 4),
|
|
350
|
+
quantity_tier2 NUMERIC(8, 4),
|
|
351
|
+
quantity_tier3 NUMERIC(8, 4),
|
|
352
|
+
quantity_tier4 NUMERIC(8, 4),
|
|
353
|
+
invoice_number TEXT NOT NULL,
|
|
354
|
+
unit_cost_base NUMERIC(4, 2) NOT NULL,
|
|
355
|
+
unit_cost_tier1 NUMERIC(4, 2),
|
|
356
|
+
unit_cost_tier2 NUMERIC(4, 2),
|
|
357
|
+
unit_cost_tier3 NUMERIC(4, 2),
|
|
358
|
+
unit_cost_tier4 NUMERIC(4, 2),
|
|
359
|
+
vat NUMERIC(5, 2) NOT NULL,
|
|
360
|
+
total_exc_vat NUMERIC(5, 2) NOT NULL,
|
|
361
|
+
total_inc_vat NUMERIC(5, 2) NOT NULL,
|
|
362
|
+
status TEXT,
|
|
363
|
+
invoice TEXT
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_uq_cp_idx ON pic_invoices (charging_processes_id);
|
|
367
|
+
|
|
368
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_uq_inv_idx ON pic_invoices (invoice_number);
|
|
369
|
+
|
|
370
|
+
-- invoice files (from Tesla App) ------------------------------------------------------------------------------
|
|
371
|
+
CREATE TABLE IF NOT EXISTS pic_invoice_files (
|
|
372
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
373
|
+
fetched_at TIMESTAMPTZ NOT NULL,
|
|
374
|
+
last_fetch_uuid TEXT NOT NULL,
|
|
375
|
+
--
|
|
376
|
+
name TEXT NOT NULL
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_invoices_name_idx ON pic_invoice_files (name);
|
|
380
|
+
|
|
381
|
+
-- invoice missing states ------------------------------------------------------------------------------
|
|
382
|
+
-- One row per Supercharger charge already paged about a missing invoice
|
|
383
|
+
-- (notify-unmatched-invoices), so each charge pages exactly once. A row is
|
|
384
|
+
-- irrelevant once an invoice arrives (the charge stops matching the query).
|
|
385
|
+
CREATE TABLE IF NOT EXISTS pic_invoice_missing_states (
|
|
386
|
+
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
387
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
388
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
389
|
+
--
|
|
390
|
+
charging_processes_id INTEGER NOT NULL, -- no foreign key --> teslamate should not fail because of this extension
|
|
391
|
+
notified_at TIMESTAMPTZ -- set when a missing-invoice notification covered this row; NULL = not yet notified
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
-- the notified upsert conflicts on charging_processes_id, so the index must be unique
|
|
395
|
+
CREATE UNIQUE INDEX IF NOT EXISTS pic_invoice_missing_uq_idx ON pic_invoice_missing_states (charging_processes_id);
|
|
396
|
+
|
|
397
|
+
-- migrate:down
|
|
398
|
+
-- Documentation of intent only. Rolling back the baseline drops the
|
|
399
|
+
-- accumulated price history, so it is never run against a real database --
|
|
400
|
+
-- sql/reset.sql is the deliberate, pic_*-scoped reset. Reverse order of the
|
|
401
|
+
-- creations above.
|
|
402
|
+
DROP TABLE IF EXISTS pic_invoice_missing_states;
|
|
403
|
+
|
|
404
|
+
DROP TABLE IF EXISTS pic_invoice_files;
|
|
405
|
+
|
|
406
|
+
DROP TABLE IF EXISTS pic_invoices;
|
|
407
|
+
|
|
408
|
+
DROP TABLE IF EXISTS pic_charging_process_links;
|
|
409
|
+
|
|
410
|
+
DROP TABLE IF EXISTS pic_charging_process_states;
|
|
411
|
+
|
|
412
|
+
DROP TABLE IF EXISTS pic_pricebooks;
|
|
413
|
+
|
|
414
|
+
DROP TABLE IF EXISTS pic_chargers;
|
|
415
|
+
|
|
416
|
+
DROP TABLE IF EXISTS pic_slugs;
|
|
417
|
+
|
|
418
|
+
DROP TABLE IF EXISTS pic_zip_import;
|
|
419
|
+
|
|
420
|
+
DROP TABLE IF EXISTS pic_zip_codes;
|
|
421
|
+
|
|
422
|
+
DROP TABLE IF EXISTS pic_zip_country;
|
|
423
|
+
|
|
424
|
+
DROP TABLE IF EXISTS pic_cluster_member;
|
|
425
|
+
|
|
426
|
+
DROP TABLE IF EXISTS pic_cluster;
|