• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 16167706152

09 Jul 2025 11:08AM UTC coverage: 88.738% (-1.0%) from 89.762%
16167706152

push

github

web-flow
refactor: Updates-2025-07-02 (#1062)

* rust 1.88

* clippy auto-fixes

* manual clippy fixes

* update deps

* cargo update

* update onnx

* cargo fmt

* update sqlfluff

121 of 142 new or added lines in 29 files covered. (85.21%)

300 existing lines in 88 files now uncovered.

111259 of 125379 relevant lines covered (88.74%)

77910.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.87
/services/src/layers/postgres_layer_db.rs
1
use super::external::{DataProvider, TypedDataProviderDefinition};
2
use super::layer::{
3
    CollectionItem, Layer, LayerCollection, LayerCollectionListOptions, LayerCollectionListing,
4
    LayerListing, Property, ProviderLayerCollectionId, ProviderLayerId, UpdateLayer,
5
    UpdateLayerCollection,
6
};
7
use super::listing::{
8
    LayerCollectionProvider, ProviderCapabilities, SearchCapabilities, SearchParameters,
9
    SearchType, SearchTypes,
10
};
11
use super::storage::{
12
    INTERNAL_PROVIDER_ID, LayerDb, LayerProviderDb, LayerProviderListing,
13
    LayerProviderListingOptions,
14
};
15
use crate::contexts::PostgresDb;
16
use crate::layers::external::DataProviderDefinition;
17
use crate::permissions::{Permission, RoleId, TxPermissionDb};
18
use crate::workflows::registry::TxWorkflowRegistry;
19
use crate::{
20
    error::{self, Result},
21
    layers::{
22
        LayerDbError,
23
        layer::{AddLayer, AddLayerCollection},
24
        listing::LayerCollectionId,
25
        storage::INTERNAL_LAYER_DB_ROOT_COLLECTION_ID,
26
    },
27
};
28
use bb8_postgres::PostgresConnectionManager;
29
use bb8_postgres::bb8::PooledConnection;
30
use bb8_postgres::tokio_postgres::{
31
    Socket,
32
    tls::{MakeTlsConnect, TlsConnect},
33
};
34
use geoengine_datatypes::dataset::{DataProviderId, LayerId};
35
use geoengine_datatypes::error::BoxedResultExt;
36
use geoengine_datatypes::util::HashMapTextTextDbType;
37
use snafu::{ResultExt, ensure};
38
use std::str::FromStr;
39
use tokio_postgres::Transaction;
40
use tonic::async_trait;
41
use uuid::Uuid;
42

43
#[async_trait]
44
impl<Tls> LayerDb for PostgresDb<Tls>
45
where
46
    Tls: MakeTlsConnect<Socket> + Clone + Send + Sync + 'static + std::fmt::Debug,
47
    <Tls as MakeTlsConnect<Socket>>::Stream: Send + Sync,
48
    <Tls as MakeTlsConnect<Socket>>::TlsConnect: Send,
49
    <<Tls as MakeTlsConnect<Socket>>::TlsConnect as TlsConnect<Socket>>::Future: Send,
50
{
51
    async fn add_layer(&self, layer: AddLayer, collection: &LayerCollectionId) -> Result<LayerId> {
56✔
52
        let layer_id = Uuid::new_v4();
28✔
53
        let layer_id = LayerId(layer_id.to_string());
28✔
54

55
        self.add_layer_with_id(&layer_id, layer, collection).await?;
28✔
56

57
        Ok(layer_id)
28✔
58
    }
56✔
59

60
    async fn update_layer(&self, id: &LayerId, layer: UpdateLayer) -> Result<()> {
2✔
61
        let layer_id =
1✔
62
            Uuid::from_str(&id.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
1✔
63
                found: id.0.clone(),
×
UNCOV
64
            })?;
×
65

66
        let mut conn = self.conn_pool.get().await?;
1✔
67
        let transaction = conn.build_transaction().start().await?;
1✔
68

69
        self.ensure_permission_in_tx(id.clone().into(), Permission::Owner, &transaction)
1✔
70
            .await
1✔
71
            .boxed_context(crate::error::PermissionDb)?;
1✔
72

73
        let workflow_id = self
1✔
74
            .register_workflow_in_tx(layer.workflow, &transaction)
1✔
75
            .await?;
1✔
76

77
        transaction.execute(
1✔
78
                "
1✔
79
                UPDATE layers
1✔
80
                SET name = $1, description = $2, symbology = $3, properties = $4, metadata = $5, workflow_id = $6
1✔
81
                WHERE id = $7;",
1✔
82
                &[
1✔
83
                    &layer.name,
1✔
84
                    &layer.description,
1✔
85
                    &layer.symbology,
1✔
86
                    &layer.properties,
1✔
87
                    &HashMapTextTextDbType::from(&layer.metadata),
1✔
88
                    &workflow_id,
1✔
89
                    &layer_id,
1✔
90
                ],
1✔
91
            )
1✔
92
            .await?;
1✔
93

94
        transaction.commit().await.map_err(Into::into)
1✔
95
    }
2✔
96

97
    async fn remove_layer(&self, id: &LayerId) -> Result<()> {
2✔
98
        let layer_id =
1✔
99
            Uuid::from_str(&id.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
1✔
100
                found: id.0.clone(),
×
UNCOV
101
            })?;
×
102

103
        let mut conn = self.conn_pool.get().await?;
1✔
104
        let transaction = conn.build_transaction().start().await?;
1✔
105

106
        self.ensure_permission_in_tx(id.clone().into(), Permission::Owner, &transaction)
1✔
107
            .await
1✔
108
            .boxed_context(crate::error::PermissionDb)?;
1✔
109

110
        transaction
1✔
111
            .execute(
1✔
112
                "
1✔
113
            DELETE FROM layers
1✔
114
            WHERE id = $1;",
1✔
115
                &[&layer_id],
1✔
116
            )
1✔
117
            .await?;
1✔
118

119
        transaction.commit().await.map_err(Into::into)
1✔
120
    }
2✔
121

