@@ -397,4 +397,209 @@ protected function validate()
397397 {
398398 return $ this ->listener ->getStrategy ($ this ->_em , $ this ->getClassMetadata ()->name )->getName () === Strategy::CLOSURE ;
399399 }
400+
401+ public function verify ()
402+ {
403+ $ nodeMeta = $ this ->getClassMetadata ();
404+ $ nodeIdField = $ nodeMeta ->getSingleIdentifierFieldName ();
405+ $ config = $ this ->listener ->getConfiguration ($ this ->_em , $ nodeMeta ->name );
406+ $ closureMeta = $ this ->_em ->getClassMetadata ($ config ['closure ' ]);
407+ $ errors = [];
408+
409+ $ q = $ this ->_em ->createQuery ("
410+ SELECT COUNT(node)
411+ FROM {$ nodeMeta ->name } AS node
412+ LEFT JOIN {$ closureMeta ->name } AS c WITH c.ancestor = node AND c.depth = 0
413+ WHERE c.id IS NULL
414+ " );
415+
416+ if ($ missingSelfRefsCount = intval ($ q ->getSingleScalarResult ())) {
417+ $ errors [] = "Missing $ missingSelfRefsCount self referencing closures " ;
418+ }
419+
420+ $ q = $ this ->_em ->createQuery ("
421+ SELECT COUNT(node)
422+ FROM {$ nodeMeta ->name } AS node
423+ INNER JOIN {$ closureMeta ->name } AS c1 WITH c1.descendant = node. {$ config ['parent ' ]}
424+ LEFT JOIN {$ closureMeta ->name } AS c2 WITH c2.descendant = node. $ nodeIdField AND c2.ancestor = c1.ancestor
425+ WHERE c2.id IS NULL AND node. $ nodeIdField <> c1.ancestor
426+ " );
427+
428+ if ($ missingClosuresCount = intval ($ q ->getSingleScalarResult ())) {
429+ $ errors [] = "Missing $ missingClosuresCount closures " ;
430+ }
431+
432+ $ q = $ this ->_em ->createQuery ("
433+ SELECT COUNT(c1.id)
434+ FROM {$ closureMeta ->name } AS c1
435+ LEFT JOIN {$ nodeMeta ->name } AS node WITH c1.descendant = node. $ nodeIdField
436+ LEFT JOIN {$ closureMeta ->name } AS c2 WITH c2.descendant = node. {$ config ['parent ' ]} AND c2.ancestor = c1.ancestor
437+ WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
438+ " );
439+
440+ if ($ invalidClosuresCount = intval ($ q ->getSingleScalarResult ())) {
441+ $ errors [] = "Found $ invalidClosuresCount invalid closures " ;
442+ }
443+
444+ if (!empty ($ config ['level ' ])) {
445+ $ levelField = $ config ['level ' ];
446+ $ maxResults = 1000 ;
447+ $ q = $ this ->_em ->createQuery ("
448+ SELECT node. $ nodeIdField AS id, node. $ levelField AS node_level, MAX(c.depth) AS closure_level
449+ FROM {$ nodeMeta ->name } AS node
450+ INNER JOIN {$ closureMeta ->name } AS c WITH c.descendant = node. $ nodeIdField
451+ GROUP BY node.id, node.level
452+ HAVING node_level IS NULL OR node_level <> closure_level
453+ " )->setMaxResults ($ maxResults );
454+
455+ if ($ invalidLevelsCount = count ($ q ->getScalarResult ())) {
456+ $ errors [] = "Found $ invalidLevelsCount invalid level values " ;
457+ }
458+ }
459+
460+ return $ errors ?: true ;
461+ }
462+
463+ public function recover ()
464+ {
465+ if ($ this ->verify () === true ) {
466+ return ;
467+ }
468+
469+ $ this ->cleanUpClosure ();
470+ $ this ->rebuildClosure ();
471+ }
472+
473+ public function rebuildClosure ()
474+ {
475+ $ nodeMeta = $ this ->getClassMetadata ();
476+ $ config = $ this ->listener ->getConfiguration ($ this ->_em , $ nodeMeta ->name );
477+ $ closureMeta = $ this ->_em ->getClassMetadata ($ config ['closure ' ]);
478+
479+ $ insertClosures = function ($ entries ) use ($ closureMeta ) {
480+ $ closureTable = $ closureMeta ->getTableName ();
481+ $ ancestorColumnName = $ this ->getJoinColumnFieldName ($ closureMeta ->getAssociationMapping ('ancestor ' ));
482+ $ descendantColumnName = $ this ->getJoinColumnFieldName ($ closureMeta ->getAssociationMapping ('descendant ' ));
483+ $ depthColumnName = $ closureMeta ->getColumnName ('depth ' );
484+
485+ $ conn = $ this ->_em ->getConnection ();
486+ $ conn ->beginTransaction ();
487+ foreach ($ entries as $ entry ) {
488+ $ conn ->insert ($ closureTable , array_combine (
489+ [$ ancestorColumnName , $ descendantColumnName , $ depthColumnName ],
490+ $ entry
491+ ));
492+ }
493+ $ conn ->commit ();
494+ };
495+
496+ $ buildClosures = function ($ dql ) use ($ insertClosures ) {
497+ $ newClosuresCount = 0 ;
498+ $ batchSize = 1000 ;
499+ $ q = $ this ->_em ->createQuery ($ dql )->setMaxResults ($ batchSize )->setCacheable (false );
500+ do {
501+ $ entries = $ q ->getScalarResult ();
502+ $ insertClosures ($ entries );
503+ $ newClosuresCount += count ($ entries );
504+ } while (count ($ entries ) > 0 );
505+ return $ newClosuresCount ;
506+ };
507+
508+ $ nodeIdField = $ nodeMeta ->getSingleIdentifierFieldName ();
509+ $ newClosuresCount = $ buildClosures ("
510+ SELECT node.id AS ancestor, node. $ nodeIdField AS descendant, 0 AS depth
511+ FROM {$ nodeMeta ->name } AS node
512+ LEFT JOIN {$ closureMeta ->name } AS c WITH c.ancestor = node AND c.depth = 0
513+ WHERE c.id IS NULL
514+ " );
515+ $ newClosuresCount += $ buildClosures ("
516+ SELECT IDENTITY(c1.ancestor) AS ancestor, node. $ nodeIdField AS descendant, c1.depth + 1 AS depth
517+ FROM {$ nodeMeta ->name } AS node
518+ INNER JOIN {$ closureMeta ->name } AS c1 WITH c1.descendant = node. {$ config ['parent ' ]}
519+ LEFT JOIN {$ closureMeta ->name } AS c2 WITH c2.descendant = node. $ nodeIdField AND c2.ancestor = c1.ancestor
520+ WHERE c2.id IS NULL AND node. $ nodeIdField <> c1.ancestor
521+ " );
522+
523+ return $ newClosuresCount ;
524+ }
525+
526+ public function cleanUpClosure ()
527+ {
528+ $ conn = $ this ->_em ->getConnection ();
529+ $ nodeMeta = $ this ->getClassMetadata ();
530+ $ nodeIdField = $ nodeMeta ->getSingleIdentifierFieldName ();
531+ $ config = $ this ->listener ->getConfiguration ($ this ->_em , $ nodeMeta ->name );
532+ $ closureMeta = $ this ->_em ->getClassMetadata ($ config ['closure ' ]);
533+ $ closureTableName = $ closureMeta ->getTableName ();
534+
535+ $ dql = "
536+ SELECT c1.id AS id
537+ FROM {$ closureMeta ->name } AS c1
538+ LEFT JOIN {$ nodeMeta ->name } AS node WITH c1.descendant = node. $ nodeIdField
539+ LEFT JOIN {$ closureMeta ->name } AS c2 WITH c2.descendant = node. {$ config ['parent ' ]} AND c2.ancestor = c1.ancestor
540+ WHERE c2.id IS NULL AND c1.descendant <> c1.ancestor
541+ " ;
542+
543+ $ deletedClosuresCount = 0 ;
544+ $ batchSize = 1000 ;
545+ $ q = $ this ->_em ->createQuery ($ dql )->setMaxResults ($ batchSize )->setCacheable (false );
546+
547+ while (($ ids = $ q ->getScalarResult ()) && !empty ($ ids )) {
548+ $ ids = array_map (function ($ el ) {
549+ return $ el ['id ' ];
550+ }, $ ids );
551+ $ query = "DELETE FROM {$ closureTableName } WHERE id IN ( " .implode (', ' , $ ids ).") " ;
552+ if (!$ conn ->executeQuery ($ query )) {
553+ throw new \RuntimeException ('Failed to remove incorrect closures ' );
554+ }
555+ $ deletedClosuresCount += count ($ ids );
556+ }
557+
558+ return $ deletedClosuresCount ;
559+ }
560+
561+ public function updateLevelValues ()
562+ {
563+ $ nodeMeta = $ this ->getClassMetadata ();
564+ $ config = $ this ->listener ->getConfiguration ($ this ->_em , $ nodeMeta ->name );
565+ $ levelUpdatesCount = 0 ;
566+
567+ if (!empty ($ config ['level ' ])) {
568+ $ levelField = $ config ['level ' ];
569+ $ nodeIdField = $ nodeMeta ->getSingleIdentifierFieldName ();
570+ $ closureMeta = $ this ->_em ->getClassMetadata ($ config ['closure ' ]);
571+
572+ $ batchSize = 1000 ;
573+ $ q = $ this ->_em ->createQuery ("
574+ SELECT node. $ nodeIdField AS id, node. $ levelField AS node_level, MAX(c.depth) AS closure_level
575+ FROM {$ nodeMeta ->name } AS node
576+ INNER JOIN {$ closureMeta ->name } AS c WITH c.descendant = node. $ nodeIdField
577+ GROUP BY node.id, node.level
578+ HAVING node_level IS NULL OR node_level <> closure_level
579+ " )->setMaxResults ($ batchSize )->setCacheable (false );
580+ do {
581+ $ entries = $ q ->getScalarResult ();
582+ $ this ->_em ->getConnection ()->beginTransaction ();
583+ foreach ($ entries as $ entry ) {
584+ unset($ entry ['node_level ' ]);
585+ $ this ->_em ->createQuery ("
586+ UPDATE {$ nodeMeta ->name } AS node SET node. $ levelField = :closure_level WHERE node. $ nodeIdField = :id
587+ " )->execute ($ entry );
588+ }
589+ $ this ->_em ->getConnection ()->commit ();
590+ $ levelUpdatesCount += count ($ entries );
591+ } while (count ($ entries ) > 0 );
592+ }
593+
594+ return $ levelUpdatesCount ;
595+ }
596+
597+ protected function getJoinColumnFieldName ($ association )
598+ {
599+ if (count ($ association ['joinColumnFieldNames ' ]) > 1 ) {
600+ throw new \RuntimeException ('More association on field ' . $ association ['fieldName ' ]);
601+ }
602+
603+ return array_shift ($ association ['joinColumnFieldNames ' ]);
604+ }
400605}
0 commit comments