Add duo_default_namespace in model

What does this MR do and why?

This is a continuation of !204649 (merged).

  1. override reader method
  2. override writer method so it would conditionally update default_duo_add_on_assignment_id instead
  3. custom validation

There will be further MRs based on this to implement API and web UI.

References

#557584

How to set up and validate locally

Currently the spec serves as coverage on various calls to this new attribute.

Database review

exists query (click to expand)

postgres.ai

Triggered when user.user_preference.duo_default_namespace is called. This is the most often call (when reading a default namespace).

Query:

SELECT
    1 AS one
FROM
    namespaces
WHERE
    namespaces.id IN ( SELECT DISTINCT
            subscription_add_on_purchases.namespace_id
        FROM
            subscription_add_on_purchases
        LEFT JOIN subscription_user_add_on_assignments ON subscription_user_add_on_assignments.add_on_purchase_id = subscription_add_on_purchases.id
    WHERE
        subscription_add_on_purchases.subscription_add_on_id IN (
            SELECT
                subscription_add_ons.id
            FROM
                subscription_add_ons
            WHERE
                subscription_add_ons.name IN (1, 3, 4, 5, 6))
            AND (started_at IS NULL
                OR started_at <= '2025-10-29')
            AND '2025-10-29' < expires_on
            AND subscription_add_on_purchases.namespace_id IN (29, 112, 33, 96, 1000000, 24, 118, 27, 111, 110, 31, 109, 102, 107, 22, 35, 9970)
            AND (subscription_add_on_purchases.subscription_add_on_id IN (
                    SELECT
                        subscription_add_ons.id
                    FROM
                        subscription_add_ons
                    WHERE
                        subscription_add_ons.name = 5)
                    OR (subscription_add_on_purchases.subscription_add_on_id IN (
                            SELECT
                                subscription_add_ons.id
                            FROM
                                subscription_add_ons
                            WHERE
                                subscription_add_ons.name IN (1, 3))
                            AND subscription_user_add_on_assignments.id IS NOT NULL)))
            AND namespaces.id = 9970
        LIMIT 1;

Execution plan:

 Limit  (cost=15.84..48.93 rows=1 width=4) (actual time=1.374..1.377 rows=1 loops=1)
   Buffers: shared hit=28 read=1
   I/O Timings: read=1.155 write=0.000
   ->  Nested Loop Semi Join  (cost=15.84..48.93 rows=1 width=4) (actual time=1.373..1.375 rows=1 loops=1)
         Buffers: shared hit=28 read=1
         I/O Timings: read=1.155 write=0.000
         ->  Index Only Scan using namespaces_pkey on public.namespaces  (cost=0.57..3.58 rows=1 width=4) (actual time=1.209..1.209 rows=1 loops=1)
               Index Cond: (namespaces.id = 9970)
               Heap Fetches: 0
               Buffers: shared hit=4 read=1
               I/O Timings: read=1.155 write=0.000
         ->  Limit  (cost=15.27..45.34 rows=1 width=8) (actual time=0.161..0.163 rows=1 loops=1)
               Buffers: shared hit=24
               I/O Timings: read=0.000 write=0.000
               ->  Nested Loop Left Join  (cost=15.27..45.34 rows=1 width=8) (actual time=0.161..0.162 rows=1 loops=1)
                     Filter: ((hashed SubPlan 1) OR ((hashed SubPlan 2) AND (subscription_user_add_on_assignments.id IS NOT NULL)))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=24
                     I/O Timings: read=0.000 write=0.000
                     ->  Nested Loop  (cost=7.66..36.16 rows=1 width=24) (actual time=0.118..0.119 rows=1 loops=1)
                           Buffers: shared hit=17
                           I/O Timings: read=0.000 write=0.000
                           ->  Seq Scan on public.subscription_add_ons  (cost=0.00..4.05 rows=3 width=8) (actual time=0.008..0.009 rows=2 loops=1)
                                 Filter: (subscription_add_ons.name = ANY ('{1,3,4,5,6}'::integer[]))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=1
                                 I/O Timings: read=0.000 write=0.000
                           ->  Index Scan using index_add_on_purchases_on_add_on_id_and_namespace_id_not_null on public.subscription_add_on_purchases  (cost=7.66..10.69 rows=1 width=24) (actual time=0.053..0.053 rows=0 loops=2)
                                 Index Cond: ((subscription_add_on_purchases.subscription_add_on_id = subscription_add_ons.id) AND (subscription_add_on_purchases.namespace_id = 9970))
                                 Filter: (((subscription_add_on_purchases.started_at IS NULL) OR (subscription_add_on_purchases.started_at <= '2025-10-29'::date)) AND ('2025-10-29'::date < subscription_add_on_purchases.expires_on) AND ((hashed SubPlan 1) OR (hashed SubPlan 2)))
                                 Rows Removed by Filter: 0
                                 Buffers: shared hit=16
                                 I/O Timings: read=0.000 write=0.000
                                 SubPlan 1
                                   ->  Index Scan using index_subscription_add_ons_on_name on public.subscription_add_ons subscription_add_ons_1  (cost=0.13..3.15 rows=1 width=8) (actual time=0.017..0.018 rows=1 loops=2)
                                         Index Cond: (subscription_add_ons_1.name = 5)
                                         Buffers: shared hit=7
                                         I/O Timings: read=0.000 write=0.000
                                 SubPlan 2
                                   ->  Seq Scan on public.subscription_add_ons subscription_add_ons_2  (cost=0.00..4.04 rows=2 width=8) (actual time=0.002..0.002 rows=2 loops=2)
                                         Filter: (subscription_add_ons_2.name = ANY ('{1,3}'::integer[]))
                                         Rows Removed by Filter: 1
                                         Buffers: shared hit=2
                                         I/O Timings: read=0.000 write=0.000
                     ->  Index Only Scan using idx_user_add_on_assignments_on_add_on_purchase_id_and_id on public.subscription_user_add_on_assignments  (cost=0.42..1.95 rows=2 width=16) (actual time=0.025..0.025 rows=1 loops=1)
                           Index Cond: (subscription_user_add_on_assignments.add_on_purchase_id = subscription_add_on_purchases.id)
                           Heap Fetches: 0
                           Buffers: shared hit=4
                           I/O Timings: read=0.000 write=0.000
