/******************************************************************************** CATALOG_TABLE_COLUMN_MOVE Function
********************************************************************************create or replace function _utility.catalog_table_column_move
(
    strSchemaName text, 
    strTableName text, 
    strColumnName text, 
    strColumnNameBefore text
)
    returns void as $$
declare
    rIndex record;
    rConstraint record;
    rColumn record;
    strSchemaTable text = strSchemaName || '.' || strTableName;
    strDdl text;
    strClusterIndex text;
begin
    -- Raise notice that a reorder is in progress
    raise notice 'Reorder columns in table %.% (% before %)', strSchemaName, strTableName, strColumnName, strColumnNameBefore;

    -- Get the cluster index
    select pg_index.relname
      into strClusterIndex
      from pg_namespace
           inner join pg_class
                on pg_class.relnamespace = pg_namespace.oid
               and pg_class.relname = strTableName
           inner join pg_index pg_index_map
                on pg_index_map.indrelid = pg_class.oid
               and pg_index_map.indisclustered = true
           inner join pg_class pg_index
                on pg_index.oid = pg_index_map.indexrelid
     where pg_namespace.nspname = strSchemaName;

    if strClusterIndex is null then
        raise exception 'Table %.% must have a cluster index before reordering', strSchemaName, strTableName;
    end if;

    -- Disable all user triggers
    strDdl = 'alter table ' || strSchemaTable || ' disable trigger user';
    raise notice '        Disable triggers [%]', strDdl;
    execute strDdl;

    -- Create temp table to hold ddl
    create temp table temp_catalogtablecolumnreorder
    (
        type text not null,
        name text not null,
        ddl text not null
    );

    -- Save index ddl in a temp table
    raise notice '    Save indexes';

    for rIndex in
        with index as
        (
            select _utility.catalog_index_list_get(strSchemaName, strTableName) as name
        ),
        index_ddl as
        (
            select index.name,
                   _utility.catalog_index_create_get(_utility.catalog_index_get(strSchemaName, index.name)) as ddl
              from index
        )
        select index.name,
               index_ddl.ddl
          from index
               left outer join index_ddl
                    on index_ddl.name = index.name
                   and index_ddl.ddl not like '%[function]%'
    loop
        raise notice '        Save %', rIndex.name;
        insert into temp_catalogtablecolumnreorder values ('index', rIndex.name, rIndex.ddl);
    end loop;

    -- Save constraint ddl in a temp table
    raise notice '    Save constraints';

    for rConstraint in
        with constraint_list as
        (
            select _utility.catalog_constraint_list_get(strSchemaName, strTableName, '{p,u,f,c}') as name
        ),
        constraint_ddl as
        (
            select constraint_list.name,
                   _utility.catalog_constraint_create_get(_utility.catalog_constraint_get(strSchemaName, strTableName, 
                                                                                          constraint_list.name)) as ddl
              from constraint_list
        )
        select constraint_list.name,
               constraint_ddl.ddl
          from constraint_list
               left outer join constraint_ddl
                    on constraint_ddl.name = constraint_list.name
    loop
        raise notice '        Save %', rConstraint.name;
        insert into temp_catalogtablecolumnreorder values ('constraint', rConstraint.name, rConstraint.ddl);
    end loop;

    -- Move column
    for rColumn in
        with table_column as
        (
            select pg_attribute.attname as name,
                   rank() over (order by pg_attribute.attnum) as rank,
                   pg_type.typname as type,
                   case when pg_attribute.atttypmod = -1 then null else ((atttypmod - 4) >> 16) & 65535 end as precision,
                   case when pg_attribute.atttypmod = -1 then null else (atttypmod - 4) & 65535 end as scale,
                   not pg_attribute.attnotnull as nullable,
                   pg_attrdef.adsrc as default,
                   pg_attribute.*
              from pg_namespace
                   inner join pg_class
                        on pg_class.relnamespace = pg_namespace.oid
                       and pg_class.relname = strTableName
                   inner join pg_attribute
                        on pg_attribute.attrelid = pg_class.oid
                       and pg_attribute.attnum >= 1
                       and pg_attribute.attisdropped = false
                   inner join pg_type
                        on pg_type.oid = pg_attribute.atttypid
                   left outer join pg_attrdef
                        on pg_attrdef.adrelid = pg_class.oid
                       and pg_attrdef.adnum = pg_attribute.attnum
             where pg_namespace.nspname = strSchemaName
             order by pg_attribute.attnum
        )
        select table_column.*
          from table_column table_column_before
               inner join table_column
                    on table_column.rank >= table_column_before.rank
                   and table_column.name <> strColumnName
         where table_column_before.name = strColumnNameBefore
    loop
        raise notice '    Move column %', rColumn.name;

        strDdl = 'alter table ' || strSchemaTable || ' rename column "' || rColumn.name || '" to "@' || rColumn.name || '@"';
        raise notice '        Rename [%]', strDdl;
        execute strDdl;
        
        strDdl = 'alter table ' || strSchemaTable || ' add "' || rColumn.name || '" ' || rColumn.type ||
                 case when rColumn.precision is not null then '(' || rColumn.precision || ', ' || rColumn.scale || ')' else '' end;
        raise notice '        Create [%]', strDdl;
        execute strDdl;
        
        strDdl = 'update ' || strSchemaTable || ' set "' || rColumn.name || '" = "@' || rColumn.name || '@"';
        raise notice '        Copy [%]', strDdl;
        execute strDdl;

        strDdl = 'alter table ' || strSchemaTable || ' drop column "@' || rColumn.name || '@"';
        raise notice '        Drop [%]', strDdl;
        execute strDdl;

        if rColumn."default" is not null then
            strDdl = 'alter table ' || strSchemaTable || ' alter column "' || rColumn.name || '" set default ' || rColumn.default;
            raise notice '        Default [%]', strDdl;
            execute strDdl;
        end if;

        if rColumn.nullable = false then
            strDdl = 'alter table ' || strSchemaTable || ' alter column "' || rColumn.name || '" set not null';
            raise notice '        Not Null [%]', strDdl;
            execute strDdl;
        end if;
    end loop;

    -- Rebuild indexes
    raise notice '    Rebuild indexes';

    for rIndex in
        select name,
               ddl
          from temp_catalogtablecolumnreorder
         where type = 'index'
    loop
        begin
            execute rIndex.ddl;
            raise notice '        Rebuild % [%]', rIndex.name, rIndex.ddl;
        exception
            when duplicate_table then
                raise notice '        Skip % [%]', rIndex.name, rIndex.ddl;
        end;
    end loop;

    -- Rebuild constraints
    raise notice '    Rebuild constraints';
    
    for rConstraint in
        select name,
               ddl
          from temp_catalogtablecolumnreorder
         where type = 'constraint'
    loop
        begin
            execute rConstraint.ddl;
            raise notice '        Rebuild % [%]', rConstraint.name, rConstraint.ddl;
        exception
            when duplicate_object or duplicate_table or invalid_table_definition then
                raise notice '        Skip % [%]', rConstraint.name, rConstraint.ddl;
        end;
    end loop;

    -- Recluster table
    strDdl = 'cluster ' || strSchemaTable || ' using ' || strClusterIndex;
    raise notice '    Recluster [%]', strDdl;
    execute strDdl;

    -- Enable all user triggers
    strDdl = 'alter table ' || strSchemaTable || ' enable trigger user';
    raise notice '    Enable triggers [%]', strDdl;
    execute strDdl;

    -- Drop temp tables
    drop table temp_catalogtablecolumnreorder;
end
$$ language plpgsql security invoker;

comment on function _utility.catalog_table_column_move(text, text, text, text) is
'Moves a column before another column in a table.  For example:

{{perform _utility.catalog_table_column_move(''attribute'', ''attribute'', ''target'', ''active'');}}

will position the "target" column right before the "active" column.  It''s not currently possible to directly move a column to the
right but this can be achieved by multiple moves of columns to the left.

There are a few caveats:
* The table must have a cluster index.  Moving columns is messy on the storage and the table needs to be re-clustered afterwards.
* Column referencing triggers will not automatically be dropped or rebuilt.
* Column specific permissions are not restored after the move.
* A column cannot be moved before the primary key if there are foreign key references from other tables.';