122
    async fn add_layer_with_id(
123
        &self,
124
        id: &LayerId,
125
        layer: AddLayer,
126
        collection: &LayerCollectionId,
127
    ) -> Result<()> {
56✔
128
        let mut conn = self.conn_pool.get().await?;
28✔
129
        let trans = conn.build_transaction().start().await?;
28✔
130

131
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &trans)
28✔
132
            .await
28✔
133
            .boxed_context(crate::error::PermissionDb)?;
28✔
134

135
        let layer_id = insert_layer(self, &trans, id, layer, collection).await?;
28✔
136

137
        // TODO: `ON CONFLICT DO NOTHING` means, we do not get an error if the permission already exists.
138
        //       Do we want that, or should we report an error and let the caller decide whether to ignore it?
139
        //       We should decide that and adjust all places where `ON CONFLICT DO NOTHING` is used.
140
        let stmt = trans
28✔
141
            .prepare(
28✔
142
                "
28✔
143
            INSERT INTO permissions (role_id, permission, layer_id)
28✔
144
            VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;",
28✔
145
            )
28✔
146
            .await?;
28✔
147

148
        trans
28✔
149
            .execute(
28✔
150
                &stmt,
28✔
151
                &[
28✔
152
                    &RoleId::from(self.session.user.id),
28✔
153
                    &Permission::Owner,
28✔
154
                    &layer_id,
28✔
155
                ],
28✔
156
            )
28✔
157
            .await?;
28✔
158

159
        trans.commit().await?;
28✔
160

161
        Ok(())
28✔
162
    }
56✔
163

164
    async fn add_layer_to_collection(
165
        &self,
166
        layer: &LayerId,
167
        collection: &LayerCollectionId,
168
    ) -> Result<()> {
6✔
169
        let mut conn = self.conn_pool.get().await?;
3✔
170
        let tx = conn.build_transaction().start().await?;
3✔
171

172
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &tx)
3✔
173
            .await
3✔
174
            .boxed_context(crate::error::PermissionDb)?;
3✔
175

176
        let layer_id =
2✔
177
            Uuid::from_str(&layer.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
3✔
178
                found: layer.0.clone(),
1✔
179
            })?;
1✔
180

181
        let collection_id =
2✔
182
            Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
2✔
183
                found: collection.0.clone(),
×
UNCOV
184
            })?;
×
185

186
        let stmt = tx
2✔
187
            .prepare(
2✔
188
                "
2✔
189
            INSERT INTO collection_layers (collection, layer)
2✔
190
            VALUES ($1, $2) ON CONFLICT DO NOTHING;",
2✔
191
            )
2✔
192
            .await?;
2✔
193

194
        tx.execute(&stmt, &[&collection_id, &layer_id]).await?;
2✔
195

196
        tx.commit().await?;
2✔
197

198
        Ok(())
2✔
199
    }
6✔
200

201
    async fn add_layer_collection(
202
        &self,
203
        collection: AddLayerCollection,
204
        parent: &LayerCollectionId,
205
    ) -> Result<LayerCollectionId> {
64✔
206
        let collection_id = Uuid::new_v4();
32✔
207
        let collection_id = LayerCollectionId(collection_id.to_string());
32✔
208

209
        self.add_layer_collection_with_id(&collection_id, collection, parent)
32✔
210
            .await?;
32✔
211

212
        Ok(collection_id)
32✔
213
    }
64✔
214

215
    async fn add_layer_collection_with_id(
216
        &self,
217
        id: &LayerCollectionId,
218
        collection: AddLayerCollection,
219
        parent: &LayerCollectionId,
220
    ) -> Result<()> {
64✔
221
        let mut conn = self.conn_pool.get().await?;
32✔
222
        let trans = conn.build_transaction().start().await?;
32✔
223

224
        self.ensure_permission_in_tx(parent.clone().into(), Permission::Owner, &trans)
32✔
225
            .await
32✔
226
            .boxed_context(crate::error::PermissionDb)?;
32✔
227

228
        let collection_id = insert_layer_collection_with_id(&trans, id, collection, parent).await?;
32✔
229

230
        let stmt = trans
32✔
231
            .prepare(
32✔
232
                "
32✔
233
            INSERT INTO permissions (role_id, permission, layer_collection_id)
32✔
234
            VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;",
32✔
235
            )
32✔
236
            .await?;
32✔
237

238
        trans
32✔
239
            .execute(
32✔
240
                &stmt,
32✔
241
                &[
32✔
242
                    &RoleId::from(self.session.user.id),
32✔
243
                    &Permission::Owner,
32✔
244
                    &collection_id,
32✔
245
                ],
32✔
246
            )
32✔
247
            .await?;
32✔
248

249
        trans.commit().await?;
32✔
250

251
        Ok(())
32✔
252
    }
64✔
253

254
    async fn add_collection_to_parent(
255
        &self,
256
        collection: &LayerCollectionId,
257
        parent: &LayerCollectionId,
258
    ) -> Result<()> {
4✔
259
        let conn = self.conn_pool.get().await?;
2✔
260
        insert_collection_parent(&conn, collection, parent).await
2✔
261
    }
4✔
262

263
    async fn remove_layer_collection(&self, collection: &LayerCollectionId) -> Result<()> {
10✔
264
        let mut conn = self.conn_pool.get().await?;
5✔
265
        let transaction = conn.build_transaction().start().await?;
5✔
266

267
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &transaction)
5✔
268
            .await
5✔
269
            .boxed_context(crate::error::PermissionDb)?;
5✔
270

271
        delete_layer_collection(&transaction, collection).await?;
5✔
272

273
        transaction.commit().await.map_err(Into::into)
3✔
274
    }
10✔
275

276
    async fn remove_layer_from_collection(
277
        &self,
278
        layer: &LayerId,
279
        collection: &LayerCollectionId,
280
    ) -> Result<()> {
6✔
281
        let mut conn = self.conn_pool.get().await?;
3✔
282
        let transaction = conn.build_transaction().start().await?;
3✔
283

284
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &transaction)
3✔
285
            .await
3✔
286
            .boxed_context(crate::error::PermissionDb)?;
3✔
287

288
        delete_layer_from_collection(&transaction, layer, collection).await?;
3✔
289

290
        transaction.commit().await.map_err(Into::into)
3✔
291
    }
6✔
292