Settings: effective_cache_size = '472585MB', jit = 'off', work_mem = '100MB', random_page_cost = '1.5', seq_page_cost = '4'

Summary

Time: 6.282 ms
  - planning: 4.719 ms
  - execution: 1.563 ms
    - I/O read: 1.155 ms
    - I/O write: 0.000 ms

Shared buffers:
  - hits: 28 (~224.00 KiB) from the buffer pool
  - reads: 1 (~8.00 KiB) from the OS file cache, including disk I/O
  - dirtied: 0
  - writes: 0
listing duo_default_namespace_candidates query (click to expand)

postgres.ai session

Triggered when user.user_preference.duo_default_namespace_candidates is called. This is called in user preference page rendering.

Query:

SELECT
    "namespaces"."id",
    "namespaces"."name",
    "namespaces"."path",
    "namespaces"."owner_id",
    "namespaces"."created_at",
    "namespaces"."updated_at",
    "namespaces"."type",
    "namespaces"."avatar",
    "namespaces"."membership_lock",
    "namespaces"."share_with_group_lock",
    "namespaces"."visibility_level",
    "namespaces"."request_access_enabled",
    "namespaces"."ldap_sync_status",
    "namespaces"."ldap_sync_error",
    "namespaces"."ldap_sync_last_update_at",
    "namespaces"."ldap_sync_last_successful_update_at",
    "namespaces"."ldap_sync_last_sync_at",
    "namespaces"."lfs_enabled",
    "namespaces"."parent_id",
    "namespaces"."shared_runners_minutes_limit",
    "namespaces"."repository_size_limit",
    "namespaces"."require_two_factor_authentication",
    "namespaces"."two_factor_grace_period",
    "namespaces"."project_creation_level",
    "namespaces"."runners_token",
    "namespaces"."file_template_project_id",
    "namespaces"."saml_discovery_token",
    "namespaces"."runners_token_encrypted",
    "namespaces"."custom_project_templates_group_id",
    "namespaces"."auto_devops_enabled",
    "namespaces"."extra_shared_runners_minutes_limit",
    "namespaces"."last_ci_minutes_notification_at",
    "namespaces"."last_ci_minutes_usage_notification_level",
    "namespaces"."subgroup_creation_level",
    "namespaces"."max_pages_size",
    "namespaces"."max_artifacts_size",
    "namespaces"."mentions_disabled",
    "namespaces"."default_branch_protection",
    "namespaces"."max_personal_access_token_lifetime",
    "namespaces"."push_rule_id",
    "namespaces"."shared_runners_enabled",
    "namespaces"."allow_descendants_override_disabled_shared_runners",
    "namespaces"."traversal_ids",
    "namespaces"."organization_id",
    "namespaces"."state"
FROM
    "namespaces"
WHERE
    "namespaces"."id" IN ( SELECT DISTINCT
            "subscription_add_on_purchases"."namespace_id"
        FROM
            "subscription_add_on_purchases"
        LEFT OUTER JOIN "subscription_user_add_on_assignments" ON "subscription_user_add_on_assignments"."add_on_purchase_id" = "subscription_add_on_purchases"."id"
WHERE
    "subscription_add_on_purchases"."subscription_add_on_id" IN (
        SELECT
            "subscription_add_ons"."id"
        FROM
            "subscription_add_ons"
        WHERE
            "subscription_add_ons"."name" IN (1, 3, 4, 5, 6))
        AND (started_at IS NULL
            OR started_at <= '2025-10-30')
        AND ('2025-10-30' < expires_on)
        AND "subscription_add_on_purchases"."namespace_id" IN (29, 112, 33, 96, 1000000, 24, 118, 27, 111, 110, 31, 109, 102, 107, 22, 35, 9970)
        AND ("subscription_add_on_purchases"."subscription_add_on_id" IN (
                SELECT
                    "subscription_add_ons"."id"
                FROM
                    "subscription_add_ons"
                WHERE
                    "subscription_add_ons"."name" = 5)
                OR "subscription_add_on_purchases"."subscription_add_on_id" IN (
                    SELECT
                        "subscription_add_ons"."id"
                    FROM
                        "subscription_add_ons"
                    WHERE
                        "subscription_add_ons"."name" IN (1, 3))
                    AND "subscription_user_add_on_assignments"."id" IS NOT NULL))

