# Copyright (c) 2008-2012 Nuxeo SA (http://nuxeo.com/) and others.
#
# All rights reserved. This program and the accompanying materials
# are made available under the terms of the Eclipse Public License v1.0
# which accompanies this distribution, and is available at
# http://www.eclipse.org/legal/epl-v10.html
#
# Contributors:
#     Florent Guillaume
#     Benoit Delbosc

# Variables used:
# ${idType} NVARCHAR(36)
# ${md5HashString}
#   for SQL Server 2005:
#     SUBSTRING(master.dbo.fn_varbintohexstr(HashBytes('MD5', @string)), 3, 32)
#   for SQL Server 2008/2012/Azure:
#     SUBSTRING(CONVERT(VARCHAR(34), HashBytes('MD5', @string), 1), 3, 32)
# ${reseedAclrModified}
#   when not Azure: DBCC CHECKIDENT('aclr_modified', RESEED, 0);
# ${fulltextCatalog} configured catalog
# ${idSequenceName}
# ${readPermissions} SELECT 'Browse'  UNION ALL   SELECT 'Read'  UNION ALL   SELECT 'ReadProperties'  UNION ALL   SELECT 'ReadRemove'  UNION ALL   SELECT 'ReadWrite'  UNION ALL   SELECT 'Everything' ;
# ${usersSeparator} Used to store aclr default is "|", but it depends on the configuration. Note that array parameters always use "|" separator.
# Conditions used:
# sequenceEnabled
# fulltextEnabled
# fulltextSearchEnabled
# aclOptimizationsEnabled
# pathOptimizationsEnabled
# softDeleteEnabled
# proxiesEnabled
# lockEscalationDisabled true if not SQL Server 2005

############################################################


#CATEGORY: beforeTableCreation


#IF: sequenceEnabled
#TEST:
SELECT 1 FROM sys.sequences WHERE name = '${idSequenceName}';


#IF: sequenceEnabled
#IF: emptyResult
CREATE SEQUENCE ${idSequenceName} START WITH 0;


#IF: fulltextSearchEnabled
#TEST:
SELECT name FROM sys.fulltext_catalogs WHERE name = '${fulltextCatalog}'

#IF: fulltextSearchEnabled
#IF: emptyResult
CREATE FULLTEXT CATALOG [${fulltextCatalog}];


############################################################


#CATEGORY: afterTableCreation


#IF: proxiesEnabled
#PROC: dbo.nxTrigCascadeDelete
CREATE TRIGGER nxTrigCascadeDelete ON [hierarchy]
INSTEAD OF DELETE AS
BEGIN
  SET NOCOUNT ON;
  -- proxies.targetid -> hierarchy.id
  WITH subtree(id, parentid) AS (
    SELECT id, parentid
    FROM deleted
  UNION ALL
    SELECT h.id, h.parentid
    FROM [hierarchy] h
    JOIN subtree ON subtree.id = h.parentid
  )
  DELETE FROM [proxies]
    FROM [proxies] p
    JOIN subtree
    ON subtree.id = p.targetid;
  -- hierarchy.parentid -> hierarchy.id
  WITH subtree(id, parentid) AS (
    SELECT id, parentid
    FROM deleted
  UNION ALL
    SELECT h.id, h.parentid
    FROM [hierarchy] h
    JOIN subtree ON subtree.id = h.parentid
  )
  DELETE FROM [hierarchy]
    FROM [hierarchy] h
    JOIN subtree
    ON subtree.id = h.id;
END;

#IF: ! proxiesEnabled
#PROC: dbo.nxTrigCascadeDelete
CREATE TRIGGER nxTrigCascadeDelete ON [hierarchy]
INSTEAD OF DELETE AS
BEGIN
  SET NOCOUNT ON;
  -- hierarchy.parentid -> hierarchy.id
  WITH subtree(id, parentid) AS (
    SELECT id, parentid
    FROM deleted
  UNION ALL
    SELECT h.id, h.parentid
    FROM [hierarchy] h
    JOIN subtree ON subtree.id = h.parentid
  )
  DELETE FROM [hierarchy]
    FROM [hierarchy] h
    JOIN subtree
    ON subtree.id = h.id;
END;


#PROC: dbo.NX_ACCESS_ALLOWED
CREATE FUNCTION NX_ACCESS_ALLOWED(@id ${idType}, @users NVARCHAR(4000), @perms NVARCHAR(4000))
RETURNS TINYINT AS
BEGIN
  DECLARE @allusers NVARCHAR(4000);
  DECLARE @allperms NVARCHAR(4000);
  DECLARE @first TINYINT;
  DECLARE @curid ${idType};
  DECLARE @newid ${idType};
  DECLARE @gr TINYINT;
  DECLARE @pe NVARCHAR(1000);
  DECLARE @us NVARCHAR(1000);
  SET @allusers = N'|' + @users + N'|';
  SET @allperms = N'|' + @perms + N'|';
  SET @first = 1;
  SET @curid = @id;
  WHILE @curid IS NOT NULL BEGIN
    DECLARE @cur CURSOR;
    SET @cur = CURSOR FAST_FORWARD FOR
      SELECT [grant], [permission], [user] FROM [acls]
      WHERE [id] = @curid ORDER BY [pos];
    OPEN @cur;
    FETCH FROM @cur INTO @gr, @pe, @us;
    WHILE @@FETCH_STATUS = 0 BEGIN
      IF @allusers LIKE (N'%|' + @us + N'|%') AND @allperms LIKE (N'%|' + @pe + N'|%')
      BEGIN
        CLOSE @cur;
        DEALLOCATE @cur;
        RETURN @gr;
      END;
      FETCH FROM @cur INTO @gr, @pe, @us;
    END;
    CLOSE @cur;
    DEALLOCATE @cur;
    SET @newid = (SELECT [parentid] FROM [hierarchy] WHERE [id] = @curid);
    IF @first = 1 AND @newid IS NULL BEGIN
      SET @newid = (SELECT [versionableid] FROM [versions] WHERE [id] = @curid);
    END;
    SET @first = 0;
    SET @curid = @newid;
  END;
  RETURN 0;
END;