293
    async fn remove_layer_collection_from_parent(
294
        &self,
295
        collection: &LayerCollectionId,
296
        parent: &LayerCollectionId,
297
    ) -> Result<()> {
4✔
298
        let mut conn = self.conn_pool.get().await?;
2✔
299
        let transaction = conn.build_transaction().start().await?;
2✔
300

301
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &transaction)
2✔
302
            .await
2✔
303
            .boxed_context(crate::error::PermissionDb)?;
2✔
304

305
        delete_layer_collection_from_parent(&transaction, collection, parent).await?;
2✔
306

307
        transaction.commit().await.map_err(Into::into)
2✔
308
    }
4✔
309

310
    async fn update_layer_collection(
311
        &self,
312
        collection: &LayerCollectionId,
313
        update: UpdateLayerCollection,
314
    ) -> Result<()> {
2✔
315
        let collection_id =
1✔
316
            Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
1✔
317
                found: collection.0.clone(),
×
UNCOV
318
            })?;
×
319

320
        let mut conn = self.conn_pool.get().await?;
1✔
321
        let transaction = conn.build_transaction().start().await?;
1✔
322

323
        self.ensure_permission_in_tx(collection.clone().into(), Permission::Owner, &transaction)
1✔
324
            .await
1✔
325
            .boxed_context(crate::error::PermissionDb)?;
1✔
326

327
        transaction
1✔
328
            .execute(
1✔
329
                "UPDATE layer_collections 
1✔
330
                SET name = $1, description = $2, properties = $3
1✔
331
                WHERE id = $4;",
1✔
332
                &[
1✔
333
                    &update.name,
1✔
334
                    &update.description,
1✔
335
                    &update.properties,
1✔
336
                    &collection_id,
1✔
337
                ],
1✔
338
            )
1✔
339
            .await?;
1✔
340

341
        transaction.commit().await.map_err(Into::into)
1✔
342
    }
2✔
343
}
344

345
fn create_search_query(full_info: bool) -> String {
32✔
346
    format!("
32✔
347
        WITH RECURSIVE parents AS (
32✔
348
            SELECT $1::uuid as id
32✔
349
            UNION ALL SELECT DISTINCT child FROM collection_children JOIN parents ON (id = parent)
32✔
350
        )
32✔
351
        SELECT DISTINCT *
32✔
352
        FROM (
32✔
353
            SELECT 
32✔
354
                {}
32✔
355
            FROM user_permitted_layer_collections u
32✔
356
                JOIN layer_collections lc ON (u.layer_collection_id = lc.id)
32✔
357
                JOIN (SELECT DISTINCT child FROM collection_children JOIN parents ON (id = parent)) cc ON (id = cc.child)
32✔
358
            WHERE u.user_id = $4 AND name ILIKE $5
32✔
359
        ) u UNION (
32✔
360
            SELECT 
32✔
361
                {}
32✔
362
            FROM user_permitted_layers ul
32✔
363
                JOIN layers uc ON (ul.layer_id = uc.id)
32✔
364
                JOIN (SELECT DISTINCT layer FROM collection_layers JOIN parents ON (collection = id)) cl ON (id = cl.layer)
32✔
365
            WHERE ul.user_id = $4 AND name ILIKE $5
32✔
366
        )
32✔
367
        ORDER BY {}name ASC
32✔
368
        LIMIT $2 
32✔
369
        OFFSET $3;",
32✔
370
        if full_info {
32✔
371
            "concat(id, '') AS id,
16✔
372
        name,
16✔
373
        description,
16✔
374
        properties,
16✔
375
        FALSE AS is_layer"
16✔
376
        } else { "name" },
16✔
377
        if full_info {
32✔
378
            "concat(id, '') AS id,
16✔
379
        name,
16✔
380
        description,
16✔
381
        properties,
16✔
382
        TRUE AS is_layer"
16✔
383
        } else { "name" },
16✔
384
        if full_info { "is_layer ASC," } else { "" })
32✔
385
}
32✔
386

387
#[async_trait]
388
impl<Tls> LayerCollectionProvider for PostgresDb<Tls>
389
where
390
    Tls: MakeTlsConnect<Socket> + Clone + Send + Sync + 'static + std::fmt::Debug,
391
    <Tls as MakeTlsConnect<Socket>>::Stream: Send + Sync,
392
    <Tls as MakeTlsConnect<Socket>>::TlsConnect: Send,
393
    <<Tls as MakeTlsConnect<Socket>>::TlsConnect as TlsConnect<Socket>>::Future: Send,
394
{
395
    fn capabilities(&self) -> ProviderCapabilities {
2✔
396
        ProviderCapabilities {
2✔
397
            listing: true,
2✔
398
            search: SearchCapabilities {
2✔
399
                search_types: SearchTypes {
2✔
400
                    fulltext: true,
2✔
401
                    prefix: true,
2✔
402
                },
2✔
403
                autocomplete: true,
2✔
404
                filters: None,
2✔
405
            },
2✔
406
        }
2✔
407
    }
2✔
408

409
    fn name(&self) -> &'static str {
×
410
        "Postgres Layer Collection Provider (Pro)"
×
411
    }
×
412

413
    fn description(&self) -> &'static str {
×
414
        "Layer collection provider for Postgres (Pro)"
×
415
    }
×
416

417
    #[allow(clippy::too_many_lines)]
