postgresqlsql-returning

Insert into multiple tables from single select


I'm attempting to perform a migration to split a "questions" table into two tables: "campaigns" and "campaign_actions" where the campaign contains the question title and the campaign action references the question id via a question_id column and then newly created campaign via a campaign_id column.

I've tried returning the fields from the initial select using the as keyword and also into. All the examples I'm seeing are inserting into multiple tables with values. I'm having trouble correlating the newly created campaign id with the existing question id for insert into the "campaign_actions" table. I've tried several things but here's my most recent attempt:

    WITH campaign_questions AS (
      INSERT INTO "campaigns" (name, tenant_id)
      SELECT 
        q.title as name,
        q.tenant_id as tenant_id
      FROM "questions" as q
      RETURNING 
        id into campaign_id,
        q.id into question_id, 
        q.app_area_id into app_area_id
    )
    INSERT INTO "campaign_actions" (campaign_id, question_id, app_area_id) 
    SELECT campaign_id, question_id, app_area_id,
    FROM campaign_questions

Some other thing's I've thought about doing: adding a temporary column on campaigns to hold the question_id and the app_area_id so I can return them from the insert and then use them to populate the campaign_actions table and then drop the columns.

Here are the DBML of the questions, campaigns, and campaign actions tables:

table questions {
  id uuid [pk, unique, default: `uuid_generate_v4()`]

  app_area_id uuid [ref: > app_areas.id]

  tenant_id text [not null]

  title text [not null]
  subtitle text

  type text [not null]

  data jsonb

  // Timestamps
  created_at timestamp [not null, default: `now()`]
  updated_at timestamp [not null, default: `now()`]
  deleted_at timestamp

  indexes {
    tenant_id [type: btree]
  }
}

table campaigns {
  id uuid [pk, unique, default: `uuid_generate_v4()`]
  
  tenant_id text [not null]

  name text [not null]
  description text
  
  start_at timestamp
  end_at timestamp
  paused_at timestamp
  closed_at timestamp

  published_at timestamp

  // Timestamps
  created_at timestamp [not null, default: `now()`]
  updated_at timestamp [not null, default: `now()`]
  deleted_at timestamp

  indexes {
    tenant_id [type: btree]
  }
}

table campaign_actions {
  id uuid [pk, unique, default: `uuid_generate_v4()`]
  
  tenant_id text [not null]
  campaign_id uuid [not null, ref: > campaigns.id]

  type text [not null, default: "question"]
  question_id uuid [ref: > questions.id]
  display_type text

  app_area_id uuid [ref: > app_areas.id]
  
  // Timestamps
  created_at timestamp [not null, default: `now()`]
  updated_at timestamp [not null, default: `now()`]
  deleted_at timestamp

  indexes {
    tenant_id [type: btree]
    campaign_id [type: btree]
    question_id [type: btree]
  }
}

I assume there is a better way or I'm missing something. Any help is appreciated!


Solution

  • The following query associates the campaign and question IDs by generating the campaign IDs prior to performing the insert:

    WITH id_pairs AS
           (SELECT uuid_generate_v4() AS campaign_id,
                   id                 AS question_id
              FROM questions),
         campaign_inserts AS
           (INSERT INTO campaigns (id, name, tenant_id)
             SELECT id_pairs.campaign_id, questions.title, tenant_id
               FROM questions
                 JOIN id_pairs
                      ON questions.id = id_pairs.question_id)
    INSERT
      INTO campaign_actions (campaign_id, question_id, app_area_id)
    SELECT id_pairs.campaign_id, questions.id, questions.app_area_id
      FROM questions
        JOIN id_pairs
             ON questions.id = id_pairs.question_id;
    
    

    The campaign_inserts CTE is basically a label for the insert into campaigns: it's never used.