#-- Deprecated since 5.9.5, see nx_children
#PROC: dbo.NX_IN_TREE
CREATE FUNCTION NX_IN_TREE(@id ${idType}, @baseid ${idType})
RETURNS TINYINT AS
BEGIN
  DECLARE @curid ${idType};
  IF @baseid IS NULL OR @id IS NULL OR @baseid = @id RETURN 0;
  SET @curid = @id;
  WHILE @curid IS NOT NULL BEGIN
    SET @curid = (SELECT [parentid] FROM [hierarchy] WHERE [id] = @curid);
    IF @curid = @baseid RETURN 1;
  END;
  RETURN 0;
END;


#IF: clusteringEnabled
#PROC: dbo.NX_CLUSTER_INVAL
CREATE PROCEDURE NX_CLUSTER_INVAL(@n SMALLINT, @i ${idType}, @f NVARCHAR(4000), @k TINYINT)
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @nid SMALLINT;
  DECLARE @cur CURSOR;
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT [nodeid] FROM [cluster_nodes] WHERE [nodeid] <> @n;
  OPEN @cur;
  FETCH FROM @cur INTO @nid;
  WHILE @@FETCH_STATUS = 0 BEGIN
    INSERT INTO [cluster_invals] ([nodeid], [id], [fragments], [kind]) VALUES (@nid, @i, @f, @k);
    FETCH FROM @cur INTO @nid;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
END;


# ------------------------------------------------------------
# -- Path management using CTE
# -- @since 5.9.5
#PROC: dbo.nx_children
CREATE FUNCTION nx_children(@root ${idType})
RETURNS TABLE AS RETURN
  WITH children (id) AS (
    SELECT id FROM hierarchy
      WHERE parentid = @root AND isproperty = 0
    UNION ALL
    SELECT h.id FROM hierarchy AS h
      JOIN children AS c ON (h.parentid = c.id)
      WHERE h.isproperty = 0
  ) SELECT id FROM children;


# ##### soft delete #####


#IF: softDeleteEnabled
LOG.INFO Soft delete enabled


#IF: softDeleteEnabled
#IF: proxiesEnabled
#PROC: dbo.NX_DELETE
CREATE PROCEDURE NX_DELETE(@ids NVARCHAR(4000), @nowTime DATETIME)
AS
-- Marks the given ids as deleted at the given time (null means now)
-- Simulates foreign keys except for the parent-child one which is done in Java
BEGIN
  SET NOCOUNT ON;
  DECLARE @pos INT;
  DECLARE @next INT;
  DECLARE @end INT;
  DECLARE @id ${idType};
  DECLARE @idtable TABLE (id ${idType});
  -- split @ids into @idtable
  SET @pos = 0;
  SET @next = 1;
  WHILE @next > 0 BEGIN
    SET @next = CHARINDEX('|', @ids, @pos + 1);
    SET @end = CASE WHEN @next > 0 THEN @next ELSE len(@ids) + 1 END;
    SET @id = SUBSTRING(@ids, @pos + 1, @end - @pos - 1);
    SET @pos = @next;
    INSERT INTO @idtable (id) VALUES (@id);
  END;
  IF @nowTime IS NULL BEGIN
    SET @nowTime = CURRENT_TIMESTAMP;
  END;
  -- soft delete
  UPDATE h
    SET h.isdeleted = 1, h.deletedtime = @nowTime
    FROM hierarchy h
    JOIN @idtable ids ON h.id = ids.id;
  -- do hard delete for foreign key proxies.targetid
  DELETE p
    FROM proxies p
    JOIN @idtable ids ON p.targetid = ids.id;
END;

#IF: softDeleteEnabled
#IF: ! proxiesEnabled
#PROC: dbo.NX_DELETE
CREATE PROCEDURE NX_DELETE(@ids NVARCHAR(4000), @nowTime DATETIME)
AS
-- Marks the given ids as deleted at the given time (null means now)
BEGIN
  SET NOCOUNT ON;
  DECLARE @pos INT;
  DECLARE @next INT;
  DECLARE @end INT;
  DECLARE @id ${idType};
  DECLARE @idtable TABLE (id ${idType});
  -- split @ids into @idtable
  SET @pos = 0;
  SET @next = 1;
  WHILE @next > 0 BEGIN
    SET @next = CHARINDEX('|', @ids, @pos + 1);
    SET @end = CASE WHEN @next > 0 THEN @next ELSE len(@ids) + 1 END;
    SET @id = SUBSTRING(@ids, @pos + 1, @end - @pos - 1);
    SET @pos = @next;
    INSERT INTO @idtable (id) VALUES (@id);
  END;
  IF @nowTime IS NULL BEGIN
    SET @nowTime = CURRENT_TIMESTAMP;
  END;
  -- soft delete
  UPDATE h
    SET h.isdeleted = 1, h.deletedtime = @nowTime
    FROM hierarchy h
    JOIN @idtable ids ON h.id = ids.id;
END;


#IF: softDeleteEnabled
#PROC: dbo.NX_DELETE_PURGE
CREATE PROCEDURE NX_DELETE_PURGE(@max INT, @beforeTime DATETIME)
AS
-- Does hard delete on soft-deleted rows earlier than beforeTime (null means all).
-- A maximum number of rows to delete can be provided (null means no limit).
-- Returns the number of rows actually deleted.
-- Rows are deleted leaves first.
BEGIN
  SET NOCOUNT ON;
  DECLARE @ndel INT;
  DECLARE @total INT;
  SET @total = 0;
  IF @beforeTime IS NULL BEGIN
    SET @beforeTime = DATEADD(day, 1, CURRENT_TIMESTAMP);
  END;
  IF @max = 0 BEGIN
    SET @max = NULL;
  END;
  WHILE 1=1 BEGIN
    -- delete some leaves in the tree of soft-deleted documents
    IF @max IS NULL
      DELETE FROM hierarchy
        WHERE isdeleted = 1 AND deletedtime < @beforeTime
        AND id NOT IN (
          -- not leaves: deleted nodes that have deleted children
          SELECT DISTINCT hpar.id FROM hierarchy hpar
            JOIN hierarchy h ON h.parentid = hpar.id
            WHERE hpar.isdeleted = 1 AND h.isdeleted = 1)
    ELSE
      DELETE FROM hierarchy WHERE id IN (
        SELECT TOP(@max) id FROM hierarchy
        WHERE isdeleted = 1 AND deletedtime < @beforeTime
        AND id NOT IN (
          -- not leaves: deleted nodes that have deleted children
          SELECT DISTINCT hpar.id FROM hierarchy hpar
            JOIN hierarchy h ON h.parentid = hpar.id
            WHERE hpar.isdeleted = 1 AND h.isdeleted = 1))
    ;
    SET @ndel = @@ROWCOUNT;
    IF @ndel = 0 BREAK;
    SET @total = @total + @ndel;
    IF @total >= @max BREAK;     -- no exit when @max = NULL
  END;
  RETURN @total;