418
    async fn load_layer_collection(
419
        &self,
420
        collection_id: &LayerCollectionId,
421
        options: LayerCollectionListOptions,
422
    ) -> Result<LayerCollection> {
42✔
423
        let mut conn = self.conn_pool.get().await?;
21✔
424
        let tx = conn.build_transaction().start().await?;
21✔
425

426
        self.ensure_permission_in_tx(collection_id.clone().into(), Permission::Read, &tx)
21✔
427
            .await
21✔
428
            .boxed_context(crate::error::PermissionDb)?;
21✔
429

430
        let collection = Uuid::from_str(&collection_id.0).map_err(|_| {
17✔
431
            crate::error::Error::IdStringMustBeUuid {
×
432
                found: collection_id.0.clone(),
×
433
            }
×
UNCOV
434
        })?;
×
435

436
        let stmt = tx
17✔
437
            .prepare(
17✔
438
                "
17✔
439
        SELECT name, description, properties
17✔
440
        FROM user_permitted_layer_collections p 
17✔
441
            JOIN layer_collections c ON (p.layer_collection_id = c.id) 
17✔
442
        WHERE p.user_id = $1 AND layer_collection_id = $2;",
17✔
443
            )
17✔
444
            .await?;
17✔
445

446
        let row = tx
17✔
447
            .query_one(&stmt, &[&self.session.user.id, &collection])
17✔
448
            .await?;
17✔
449

450
        let name: String = row.get(0);
17✔
451
        let description: String = row.get(1);
17✔
452
        let properties: Vec<Property> = row.get(2);
17✔
453

454
        let stmt = tx
17✔
455
            .prepare(
17✔
456
                "
17✔
457
        SELECT DISTINCT id, name, description, properties, is_layer
17✔
458
        FROM (
17✔
459
            SELECT 
17✔
460
                concat(id, '') AS id, 
17✔
461
                name, 
17✔
462
                description, 
17✔
463
                properties, 
17✔
464
                FALSE AS is_layer
17✔
465
            FROM user_permitted_layer_collections u 
17✔
466
                JOIN layer_collections lc ON (u.layer_collection_id = lc.id)
17✔
467
                JOIN collection_children cc ON (layer_collection_id = cc.child)
17✔
468
            WHERE u.user_id = $4 AND cc.parent = $1
17✔
469
        ) u UNION (
17✔
470
            SELECT 
17✔
471
                concat(id, '') AS id, 
17✔
472
                name, 
17✔
473
                description, 
17✔
474
                properties, 
17✔
475
                TRUE AS is_layer
17✔
476
            FROM user_permitted_layers ul
17✔
477
                JOIN layers uc ON (ul.layer_id = uc.id) 
17✔
478
                JOIN collection_layers cl ON (layer_id = cl.layer)
17✔
479
            WHERE ul.user_id = $4 AND cl.collection = $1
17✔
480
        )
17✔
481
        ORDER BY is_layer ASC, name ASC
17✔
482
        LIMIT $2 
17✔
483
        OFFSET $3;            
17✔
484
        ",
17✔
485
            )
17✔
486
            .await?;
17✔
487

488
        let rows = tx
17✔
489
            .query(
17✔
490
                &stmt,
17✔
491
                &[
17✔
492
                    &collection,
17✔
493
                    &i64::from(options.limit),
17✔
494
                    &i64::from(options.offset),
17✔
495
                    &self.session.user.id,
17✔
496
                ],
17✔
497
            )
17✔
498
            .await?;
17✔
499

500
        let items = rows
17✔
501
            .into_iter()
17✔
502
            .map(|row| {
19✔
503
                let is_layer: bool = row.get(4);
19✔
504

505
                if is_layer {
19✔
506
                    Ok(CollectionItem::Layer(LayerListing {
7✔
507
                        r#type: Default::default(),
7✔
508
                        id: ProviderLayerId {
7✔
509
                            provider_id: INTERNAL_PROVIDER_ID,
7✔
510
                            layer_id: LayerId(row.get(0)),
7✔
511
                        },
7✔
512
                        name: row.get(1),
7✔
513
                        description: row.get(2),
7✔
514
                        properties: row.get(3),
7✔
515
                    }))
7✔
516
                } else {
517
                    Ok(CollectionItem::Collection(LayerCollectionListing {
12✔
518
                        r#type: Default::default(),
12✔
519
                        id: ProviderLayerCollectionId {
12✔
520
                            provider_id: INTERNAL_PROVIDER_ID,
12✔
521
                            collection_id: LayerCollectionId(row.get(0)),
12✔
522
                        },
12✔
523
                        name: row.get(1),
12✔
524
                        description: row.get(2),
12✔
525
                        properties: row.get(3),
12✔
526
                    }))
12✔
527
                }
528
            })
19✔
529
            .collect::<Result<Vec<CollectionItem>>>()?;
17✔
530

531
        tx.commit().await?;
17✔
532

533
        Ok(LayerCollection {
17✔
534
            id: ProviderLayerCollectionId {
17✔
535
                provider_id: INTERNAL_PROVIDER_ID,
17✔
536
                collection_id: collection_id.clone(),
17✔
537
            },
17✔
538
            name,
17✔
539
            description,
17✔
540
            items,
17✔
541
            entry_label: None,
17✔
542
            properties,
17✔
543
        })
17✔
544
    }
42✔
545

546
    #[allow(clippy::too_many_lines)]