Execution plan:

 Nested Loop  (cost=15.92..108.50 rows=2 width=374) (actual time=0.141..2.268 rows=1 loops=1)
   Buffers: shared hit=3776
   I/O Timings: read=0.000 write=0.000
   ->  Unique  (cost=15.36..101.33 rows=2 width=8) (actual time=0.123..2.250 rows=1 loops=1)
         Buffers: shared hit=3768
         I/O Timings: read=0.000 write=0.000
         ->  Nested Loop  (cost=15.36..101.33 rows=2 width=8) (actual time=0.123..2.140 rows=1766 loops=1)
               Buffers: shared hit=3768
               I/O Timings: read=0.000 write=0.000
               ->  Nested Loop Left Join  (cost=15.23..98.78 rows=2 width=16) (actual time=0.117..0.860 rows=1766 loops=1)
                     Filter: ((hashed SubPlan 1) OR ((hashed SubPlan 2) AND (subscription_user_add_on_assignments.id IS NOT NULL)))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=236
                     I/O Timings: read=0.000 write=0.000
                     ->  Index Scan using index_subscription_add_on_purchases_on_namespace_id_add_on_id on public.subscription_add_on_purchases  (cost=7.62..85.64 rows=3 width=24) (actual time=0.080..0.086 rows=2 loops=1)
                           Index Cond: (subscription_add_on_purchases.namespace_id = ANY ('{29,112,33,96,1000000,24,118,27,111,110,31,109,102,107,22,35,9970}'::bigint[]))
                           Filter: (((subscription_add_on_purchases.started_at IS NULL) OR (subscription_add_on_purchases.started_at <= '2025-10-30'::date)) AND ('2025-10-30'::date < subscription_add_on_purchases.expires_on) AND ((hashed SubPlan 1) OR (hashed SubPlan 2)))
                           Rows Removed by Filter: 0
                           Buffers: shared hit=62
                           I/O Timings: read=0.000 write=0.000
                           SubPlan 1
                             ->  Index Scan using index_subscription_add_ons_on_name on public.subscription_add_ons subscription_add_ons_1  (cost=0.13..3.15 rows=1 width=8) (actual time=0.007..0.007 rows=1 loops=2)
                                   Index Cond: (subscription_add_ons_1.name = 5)
                                   Buffers: shared hit=7
                                   I/O Timings: read=0.000 write=0.000
                           SubPlan 2
                             ->  Seq Scan on public.subscription_add_ons subscription_add_ons_2  (cost=0.00..4.04 rows=2 width=8) (actual time=0.002..0.003 rows=2 loops=2)
                                   Filter: (subscription_add_ons_2.name = ANY ('{1,3}'::integer[]))
                                   Rows Removed by Filter: 1
                                   Buffers: shared hit=2
                                   I/O Timings: read=0.000 write=0.000
                     ->  Index Only Scan using idx_user_add_on_assignments_on_add_on_purchase_id_and_id on public.subscription_user_add_on_assignments  (cost=0.42..1.95 rows=2 width=16) (actual time=0.015..0.193 rows=882 loops=2)
                           Index Cond: (subscription_user_add_on_assignments.add_on_purchase_id = subscription_add_on_purchases.id)
                           Heap Fetches: 640
                           Buffers: shared hit=171
                           I/O Timings: read=0.000 write=0.000
               ->  Index Scan using subscription_add_ons_pkey on public.subscription_add_ons  (cost=0.13..1.65 rows=1 width=8) (actual time=0.001..0.001 rows=1 loops=1766)
                     Index Cond: (subscription_add_ons.id = subscription_add_on_purchases.subscription_add_on_id)
                     Filter: (subscription_add_ons.name = ANY ('{1,3,4,5,6}'::integer[]))
                     Rows Removed by Filter: 0
                     Buffers: shared hit=3532
                     I/O Timings: read=0.000 write=0.000
   ->  Index Scan using namespaces_pkey on public.namespaces  (cost=0.57..3.58 rows=1 width=374) (actual time=0.017..0.017 rows=1 loops=1)
         Index Cond: (namespaces.id = subscription_add_on_purchases.namespace_id)
         Buffers: shared hit=8
         I/O Timings: read=0.000 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', jit = 'off', work_mem = '100MB', random_page_cost = '1.5'

Summary

Time: 6.533 ms  
  - planning: 4.124 ms  
  - execution: 2.409 ms  
    - I/O read: 0.000 ms  
    - I/O write: 0.000 ms  
  
Shared buffers:  
  - hits: 3776 (~29.50 MiB) from the buffer pool  
  - reads: 0 from the OS file cache, including disk I/O  
  - dirtied: 0  
  - writes: 0  

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #557584

Edited by Mark Chao

Merge request reports

Loading