Browse Source

HPCC-8049 Add Rename Physical to transactions

Adding RenamePhysical functionality to transactions with auto-commit
feature, when no transaction is involved. Updated FileServices and
others to cope with change.

Signed-off-by: Renato Golin <rengolin@hpccsystems.com>
Renato Golin 12 years ago
parent
commit
c66ccc9b95

+ 162 - 80
dali/base/dadfs.cpp

@@ -910,7 +910,7 @@ public:
     void addEntry(CDfsLogicalFileName &lfn,IPropertyTree *root,bool superfile, bool ignoreexists);
     bool removeEntry(const char *_logicalname,IUserDescriptor *user, unsigned timeoutms=INFINITE);
     bool removePhysical(const char *_logicalname,IUserDescriptor *user,const char *cluster,IMultiException *mexcept);
-    bool renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IMultiException *exceptions);
+    void renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IDistributedFileTransaction *transaction);
     void removeEmptyScope(const char *name);
 
     IDistributedSuperFile *lookupSuperFile(const char *logicalname,IUserDescriptor *user,IDistributedFileTransaction *transaction,unsigned timeout=INFINITE);
@@ -6666,6 +6666,153 @@ public:
     }
 };
 
+/**
+ * Renames a file within a transaction.
+ */
+class CRenameFileAction: public CDFAction
+{
+    CDfsLogicalFileName logicalname;
+    CDfsLogicalFileName newName;
+    CDistributedFileDirectory *parent;
+    Linked<IDistributedFile> file;
+    IUserDescriptor *user;
+public:
+    CRenameFileAction(IDistributedFileTransaction *_transaction,
+                      CDistributedFileDirectory *parent,
+                      IUserDescriptor *_user,
+                      const char *_flname,
+                      const char *_newname)
+        : CDFAction(_transaction), user(_user)
+    {
+        logicalname.set(_flname);
+        // Basic consistency checking
+        newName.set(_newname);
+        if (newName.isExternal())
+            throw MakeStringException(-1,"rename: cannot rename to external file");
+        if (newName.isForeign())
+            throw MakeStringException(-1,"rename: cannot rename to foreign file");
+
+        // We *have* to make sure the source file exists and can be renamed
+        file.setown(transaction->lookupFile(logicalname.get(), SDS_SUB_LOCK_TIMEOUT));
+        if (!file)
+            ThrowStringException(-1, "rename: file %s doesn't exist in the file system", logicalname.get());
+        if (file->querySuperFile())
+            throw MakeStringException(-1,"rename: cannot rename file %s as is SuperFile",_flname);
+        StringBuffer reason;
+        if (!file->canRemove(reason))
+            throw MakeStringException(-1,"rename: %s",reason.str());
+    }
+    virtual ~CRenameFileAction() {}
+    bool prepare()
+    {
+        addFileLock(file);
+        if (lock())
+            return true;
+        unlock();
+        return false;
+    }
+    void run()
+    {
+        doRename(newName.get());
+    }
+
+    void rollback()
+    {
+        // Only roll back if already renamed
+        const char * filename = file->queryLogicalName();
+        if (strcmp(filename, logicalname.get()) != 0)
+            doRename(logicalname.get());
+        CDFAction::rollback();
+    }
+
+private:
+    void doRename(const char *to)
+    {
+        // Make sure files are not the same
+        const char * filename = file->queryLogicalName();
+        if (strcmp(filename, to) == 0)
+            ThrowStringException(-1, "rename: cannot rename file %s to itself", filename);
+
+        // Rename current file to dest name
+        CDfsLogicalFileName fromName;
+        fromName.set(filename);
+        CDfsLogicalFileName toName;
+        toName.set(to);
+
+        StringBuffer oldcluster;
+        fromName.getCluster(oldcluster);
+        StringBuffer newcluster;
+        toName.getCluster(newcluster);
+        bool mergeinto = false;
+        bool splitfrom = false;
+
+        // Gather cluster info, make sure they match (for existing dest files)
+        Owned<IDistributedFile> newFile = transaction->lookupFile(toName.get(), SDS_SUB_LOCK_TIMEOUT);
+        Owned<IDistributedFile> oldfile;
+        if (newFile) {
+            if (newcluster.length()) {
+                if (oldcluster.length())
+                    throw MakeStringException(-1,"rename: cannot specify both source and destination clusters on rename");
+                if (newFile->findCluster(newcluster.str())!=NotFound)
+                    throw MakeStringException(-1,"rename: cluster %s already part of file %s",newcluster.str(),toName.get());
+                if (file->numClusters()!=1)
+                    throw MakeStringException(-1,"rename: source file %s has more than one cluster",fromName.get());
+                // check compatible here ** TBD
+                mergeinto = true;
+            }
+            else {
+                unlock();
+                ThrowStringException(-1, "rename: file %s already exist in the file system", toName.get());
+            }
+        }
+        else if (oldcluster.length())
+        {
+            if (newcluster.length())
+                throw MakeStringException(-1,"rename: cannot specify both source and destination clusters on rename");
+            if (file->numClusters()==1)
+                throw MakeStringException(-1,"rename: cannot rename sole cluster %s",oldcluster.str());
+            if (file->findCluster(oldcluster.str())==NotFound)
+                throw MakeStringException(-1,"rename: cannot find cluster %s",oldcluster.str());
+            oldfile.setown(file.getClear());
+            Owned<IFileDescriptor> newdesc = oldfile->getFileDescriptor(oldcluster.str());
+            file.setown(parent->createNew(newdesc));
+            splitfrom = true;
+        }
+
+        // Physical Rename
+        Owned<IMultiException> exceptions = MakeMultiException();
+        if (!file->renamePhysicalPartFiles(toName.get(),newcluster,exceptions))
+        {
+            unlock();
+            StringBuffer errors;
+            exceptions->errorMessage(errors);
+            ThrowStringException(-1, "rename: could not rename logical file %s to %s: %s", fromName.get(), toName.get(), errors.str());
+        }
+
+        // Logical rename and cleanup
+        if (splitfrom) {
+            oldfile->removeCluster(oldcluster.str());
+            file->attach(toName.get(), user);
+        }
+        else if (mergeinto) {
+            ClusterPartDiskMapSpec mspec = file->queryPartDiskMapping(0);
+            file->detach();
+            newFile->addCluster(newcluster.str(),mspec);
+            parent->fixDates(newFile);
+        }
+        else {
+            // We need to unlock the old file / re-lock the new file
+            unlock();
+            file->rename(toName.get(),user);
+            lock();
+        }
+        // MORE: If the logical rename fails, we should roll back the physical renaming
+        // What if the physical renaming-back fails?!
+        // For now, leaving as it was, since physical renaming is more prone to errors than logical
+        // And checks were made earlier to make sure it was safe to rename
+    }
+};
+
 // MORE: This should be implemented in DFSAccess later on
 IDistributedSuperFile *CDistributedFileDirectory::createSuperFile(const char *_logicalname,IUserDescriptor *user, bool _interleaved,bool ifdoesnotexist,IDistributedFileTransaction *transaction)
 {
@@ -6888,7 +7035,7 @@ bool CDistributedFileDirectory::removePhysical(const char *_logicalname,IUserDes
 }
 
     
-bool CDistributedFileDirectory::renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IMultiException *exceptions)
+void CDistributedFileDirectory::renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IDistributedFileTransaction *transaction)
 {
     CriticalBlock block(removesect);
     if (!user)
@@ -6903,85 +7050,20 @@ bool CDistributedFileDirectory::renamePhysical(const char *oldname,const char *n
     CDfsLogicalFileName oldlogicalname; 
     oldlogicalname.set(oldname);
     checkLogicalName(oldlogicalname,user,true,true,false,"rename");
-    Owned<IDistributedFile> file = lookup(oldlogicalname,user,true,NULL,defaultTimeout); 
-    if (!file) {
-        ERRLOG("renamePhysical: %s does not exist",oldname);
-        return false;
-    }   
-    if (file->querySuperFile()) 
-        throw MakeStringException(-1,"CDistributedFileDirectory::renamePhysical Cannot rename file %s as is SuperFile",oldname);
-    StringBuffer reason;
-    if (!file->canRemove(reason))
-        throw MakeStringException(-1,"CDistributedFileDirectory::renamePhysical %s",reason.str());
-    CDfsLogicalFileName newlogicalname; 
-    newlogicalname.set(newname);
-    if (newlogicalname.isExternal()) 
-        throw MakeStringException(-1,"renamePhysical cannot rename to external file");
-    if (newlogicalname.isForeign()) 
-        throw MakeStringException(-1,"renamePhysical cannot rename to foreign file");
-    StringBuffer oldcluster;
-    oldlogicalname.getCluster(oldcluster);
-    StringBuffer newcluster;
-    newlogicalname.getCluster(newcluster);
-    Owned<IDistributedFile> newfile = lookup(newlogicalname.get(),user,true,NULL, defaultTimeout); 
-    Owned<IDistributedFile> oldfile;
-    bool mergeinto = false;
-    bool splitfrom = false;
-    if (newfile) {
-        if (newcluster.length()) {
-            if (oldcluster.length()) 
-                throw MakeStringException(-1,"cannot specify both source and destination clusters on rename");
-            if (newfile->findCluster(newcluster.str())!=NotFound) 
-                throw MakeStringException(-1,"renamePhysical cluster %s already part of file %s",newcluster.str(),newname);
-            if (file->numClusters()!=1) 
-                throw MakeStringException(-1,"renamePhysical source file %s has more than one cluster",oldname);
-            // check compatible here ** TBD
-            mergeinto = true;
-        }
-        else {
-            ERRLOG("renamePhysical %s already exists",newname);
-            return false;
-        }
 
-    }
-    else if (oldcluster.length()) {
-        if (newcluster.length()) 
-            throw MakeStringException(-1,"cannot specify both source and destination clusters on rename");
-        if (file->numClusters()==1) 
-            throw MakeStringException(-1,"cannot rename sole cluster %s",oldcluster.str());
-        if (file->findCluster(oldcluster.str())==NotFound) 
-            throw MakeStringException(-1,"renamePhysical cannot find cluster %s",oldcluster.str());
-        oldfile.setown(file.getClear());
-        Owned<IFileDescriptor> newdesc = oldfile->getFileDescriptor(oldcluster.str());
-        file.setown(createNew(newdesc));
-        splitfrom = true;
-    }
-    
-    try {
-        if (!file->renamePhysicalPartFiles(newlogicalname.get(),splitfrom?oldcluster.str():NULL,exceptions))
-            return false;
-    }
-    catch (IException *e)
-    {
-        StringBuffer msg("Renaming ");
-        msg.append(oldname).append(" to ").append(newname);
-        EXCLOG(e,msg.str());
-        e->Release();
-        return false;
-    }
-    if (splitfrom) {
-        oldfile->removeCluster(oldcluster.str());
-        file->attach(newlogicalname.get(),user);
-    }
-    else if (mergeinto) {
-        ClusterPartDiskMapSpec mspec = file->queryPartDiskMapping(0);
-        file->detach();
-        newfile->addCluster(newcluster.str(),mspec);
-        fixDates(newfile);
-    }
-    else
-        file->rename(newname,user);
-    return true;
+    // Create a local transaction that will be destroyed (but never touch the external transaction)
+    Linked<IDistributedFileTransaction> localtrans;
+    if (transaction) {
+        localtrans.set(transaction);
+    } else {
+        // TODO: Make it explicit in the API that a transaction is required
+        localtrans.setown(new CDistributedFileTransaction(user));
+     }
+
+    // action is owned by transaction (acquired on CDFAction's c-tor) so don't unlink or delete!
+    CRenameFileAction *action = new CRenameFileAction(localtrans, this, user, oldlogicalname.get(), newname);
+
+    localtrans->autoCommit();
 }
 
 void CDistributedFileDirectory::fixDates(IDistributedFile *file)

+ 1 - 1
dali/base/dadfs.hpp

@@ -459,7 +459,7 @@ interface IDistributedFileDirectory: extends IInterface
 
     virtual bool removeEntry(const char *name,IUserDescriptor *user, unsigned timeoutms=INFINITE) = 0;  // equivalent to lookup/detach/release
     virtual bool removePhysical(const char *name,IUserDescriptor *user,const char *cluster=NULL,IMultiException *exceptions=NULL) = 0;                           // removes the physical parts as well as entry
-    virtual bool renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IMultiException *exceptions=NULL) = 0;                         // renames the physical parts as well as entry
+    virtual void renamePhysical(const char *oldname,const char *newname,IUserDescriptor *user,IDistributedFileTransaction *transaction) = 0;                         // renames the physical parts as well as entry
     virtual void removeEmptyScope(const char *scope) = 0;   // does nothing if called on non-empty scope
     
 