547
    async fn search(
548
        &self,
549
        collection_id: &LayerCollectionId,
550
        search: SearchParameters,
551
    ) -> Result<LayerCollection> {
32✔
552
        let mut conn = self.conn_pool.get().await?;
16✔
553
        let tx = conn.build_transaction().start().await?;
16✔
554

555
        self.ensure_permission_in_tx(collection_id.clone().into(), Permission::Read, &tx)
16✔
556
            .await
16✔
557
            .boxed_context(crate::error::PermissionDb)?;
16✔
558

559
        let collection = Uuid::from_str(&collection_id.0).map_err(|_| {
16✔
560
            crate::error::Error::IdStringMustBeUuid {
×
561
                found: collection_id.0.clone(),
×
562
            }
×
UNCOV
563
        })?;
×
564

565
        let stmt = tx
16✔
566
            .prepare(
16✔
567
                "
16✔
568
        SELECT name, description, properties
16✔
569
        FROM user_permitted_layer_collections p 
16✔
570
            JOIN layer_collections c ON (p.layer_collection_id = c.id) 
16✔
571
        WHERE p.user_id = $1 AND layer_collection_id = $2;",
16✔
572
            )
16✔
573
            .await?;
16✔
574

575
        let row = tx
16✔
576
            .query_one(&stmt, &[&self.session.user.id, &collection])
16✔
577
            .await?;
16✔
578

579
        let name: String = row.get(0);
16✔
580
        let description: String = row.get(1);
16✔
581
        let properties: Vec<Property> = row.get(2);
16✔
582

583
        let pattern = match search.search_type {
16✔
584
            SearchType::Fulltext => {
585
                format!("%{}%", search.search_string)
11✔
586
            }
587
            SearchType::Prefix => {
588
                format!("{}%", search.search_string)
5✔
589
            }
590
        };
591

592
        let stmt = tx.prepare(&create_search_query(true)).await?;
16✔
593

594
        let rows = tx
16✔
595
            .query(
16✔
596
                &stmt,
16✔
597
                &[
16✔
598
                    &collection,
16✔
599
                    &i64::from(search.limit),
16✔
600
                    &i64::from(search.offset),
16✔
601
                    &self.session.user.id,
16✔
602
                    &pattern,
16✔
603
                ],
16✔
604
            )
16✔
605
            .await?;
16✔
606

607
        let items = rows
16✔
608
            .into_iter()
16✔
609
            .map(|row| {
31✔
610
                let is_layer: bool = row.get(4);
31✔
611

612
                if is_layer {
31✔
613
                    Ok(CollectionItem::Layer(LayerListing {
13✔
614
                        r#type: Default::default(),
13✔
615
                        id: ProviderLayerId {
13✔
616
                            provider_id: INTERNAL_PROVIDER_ID,
13✔
617
                            layer_id: LayerId(row.get(0)),
13✔
618
                        },
13✔
619
                        name: row.get(1),
13✔
620
                        description: row.get(2),
13✔
621
                        properties: row.get(3),
13✔
622
                    }))
13✔
623
                } else {
624
                    Ok(CollectionItem::Collection(LayerCollectionListing {
18✔
625
                        r#type: Default::default(),
18✔
626
                        id: ProviderLayerCollectionId {
18✔
627
                            provider_id: INTERNAL_PROVIDER_ID,
18✔
628
                            collection_id: LayerCollectionId(row.get(0)),
18✔
629
                        },
18✔
630
                        name: row.get(1),
18✔
631
                        description: row.get(2),
18✔
632
                        properties: row.get(3),
18✔
633
                    }))
18✔
634
                }
635
            })
31✔
636
            .collect::<Result<Vec<CollectionItem>>>()?;
16✔
637

638
        tx.commit().await?;
16✔
639

640
        Ok(LayerCollection {
16✔
641
            id: ProviderLayerCollectionId {
16✔
642
                provider_id: INTERNAL_PROVIDER_ID,
16✔
643
                collection_id: collection_id.clone(),
16✔
644
            },
16✔
645
            name,
16✔
646
            description,
16✔
647
            items,
16✔
648
            entry_label: None,
16✔
649
            properties,
16✔
650
        })
16✔
651
    }
32✔
652

653
    #[allow(clippy::too_many_lines)]
654
    async fn autocomplete_search(
655
        &self,
656
        collection_id: &LayerCollectionId,
657
        search: SearchParameters,
658
    ) -> Result<Vec<String>> {
32✔
659
        let mut conn = self.conn_pool.get().await?;
16✔
660
        let tx = conn.build_transaction().start().await?;
16✔
661

662
        self.ensure_permission_in_tx(collection_id.clone().into(), Permission::Read, &tx)
16✔
663
            .await
16✔
664
            .boxed_context(crate::error::PermissionDb)?;
16✔
665

666
        let collection = Uuid::from_str(&collection_id.0).map_err(|_| {
16✔
667
            crate::error::Error::IdStringMustBeUuid {
×
668
                found: collection_id.0.clone(),
×
669
            }
×
UNCOV
670
        })?;
×
671

672
        let pattern = match search.search_type {
16✔
673
            SearchType::Fulltext => {
674
                format!("%{}%", search.search_string)
11✔
675
            }
676
            SearchType::Prefix => {
677
                format!("{}%", search.search_string)
5✔
678
            }
679
        };
680

681
        let stmt = tx.prepare(&create_search_query(false)).await?;
16✔
682

683
        let rows = tx
16✔
684
            .query(
16✔
685
                &stmt,
16✔
686
                &[
16✔
687
                    &collection,
16✔
688
                    &i64::from(search.limit),
16✔
689
                    &i64::from(search.offset),
16✔
690
                    &self.session.user.id,
16✔
691
                    &pattern,
16✔
692
                ],
16✔
693
            )
16✔
694
            .await?;
16✔
695

696
        let items = rows
16✔
697
            .into_iter()
16✔
698
            .map(|row| Ok(row.get::<usize, &str>(0).to_string()))
31✔
699
            .collect::<Result<Vec<String>>>()?;
16✔
700

701
        tx.commit().await?;
16✔
702

703
        Ok(items)
16✔
704
    }
32✔
705

706
    async fn get_root_layer_collection_id(&self) -> Result<LayerCollectionId> {
64✔
707
        Ok(LayerCollectionId(
32✔
708
            INTERNAL_LAYER_DB_ROOT_COLLECTION_ID.to_string(),
32✔
709
        ))
32✔
710
    }
64✔
711

712
    async fn load_layer(&self, id: &LayerId) -> Result<Layer> {
40✔
713
        let mut conn = self.conn_pool.get().await?;
20✔
714
        let tx = conn.build_transaction().start().await?;
20✔
715

716
        self.ensure_permission_in_tx(id.clone().into(), Permission::Read, &tx)
20✔
717
            .await
20✔
718
            .boxed_context(crate::error::PermissionDb)?;
20✔
719

720
        let layer_id =
15✔
721
            Uuid::from_str(&id.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
15✔
722
                found: id.0.clone(),
×
UNCOV
723
            })?;
×
724

725
        let stmt = tx
15✔
726
            .prepare(
15✔
727
                "
15✔
728
            SELECT 
15✔
729
                l.name,
15✔
730
                l.description,
15✔
731
                w.workflow,
15✔
732
                l.symbology,
15✔
733
                l.properties,
15✔
734
                l.metadata
15✔
735
            FROM 
15✔
736
                layers l JOIN workflows w ON (l.workflow_id = w.id)
15✔
737
            WHERE 
15✔
738
                l.id = $1;",
15✔
739
            )
15✔
740
            .await?;
15✔
741

742
        let row = tx
15✔
743
            .query_one(&stmt, &[&layer_id])
15✔
744
            .await
15✔
745
            .map_err(|_error| LayerDbError::NoLayerForGivenId { id: id.clone() })?;
