1use crate::sql_telemetry_db::create_tables;
2use anyhow::{Context, Result};
3use micromegas_tracing::prelude::*;
4use sqlx::Executor;
5use sqlx::Row;
6
7pub const LATEST_DATA_LAKE_SCHEMA_VERSION: i32 = 11;
9
10pub async fn read_data_lake_schema_version(tr: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> i32 {
12 match sqlx::query(
13 "SELECT version
14 FROM migration;",
15 )
16 .fetch_one(&mut **tr)
17 .await
18 {
19 Ok(row) => row.get("version"),
20 Err(e) => {
21 info!(
22 "Error reading data lake schema version, assuming version 0: {}",
23 e
24 );
25 0
26 }
27 }
28}
29
30pub async fn warn_if_data_lake_schema_stale(pool: &sqlx::Pool<sqlx::Postgres>) {
38 let version = match pool.begin().await {
39 Ok(mut tr) => read_data_lake_schema_version(&mut tr).await,
40 Err(e) => {
41 warn!("could not check data lake schema version: {e}");
42 return;
43 }
44 };
45 if version < LATEST_DATA_LAKE_SCHEMA_VERSION {
46 warn!(
47 "data lake schema is v{version}, behind the latest v{LATEST_DATA_LAKE_SCHEMA_VERSION}; \
48 the seeded 'public' audience_grants rows have not migrated in yet, so every query \
49 will resolve to an empty read scope and return zero rows until the schema is upgraded; \
50 on a schema older than v10 the `groups`/`group_members` tables do not exist yet, so \
51 every request fails with a retryable 503 until the migration runs, because the group \
52 store cannot load"
53 );
54 }
55}
56
57pub async fn upgrade_data_lake_schema_v2(
59 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
60) -> Result<()> {
61 tr.execute("ALTER TABLE blocks ADD insert_time TIMESTAMPTZ;")
62 .await
63 .with_context(|| "adding column insert_time to blocks table")?;
64 tr.execute("UPDATE blocks SET insert_time=end_time WHERE insert_time is NULL;")
65 .await
66 .with_context(|| "use end_time as insert_time to backfill missing data")?;
67 tr.execute("CREATE INDEX block_begin_time on blocks(begin_time);")
68 .await
69 .with_context(|| "adding index block_begin_time")?;
70 tr.execute("CREATE INDEX block_end_time on blocks(end_time);")
71 .await
72 .with_context(|| "adding index block_end_time")?;
73 tr.execute("CREATE INDEX block_insert_time on blocks(insert_time);")
74 .await
75 .with_context(|| "adding index block_insert_time")?;
76 tr.execute("CREATE INDEX process_insert_time on processes(insert_time);")
77 .await
78 .with_context(|| "adding index process_insert_time")?;
79 tr.execute("UPDATE migration SET version=2;")
80 .await
81 .with_context(|| "Updating data lake schema version to 2")?;
82 Ok(())
83}
84
85pub async fn upgrade_data_lake_schema_v3(
88 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
89) -> Result<()> {
90 tr.execute("DROP INDEX IF EXISTS process_id;")
91 .await
92 .with_context(|| "dropping old non-unique index process_id")?;
93 tr.execute("DROP INDEX IF EXISTS stream_id;")
94 .await
95 .with_context(|| "dropping old non-unique index stream_id")?;
96 tr.execute("DROP INDEX IF EXISTS block_id;")
97 .await
98 .with_context(|| "dropping old non-unique index block_id")?;
99 tr.execute("UPDATE migration SET version=3;")
100 .await
101 .with_context(|| "updating data lake schema version to 3")?;
102 Ok(())
103}
104
105pub async fn upgrade_data_lake_schema_v4(
108 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
109) -> Result<()> {
110 tr.execute("ALTER TABLE streams ADD COLUMN format TEXT NOT NULL DEFAULT 'micromegas-transit';")
111 .await
112 .with_context(|| "adding column format to streams table")?;
113 tr.execute("UPDATE migration SET version=4;")
114 .await
115 .with_context(|| "updating data lake schema version to 4")?;
116 Ok(())
117}
118
119pub async fn upgrade_data_lake_schema_v5(
128 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
129) -> Result<()> {
130 tr.execute(
131 "CREATE TABLE ingestion_api_keys (
132 key_id UUID PRIMARY KEY,
133 key_hash BYTEA NOT NULL,
134 name VARCHAR(255) NOT NULL,
135 created_at TIMESTAMPTZ NOT NULL,
136 created_by VARCHAR(255) NOT NULL,
137 last_used_at TIMESTAMPTZ,
138 revoked_at TIMESTAMPTZ,
139 revoked_by VARCHAR(255)
140 );",
141 )
142 .await
143 .with_context(|| "creating table ingestion_api_keys")?;
144 tr.execute("CREATE UNIQUE INDEX ingestion_api_keys_key_hash ON ingestion_api_keys(key_hash);")
145 .await
146 .with_context(|| "creating unique index ingestion_api_keys_key_hash")?;
147 tr.execute(
148 "CREATE TABLE analytics_api_keys (
149 key_id UUID PRIMARY KEY,
150 key_hash BYTEA NOT NULL,
151 name VARCHAR(255) NOT NULL,
152 created_at TIMESTAMPTZ NOT NULL,
153 created_by VARCHAR(255) NOT NULL,
154 last_used_at TIMESTAMPTZ,
155 revoked_at TIMESTAMPTZ,
156 revoked_by VARCHAR(255)
157 );",
158 )
159 .await
160 .with_context(|| "creating table analytics_api_keys")?;
161 tr.execute("CREATE UNIQUE INDEX analytics_api_keys_key_hash ON analytics_api_keys(key_hash);")
162 .await
163 .with_context(|| "creating unique index analytics_api_keys_key_hash")?;
164 tr.execute("UPDATE migration SET version=5;")
165 .await
166 .with_context(|| "updating data lake schema version to 5")?;
167 Ok(())
168}
169
170pub async fn upgrade_data_lake_schema_v6(
180 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
181) -> Result<()> {
182 tr.execute("ALTER TABLE ingestion_api_keys ADD COLUMN audience VARCHAR(255);")
183 .await
184 .with_context(|| "adding column audience to ingestion_api_keys table")?;
185 tr.execute("UPDATE ingestion_api_keys SET audience = 'public' WHERE audience IS NULL;")
186 .await
187 .with_context(|| "backfilling audience to 'public' on ingestion_api_keys")?;
188 tr.execute("ALTER TABLE ingestion_api_keys ALTER COLUMN audience SET NOT NULL;")
189 .await
190 .with_context(|| "setting audience NOT NULL on ingestion_api_keys")?;
191 tr.execute(
192 "ALTER TABLE ingestion_api_keys ADD CONSTRAINT ingestion_api_keys_audience_name \
193 CHECK (audience ~ '^[A-Za-z0-9_-]+$');",
194 )
195 .await
196 .with_context(|| "adding audience-name CHECK constraint on ingestion_api_keys")?;
197 tr.execute("UPDATE migration SET version=6;")
198 .await
199 .with_context(|| "updating data lake schema version to 6")?;
200 Ok(())
201}
202
203pub async fn upgrade_data_lake_schema_v7(
216 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
217) -> Result<()> {
218 tr.execute(
219 "CREATE TABLE audience_grants (
220 audience VARCHAR(255) NOT NULL,
221 axis VARCHAR(4) NOT NULL CHECK (axis IN ('read', 'mint')),
222 selector VARCHAR(255) NOT NULL,
223 created_at TIMESTAMPTZ NOT NULL,
224 created_by VARCHAR(255) NOT NULL,
225 PRIMARY KEY (audience, axis, selector),
226 CONSTRAINT audience_grants_audience_name CHECK (audience ~ '^[A-Za-z0-9_-]+$'),
227 CONSTRAINT audience_grants_selector_shape
228 CHECK (selector = '*' OR selector ~ '^(user|group):.+$')
229 );",
230 )
231 .await
232 .with_context(|| "creating table audience_grants")?;
233 tr.execute("UPDATE migration SET version=7;")
234 .await
235 .with_context(|| "updating data lake schema version to 7")?;
236 Ok(())
237}
238
239pub async fn upgrade_data_lake_schema_v8(
255 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
256) -> Result<()> {
257 tr.execute("ALTER TABLE processes ADD COLUMN audience VARCHAR(255);")
258 .await
259 .with_context(|| "adding column audience to processes table")?;
260 tr.execute("ALTER TABLE streams ADD COLUMN audience VARCHAR(255);")
261 .await
262 .with_context(|| "adding column audience to streams table")?;
263 tr.execute("ALTER TABLE blocks ADD COLUMN audience VARCHAR(255);")
264 .await
265 .with_context(|| "adding column audience to blocks table")?;
266 tr.execute(
267 "ALTER TABLE processes ADD CONSTRAINT processes_audience_name \
268 CHECK (audience ~ '^[A-Za-z0-9_-]+$') NOT VALID;",
269 )
270 .await
271 .with_context(|| "adding audience-name CHECK constraint on processes")?;
272 tr.execute(
273 "ALTER TABLE streams ADD CONSTRAINT streams_audience_name \
274 CHECK (audience ~ '^[A-Za-z0-9_-]+$') NOT VALID;",
275 )
276 .await
277 .with_context(|| "adding audience-name CHECK constraint on streams")?;
278 tr.execute(
279 "ALTER TABLE blocks ADD CONSTRAINT blocks_audience_name \
280 CHECK (audience ~ '^[A-Za-z0-9_-]+$') NOT VALID;",
281 )
282 .await
283 .with_context(|| "adding audience-name CHECK constraint on blocks")?;
284 tr.execute("UPDATE migration SET version=8;")
285 .await
286 .with_context(|| "updating data lake schema version to 8")?;
287 Ok(())
288}
289
290pub async fn upgrade_data_lake_schema_v9(
298 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
299) -> Result<()> {
300 tr.execute(
301 "INSERT INTO audience_grants (audience, axis, selector, created_at, created_by)
302 VALUES ('public', 'read', '*', now(), 'default'),
303 ('public', 'mint', '*', now(), 'default')
304 ON CONFLICT DO NOTHING;",
305 )
306 .await
307 .with_context(|| "seeding public read/mint grants")?;
308 tr.execute("UPDATE migration SET version=9;")
309 .await
310 .with_context(|| "updating data lake schema version to 9")?;
311 Ok(())
312}
313
314fn is_valid_group_name(name: &str) -> bool {
318 !name.is_empty()
319 && name.len() <= 255
320 && name
321 .bytes()
322 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
323}
324
325pub async fn upgrade_data_lake_schema_v10(
337 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
338) -> Result<()> {
339 tr.execute(
340 "CREATE TABLE groups (
341 name VARCHAR(255) PRIMARY KEY,
342 description TEXT,
343 created_at TIMESTAMPTZ NOT NULL,
344 created_by VARCHAR(255) NOT NULL,
345 CONSTRAINT groups_name CHECK (name ~ '^[A-Za-z0-9_-]+$')
346 );",
347 )
348 .await
349 .with_context(|| "creating table groups")?;
350 tr.execute(
351 "CREATE TABLE group_members (
352 group_name VARCHAR(255) NOT NULL REFERENCES groups(name) ON DELETE CASCADE,
353 member VARCHAR(255) NOT NULL,
354 created_at TIMESTAMPTZ NOT NULL,
355 created_by VARCHAR(255) NOT NULL,
356 PRIMARY KEY (group_name, member),
357 CONSTRAINT group_members_selector_shape CHECK (member = '*' OR member ~ '^(user|group):.+$')
358 );",
359 )
360 .await
361 .with_context(|| "creating table group_members")?;
362
363 tr.execute(
364 "INSERT INTO groups (name, description, created_at, created_by)
365 VALUES ('admins', 'Deployment administrators', now(), 'default')
366 ON CONFLICT (name) DO NOTHING;",
367 )
368 .await
369 .with_context(|| "seeding the admins group")?;
370
371 tr.execute(
372 "INSERT INTO group_members (group_name, member, created_at, created_by)
373 VALUES ('admins', '*', now(), 'default')
374 ON CONFLICT (group_name, member) DO NOTHING;",
375 )
376 .await
377 .with_context(|| "seeding the admins group with the wildcard member")?;
378
379 let rows =
380 sqlx::query("SELECT DISTINCT selector FROM audience_grants WHERE selector LIKE 'group:%';")
381 .fetch_all(&mut **tr)
382 .await
383 .with_context(|| "querying distinct group: selectors from audience_grants")?;
384 for row in rows {
385 let selector: String = row.try_get("selector").context("reading selector")?;
386 let name = selector
387 .strip_prefix("group:")
388 .expect("selector matched the LIKE 'group:%' filter");
389 if !is_valid_group_name(name) {
390 warn!(
391 "legacy audience_grants selector group:{name:?} does not match the group name \
392 charset [A-Za-z0-9_-]{{1,255}}; left as an inert grant row, not backfilled into \
393 a group"
394 );
395 continue;
396 }
397 sqlx::query(
398 "INSERT INTO groups (name, description, created_at, created_by)
399 VALUES ($1, NULL, now(), 'migration')
400 ON CONFLICT (name) DO NOTHING;",
401 )
402 .bind(name)
403 .execute(&mut **tr)
404 .await
405 .with_context(|| format!("backfilling empty group {name:?} from a legacy grant"))?;
406 info!("backfilled empty group {name:?} from a legacy audience_grants selector");
407 }
408
409 tr.execute("UPDATE migration SET version=10;")
410 .await
411 .with_context(|| "updating data lake schema version to 10")?;
412 Ok(())
413}
414
415pub async fn upgrade_data_lake_schema_v11(
421 tr: &mut sqlx::Transaction<'_, sqlx::Postgres>,
422) -> Result<()> {
423 tr.execute("ALTER TABLE ingestion_api_keys ADD COLUMN allowed_cidrs TEXT[];")
424 .await
425 .with_context(|| "adding column allowed_cidrs to ingestion_api_keys table")?;
426 tr.execute("ALTER TABLE analytics_api_keys ADD COLUMN allowed_cidrs TEXT[];")
427 .await
428 .with_context(|| "adding column allowed_cidrs to analytics_api_keys table")?;
429 tr.execute("UPDATE migration SET version=11;")
430 .await
431 .with_context(|| "updating data lake schema version to 11")?;
432 Ok(())
433}
434
435async fn check_index_is_valid(pool: &sqlx::Pool<sqlx::Postgres>, index_name: &str) -> Result<bool> {
440 let row = sqlx::query(
441 "SELECT i.indisvalid
442 FROM pg_class c
443 JOIN pg_index i ON i.indexrelid = c.oid
444 WHERE c.relname = $1;",
445 )
446 .bind(index_name)
447 .fetch_optional(pool)
448 .await
449 .with_context(|| format!("querying pg_index for {index_name}"))?;
450
451 let row = row.with_context(|| format!("index {index_name} not found in pg_class"))?;
452 let is_valid: bool = row.get("indisvalid");
453
454 if !is_valid {
455 info!("index {index_name} is INVALID, dropping it");
456 sqlx::query(&format!("DROP INDEX IF EXISTS {index_name}"))
457 .execute(pool)
458 .await
459 .with_context(|| format!("dropping invalid index {index_name}"))?;
460 return Ok(false);
461 }
462
463 Ok(true)
464}
465
466async fn validate_unique_indexes(pool: &sqlx::Pool<sqlx::Postgres>) -> Result<()> {
469 let index_names = [
470 "processes_process_id_unique",
471 "streams_stream_id_unique",
472 "blocks_block_id_unique",
473 ];
474
475 let mut invalid_indexes = Vec::new();
476 for name in &index_names {
477 if !check_index_is_valid(pool, name).await? {
478 invalid_indexes.push(*name);
479 }
480 }
481
482 if !invalid_indexes.is_empty() {
483 anyhow::bail!(
484 "invalid indexes detected and dropped: {}. The migration will be retried on next startup.",
485 invalid_indexes.join(", ")
486 );
487 }
488
489 Ok(())
490}
491
492pub async fn execute_migration(pool: sqlx::Pool<sqlx::Postgres>) -> Result<()> {
494 let mut current_version = read_data_lake_schema_version(&mut pool.begin().await?).await;
495 if 0 == current_version {
496 info!("creating v1 data_lake_schema");
497 let mut tr = pool.begin().await?;
498 create_tables(&mut tr).await?;
499 current_version = read_data_lake_schema_version(&mut tr).await;
500 tr.commit().await?;
501 }
502 if 1 == current_version {
503 info!("upgrading data_lake_schema to v2");
504 let mut tr = pool.begin().await?;
505 upgrade_data_lake_schema_v2(&mut tr).await?;
506 current_version = read_data_lake_schema_version(&mut tr).await;
507 tr.commit().await?;
508 }
509 if 2 == current_version {
510 info!("upgrading data_lake_schema to v3");
511 sqlx::query("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS processes_process_id_unique ON processes(process_id);")
515 .execute(&pool)
516 .await
517 .with_context(|| "creating unique index on processes(process_id)")?;
518 sqlx::query("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS streams_stream_id_unique ON streams(stream_id);")
519 .execute(&pool)
520 .await
521 .with_context(|| "creating unique index on streams(stream_id)")?;
522 sqlx::query("CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS blocks_block_id_unique ON blocks(block_id);")
523 .execute(&pool)
524 .await
525 .with_context(|| "creating unique index on blocks(block_id)")?;
526
527 validate_unique_indexes(&pool).await?;
528
529 let mut tr = pool.begin().await?;
530 upgrade_data_lake_schema_v3(&mut tr).await?;
531 current_version = read_data_lake_schema_version(&mut tr).await;
532 tr.commit().await?;
533 }
534 if 3 == current_version {
535 info!("upgrading data_lake_schema to v4");
536 let mut tr = pool.begin().await?;
537 upgrade_data_lake_schema_v4(&mut tr).await?;
538 current_version = read_data_lake_schema_version(&mut tr).await;
539 tr.commit().await?;
540 }
541 if 4 == current_version {
542 info!("upgrading data_lake_schema to v5");
543 let mut tr = pool.begin().await?;
544 upgrade_data_lake_schema_v5(&mut tr).await?;
545 current_version = read_data_lake_schema_version(&mut tr).await;
546 tr.commit().await?;
547 }
548 if 5 == current_version {
549 info!("upgrading data_lake_schema to v6");
550 let mut tr = pool.begin().await?;
551 upgrade_data_lake_schema_v6(&mut tr).await?;
552 current_version = read_data_lake_schema_version(&mut tr).await;
553 tr.commit().await?;
554 }
555 if 6 == current_version {
556 info!("upgrading data_lake_schema to v7");
557 let mut tr = pool.begin().await?;
558 upgrade_data_lake_schema_v7(&mut tr).await?;
559 current_version = read_data_lake_schema_version(&mut tr).await;
560 tr.commit().await?;
561 }
562 if 7 == current_version {
563 info!("upgrading data_lake_schema to v8");
564 let mut tr = pool.begin().await?;
565 upgrade_data_lake_schema_v8(&mut tr).await?;
566 current_version = read_data_lake_schema_version(&mut tr).await;
567 tr.commit().await?;
568 }
569 if 8 == current_version {
570 info!("upgrading data_lake_schema to v9");
571 let mut tr = pool.begin().await?;
572 upgrade_data_lake_schema_v9(&mut tr).await?;
573 current_version = read_data_lake_schema_version(&mut tr).await;
574 tr.commit().await?;
575 }
576 if 9 == current_version {
577 info!("upgrading data_lake_schema to v10");
578 let mut tr = pool.begin().await?;
579 upgrade_data_lake_schema_v10(&mut tr).await?;
580 current_version = read_data_lake_schema_version(&mut tr).await;
581 tr.commit().await?;
582 }
583 if 10 == current_version {
584 info!("upgrading data_lake_schema to v11");
585 let mut tr = pool.begin().await?;
586 upgrade_data_lake_schema_v11(&mut tr).await?;
587 current_version = read_data_lake_schema_version(&mut tr).await;
588 tr.commit().await?;
589 }
590 assert_eq!(current_version, LATEST_DATA_LAKE_SCHEMA_VERSION);
591 Ok(())
592}