Bug 18811: DBRev 17.05.00.006
[koha.git] / t / db_dependent / UsageStats.t
blob388bb315bf6406508a3e42136b740eb10f7edcf3
1 # Copyright 2015 BibLibre
3 # This file is part of Koha.
5 # Koha is free software; you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation; either version 3 of the License, or (at your option) any later
8 # version.
10 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with Koha; if not, see <http://www.gnu.org/licenses>.
17 use Modern::Perl;
18 use Test::More tests => 57;
19 use t::lib::Mocks qw(mock_preference);
20 use t::lib::TestBuilder;
21 use POSIX qw(strftime);
22 use Data::Dumper;
23 use Koha::Biblios;
25 use Koha::Libraries;
27 BEGIN {
28     use_ok('C4::UsageStats');
29     use_ok('C4::Context');
30     use_ok('C4::Biblio');
31     use_ok( 'C4::AuthoritiesMarc', qw(AddAuthority) );
32     use_ok('C4::Reserves');
33     use_ok('MARC::Record');
34     use_ok('Koha::Acquisition::Orders');
37 can_ok(
38     'C4::UsageStats', qw(
39       NeedUpdate
40       BuildReport
41       ReportToCommunity
42       _count )
45 my $schema  = Koha::Database->new->schema;
46 $schema->storage->txn_begin;
47 my $builder = t::lib::TestBuilder->new;
48 my $dbh = C4::Context->dbh;
50 $dbh->do('DELETE FROM issues');
51 $dbh->do('DELETE FROM biblio');
52 $dbh->do('DELETE FROM items');
53 $dbh->do('DELETE FROM auth_header');
54 $dbh->do('DELETE FROM old_issues');
55 $dbh->do('DELETE FROM old_reserves');
56 $dbh->do('DELETE FROM borrowers');
57 $dbh->do('DELETE FROM aqorders');
58 $dbh->do('DELETE FROM subscription');
60 #################################################
61 #             Testing Subs
62 #################################################
64 # ---------- Testing NeedUpdate -----------------
66 #Mocking C4::Context->preference("UsageStatsLastUpdateTime") to 0
67 my $now = strftime( "%s", localtime );
68 t::lib::Mocks::mock_preference( "UsageStatsLastUpdateTime", 0 );
70 my $update = C4::UsageStats->NeedUpdate;
71 is( $update, 1, "There is no last update, update needed" );
73 #Mocking C4::Context->preference("UsageStatsLastUpdateTime") to now
74 $now = strftime( "%s", localtime );
75 t::lib::Mocks::mock_preference( "UsageStatsLastUpdateTime", $now );
77 $update = C4::UsageStats->NeedUpdate;
78 is( $update, 0, "Last update just be done, no update needed " );
80 my $nb_of_libraries = Koha::Libraries->count;
82 # ---------- Testing BuildReport ----------------
84 #Test report->library -----------------
85 #mock to 0
86 t::lib::Mocks::mock_preference( "UsageStatsID",          0 );
87 t::lib::Mocks::mock_preference( "UsageStatsLibraryName", 0 );
88 t::lib::Mocks::mock_preference( "UsageStatsLibrariesInfo",  0 );
89 t::lib::Mocks::mock_preference( "UsageStatsLibraryType", 0 );
90 t::lib::Mocks::mock_preference( "UsageStatsCountry",     0 );
91 t::lib::Mocks::mock_preference( "UsageStatsLibraryUrl",  0 );
93 my $report = C4::UsageStats->BuildReport();
95 isa_ok( $report,              'HASH',  '$report is a HASH' );
96 isa_ok( $report->{libraries}, 'ARRAY', '$report->{libraries} is an ARRAY' );
97 is( scalar( @{ $report->{libraries} } ), 0, "There are 0 fields in libraries, libraries info are not shared" );
98 is( $report->{installation}->{koha_id}, 0,  "UsageStatsID          is good" );
99 is( $report->{installation}->{name},    '', "UsageStatsLibraryName is good" );
100 is( $report->{installation}->{url},     '', "UsageStatsLibraryUrl  is good" );
101 is( $report->{installation}->{type},    '', "UsageStatsLibraryType is good" );
102 is( $report->{installation}->{country}, '', "UsageStatsCountry     is good" );
105 #mock with values
106 t::lib::Mocks::mock_preference( "UsageStatsID",          1 );
107 t::lib::Mocks::mock_preference( "UsageStatsLibraryName", 'NAME' );
108 t::lib::Mocks::mock_preference( "UsageStatsLibraryUrl",  'URL' );
109 t::lib::Mocks::mock_preference( "UsageStatsLibraryType", 'TYPE' );
110 t::lib::Mocks::mock_preference( "UsageStatsCountry",     'COUNTRY' );
111 t::lib::Mocks::mock_preference( "UsageStatsLibrariesInfo", 1 );
112 t::lib::Mocks::mock_preference( "UsageStatsGeolocation", 1 );
115 $report = C4::UsageStats->BuildReport();
117 isa_ok( $report,              'HASH',  '$report is a HASH' );
118 isa_ok( $report->{libraries}, 'ARRAY', '$report->{libraries} is an ARRAY' );
119 is( scalar( @{ $report->{libraries} } ), $nb_of_libraries, "There are 6 fields in $report->{libraries}" );
120 is( $report->{installation}->{koha_id}, 1,     "UsageStatsID          is good" );
121 is( $report->{installation}->{name},   'NAME', "UsageStatsLibraryName is good" );
122 is( $report->{installation}->{url},     'URL', "UsageStatsLibraryUrl  is good" );
123 is( $report->{installation}->{type},   'TYPE', "UsageStatsLibraryType is good" );
124 is( $report->{installation}->{country}, 'COUNTRY', "UsageStatsCountry is good" );
126 #Test report->volumetry ---------------
127 #with original values
128 $report = C4::UsageStats->BuildReport();
130 isa_ok( $report,              'HASH', '$report is a HASH' );
131 isa_ok( $report->{volumetry}, 'HASH', '$report->{volumetry} is a HASH' );
132 is( scalar( keys %{$report->{volumetry}} ), 8, "There are 8 fields in $report->{volumetry}" );
133 is( $report->{volumetry}->{biblio},         0, "There is no biblio" );
134 is( $report->{volumetry}->{items},          0, "There is no items" );
135 is( $report->{volumetry}->{auth_header},    0, "There is no auth_header" );
136 is( $report->{volumetry}->{old_issues},     0, "There is no old_issues" );
137 is( $report->{volumetry}->{old_reserves},   0, "There is no old_reserves" );
138 is( $report->{volumetry}->{borrowers},      0, "There is no borrowers" );
139 is( $report->{volumetry}->{aqorders},       0, "There is no aqorders" );
140 is( $report->{volumetry}->{subscription},   0, "There is no subscription" );
142 #after adding objects
143 construct_objects_needed();
145 $report = C4::UsageStats->BuildReport();
147 isa_ok( $report,              'HASH', '$report is a HASH' );
148 isa_ok( $report->{volumetry}, 'HASH', '$report->{volumetry} is a HASH' );
149 is( scalar( keys %{$report->{volumetry}} ), 8, "There are 8 fields in $report->{volumetry}" );
150 is( $report->{volumetry}->{biblio},         3, "There are 3 biblio" );
151 is( $report->{volumetry}->{items},          3, "There are 3 items" );
152 is( $report->{volumetry}->{auth_header},    2, "There are 2 auth_header" );
153 is( $report->{volumetry}->{old_issues},     1, "There is  1 old_issues" );
154 is( $report->{volumetry}->{old_reserves},   1, "There is  1 old_reserves" );
155 is( $report->{volumetry}->{borrowers},      3, "There are 3 borrowers" );
156 is( $report->{volumetry}->{aqorders},       1, "There is  1 aqorders" );
157 is( $report->{volumetry}->{subscription},   1, "There is  1 subscription" );
159 #Test report->systempreferences -------
160 #mock to 0
161 mocking_systempreferences_to_a_set_value(0);
163 $report = C4::UsageStats->BuildReport();
164 isa_ok( $report,                      'HASH', '$report is a HASH' );
165 isa_ok( $report->{systempreferences}, 'HASH', '$report->{systempreferences} is a HASH' );
166 verif_systempreferences_values( $report, 0 );
168 #mock with values
169 mocking_systempreferences_to_a_set_value(1);
171 $report = C4::UsageStats->BuildReport();
172 isa_ok( $report,                      'HASH', '$report is a HASH' );
173 isa_ok( $report->{systempreferences}, 'HASH', '$report->{systempreferences} is a HASH' );
174 verif_systempreferences_values( $report, 1 );
176 #Test if unwanted syspref are not sent
177 is( $report->{systempreferences}->{useDischarge}, undef, 'useDischarge should not be shared');
178 is( $report->{systempreferences}->{OpacUserJS},   undef, 'OpacUserJS   should not be shared');
180 # ---------- Testing ReportToCommunity ----------
182 # ---------- Testing _count ---------------------
183 my $query = '
184   SELECT count(*)
185   FROM   borrowers
186   ';
187 my $count = $dbh->selectrow_array($query);
189 my $nb_fields = C4::UsageStats::_count('borrowers');
190 is( $nb_fields, $count, "_count return the good number of fields" );
192 #################################################
193 #             Subs
194 #################################################
196 # Adding :
197 # 3 borrowers
198 # 4 biblio
199 # 3 biblio items
200 # 3 items
201 # 2 auth_header
202 # 1 old_issues
203 # 1 old_reserves
204 # 1 subscription
205 # 1 aqorders
206 sub construct_objects_needed {
208     # ---------- 3 borrowers  ---------------------
209     my $surname1     = 'Borrower 1';
210     my $surname2     = 'Borrower 2';
211     my $surname3     = 'Borrower 3';
212     my $firstname1   = 'firstname 1';
213     my $firstname2   = 'firstname 2';
214     my $firstname3   = 'firstname 3';
215     my $cardnumber1  = 'test_card1';
216     my $cardnumber2  = 'test_card2';
217     my $cardnumber3  = 'test_card3';
218     my $categorycode = $builder->build({ source => 'Category' })->{categorycode};
219     my $branchcode = $builder->build({ source => 'Branch' })->{branchcode};
221     my $query = '
222     INSERT INTO borrowers
223       (surname, firstname, cardnumber, branchcode, categorycode)
224     VALUES (?,?,?,?,?)';
225     my $insert_sth = $dbh->prepare($query);
226     $insert_sth->execute( $surname1, $firstname1, $cardnumber1, $branchcode, $categorycode );
227     my $borrowernumber1 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
228     $insert_sth->execute( $surname2, $firstname2, $cardnumber2, $branchcode, $categorycode );
229     my $borrowernumber2 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
230     $insert_sth->execute( $surname3, $firstname3, $cardnumber3, $branchcode, $categorycode );
231     my $borrowernumber3 = $dbh->last_insert_id( undef, undef, 'borrowers', undef );
233     # ---------- 3 biblios -----------------------
234     my $title1  = 'Title 1';
235     my $title2  = 'Title 2';
236     my $title3  = 'Title 3';
237     my $author1 = 'Author 1';
238     my $author2 = 'Author 2';
239     my $author3 = 'Author 3';
241     $query = '
242     INSERT INTO biblio
243       (title, author)
244     VALUES (?,?)';
245     $insert_sth = $dbh->prepare($query);
246     $insert_sth->execute( $title1, $author1 );
247     my $biblionumber1 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
248     $insert_sth->execute( $title2, undef );
249     my $biblionumber2 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
250     $insert_sth->execute( $title3, $author3 );
251     my $biblionumber3 = $dbh->last_insert_id( undef, undef, 'biblio', undef );
253     # ---------- 3 biblio items  -------------------------
254     $query = '
255     INSERT INTO biblioitems
256       (biblionumber, itemtype)
257     VALUES (?,?)';
258     $insert_sth = $dbh->prepare($query);
259     $insert_sth->execute( $biblionumber1, 'Book' );
260     my $biblioitemnumber1 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
261     $insert_sth->execute( $biblionumber2, 'Music' );
262     my $biblioitemnumber2 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
263     $insert_sth->execute( $biblionumber3, 'Book' );
264     my $biblioitemnumber3 = $dbh->last_insert_id( undef, undef, 'biblioitems', undef );
266     # ---------- 3 items  -------------------------
267     my $barcode1 = '111111';
268     my $barcode2 = '222222';
269     my $barcode3 = '333333';
271     $query = '
272     INSERT INTO items
273       (biblionumber, biblioitemnumber, barcode, itype)
274     VALUES (?,?,?,?)';
275     $insert_sth = $dbh->prepare($query);
276     $insert_sth->execute( $biblionumber1, $biblioitemnumber1, $barcode1, 'Book' );
277     my $item_number1 = $dbh->last_insert_id( undef, undef, 'items', undef );
278     $insert_sth->execute( $biblionumber2, $biblioitemnumber2, $barcode2, 'Music' );
279     my $item_number2 = $dbh->last_insert_id( undef, undef, 'items', undef );
280     $insert_sth->execute( $biblionumber3, $biblioitemnumber3, $barcode3, 'Book' );
281     my $item_number3 = $dbh->last_insert_id( undef, undef, 'items', undef );
283     # ---------- Add 2 auth_header
284     $query = '
285     INSERT INTO auth_header
286       (authtypecode)
287     VALUES (?)';
288     $insert_sth = $dbh->prepare($query);
289     $insert_sth->execute('authtypecode1');
290     my $authid1 = $dbh->last_insert_id( undef, undef, 'auth_header', undef );
291     $insert_sth->execute('authtypecode2');
292     my $authid2 = $dbh->last_insert_id( undef, undef, 'auth_header', undef );
294     # ---------- Add 1 old_issues
295     $query = '
296     INSERT INTO old_issues
297       (borrowernumber, branchcode, itemnumber)
298     VALUES (?,?,?)';
299     $insert_sth = $dbh->prepare($query);
300     $insert_sth->execute( $borrowernumber1, $branchcode, $item_number1 );
301     my $issue_id1 = $dbh->last_insert_id( undef, undef, 'old_issues', undef );
303     # ---------- Add 1 old_reserves
304     AddReserve( $branchcode, $borrowernumber1, $biblionumber1, '', 1, undef, undef, '', 'Title', undef, undef );
305     my $biblio = Koha::Biblios->find( $biblionumber1 );
306     my $holds = $biblio->holds;
307     CancelReserve( { reserve_id => $holds->next->reserve_id } );
309     # ---------- Add 1 aqbudgets
310     $query = '
311     INSERT INTO aqbudgets
312       (budget_amount)
313     VALUES (?)';
314     $insert_sth = $dbh->prepare($query);
315     $insert_sth->execute("20.0");
316     my $aqbudgets1 = $dbh->last_insert_id( undef, undef, 'aqbudgets', undef );
318     # ---------- Add 1 aqorders
319     $query = '
320     INSERT INTO aqorders
321       (budget_id, basketno, biblionumber, invoiceid, subscriptionid)
322     VALUES (?,?,?,?,?)';
323     $insert_sth = $dbh->prepare($query);
324     $insert_sth->execute( $aqbudgets1, undef, undef, undef, undef );
325     my $aqorders1 = $dbh->last_insert_id( undef, undef, 'aqorders', undef );
327     # --------- Add 1 subscription
328     $query = '
329     INSERT INTO subscription
330       (biblionumber)
331     VALUES (?)';
332     $insert_sth = $dbh->prepare($query);
333     $insert_sth->execute($biblionumber1);
334     my $subscription1 = $dbh->last_insert_id( undef, undef, 'subscription', undef );
338 #Change systempreferences values to $set_value
339 sub mocking_systempreferences_to_a_set_value {
340     my $set_value = shift;
342     foreach (
343         qw/
344         AcqCreateItem
345         AcqWarnOnDuplicateInvoice
346         AcqViewBaskets
347         BasketConfirmations
348         OrderPdfFormat
349         casAuthentication
350         casLogout
351         AllowPKIAuth
352         DebugLevel
353         delimiter
354         noItemTypeImages
355         virtualshelves
356         AutoLocation
357         IndependentBranches
358         SessionStorage
359         Persona
360         AuthDisplayHierarchy
361         AutoCreateAuthorities
362         BiblioAddsAuthorities
363         AuthorityMergeLimit
364         AuthorityMergeMode
365         UseAuthoritiesForTracings
366         CatalogModuleRelink
367         hide_marc
368         IntranetBiblioDefaultView
369         LabelMARCView
370         OpacSuppression
371         SeparateHoldings
372         UseControlNumber
373         advancedMARCeditor
374         DefaultClassificationSource
375         EasyAnalyticalRecords
376         autoBarcode
377         item-level_itypes
378         marcflavour
379         PrefillItem
380         z3950NormalizeAuthor
381         SpineLabelAutoPrint
382         SpineLabelShowPrintOnBibDetails
383         BlockReturnOfWithdrawnItems
384         CalculateFinesOnReturn
385         AgeRestrictionOverride
386         AllFinesNeedOverride
387         AllowFineOverride
388         AllowItemsOnHoldCheckout
389         AllowItemsOnHoldCheckoutSCO
390         AllowNotForLoanOverride
391         AllowRenewalLimitOverride
392         AllowReturnToBranch
393         AllowTooManyOverride
394         AutomaticItemReturn
395         AutoRemoveOverduesRestrictions
396         CircControl
397         HomeOrHoldingBranch
398         HomeOrHoldingBranchReturn
399         InProcessingToShelvingCart
400         IssueLostItem
401         IssuingInProcess
402         ManInvInNoissuesCharge
403         OverduesBlockCirc
404         RenewalPeriodBase
405         RenewalSendNotice
406         RentalsInNoissuesCharge
407         ReturnBeforeExpiry
408         ReturnToShelvingCart
409         TransfersMaxDaysWarning
410         UseBranchTransferLimits
411         useDaysMode
412         UseTransportCostMatrix
413         UseCourseReserves
414         finesCalendar
415         FinesIncludeGracePeriod
416         finesMode
417         RefundLostOnReturnControl
418         WhenLostChargeReplacementFee
419         WhenLostForgiveFine
420         AllowHoldDateInFuture
421         AllowHoldPolicyOverride
422         AllowHoldsOnDamagedItems
423         AllowHoldsOnPatronsPossessions
424         AutoResumeSuspendedHolds
425         canreservefromotherbranches
426         decreaseLoanHighHolds
427         DisplayMultiPlaceHold
428         emailLibrarianWhenHoldIsPlaced
429         ExpireReservesMaxPickUpDelay
430         OPACAllowHoldDateInFuture
431         OPACAllowUserToChooseBranch
432         ReservesControlBranch
433         ReservesNeedReturns
434         SuspendHoldsIntranet
435         SuspendHoldsOpac
436         TransferWhenCancelAllWaitingHolds
437         AllowAllMessageDeletion
438         AllowOfflineCirculation
439         CircAutocompl
440         CircAutoPrintQuickSlip
441         DisplayClearScreenButton
442         FilterBeforeOverdueReport
443         FineNotifyAtCheckin
444         itemBarcodeFallbackSearch
445         itemBarcodeInputFilter
446         previousIssuesDefaultSortOrder
447         RecordLocalUseOnReturn
448         soundon
449         SpecifyDueDate
450         todaysIssuesDefaultSortOrder
451         UpdateTotalIssuesOnCirc
452         UseTablesortForCirc
453         WaitingNotifyAtCheckin
454         AllowSelfCheckReturns
455         AutoSelfCheckAllowed
456         FRBRizeEditions
457         OPACFRBRizeEditions
458         AmazonCoverImages
459         OPACAmazonCoverImages
460         Babeltheque
461         BakerTaylorEnabled
462         GoogleJackets
463         HTML5MediaEnabled
464         IDreamBooksReadometer
465         IDreamBooksResults
466         IDreamBooksReviews
467         LibraryThingForLibrariesEnabled
468         LocalCoverImages
469         OPACLocalCoverImages
470         NovelistSelectEnabled
471         XISBN
472         OpenLibraryCovers
473         OpenLibrarySearch
474         UseKohaPlugins
475         SyndeticsEnabled
476         TagsEnabled
477         CalendarFirstDayOfWeek
478         opaclanguagesdisplay
479         AuthoritiesLog
480         BorrowersLog
481         CataloguingLog
482         FinesLog
483         IssueLog
484         LetterLog
485         ReturnLog
486         SubscriptionLog
487         BiblioDefaultView
488         COinSinOPACResults
489         DisplayOPACiconsXSLT
490         hidelostitems
491         HighlightOwnItemsOnOPAC
492         OpacAddMastheadLibraryPulldown
493         OPACDisplay856uAsImage
494         OpacHighlightedWords
495         OpacKohaUrl
496         OpacMaintenance
497         OpacPublic
498         OpacSeparateHoldings
499         OPACShowBarcode
500         OPACShowCheckoutName
501         OpacShowFiltersPulldownMobile
502         OPACShowHoldQueueDetails
503         OpacShowLibrariesPulldownMobile
504         OpacShowRecentComments
505         OPACShowUnusedAuthorities
506         OpacStarRatings
507         opacthemes
508         OPACURLOpenInNewWindow
509         OpacAuthorities
510         opacbookbag
511         OpacBrowser
512         OpacBrowseResults
513         OpacCloud
514         OPACFinesTab
515         OpacHoldNotes
516         OpacItemLocation
517         OpacPasswordChange
518         OPACPatronDetails
519         OPACpatronimages
520         OPACPopupAuthorsSearch
521         OpacTopissue
522         opacuserlogin
523         QuoteOfTheDay
524         RequestOnOpac
525         reviewson
526         ShowReviewer
527         ShowReviewerPhoto
528         SocialNetworks
529         suggestion
530         AllowPurchaseSuggestionBranchChoice
531         OpacAllowPublicListCreation
532         OpacAllowSharingPrivateLists
533         OpacRenewalAllowed
534         OpacRenewalBranch
535         OPACViewOthersSuggestions
536         SearchMyLibraryFirst
537         singleBranchMode
538         AnonSuggestions
539         EnableOpacSearchHistory
540         OPACPrivacy
541         opacreadinghistory
542         TrackClicks
543         PatronSelfRegistration
544         OPACShelfBrowser
545         AutoEmailOpacUser
546         AutoEmailPrimaryAddress
547         autoMemberNum
548         BorrowerRenewalPeriodBase
549         checkdigit
550         EnableBorrowerFiles
551         EnhancedMessagingPreferences
552         ExtendedPatronAttributes
553         intranetreadinghistory
554         memberofinstitution
555         patronimages
556         TalkingTechItivaPhoneNotification
557         uppercasesurnames
558         IncludeSeeFromInSearches
559         OpacGroupResults
560         QueryAutoTruncate
561         QueryFuzzy
562         QueryStemming
563         QueryWeightFields
564         TraceCompleteSubfields
565         TraceSubjectSubdivisions
566         UseICU
567         UseQueryParser
568         defaultSortField
569         displayFacetCount
570         OPACdefaultSortField
571         OPACItemsResultsDisplay
572         expandedSearchOption
573         IntranetNumbersPreferPhrase
574         OPACNumbersPreferPhrase
575         opacSerialDefaultTab
576         RenewSerialAddsSuggestion
577         RoutingListAddReserves
578         RoutingSerials
579         SubscriptionHistory
580         Display856uAsImage
581         DisplayIconsXSLT
582         template
583         yuipath
584         HidePatronName
585         intranetbookbag
586         StaffDetailItemSelection
587         viewISBD
588         viewLabeledMARC
589         viewMARC
590         ILS-DI
591         OAI-PMH
592         version
593         AudioAlerts
594         /
595       ) {
596         t::lib::Mocks::mock_preference( $_, $set_value );
597     }
600 #Test if all systempreferences are at $value_to_test
601 sub verif_systempreferences_values {
602     my ( $report, $value_to_test ) = @_;
604     my @missings;
605     foreach my $key ( keys %{$report->{systempreferences}} ) {
606         if ( $report->{systempreferences}->{$key} ne $value_to_test ) {
607             warn $key;
608             push @missings, $key;
609         }
610     }
611     unless ( @missings ) {
612         ok(1, 'All prefs are present');
613     } else {
614         ok(0, 'Some prefs are missing: ' . Dumper(\@missings));
615     }
618 $schema->storage->txn_rollback;