15✔
746

747
        tx.commit().await?;
15✔
748

749
        Ok(Layer {
750
            id: ProviderLayerId {
15✔
751
                provider_id: INTERNAL_PROVIDER_ID,
15✔
752
                layer_id: id.clone(),
15✔
753
            },
15✔
754
            name: row.get(0),
15✔
755
            description: row.get(1),
15✔
756
            workflow: serde_json::from_value(row.get(2)).context(crate::error::SerdeJson)?,
15✔
757
            symbology: row.get(3),
15✔
758
            properties: row.get(4),
15✔
759
            metadata: row.get::<_, HashMapTextTextDbType>(5).into(),
15✔
760
        })
761
    }
40✔
762
}
763

764
#[async_trait]
765
impl<Tls> LayerProviderDb for PostgresDb<Tls>
766
where
767
    Tls: MakeTlsConnect<Socket> + Clone + Send + Sync + 'static + std::fmt::Debug,
768
    <Tls as MakeTlsConnect<Socket>>::Stream: Send + Sync,
769
    <Tls as MakeTlsConnect<Socket>>::TlsConnect: Send,
770
    <<Tls as MakeTlsConnect<Socket>>::TlsConnect as TlsConnect<Socket>>::Future: Send,
771
{
772
    async fn add_layer_provider(
773
        &self,
774
        provider: TypedDataProviderDefinition,
775
    ) -> Result<DataProviderId> {
18✔
776
        ensure!(self.session.is_admin(), error::PermissionDenied);
9✔
777

778
        let conn = self.conn_pool.get().await?;
9✔
779

780
        let prio = DataProviderDefinition::<Self>::priority(&provider);
9✔
781
        let clamp_prio = prio.clamp(-1000, 1000);
9✔
782

783
        if prio != clamp_prio {
9✔
784
            log::warn!(
×
785
                "The priority of the provider {} is out of range! --> clamped {} to {}",
×
786
                DataProviderDefinition::<Self>::name(&provider),
×
787
                prio,
788
                clamp_prio
789
            );
790
        }
9✔
791

792
        let stmt = conn
9✔
793
            .prepare(
9✔
794
                "
9✔
795
              INSERT INTO layer_providers (
9✔
796
                  id, 
9✔
797
                  type_name, 
9✔
798
                  name,
9✔
799
                  definition,
9✔
800
                  priority
9✔
801
              )
9✔
802
              VALUES ($1, $2, $3, $4, $5)",
9✔
803
            )
9✔
804
            .await?;
9✔
805

806
        let id = DataProviderDefinition::<Self>::id(&provider);
9✔
807
        conn.execute(
9✔
808
            &stmt,
9✔
809
            &[
9✔
810
                &id,
9✔
811
                &DataProviderDefinition::<Self>::type_name(&provider),
9✔
812
                &DataProviderDefinition::<Self>::name(&provider),
9✔
813
                &provider,
9✔
814
                &clamp_prio,
9✔
815
            ],
9✔
816
        )
9✔
817
        .await?;
9✔
818
        Ok(id)
9✔
819
    }
18✔
820

821
    async fn list_layer_providers(
822
        &self,
823
        options: LayerProviderListingOptions,
824
    ) -> Result<Vec<LayerProviderListing>> {
4✔
825
        // TODO: permission
826
        let conn = self.conn_pool.get().await?;
2✔
827

828
        let stmt = conn
2✔
829
            .prepare(
2✔
830
                "
2✔
831
                SELECT 
2✔
832
                    id, 
2✔
833
                    name,
2✔
834
                    type_name,
2✔
835
                    priority
2✔
836
                FROM 
2✔
837
                    layer_providers
2✔
838
                WHERE
2✔
839
                    priority > -1000
2✔
840
                ORDER BY priority desc, name ASC
2✔
841
                LIMIT $1 
2✔
842
                OFFSET $2;
2✔
843
                ",
2✔
844
            )
2✔
845
            .await?;
2✔
846

847
        let rows = conn
2✔
848
            .query(
2✔
849
                &stmt,
2✔
850
                &[&i64::from(options.limit), &i64::from(options.offset)],
2✔
851
            )
2✔
852
            .await?;
2✔
853

854
        Ok(rows
2✔
855
            .iter()
2✔
856
            .map(|row| LayerProviderListing {
2✔
857
                id: row.get(0),
2✔
858
                name: row.get(1),
2✔
859
                priority: row.get(3),
2✔
860
            })
2✔
861
            .collect())
2✔
862
    }
4✔
863

864
    async fn load_layer_provider(&self, id: DataProviderId) -> Result<Box<dyn DataProvider>> {
38✔
865
        // TODO: permissions
866
        let conn = self.conn_pool.get().await?;
19✔
867

868
        let stmt = conn
19✔
869
            .prepare(
19✔
870
                "
19✔
871
                SELECT
19✔
872
                    definition
19✔
873
                FROM
19✔
874
                    layer_providers
19✔
875
                WHERE
19✔
876
                    id = $1
19✔
877
                ",
19✔
878
            )
19✔
879
            .await?;
19✔
880

881
        let row = conn.query_one(&stmt, &[&id]).await?;
19✔
882
        let definition: TypedDataProviderDefinition = row.get(0);
12✔
883

884
        return Box::new(definition)
12✔
885
            .initialize(PostgresDb {
12✔
886
                conn_pool: self.conn_pool.clone(),
12✔
887
                session: self.session.clone(),
12✔
888
            })
12✔
889
            .await;
12✔
890
    }
38✔
891
}
892