+ 1 - 2
dali/dfu/dfurun.cpp

@@ -1443,8 +1443,7 @@ public:
                         newfile.clear();
                         StringBuffer fromname(srcName);
                         srcFile.clear();
-                        if (!queryDistributedFileDirectory().renamePhysical(fromname.str(),toname.str(),userdesc,NULL))
-                            throw MakeStringException(-1,"rename failed"); // could do with better error here
+                        queryDistributedFileDirectory().renamePhysical(fromname.str(),toname.str(),userdesc,NULL);
                         StringBuffer timetaken;
                         timetaken.appendf("%dms",msTick()-start);
                         progress->setDone(timetaken.str(),0,true);

+ 104 - 14
dali/regress/daregress.cpp

@@ -33,6 +33,7 @@
 #include "dafdesc.hpp"
 #include "dasds.hpp"
 #include "danqs.hpp"
+#include "dautils.hpp"
 
 static int errorcount;
 
@@ -283,8 +284,12 @@ static IFileDescriptor *createFileDescriptor(const char* dir, const char* name,
     Owned<IFileDescriptor>fdesc = createFileDescriptor();
     fdesc->setDefaultDir(dir);
     StringBuffer s;
+    SocketEndpoint ep;
+    ep.setLocalHost(0);
+    StringBuffer ip;
+    ep.getIpText(ip);
     for (unsigned k=0;k<parts;k++) {
-        s.clear().append("192.168.1.10");
+        s.clear().append(ip);
         Owned<INode> node = createINode(s.str());
         pp->setPropInt64("@size",recSize);
         s.clear().append(name);
@@ -294,17 +299,20 @@ static IFileDescriptor *createFileDescriptor(const char* dir, const char* name,
         fdesc->setPart(k,node,s.str(),pp);
     }
     fdesc->queryProperties().setPropInt("@recordSize",recSize);
+    fdesc->setDefaultDir(dir);
     return fdesc.getClear();
 }
 
 static bool setupDFS(const char *scope, unsigned supersToDel=3, unsigned subsToCreate=4)
 {
-    StringBuffer buf;
-    buf.append("regress::").append(scope);
+    StringBuffer bufScope;
+    bufScope.append("regress::").append(scope);
+    StringBuffer bufDir;
+    bufDir.append("regress/").append(scope);
 
-    printf("Cleaning up '%s' scope\n", buf.str());
+    printf("Cleaning up '%s' scope\n", bufScope.str());
     for (unsigned i=1; i<=supersToDel; i++) {
-        StringBuffer super = buf;
+        StringBuffer super = bufScope;
         super.append("::super").append(i);
         if (dir.exists(super.str(),user,false,true) && !dir.removeEntry(super.str(), user)) {
             ERROR1("Can't remove %s", super.str());
@@ -316,21 +324,35 @@ static bool setupDFS(const char *scope, unsigned supersToDel=3, unsigned subsToC
     for (unsigned i=1; i<=subsToCreate; i++) {
         StringBuffer name;
         name.append("sub").append(i);
-        StringBuffer sub = buf;
+        StringBuffer sub = bufScope;
         sub.append("::").append(name);
 
         // Remove first
-        if (dir.exists(sub.str(),user,true,false) && !dir.removeEntry(sub.str(), user)) {
+        if (dir.exists(sub.str(),user,true,false) && !dir.removePhysical(sub.str(), user, NULL, NULL)) {
             ERROR1("Can't remove %s", sub.str());
             return false;
         }
 
-        // Create the sub file with an arbitrary format
-        Owned<IFileDescriptor> subd = createFileDescriptor(scope, name, 3, 17);
-        Owned<IDistributedFile> dsub = dir.createNew(subd);
-        dsub->attach(sub.str(),user);
-        subd.clear();
-        dsub.clear();
+        {
+            // Create the sub file with an arbitrary format
+            Owned<IFileDescriptor> subd = createFileDescriptor(bufDir.str(), name.str(), 1, 17);
+            Owned<IPartDescriptor> partd = subd->queryPart(0);
+            RemoteFilename rfn;
+            partd->getFilename(0, rfn);
+            StringBuffer fname;
+            rfn.getPath(fname);
+            if (!recursiveCreateDirectoryForFile(fname.str())) {
+                ERROR1("Can't create parent dir for %s", fname.str());
+                return false;
+            }
+            OwnedIFile ifile = createIFile(fname.str());
+            Owned<IFileIO> io;
+            io.setown(ifile->open(IFOcreate));
+            io->write(0, 17, "12345678901234567");
+            io->close();
+            Owned<IDistributedFile> dsub = dir.createNew(subd, sub.str());
+            dsub->attach(sub.str(),user);
+        }
 
         // Make sure it got created
         if (!dir.exists(sub.str(),user,true,false)) {
@@ -886,8 +908,9 @@ static void testDFSDel()
             ERROR("Could remove sub, this will make the DFS inconsistent!");
             return;
         }
-    } catch (IException *) {
+    } catch (IException *e) {
         // expecting an exception
+        e->Release();
     }
 
     printf("Deleting 'regress::del::super1, should work\n");
@@ -902,6 +925,72 @@ static void testDFSDel()
     }
 }
 
+static void testDFSRename()
+{
+    Owned<IDistributedFileTransaction> transaction = createDistributedFileTransaction(user); // disabled, auto-commit
+
+    if (dir.exists("regress::rename::other1",user,false,false) && !dir.removePhysical("regress::rename::other1", user, NULL, NULL)) {
+        ERROR("Can't remove 'regress::rename::other1'");
+        return;
+    }
+    if (dir.exists("regress::rename::other2",user,false,false) && !dir.removePhysical("regress::rename::other2", user, NULL, NULL)) {
+        ERROR("Can't remove 'regress::rename::other2'");
+        return;
+    }
+
+    if (!setupDFS("rename"))
+        return;
+
+    try {
+        printf("Renaming 'regress::rename::sub1 to 'sub2' with auto-commit, should fail\n");
+        dir.renamePhysical("regress::rename::sub1", "regress::rename::sub2", user, transaction);
+        ERROR("Renamed to existing file should have failed!");
+        return;
+    } catch (IException *e) {
+        // Expecting exception
+        e->Release();
+    }
+
+    printf("Renaming 'regress::rename::sub1 to 'other1' with auto-commit\n");
+    dir.renamePhysical("regress::rename::sub1", "regress::rename::other1", user, transaction);
+    if (!dir.exists("regress::rename::other1", user, true, false))
+    {
+        ERROR("Renamed to other failed");
+        return;
+    }
+
+    printf("Renaming 'regress::rename::sub2 to 'other2' and rollback\n");
+    transaction->start();
+    dir.renamePhysical("regress::rename::sub2", "regress::rename::other2", user, transaction);
+    transaction->rollback();
+    if (dir.exists("regress::rename::other2", user, true, false))
+    {
+        ERROR("Renamed to other2 when it shouldn't");
+        return;
+    }
+
+    printf("Renaming 'regress::rename::sub2 to 'other2' and commit\n");
+    transaction->start();
+    dir.renamePhysical("regress::rename::sub2", "regress::rename::other2", user, transaction);
+    transaction->commit();
+    if (!dir.exists("regress::rename::other2", user, true, false))
+    {
+        ERROR("Renamed to other failed");
+        return;
+    }
+
+    try {
+        printf("Renaming 'regress::rename::sub3 to 'sub3' with auto-commit, should fail\n");
+        dir.renamePhysical("regress::rename::sub3", "regress::rename::sub3", user, transaction);
+        ERROR("Renamed to same file should have failed!");
+        return;
+    } catch (IException *e) {
+        // Expecting exception
+        e->Release();
+    }
+
+}
+
 // ======================================================================= Test Engine
 
 struct TestArray {
@@ -959,6 +1048,7 @@ void initTests() {
     registerTest("DFS transaction", testDFSTrans);
     registerTest("DFS promote super file", testDFSPromote);
     registerTest("DFS subdel", testDFSDel);
+    registerTest("DFS rename", testDFSRename);
     registerTest("SDS subscriptions", testSDSSubs);
 }
 

+ 10 - 12
plugins/fileservices/fileservices.cpp

@@ -459,24 +459,22 @@ FILESERVICES_API void FILESERVICES_CALL fsRenameLogicalFile(ICodeContext *ctx, c
     constructLogicalName(ctx, oldname, lfn);
     constructLogicalName(ctx, newname, nlfn);
 
+    IDistributedFileTransaction *transaction = ctx->querySuperFileTransaction();
     Linked<IUserDescriptor> udesc = ctx->queryUserDescriptor();
-    Owned<IMultiException> exceptions = MakeMultiException();
-
-    if (queryDistributedFileDirectory().renamePhysical(lfn.str(),nlfn.str(),udesc,exceptions)) {
+    try {
+        queryDistributedFileDirectory().renamePhysical(lfn.str(),nlfn.str(),udesc,transaction);
         StringBuffer s("RenameLogicalFile ('");
         s.append(lfn).append(", '").append(nlfn).append("') done");
         WUmessage(ctx,ExceptionSeverityInformation,NULL,s.str());
         AuditMessage(ctx,"RenameLogicalFile",lfn.str(),nlfn.str());
     }
-    else { // failed
-        unsigned n = exceptions->ordinality();
-        for (unsigned i=0;i<n;i++) {
-            StringBuffer s;
-            exceptions->item(i).errorMessage(s);
-            WUmessage(ctx,ExceptionSeverityWarning,"RenameLogicalFile",s.str());
-        }
-        throw MakeStringException(0, "Could not rename logical file %s to %s", lfn.str(), nlfn.str());
-    }
+    catch (IException *e)
+    {
+        StringBuffer s;
+        e->errorMessage(s);
+        WUmessage(ctx,ExceptionSeverityWarning,"RenameLogicalFile",s.str());
+        throw e;
+     }
 }
 
 

+ 34 - 0
testing/ecl/key/superfile9.xml

@@ -0,0 +1,34 @@
+<Dataset name='Result 1'>
+</Dataset>
+<Dataset name='Result 2'>
+</Dataset>
+<Dataset name='Result 3'>
+ <Row><Result_3>true</Result_3></Row>
+</Dataset>
+<Dataset name='Result 4'>
+ <Row><Result_4>true</Result_4></Row>
+</Dataset>
+<Dataset name='Result 5'>
+ <Row><Result_5>false</Result_5></Row>
+</Dataset>
+<Dataset name='Result 6'>
+ <Row><Result_6>false</Result_6></Row>
+</Dataset>
+<Dataset name='Result 7'>
+ <Row><Result_7>false</Result_7></Row>
+</Dataset>
+<Dataset name='Result 8'>
+ <Row><Result_8>true</Result_8></Row>
+</Dataset>
+<Dataset name='Result 9'>
+ <Row><Result_9>true</Result_9></Row>
+</Dataset>
+<Dataset name='Result 10'>
+ <Row><Result_10>false</Result_10></Row>
+</Dataset>
+<Dataset name='Result 11'>
+ <Row><Result_11>false</Result_11></Row>
+</Dataset>
+<Dataset name='Result 12'>
+ <Row><Result_12>true</Result_12></Row>
+</Dataset>

+ 71 - 0
testing/ecl/superfile9.ecl

@@ -0,0 +1,71 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+import Std.System.Thorlib;
+import Std.File AS FileServices;
+import Std.Str;
+// Super File regression test
+//noRoxie
+
+rec :=
+RECORD
+        integer i;
+    string1 id;
+END;
+
+ds1 := DATASET([{1,'A'}, {1,'B'}, {1,'C'}], rec);
+ds2 := DATASET([{1,'A'}, {1,'B'}, {1,'C'}], rec);
+
+clusterLFNPrefix := thorlib.getExpandLogicalName('regress::');
+
+string stripPrefix(string qlfn) := IF (Str.Find(qlfn, clusterLFNprefix, 1) = 1, Str.FindReplace(qlfn, clusterLFNPrefix, ''), qlfn);
+
+conditionalDelete(string lfn) := FUNCTION
+        RETURN IF(FileServices.FileExists(lfn), FileServices.DeleteLogicalFile(lfn));
+END;
+
+
+SEQUENTIAL(
+  // Prepare
+  conditionalDelete ('regress::subfile12'),
+  conditionalDelete ('regress::subfile13'),
+  OUTPUT(ds1,,'regress::subfile10',overwrite),
+  OUTPUT(ds2,,'regress::subfile11',overwrite),
+  OUTPUT(FileServices.FileExists('regress::subfile10')), // true
+  OUTPUT(FileServices.FileExists('regress::subfile11')), // true
+  OUTPUT(FileServices.FileExists('regress::subfile12')), // false
+  OUTPUT(FileServices.FileExists('regress::subfile13')), // false
+
+  // Rename Auto-commit
+  FileServices.RenameLogicalFile('regress::subfile10','regress::subfile12'),
+  OUTPUT(FileServices.FileExists('regress::subfile10')), // false
+  OUTPUT(FileServices.FileExists('regress::subfile12')), // true
+
+  // Rename + Rollback
+  FileServices.StartSuperFileTransaction(),
+  FileServices.RenameLogicalFile('regress::subfile11','regress::subfile13'),
+  FileServices.FinishSuperFileTransaction(true),    // rollback
+  OUTPUT(FileServices.FileExists('regress::subfile11')), // true
+  OUTPUT(FileServices.FileExists('regress::subfile13')), // false
+
+  // Rename + Commit
+  FileServices.StartSuperFileTransaction(),
+  FileServices.RenameLogicalFile('regress::subfile11','regress::subfile13'),
+  FileServices.FinishSuperFileTransaction(),    // commit
+  OUTPUT(FileServices.FileExists('regress::subfile11')), // false
+  OUTPUT(FileServices.FileExists('regress::subfile13')), // true
+);