END;


# ------------------------------------------------------------
# -- PATH OPTIMIZATIONS

# ------------------------------------------------------------
# -- ancestors table
# -- ancestors field contains the list of parents from root to parent
#TEST:
#SET_IF_EMPTY: create_table_ancestors
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ancestors' AND TABLE_CATALOG = db_name();

#IF: create_table_ancestors
CREATE TABLE ancestors (
  hierarchy_id ${idType} NOT NULL,
  ancestor ${idType},
  CONSTRAINT ancestors_hierarchy_id_fk FOREIGN KEY (hierarchy_id) REFERENCES hierarchy (id) ON DELETE CASCADE
);


#IF: pathOptimizationsEnabled
#IF: ! create_table_ancestors
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'ancestors_hierarchy_id_idx'

#IF: pathOptimizationsEnabled
#IF: create_table_ancestors OR: emptyResult
CREATE CLUSTERED INDEX [ancestors_hierarchy_id_idx] ON [dbo].[ancestors] ([hierarchy_id])


#IF: pathOptimizationsEnabled
#IF: ! create_table_ancestors
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'ancestors_ancestor_idx'

#IF: pathOptimizationsEnabled
#IF: create_table_ancestors OR: emptyResult
CREATE INDEX [ancestors_ancestor_idx] ON [dbo].[ancestors] ([ancestor])


#PROC: dbo.nx_get_ancestors
CREATE FUNCTION nx_get_ancestors(@id ${idType})
RETURNS @output TABLE (ancestor_id ${idType}) AS
  -- List ancestors of a document
BEGIN
  DECLARE @first TINYINT;
  DECLARE @curid ${idType};
  DECLARE @newid ${idType};
  SET @first = 1;
  SET @curid = @id;
  WHILE @curid IS NOT NULL BEGIN
    SET @newid = (SELECT [parentid] FROM [hierarchy] WHERE [id] = @curid);
    IF @curid IS NOT NULL AND @curid <> @id BEGIN
      INSERT INTO @output (ancestor_id) VALUES (@curid);
    END;
    IF @first = 1 AND @newid IS NULL BEGIN
      SET @newid = (SELECT [versionableid] FROM [versions] WHERE [id] = @curid);
    END;
    SET @first = 0;
    SET @curid = @newid;
  END;
  RETURN;
END;

#PROC: dbo.nx_init_ancestors
CREATE PROCEDURE nx_init_ancestors AS
BEGIN
  SET NOCOUNT ON;
  TRUNCATE TABLE ancestors;
  DECLARE @cur CURSOR;
  DECLARE @id ${idType};
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT id FROM hierarchy WHERE isproperty=0;
  OPEN @cur;
  FETCH FROM @cur INTO @id;
  WHILE @@FETCH_STATUS = 0 BEGIN
    INSERT INTO ancestors (hierarchy_id, ancestor)
      SELECT @id, ancestor_id FROM dbo.nx_get_ancestors(@id);
    FETCH FROM @cur INTO @id;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
END;


#PROC: dbo.nx_trig_ancestor_insert
CREATE TRIGGER nx_trig_ancestor_insert ON [hierarchy]
AFTER INSERT AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @cur CURSOR;
  DECLARE @id ${idType};
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT id FROM inserted WHERE isproperty = 0;
  OPEN @cur;
  FETCH FROM @cur INTO @id;
  WHILE @@FETCH_STATUS = 0 BEGIN
    INSERT INTO ancestors (hierarchy_id, ancestor)
      SELECT @id, ancestor_id FROM dbo.nx_get_ancestors(@id);
    FETCH FROM @cur INTO @id;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
END;


#IF: ! pathOptimizationsEnabled
ALTER TABLE [hierarchy] DISABLE TRIGGER nx_trig_ancestor_insert


#PROC: dbo.nx_trig_ancestor_update
CREATE TRIGGER nx_trig_ancestor_update ON [hierarchy]
AFTER UPDATE AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @cur CURSOR;
  DECLARE @cur2 CURSOR;
  DECLARE @id ${idType};
  DECLARE @sid ${idType};
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT OLD.id FROM deleted OLD
    JOIN [inserted] NEW ON OLD.id = NEW.id
    WHERE NEW.isproperty = 0 AND OLD.parentid <> NEW.parentid;
  OPEN @cur;
  FETCH FROM @cur INTO @id;
  WHILE @@FETCH_STATUS = 0 BEGIN
    SET @cur2 = CURSOR FAST_FORWARD FOR
      -- distinct is required because the table is updated between fetches
      SELECT DISTINCT(hierarchy_id) FROM ancestors WHERE ancestor = @id;
    OPEN @cur2;
    FETCH FROM @cur2 INTO @sid;
    WHILE @@FETCH_STATUS = 0 BEGIN
      -- delete ancestors
      DELETE FROM ancestors WHERE hierarchy_id = @sid;
      -- insert new one
      INSERT INTO ancestors (hierarchy_id, ancestor)
        SELECT @sid, ancestor_id FROM dbo.nx_get_ancestors(@sid);
      FETCH FROM @cur2 INTO @sid;
    END;
    CLOSE @cur2;
    DEALLOCATE @cur2;
    DELETE FROM ancestors WHERE hierarchy_id = @id;
    INSERT INTO ancestors (hierarchy_id, ancestor)
      SELECT @id, ancestor_id FROM dbo.nx_get_ancestors(@id);
    FETCH FROM @cur INTO @id;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
END;


#IF: ! pathOptimizationsEnabled
ALTER TABLE [hierarchy] DISABLE TRIGGER nx_trig_ancestor_update