893
/// delete all collections without parent collection
894
async fn _remove_collections_without_parent_collection(
5✔
895
    transaction: &tokio_postgres::Transaction<'_>,
5✔
896
) -> Result<()> {
5✔
897
    // HINT: a recursive delete statement seems reasonable, but hard to implement in postgres
898
    //       because you have a graph with potential loops
899

900
    let remove_layer_collections_without_parents_stmt = transaction
5✔
901
        .prepare(
5✔
902
            "DELETE FROM layer_collections
5✔
903
                 WHERE  id <> $1 -- do not delete root collection
5✔
904
                 AND    id NOT IN (
5✔
905
                    SELECT child FROM collection_children
5✔
906
                 );",
5✔
907
        )
5✔
908
        .await?;
5✔
909
    while 0 < transaction
8✔
910
        .execute(
8✔
911
            &remove_layer_collections_without_parents_stmt,
8✔
912
            &[&INTERNAL_LAYER_DB_ROOT_COLLECTION_ID],
8✔
913
        )
8✔
914
        .await?
8✔
915
    {
3✔
916
        // whenever one collection is deleted, we have to check again if there are more
3✔
917
        // collections without parents
3✔
918
    }
3✔
919

920
    Ok(())
5✔
921
}
5✔
922

923
/// delete all layers without parent collection
924
#[allow(clippy::used_underscore_items)] // TODO: maybe rename?
925
async fn _remove_layers_without_parent_collection(
8✔
926
    transaction: &tokio_postgres::Transaction<'_>,
8✔
927
) -> Result<()> {
8✔
928
    let remove_layers_without_parents_stmt = transaction
8✔
929
        .prepare(
8✔
930
            "DELETE FROM layers
8✔
931
                 WHERE id NOT IN (
8✔
932
                    SELECT layer FROM collection_layers
8✔
933
                 );",
8✔
934
        )
8✔
935
        .await?;
8✔
936
    transaction
8✔
937
        .execute(&remove_layers_without_parents_stmt, &[])
8✔
938
        .await?;
8✔
939

940
    Ok(())
8✔
941
}
8✔
942

943
pub async fn insert_layer<W: TxWorkflowRegistry>(
28✔
944
    workflow_registry: &W,
28✔
945
    trans: &Transaction<'_>,
28✔
946
    id: &LayerId,
28✔
947
    layer: AddLayer,
28✔
948
    collection: &LayerCollectionId,
28✔
949
) -> Result<Uuid> {
28✔
950
    let layer_id = Uuid::from_str(&id.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
28✔
951
        found: collection.0.clone(),
×
UNCOV
952
    })?;
×
953

954
    let collection_id =
28✔
955
        Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
28✔
956
            found: collection.0.clone(),
×
UNCOV
957
        })?;
×
958

959
    let workflow_id = workflow_registry
28✔
960
        .register_workflow_in_tx(layer.workflow, trans)
28✔
961
        .await?;
28✔
962

963
    let stmt = trans
28✔
964
        .prepare(
28✔
965
            "
28✔
966
            INSERT INTO layers (id, name, description, workflow_id, symbology, properties, metadata)
28✔
967
            VALUES ($1, $2, $3, $4, $5, $6, $7);",
28✔
968
        )
28✔
969
        .await?;
28✔
970

971
    trans
28✔
972
        .execute(
28✔
973
            &stmt,
28✔
974
            &[
28✔
975
                &layer_id,
28✔
976
                &layer.name,
28✔
977
                &layer.description,
28✔
978
                &workflow_id,
28✔
979
                &layer.symbology,
28✔
980
                &layer.properties,
28✔
981
                &HashMapTextTextDbType::from(&layer.metadata),
28✔
982
            ],
28✔
983
        )
28✔
984
        .await?;
28✔
985

986
    let stmt = trans
28✔
987
        .prepare(
28✔
988
            "
28✔
989
            INSERT INTO collection_layers (collection, layer)
28✔
990
            VALUES ($1, $2) ON CONFLICT DO NOTHING;",
28✔
991
        )
28✔
992
        .await?;
28✔
993

994
    trans.execute(&stmt, &[&collection_id, &layer_id]).await?;
28✔
995

996
    Ok(layer_id)
28✔
997
}
28✔
998

999
pub async fn insert_layer_collection_with_id(
32✔
1000
    trans: &Transaction<'_>,
32✔
1001
    id: &LayerCollectionId,
32✔
1002
    collection: AddLayerCollection,
32✔
1003
    parent: &LayerCollectionId,
32✔
1004
) -> Result<Uuid> {
32✔
1005
    let collection_id =
32✔
1006
        Uuid::from_str(&id.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
32✔
1007
            found: id.0.clone(),
×
UNCOV
1008
        })?;
×
1009

1010
    let parent =
32✔
1011
        Uuid::from_str(&parent.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
32✔
1012
            found: parent.0.clone(),
×
UNCOV
1013
        })?;
×
1014

1015
    let stmt = trans
32✔
1016
        .prepare(
32✔
1017
            "
32✔
1018
        INSERT INTO layer_collections (id, name, description, properties)
32✔
1019
        VALUES ($1, $2, $3, $4);",
32✔
1020
        )
32✔
1021
        .await?;
32✔
1022

1023
    trans
32✔
1024
        .execute(
32✔
1025
            &stmt,
32✔
1026
            &[
32✔
1027
                &collection_id,
32✔
1028
                &collection.name,
32✔
1029
                &collection.description,
32✔
1030
                &collection.properties,
32✔
1031
            ],
32✔
1032
        )
32✔
1033
        .await?;
32✔
1034

1035
    let stmt = trans
32✔
1036
        .prepare(
32✔
1037
            "
32✔
1038
        INSERT INTO collection_children (parent, child)
32✔
1039
        VALUES ($1, $2) ON CONFLICT DO NOTHING;",
32✔
1040
        )
32✔
1041
        .await?;
32✔
1042

1043
    trans.execute(&stmt, &[&parent, &collection_id]).await?;
32✔
1044

1045
    Ok(collection_id)
32✔
1046
}
32✔
1047

1048
pub async fn insert_collection_parent<Tls>(
2✔
1049
    conn: &PooledConnection<'_, PostgresConnectionManager<Tls>>,
2✔
1050
    collection: &LayerCollectionId,
2✔
1051
    parent: &LayerCollectionId,
2✔
1052
) -> Result<()>
2✔
1053
where
2✔
1054
    Tls: MakeTlsConnect<Socket> + Clone + Send + Sync + 'static + std::fmt::Debug,
2✔
1055
    <Tls as MakeTlsConnect<Socket>>::Stream: Send + Sync,
2✔
1056
    <Tls as MakeTlsConnect<Socket>>::TlsConnect: Send,
