在SQL中复制文件夹名称冲突

时间:2018-01-25 13:51:16

标签: sql tsql recursion

我在Windows应用程序中实现了文件资源管理器,文件结构使用hierarchyid类型存储在T-SQL数据库中。

复制文件夹时需要以与Explorer类似的方式处理名称冲突,但不完全相同。我在C#中使用递归方法实现了这一点,但现在告诉我它需要在SQL中完成,并且不允许覆盖文件夹。

例如,我在根目录中有以下文件夹结构: -

  • 文件夹
  • 文件夹(2)
  • 文件夹(4)

在其他地方,我有以下文件夹,我想复制到根目录: -

  • 文件夹
  • 文件夹(2)
  • 文件夹(3)

所以...

  • 文件夹将成为Folder(3),因为该名称是3,而3是下一个可用的数字。
  • 文件夹(2)将成为文件夹(2)(2),因为该名称已被删除。
  • 文件夹(3)将成为文件夹(3)(2),因为文件夹(3)现在是在我们上面的重命名操作之后拍摄的。

老实说,我认为这是一个递归问题,因为在选择新名称后,可能还需要重命名其他文件夹以将其考虑在内,然后需要考虑更多文件夹,等等......

我的想法是一个递归的CTE,但我真的很难想出任何东西。这是测试数据: -

declare @existing table
(
    folderid uniqueidentifier,
    displayname varchar(20)
)

declare @folderstocopy table
(
    folderid uniqueidentifier,
    displayname varchar(20)
)

insert @existing
values (newid(),'Folder'),(newid(),'Folder (2)'), (newid(),'Folder (4)')


insert @folderstocopy
values (newid(),'Folder'),(newid(),'Folder (2)'), (newid(),'Folder (3)')

--TODO some logic to deal with name clashes and insert from @folderstocopy into @existing

select * from @existing

提前感谢您的帮助。

1 个答案:

答案 0 :(得分:1)

如果不使用某种形式的循环/光标,这是不可能的。 CTE是一种走静态层次结构的便捷方式,但您的问题需要动态层次结构。

在静态层次结构中,每个入口点都有一个确定数量的步骤,无论有多少对象通过该点。例如,我和CEO之间有两份报告。如果我想计算其他几个员工的步行长度,我在层次结构中走的长度总是两个。

在您的示例中,步行的长度会根据您要复制的其他文件夹以及处理它们的顺序而更改。如果你想复制'文件夹'和'文件夹(3)',然后'文件夹'或'文件夹(3)'将具有不同的步行长度,具体取决于层次结构的第一步。处理完第一个文件夹后,层次结构将被更改,从而使层次结构动态化。

以下代码遍历每个文件夹的现有层次结构,但不会产生所需的答案,因为这两个文件夹都是'和'文件夹(3)'最终使用相同的名称

 declare @existing table (folderid uniqueidentifier, displayname varchar(20));

declare @folderstocopy table (folderid uniqueidentifier, displayname varchar(20));

insert @existing
values
    (newid(), 'Folder')
  , (newid(), 'Folder (2)')
  , (newid(), 'Folder (4)');

insert @folderstocopy
values
    (newid(), 'Folder')
  , (newid(), 'Folder (2)')
  , (newid(), 'Folder (3)');

--TODO some logic to deal with name clashes and insert from @folderstocopy into @existing
with cte1 as
    (
        -- anchor
        select
            a.folderid
          , a.displayname
          , b.displayname as existingmatch
          , 1 as lvl
        from
            @folderstocopy as a
        left join
            @existing as b
            on
            a.displayname = b.displayname

        -- recursive
        union all
        select
            a.folderid
          , a.displayname
          , b.displayname as existingmatch
          , a.lvl + 1
        from
            cte1 as a
        inner join
            @existing as b
            on
            a.displayname + ' (' + cast(lvl + 1 as varchar(255)) + ')' = b.displayname
    )
select
    a.folderid
  , a.displayname as originaldisplayname
  , case
        when a.MaxLvl = 1
            then
            a.displayname
        else
            a.displayname + ' (' + cast(a.MaxLvl as varchar(255)) + ')' end as newdisplayname
from
    (
        select
             cte1.folderid
           , cte1.displayname
           , max(case when existingmatch is not null then lvl + 1 else lvl end) as MaxLvl
        from cte1
        group by
             cte1.folderid
           , cte1.displayname
    ) as a;

一种可能的解决方案是在循环内使用CTE,如下所示。在循环的每次迭代中,您可以通过向层次结构添加最低文件夹ID来解决冲突。

declare @existing table (folderid uniqueidentifier, displayname varchar(20));

declare @folderstocopy table (folderid uniqueidentifier, displayname varchar(20));

insert @existing
values
    (newid(), 'Folder')
  , (newid(), 'Folder (2)')
  , (newid(), 'Folder (4)');

insert @folderstocopy
values
    (newid(), 'Folder')
  , (newid(), 'Folder (2)')
  , (newid(), 'Folder (3)');

--TODO some logic to deal with name clashes and insert from @folderstocopy into @existing
while exists (select * from @folderstocopy)
begin;
    with cte1 as
        (
            -- anchor
            select
                a.folderid
              , a.displayname
              , b.displayname as existingmatch
              , 1 as lvl
            from
                @folderstocopy as a
            left join
                @existing as b
                on
                a.displayname = b.displayname

            -- recursive
            union all
            select
                a.folderid
              , a.displayname
              , b.displayname as existingmatch
              , a.lvl + 1
            from
                cte1 as a
            inner join
                @existing as b
                on
                a.displayname + ' (' + cast(lvl + 1 as varchar(255)) + ')' = b.displayname
        )
       , cte2 as
        (
            select
                a.folderid
              , a.displayname as originaldisplayname
              , case
                    when a.MaxLvl = 1
                        then
                        a.displayname
                    else
                        a.displayname + ' (' + cast(a.MaxLvl as varchar(255)) + ')' end as newdisplayname
            from
                (
                    select
                         cte1.folderid
                       , cte1.displayname
                       , max(case when existingmatch is not null then lvl + 1 else lvl end) as MaxLvl
                    from cte1
                    group by
                         cte1.folderid
                       , cte1.displayname
                ) as a
        )
    insert into @existing (folderid, displayname)
    select
         min(folderid) as folderid
       , a.newdisplayname
    from cte2 as a
    group by
         a.newdisplayname;

    delete from @folderstocopy where folderid in (select folderid from @existing);
end;

select * from @existing;