# Init ancestors table if just-created or empty

#IF: pathOptimizationsEnabled
#IF: create_table_ancestors
EXEC nx_init_ancestors

#IF: pathOptimizationsEnabled
#IF: ! create_table_ancestors
#TEST:
SELECT TOP 1 hierarchy_id FROM ancestors

#IF: pathOptimizationsEnabled
#IF: create_table_ancestors OR: emptyResult
EXEC nx_init_ancestors


# Clear ancestors table if no optims, in case we re-activate optims later

#IF: ! pathOptimizationsEnabled
#TEST:
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'ancestors' AND TABLE_CATALOG = db_name();

#IF: ! pathOptimizationsEnabled
#IF: ! emptyResult
#TEST:
SELECT TOP 1 hierarchy_id FROM ancestors

#IF: ! pathOptimizationsEnabled
#IF: ! emptyResult
TRUNCATE TABLE ancestors;


# ancestors ids (since Nuxeo 5.5)

#IF: pathOptimizationsEnabled
#PROC: dbo.NX_ANCESTORS
CREATE FUNCTION NX_ANCESTORS(@ids NVARCHAR(4000))
RETURNS @output TABLE (id ${idType}) AS
BEGIN
  DECLARE @pos INT;
  DECLARE @next INT;
  DECLARE @end INT;
  DECLARE @id ${idType};
  SET @pos = 0;
  SET @next = 1;
  WHILE @next > 0 BEGIN
    SET @next = CHARINDEX('|', @ids, @pos + 1);
    SET @end = CASE WHEN @next > 0 THEN @next ELSE len(@ids) + 1 END;
    SET @id = SUBSTRING(@ids, @pos + 1, @end - @pos - 1);
    SET @pos = @next;
    --
    INSERT INTO @output (id) SELECT [ancestor] FROM [ancestors] WHERE [hierarchy_id] = @id;
  END;
  RETURN;
END;

#IF: !pathOptimizationsEnabled
#PROC: dbo.NX_ANCESTORS
CREATE FUNCTION NX_ANCESTORS(@ids NVARCHAR(4000))
RETURNS @output TABLE (id ${idType}) AS
BEGIN
  DECLARE @pos INT;
  DECLARE @next INT;
  DECLARE @end INT;
  DECLARE @id ${idType};
  SET @pos = 0;
  SET @next = 1;
  WHILE @next > 0 BEGIN
    SET @next = CHARINDEX('|', @ids, @pos + 1);
    SET @end = CASE WHEN @next > 0 THEN @next ELSE len(@ids) + 1 END;
    SET @id = SUBSTRING(@ids, @pos + 1, @end - @pos - 1);
    SET @pos = @next;
    --
    WHILE @id IS NOT NULL BEGIN
      SET @id = (SELECT [parentid] FROM [hierarchy] WHERE [id] = @id);
      IF @id IS NOT NULL BEGIN
        INSERT INTO @output (id) VALUES (@id);
      END;
    END;
  END;
  RETURN;
END;


# ------------------------------------------------------------
# -- ACLR (aka READ ACL) OPTIMIZATIONS

# ------------------------------------------------------------
# -- Read acls table
# -- acl ex: jsmith,administrators,-Everyone
# -- acl_id = md5(acl)
#TEST:
#SET_IF_EMPTY: create_table_aclr
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'aclr' AND TABLE_CATALOG = db_name();

#IF: create_table_aclr
CREATE TABLE aclr (
  acl_id CHAR(32) NOT NULL,
  acl NVARCHAR(4000)
)

#IF: create_table_aclr
#IF: lockEscalationDisabled
ALTER TABLE aclr SET (LOCK_ESCALATION=DISABLE);


#IF: ! create_table_aclr
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'aclr_acl_id_idx'

#IF: create_table_aclr OR: emptyResult
CREATE CLUSTERED INDEX [aclr_acl_id_idx] ON [dbo].[aclr] ([acl_id])


# -- Known users table
# -- principals ex: {members,jsmith,Everyone}
# -- user_id = md5(principals)
#TEST:
#SET_IF_EMPTY: create_table_aclr_user
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'aclr_user' AND TABLE_CATALOG = db_name();

#IF: create_table_aclr_user
CREATE TABLE aclr_user (
  user_id CHAR(32) NOT NULL,
  principals NVARCHAR(4000),
  CONSTRAINT user_id_unique UNIQUE(user_id)
)

# -- add unique constraint if missing (upgrade from < 5.9.3)
#IF: ! create_table_aclr_user
#TEST:
SELECT 1 WHERE OBJECT_ID('dbo.user_id_unique', 'UQ') IS NOT NULL

#IF: ! create_table_aclr_user
#IF: emptyResult
ALTER TABLE aclr_user ADD CONSTRAINT user_id_unique UNIQUE(user_id);


# -- drop index not needed anymore due to above constraint's implicit index
#IF: ! create_table_aclr_user
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'aclr_user_user_id_idx'

#IF: ! create_table_aclr_user
#IF: ! emptyResult
DROP INDEX [aclr_user_user_id_idx] ON [dbo].[aclr_user]


# -- Jonction between aclr and aclr_user
#TEST:
#SET_IF_EMPTY: create_table_aclr_user_map
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME =  'aclr_user_map' AND TABLE_CATALOG = db_name();

#IF: create_table_aclr_user_map
CREATE TABLE aclr_user_map (
  user_id CHAR(32) NOT NULL,
  acl_id CHAR(32) NOT NULL,
  CONSTRAINT aclrum_unique UNIQUE(user_id, acl_id)
)

# -- add unique constraint if missing (upgrade from < 5.9.3)
#IF: ! create_table_aclr_user_map
#TEST:
SELECT 1 WHERE OBJECT_ID('dbo.aclrum_unique', 'UQ') IS NOT NULL

#IF: ! create_table_aclr_user_map
#IF: emptyResult
ALTER TABLE aclr_user_map ADD CONSTRAINT aclrum_unique UNIQUE(user_id, acl_id);

# -- drop index not needed anymore due to above constraint's implicit index
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'aclr_user_map_user_id_idx'

#IF: ! emptyResult
DROP INDEX [aclr_user_map_user_id_idx] ON [dbo].[aclr_user_map]

# -- drop index not needed anymore due to above constraint's implicit index
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'aclr_user_map_acl_id_idx'

#IF: ! emptyResult
DROP INDEX [aclr_user_map_acl_id_idx] ON [dbo].[aclr_user_map]


# -- Associate a read acl for each hierarchy entry
#TEST:
#SET_IF_EMPTY: create_table_hierarchy_read_acl
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME =  'hierarchy_read_acl' AND TABLE_CATALOG = db_name();

#IF: create_table_hierarchy_read_acl
CREATE TABLE hierarchy_read_acl (
  _oid INTEGER IDENTITY NOT NULL,
  id ${idType} NOT NULL, -- doc id
  acl_id CHAR(32),      -- acl id in ACLR
  CONSTRAINT hierarchy_read_acl_id_fk FOREIGN KEY (id) REFERENCES hierarchy (id) ON DELETE CASCADE
)

#IF: create_table_hierarchy_read_acl
#IF: lockEscalationDisabled
ALTER TABLE hierarchy_read_acl SET (LOCK_ESCALATION=DISABLE);

#IF: create_table_hierarchy_read_acl
CREATE CLUSTERED INDEX [hierarchy_read_acl_oid_idx] ON [dbo].[hierarchy_read_acl] ([_oid])


# Add a new _oid clustered column if not present
#IF: ! create_table_hierarchy_read_acl
#TEST:
#SET_IF_EMPTY: needOid
SELECT 1 FROM sys.columns WHERE [name] = N'_oid' AND [object_id] = OBJECT_ID(N'hierarchy_read_acl')

#IF: needOid
ALTER TABLE [hierarchy_read_acl] DROP CONSTRAINT [hierarchy_read_acl_id_fk]

#IF: needOid
DROP INDEX [hierarchy_read_acl_acl_id_idx] ON [dbo].[hierarchy_read_acl]

#IF: needOid
ALTER TABLE [hierarchy_read_acl] ADD [_oid] INTEGER NOT NULL IDENTITY

#IF: needOid
CREATE CLUSTERED INDEX [hierarchy_read_acl_oid_idx] ON [dbo].[hierarchy_read_acl] ([_oid])

#IF: needOid
ALTER TABLE [hierarchy_read_acl] ADD CONSTRAINT hierarchy_read_acl_id_fk FOREIGN KEY (id) REFERENCES hierarchy (id) ON DELETE CASCADE


# add index
#IF: ! create_table_hierarchy_read_acl
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'hierarchy_read_acl_acl_id_idx';

#IF: create_table_hierarchy_read_acl OR: emptyResult
CREATE INDEX [hierarchy_read_acl_acl_id_idx] ON [dbo].[hierarchy_read_acl] ([acl_id])


# -- Log modified document that require an aclr update
#TEST:
#SET_IF_EMPTY: create_table_aclr_modified
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'aclr_modified' AND TABLE_CATALOG = db_name();

#IF: create_table_aclr_modified
CREATE TABLE aclr_modified (
  id INT NOT NULL IDENTITY,
  spid INTEGER NOT NULL,
  hierarchy_id ${idType},
  is_new TINYINT
)

#IF: create_table_aclr_modified
CREATE CLUSTERED INDEX aclr_modified_spid_idx ON aclr_modified (spid)

#IF: create_table_aclr_modified
#IF: lockEscalationDisabled
ALTER TABLE aclr_modified SET (LOCK_ESCALATION=DISABLE);


#-- List of permission that grant the read access
#TEST:
#SET_IF_EMPTY: create_table_aclr_permission
SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME =  'aclr_permission' AND TABLE_CATALOG = db_name();

#IF: create_table_aclr_permission
CREATE TABLE aclr_permission (
  permission NVARCHAR(256)
)

# add index
#IF: ! create_table_aclr_permission
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'aclr_permission_idx';

#IF: create_table_aclr_permission OR: emptyResult
CREATE CLUSTERED INDEX [aclr_permission_idx] ON [dbo].[aclr_permission] ([permission])


# dump browse permissions into table
#IF: ! create_table_aclr_permission
#TEST:
SELECT 1 FROM aclr_permission

#IF: create_table_aclr_permission OR: emptyResult
INSERT INTO aclr_permission ${readPermissions};


# -- FUNCTIONS --------------------------------------------------

#PROC: dbo.nx_get_local_read_acl
CREATE FUNCTION nx_get_local_read_acl(@id ${idType})
RETURNS NVARCHAR(4000) AS
-- Compute the read acl for hierarchy id using a local acl
BEGIN
  DECLARE @curid ${idType};
  DECLARE @op NVARCHAR(250);
  DECLARE @read_acl NVARCHAR(4000);
  DECLARE @cur CURSOR;
  SET @curid = @id;
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT CASE
       WHEN ([grant] = 0) THEN N'-'
       WHEN ([grant] = 1) THEN N''
       ELSE NULL
      END + [user] AS op
    FROM acls
    WHERE [id] = @curid AND
      [permission] IN (SELECT [permission] FROM aclr_permission) AND
      ([status] IS NULL OR [status] = 1)
    ORDER BY [pos];
  OPEN @cur;
  FETCH FROM @cur INTO @op;
  WHILE @@FETCH_STATUS = 0 BEGIN
    IF @op IS NOT NULL BEGIN
      IF @read_acl IS NULL
        SET @read_acl = @op;
      ELSE
        SET @read_acl = @read_acl + N'${usersSeparator}' + @op;
    END;
    FETCH FROM @cur INTO @op;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
  RETURN @read_acl;
END;


#PROC: dbo.nx_get_read_acl
CREATE FUNCTION nx_get_read_acl(@id ${idType})
RETURNS NVARCHAR(4000) AS
-- Compute the merged read acl for a doc id
BEGIN
  DECLARE @curid ${idType};
  DECLARE @newid ${idType};
  DECLARE @read_acl NVARCHAR(4000);
  DECLARE @ret NVARCHAR(4000);
  DECLARE @first TINYINT;
  DECLARE @pos INT;
  SET @curid = @id;
  SET @first = 1;
  WHILE @curid IS NOT NULL BEGIN
    SET @read_acl = (SELECT dbo.nx_get_local_read_acl(@curid));
    IF @read_acl IS NOT NULL
      IF @ret IS NULL
        SET @ret = @read_acl;
      ELSE
        SET @ret = @ret + N'${usersSeparator}' + @read_acl;
    SET @newid = (SELECT parentid FROM hierarchy WHERE [id] = @curid);
    IF @first = 1 AND @newid IS NULL
      SET @newid = (SELECT versionableid FROM versions WHERE [id] = @curid)
    SET @first = 0;
    SET @curid = @newid;
    -- Stop on deny all
    SET @pos = CHARINDEX('-Everyone', @ret);
    IF @pos > 0
      SET @curid = NULL;
  END;
  IF @ret IS NULL
    SET @ret = N'_empty';
  RETURN @ret;
END;


#PROC: dbo.nx_md5
CREATE FUNCTION nx_md5(@string NVARCHAR(4000))
RETURNS CHAR(32) AS
BEGIN
  DECLARE @hash CHAR(32);
  SET @hash = (${md5HashString});
  RETURN @hash;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_get_read_permissions
CREATE FUNCTION nx_get_read_permissions()
RETURNS NVARCHAR(4000) AS
BEGIN
  -- build the list of read permissions
  DECLARE @ret NVARCHAR(4000);
  SET @ret = N'';
  DECLARE @cur CURSOR;
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT permission  FROM aclr_permission;
  OPEN @cur;
  DECLARE @perm NVARCHAR(250);
  FETCH FROM @cur INTO @perm;
  WHILE @@FETCH_STATUS = 0 BEGIN
    SET @ret = @ret + N'|' + @perm;
    FETCH FROM @cur INTO @perm;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
  RETURN @ret;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_get_read_acl_id
CREATE FUNCTION nx_get_read_acl_id(@id ${idType})
RETURNS CHAR(32) AS
BEGIN
  DECLARE @hash CHAR(32);
  SET @hash = dbo.nx_md5(dbo.nx_get_read_acl(@id));
  RETURN @hash;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_list_read_acls_for
CREATE FUNCTION nx_list_read_acls_for(@users NVARCHAR(4000))
RETURNS @output TABLE (acl_id CHAR(32)) AS
  -- List matching read acl ids for a list of user/groups
BEGIN
  DECLARE @permissions NVARCHAR(4000);
  SET @permissions = dbo.nx_get_read_permissions();
  INSERT INTO @output (acl_id)
    SELECT acl_id FROM hierarchy_read_acl r
    JOIN hierarchy h ON r.id = h.id
    -- do not use orphan version
    WHERE h.isversion IS NULL
    GROUP BY acl_id
    HAVING dbo.NX_ACCESS_ALLOWED(max(r.id), @users, @permissions) = 1;
  RETURN;
END;


# -- TRIGGERS --------------------------------------------------


#PROC: dbo.nx_trig_acls_modified
CREATE TRIGGER nx_trig_acls_modified ON [acls]
AFTER INSERT, UPDATE, DELETE AS
BEGIN
  SET NOCOUNT ON;
  INSERT INTO aclr_modified (hierarchy_id, is_new, spid)
    SELECT DISTINCT(id), 0, @@SPID FROM inserted;
  INSERT INTO aclr_modified (hierarchy_id, is_new, spid)
    SELECT DISTINCT(id), 0, @@SPID FROM deleted;
END;

#IF: ! aclOptimizationsEnabled
ALTER TABLE [acls] DISABLE TRIGGER nx_trig_acls_modified


#PROC: dbo.nx_trig_hierarchy_insert
CREATE TRIGGER nx_trig_hierarchy_insert ON [hierarchy]
AFTER INSERT AS
BEGIN
  SET NOCOUNT ON;
  INSERT INTO aclr_modified (hierarchy_id, is_new, spid)
    SELECT DISTINCT(id), 1, @@SPID FROM inserted
    WHERE isproperty = 0;
END;

#IF: ! aclOptimizationsEnabled
ALTER TABLE [hierarchy] DISABLE TRIGGER nx_trig_hierarchy_insert


#PROC: dbo.nx_trig_hierarchy_update
CREATE TRIGGER nx_trig_hierarchy_update ON [hierarchy]
AFTER UPDATE AS
BEGIN
  SET NOCOUNT ON;
  INSERT INTO aclr_modified (hierarchy_id, is_new, spid)
    SELECT DISTINCT(OLD.id), 0, @@SPID FROM deleted OLD
      JOIN [inserted] NEW ON OLD.id = NEW.id
      WHERE NEW.isproperty = 0 AND OLD.parentid <> NEW.parentid;
END;

#IF: ! aclOptimizationsEnabled
ALTER TABLE [hierarchy] DISABLE TRIGGER nx_trig_hierarchy_update


#PROC: dbo.nx_trig_aclr_modified
CREATE TRIGGER nx_trig_aclr_modified ON [aclr]
AFTER INSERT AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @permissions NVARCHAR(4000);
  SET @permissions = dbo.nx_get_read_permissions();
  SET XACT_ABORT OFF; -- needed in trigger to avoid doomed transactions
  BEGIN TRY
    INSERT INTO aclr_user_map
      SELECT u.user_id, NEW.acl_id
      FROM aclr_user AS u, inserted AS NEW
      JOIN hierarchy_read_acl AS r ON NEW.acl_id = r.acl_id
      JOIN hierarchy AS h ON r.id = h.id
      -- do not use orphan version
      WHERE h.isversion IS NULL AND
        NOT EXISTS (SELECT 1 FROM aclr_user_map WHERE user_id=u.user_id AND acl_id = NEW.acl_id)
      GROUP BY u.user_id, u.principals, NEW.acl_id
      HAVING dbo.NX_ACCESS_ALLOWED(max(h.id), u.principals, @permissions) = 1
  END TRY
  BEGIN CATCH
    -- ignore Msg 2627 Violation of UNIQUE KEY constraint 'aclrum_user_id_unique'
    IF ERROR_NUMBER() <> 2627 BEGIN
      -- reraise other errors
      DECLARE @ErrorMessage NVARCHAR(4000);
      DECLARE @ErrorSeverity INT;
      DECLARE @ErrorState INT;
      SELECT @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS NVARCHAR(10))
                 + ' ' + ERROR_MESSAGE(),
             @ErrorSeverity = ERROR_SEVERITY(),
             @ErrorState = ERROR_STATE();
      RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState)
    END;
  END CATCH;
END;


#IF: ! aclOptimizationsEnabled
ALTER TABLE [aclr] DISABLE TRIGGER nx_trig_aclr_modified


#PROC: dbo.nx_trig_hier_read_acl_mod
CREATE TRIGGER nx_trig_hier_read_acl_mod ON [hierarchy_read_acl]
AFTER INSERT, UPDATE AS
BEGIN
  SET NOCOUNT ON;
  -- SQLServer does not support "For Each Row"
  INSERT INTO aclr
    SELECT NEW.acl_id, dbo.nx_get_read_acl(NEW.id) FROM
    (SELECT acl_id, max(id) AS id FROM inserted AS i
     WHERE acl_id IS NOT NULL AND
       NOT EXISTS(SELECT 1 FROM aclr AS r WHERE r.acl_id = i.acl_id)
     GROUP BY acl_id) AS NEW;
END;

#IF: ! aclOptimizationsEnabled
ALTER TABLE [hierarchy_read_acl] DISABLE TRIGGER nx_trig_hier_read_acl_mod


# -- PROCEDURES --------------------------------------------------


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_prepare_user_read_acls
CREATE PROCEDURE nx_prepare_user_read_acls @users NVARCHAR(4000) AS
  -- prepare the read aclr for the user
BEGIN
  SET NOCOUNT ON;
  DECLARE @user_md5 CHAR(32);
  SET @user_md5 = dbo.nx_md5(@users);
  IF NOT EXISTS (SELECT 1 FROM aclr_user WHERE user_id = @user_md5) BEGIN
    BEGIN TRY
      INSERT INTO aclr_user VALUES (@user_md5, @users);
    END TRY
    BEGIN CATCH
      -- ignore Msg 2627 Violation of UNIQUE KEY constraint 'user_id_unique'
      IF ERROR_NUMBER() <> 2627 BEGIN
        -- reraise other errors
        DECLARE @ErrorMessage NVARCHAR(4000);
        DECLARE @ErrorSeverity INT;
        DECLARE @ErrorState INT;
        SELECT @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS NVARCHAR(10))
                   + ' ' + ERROR_MESSAGE(),
               @ErrorSeverity = ERROR_SEVERITY(),
               @ErrorState = ERROR_STATE();
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState)
      END;
    END CATCH;
    BEGIN TRY
      INSERT INTO aclr_user_map SELECT @user_md5, acl_id FROM dbo.nx_list_read_acls_for(@users) AS acl_id;
    END TRY
    BEGIN CATCH
      -- ignore Msg 2627 Violation of UNIQUE KEY constraint 'aclrum_user_id_unique'
      IF ERROR_NUMBER() <> 2627 BEGIN
        -- reraise other errors
        DECLARE @ErrorMessage2 NVARCHAR(4000);
        DECLARE @ErrorSeverity2 INT;
        DECLARE @ErrorState2 INT;
        SELECT @ErrorMessage2 = 'Msg ' + CAST(ERROR_NUMBER() AS NVARCHAR(10))
                   + ' ' + ERROR_MESSAGE(),
               @ErrorSeverity2 = ERROR_SEVERITY(),
               @ErrorState2 = ERROR_STATE();
        RAISERROR (@ErrorMessage2, @ErrorSeverity2, @ErrorState2)
      END;
    END CATCH;
  END;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_rebuild_read_acls
CREATE PROCEDURE nx_rebuild_read_acls
  -- Rebuild the read acls tables
AS
BEGIN
  SET NOCOUNT ON;
  TRUNCATE TABLE aclr;
  TRUNCATE TABLE aclr_user;
  TRUNCATE TABLE aclr_user_map;
  TRUNCATE TABLE hierarchy_read_acl;
  INSERT INTO hierarchy_read_acl
    SELECT id, dbo.nx_get_read_acl_id(id)
      FROM (SELECT id FROM hierarchy WHERE isproperty = 0) AS foo;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_vacuum_read_acls
CREATE PROCEDURE nx_vacuum_read_acls
  -- Remove unused read acls entries
AS
BEGIN
  SET NOCOUNT ON;
  DELETE FROM aclr WHERE acl_id IN (SELECT r.acl_id FROM aclr r
    LEFT JOIN hierarchy_read_acl h ON r.acl_id=h.acl_id
    WHERE h.acl_id IS NULL);
  TRUNCATE TABLE aclr_user;
  TRUNCATE TABLE aclr_user_map;
END;


#IF: aclOptimizationsEnabled
#PROC: dbo.nx_update_read_acls
CREATE PROCEDURE nx_update_read_acls
  -- Rebuild only necessary read acls
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @ids TABLE (id ${idType}, acl_id CHAR(32));
  --
  -- 0/ Clean aclr
  INSERT INTO @ids SELECT NULL, r.acl_id FROM aclr r
    LEFT JOIN hierarchy_read_acl h ON r.acl_id=h.acl_id
    WHERE h.acl_id IS NULL ORDER BY 1;
  DELETE a FROM aclr a JOIN @ids i ON a.acl_id = i.acl_id;
  DELETE FROM @ids;
  --
  -- 1/ Get new doc to insert into hierarchy_read_acl
  DELETE FROM aclr_modified OUTPUT DELETED.hierarchy_id, NULL INTO @ids WHERE spid = @@SPID AND is_new = 1;
  INSERT INTO hierarchy_read_acl SELECT i.id, dbo.nx_get_read_acl_id(i.id) FROM @ids i
    JOIN hierarchy h ON i.id = h.id
    LEFT JOIN hierarchy_read_acl r ON r.id = i.id
    WHERE r.acl_id IS NULL;
  DELETE FROM @ids;
  --
  -- 2/ Get the list of doc to update
  DELETE FROM aclr_modified OUTPUT DELETED.hierarchy_id, NULL INTO @ids WHERE spid = @@SPID AND is_new = 0;
  WHILE @@rowcount > 0 BEGIN
    INSERT INTO @ids SELECT h.id, NULL
      FROM hierarchy AS h
      JOIN  @ids AS i on h.parentid = i.id
      WHERE h.isproperty = 0 AND NOT EXISTS(SELECT id FROM @ids d WHERE d.id = h.id);
  END;
  --
  -- 3/ Compute the read ACLs for updated documents
  UPDATE @ids SET acl_id = dbo.nx_get_read_acl_id(id);
  UPDATE h SET acl_id = i.acl_id
    FROM @ids i
    JOIN hierarchy_read_acl h ON i.id = h.id;
 END;


# -- INIT --------------------------------------------------
# build the read acls if empty, this takes care of the upgrade

#IF: aclOptimizationsEnabled
#IF: create_table_aclr
LOG.INFO Upgrading to optimized acls

#IF: aclOptimizationsEnabled
#IF: create_table_aclr
EXEC nx_rebuild_read_acls


# -- END OF ACLR ---------------------------------------------


# ------------------------------------------------------------
# -- MISC INDEXES for perf

#TEST:
SELECT 1 WHERE EXISTS (SELECT 1 FROM sys.tables WHERE name = 'NXP_LOGS')
       AND NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'NXP_LOGS_LOG_DOC_UUID_idx');

#IF: ! emptyResult
CREATE INDEX [NXP_LOGS_LOG_DOC_UUID_idx] ON [dbo].[NXP_LOGS]([LOG_DOC_UUID])


#TEST:
SELECT 1 WHERE EXISTS (SELECT 1 FROM sys.tables WHERE name = 'NXP_LOGS')
       AND NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'NXP_LOGS_LOG_DATE_idx');

#IF: ! emptyResult
CREATE INDEX [NXP_LOGS_LOG_DATE_idx] ON [dbo].[NXP_LOGS]([LOG_DATE])


#TEST:
SELECT 1 WHERE EXISTS (SELECT 1 FROM sys.tables WHERE name = 'NXP_LOGS')
       AND NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'NXP_LOGS_LOG_EVENT_DATE_idx');

#IF: ! emptyResult
CREATE INDEX [NXP_LOGS_LOG_EVENT_DATE_idx] ON [dbo].[NXP_LOGS]([LOG_EVENT_DATE])


#IF: fulltextEnabled
#IF: ! create_table_fulltext
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'fulltext_jobid_idx';

#IF: fulltextEnabled
#IF: create_table_fulltext OR: emptyResult
CREATE INDEX [fulltext_jobid_idx] ON [dbo].[fulltext]([jobid])


#IF: ! create_table_dublincore
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'dublincore_modified_idx';

#IF: create_table_dublincore OR: emptyResult
CREATE INDEX [dublincore_modified_idx] ON [dbo].[dublincore]([modified])


#IF: ! create_table_dc_contributors
#TEST:
SELECT 1 FROM sys.indexes WHERE name = 'dc_contributors_item_idx';

#IF: create_table_dc_contributors OR: emptyResult
CREATE INDEX [dc_contributors_item_idx] ON [dbo].[dc_contributors]([item])


# -- END OF INDEX ------------------------------------------


############################################################


#CATEGORY: upgradeVersions

UPDATE hierarchy SET isversion = 1
  FROM hierarchy JOIN versions ON hierarchy.id = versions.id;

IF OBJECT_ID('dbo.NX_UPGRADE_VERSIONS', 'P') IS NOT NULL
  DROP PROCEDURE dbo.NX_UPGRADE_VERSIONS;

CREATE PROCEDURE NX_UPGRADE_VERSIONS
AS
BEGIN
  SET NOCOUNT ON;
  DECLARE @series ${idType};
  DECLARE @latest TINYINT;
  DECLARE @latestmajor TINYINT;
  DECLARE @major TINYINT;
  DECLARE @cur CURSOR;
  DECLARE @curid ${idType};
  DECLARE @curvid ${idType};
  DECLARE @curmaj BIGINT;
  DECLARE @curmin BIGINT;
  SET @series = '-';
  SET @latest = 0;
  SET @latestmajor = 0;
  SET @cur = CURSOR FAST_FORWARD FOR
    SELECT v.id, v.versionableid, h.majorversion, h.minorversion
      FROM versions v JOIN hierarchy h ON v.id = h.id
      ORDER BY v.versionableid, v.created DESC;
  OPEN @cur;
  FETCH FROM @cur INTO @curid, @curvid, @curmaj, @curmin;
  WHILE @@FETCH_STATUS = 0 BEGIN
    IF @curvid <> @series
    BEGIN
      -- restart
      SET @latest = 1;
      SET @latestmajor = 1;
      SET @series = @curvid;
    END;
    SET @major = CASE WHEN @curmin = 0 THEN 1 ELSE 0 END;
    UPDATE versions SET
        label = CONVERT(NVARCHAR, @curmaj) + '.' + CONVERT(NVARCHAR, @curmin),
        islatest = @latest,
        islatestmajor = CASE WHEN @major = 1 and @latestmajor = 1 THEN 1 ELSE 0 END
      WHERE id = @curid;
    -- next
    SET @latest = 0;
    IF @major = 1 SET @latestmajor = 0;
    -- loop
    FETCH FROM @cur INTO @curid, @curvid, @curmaj, @curmin;
  END;
  CLOSE @cur;
  DEALLOCATE @cur;
END;

EXEC NX_UPGRADE_VERSIONS

DROP PROCEDURE dbo.NX_UPGRADE_VERSIONS;

############################################################


#CATEGORY: upgradeLastContributor

UPDATE dublincore SET lastcontributor = dc_c.item
  FROM dublincore dc
    JOIN (SELECT id, max(pos) AS pos FROM dc_contributors GROUP BY id) AS tmp ON (dc.id = tmp.id)
    JOIN dc_contributors dc_c ON (tmp.id = dc_c.id AND tmp.pos = dc_c.pos)
  WHERE dc.lastcontributor IS NULL;


############################################################


#CATEGORY: upgradeLocks

ALTER TABLE locks DROP CONSTRAINT locks_id_hierarchy_fk;

DELETE FROM locks WHERE lock IS NULL;

UPDATE locks SET
  owner = SUBSTRING(lock, 1, CHARINDEX(':', lock) - 1),
  created = CONVERT(DATETIME, SUBSTRING(lock, CHARINDEX(':', lock) + 1, 9999), 107)
  WHERE owner IS NULL