2✔
1057
    <<Tls as MakeTlsConnect<Socket>>::TlsConnect as TlsConnect<Socket>>::Future: Send,
2✔
1058
{
2✔
1059
    let collection =
2✔
1060
        Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
2✔
1061
            found: collection.0.clone(),
×
UNCOV
1062
        })?;
×
1063

1064
    let parent =
2✔
1065
        Uuid::from_str(&parent.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
2✔
1066
            found: parent.0.clone(),
×
UNCOV
1067
        })?;
×
1068

1069
    let stmt = conn
2✔
1070
        .prepare(
2✔
1071
            "
2✔
1072
        INSERT INTO collection_children (parent, child)
2✔
1073
        VALUES ($1, $2) ON CONFLICT DO NOTHING;",
2✔
1074
        )
2✔
1075
        .await?;
2✔
1076

1077
    conn.execute(&stmt, &[&parent, &collection]).await?;
2✔
1078

1079
    Ok(())
2✔
1080
}
2✔
1081

1082
pub async fn delete_layer_collection(
5✔
1083
    transaction: &Transaction<'_>,
5✔
1084
    collection: &LayerCollectionId,
5✔
1085
) -> Result<()> {
5✔
1086
    let collection =
5✔
1087
        Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
5✔
1088
            found: collection.0.clone(),
×
UNCOV
1089
        })?;
×
1090

1091
    if collection == INTERNAL_LAYER_DB_ROOT_COLLECTION_ID {
5✔
1092
        return Err(LayerDbError::CannotRemoveRootCollection.into());
2✔
1093
    }
3✔
1094

1095
    // delete the collection!
1096
    // on delete cascade removes all entries from `collection_children` and `collection_layers`
1097

1098
    let remove_layer_collection_stmt = transaction
3✔
1099
        .prepare(
3✔
1100
            "DELETE FROM layer_collections
3✔
1101
             WHERE id = $1;",
3✔
1102
        )
3✔
1103
        .await?;
3✔
1104
    transaction
3✔
1105
        .execute(&remove_layer_collection_stmt, &[&collection])
3✔
1106
        .await?;
3✔
1107

1108
    #[allow(clippy::used_underscore_items)] // TODO: maybe rename?
1109
    _remove_collections_without_parent_collection(transaction).await?;
3✔
1110

1111
    #[allow(clippy::used_underscore_items)] // TODO: maybe rename?
1112
    _remove_layers_without_parent_collection(transaction).await?;
3✔
1113

1114
    Ok(())
3✔
1115
}
5✔
1116

1117
pub async fn delete_layer_from_collection(
3✔
1118
    transaction: &Transaction<'_>,
3✔
1119
    layer: &LayerId,
3✔
1120
    collection: &LayerCollectionId,
3✔
1121
) -> Result<()> {
3✔
1122
    let collection_uuid =
3✔
1123
        Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
3✔
1124
            found: collection.0.clone(),
×
UNCOV
1125
        })?;
×
1126

1127
    let layer_uuid =
3✔
1128
        Uuid::from_str(&layer.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
3✔
1129
            found: layer.0.clone(),
×
UNCOV
1130
        })?;
×
1131

1132
    let remove_layer_collection_stmt = transaction
3✔
1133
        .prepare(
3✔
1134
            "DELETE FROM collection_layers
3✔
1135
             WHERE collection = $1
3✔
1136
             AND layer = $2;",
3✔
1137
        )
3✔
1138
        .await?;
3✔
1139
    let num_results = transaction
3✔
1140
        .execute(
3✔
1141
            &remove_layer_collection_stmt,
3✔
1142
            &[&collection_uuid, &layer_uuid],
3✔
1143
        )
3✔
1144
        .await?;
3✔
1145

1146
    if num_results == 0 {
3✔
1147
        return Err(LayerDbError::NoLayerForGivenIdInCollection {
×
1148
            layer: layer.clone(),
×
1149
            collection: collection.clone(),
×
1150
        }
×
1151
        .into());
×
1152
    }
3✔
1153

1154
    #[allow(clippy::used_underscore_items)] // TODO: maybe rename?
1155
    _remove_layers_without_parent_collection(transaction).await?;
3✔
1156

1157
    Ok(())
3✔
1158
}
3✔
1159

1160
pub async fn delete_layer_collection_from_parent(
2✔
1161
    transaction: &Transaction<'_>,
2✔
1162
    collection: &LayerCollectionId,
2✔
1163
    parent: &LayerCollectionId,
2✔
1164
) -> Result<()> {
2✔
1165
    let collection_uuid =
2✔
1166
        Uuid::from_str(&collection.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
2✔
1167
            found: collection.0.clone(),
×
UNCOV
1168
        })?;
×
1169

1170
    let parent_collection_uuid =
2✔
1171
        Uuid::from_str(&parent.0).map_err(|_| crate::error::Error::IdStringMustBeUuid {
2✔
1172
            found: parent.0.clone(),
×
UNCOV
1173
        })?;
×
1174

1175
    let remove_layer_collection_stmt = transaction
2✔
1176
        .prepare(
2✔
1177
            "DELETE FROM collection_children
2✔
1178
             WHERE child = $1
2✔
1179
             AND parent = $2;",
2✔
1180
        )
2✔
1181
        .await?;
2✔
1182
    let num_results = transaction
2✔
1183
        .execute(
2✔
1184
            &remove_layer_collection_stmt,
2✔
1185
            &[&collection_uuid, &parent_collection_uuid],
2✔
1186
        )
2✔
1187
        .await?;
2✔
1188

1189
    if num_results == 0 {
2✔
1190
        return Err(LayerDbError::NoCollectionForGivenIdInCollection {
×
1191
            collection: collection.clone(),
×
1192
            parent: parent.clone(),
×
1193
        }
×
1194
        .into());
×
1195
    }
2✔
1196

1197
    #[allow(clippy::used_underscore_items)] // TODO: maybe rename?
1198
    _remove_collections_without_parent_collection(transaction).await?;
2✔
1199

1200
    #[allow(clippy::used_underscore_items)] // TODO: maybe rename?
1201
    _remove_layers_without_parent_collection(transaction).await?;
2✔
1202

1203
    Ok(())
2✔
1204
}
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc