fix ticketing system error on bootstrap of new install
[freeside.git] / rt / sbin / rt-setup-fulltext-index.in
1 #!@PERL@
2 # BEGIN BPS TAGGED BLOCK {{{
3 #
4 # COPYRIGHT:
5 #
6 # This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
7 #                                          <sales@bestpractical.com>
8 #
9 # (Except where explicitly superseded by other copyright notices)
10 #
11 #
12 # LICENSE:
13 #
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18 #
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
29 #
30 #
31 # CONTRIBUTION SUBMISSION POLICY:
32 #
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38 #
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47 #
48 # END BPS TAGGED BLOCK }}}
49 use strict;
50 use warnings;
51 no warnings 'once';
52
53 # fix lib paths, some may be relative
54 BEGIN { # BEGIN RT CMD BOILERPLATE
55     require File::Spec;
56     require Cwd;
57     my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@");
58     my $bin_path;
59
60     for my $lib (@libs) {
61         unless ( File::Spec->file_name_is_absolute($lib) ) {
62             $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1];
63             $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib );
64         }
65         unshift @INC, $lib;
66     }
67
68 }
69
70 BEGIN {
71     use RT;
72     RT::LoadConfig();
73     RT::Init();
74 };
75 use RT::Interface::CLI ();
76
77 my %DB = (
78     type           => scalar RT->Config->Get('DatabaseType'),
79     user           => scalar RT->Config->Get('DatabaseUser'),
80     admin          => scalar RT->Config->Get('DatabaseAdmin'),
81     admin_password => undef,
82 );
83
84 my %OPT = (
85     help        => 0,
86     ask         => 1,
87     dryrun      => 0,
88     attachments => 1,
89 );
90
91 my %DEFAULT;
92 if ( $DB{'type'} eq 'Pg' ) {
93     %DEFAULT = (
94         table  => 'AttachmentsIndex',
95         column => 'ContentIndex',
96     );
97 }
98 elsif ( $DB{'type'} eq 'mysql' ) {
99     %DEFAULT = (
100         table => 'AttachmentsIndex',
101     );
102 }
103 elsif ( $DB{'type'} eq 'Oracle' ) {
104     %DEFAULT = (
105         prefix => 'rt_fts_',
106     );
107 }
108
109 use Getopt::Long qw(GetOptions);
110 GetOptions(
111     'h|help!'        => \$OPT{'help'},
112     'ask!'           => \$OPT{'ask'},
113     'dry-run!'       => \$OPT{'dryrun'},
114     'attachments!'   => \$OPT{'attachments'},
115
116     'table=s'        => \$OPT{'table'},
117     'column=s'       => \$OPT{'column'},
118     'url=s'          => \$OPT{'url'},
119     'maxmatches=i'   => \$OPT{'maxmatches'},
120     'index-type=s'   => \$OPT{'index-type'},
121
122     'dba=s'          => \$DB{'admin'},
123     'dba-password=s' => \$DB{'admin_password'},
124     'limit=i'        => \$DB{'batch-size'},
125 ) or show_help();
126
127 if ( $OPT{'help'} || (!$DB{'admin'} && $DB{'type'} eq 'Oracle' ) ) {
128     show_help( !$OPT{'help'} );
129 }
130
131 my $dbh = $RT::Handle->dbh;
132 $dbh->{'RaiseError'} = 1;
133 $dbh->{'PrintError'} = 1;
134
135 # MySQL could either be native of sphinx; find out which
136 if ($DB{'type'} eq "mysql") {
137     my $index_type = lc($OPT{'index-type'} || '');
138
139     # Default to sphinx on < 5.6, and error if they provided mysql
140     my $msg;
141     if ($RT::Handle->dbh->{mysql_serverversion} < 50600) {
142         $msg = "Complete support for full-text search requires MySQL 5.6 or higher.  For prior\n"
143               ."versions such as yours, full-text indexing can either be provided using MyISAM\n"
144               ."tables, or the external Sphinx indexer.  Using MyISAM tables requires that your\n"
145               ."database be tuned to support them, as RT uses InnoDB tables for all other content.\n"
146               ."Using Sphinx will require recompiling MySQL.  Which indexing solution would you\n"
147               ."prefer?"
148     } else {
149         $msg = "MySQL 5.6 and above support native full-text indexing; for compatibility\n"
150               ."with earlier versions of RT, the external Sphinx indexer is still supported.\n"
151               ."Which indexing solution would you prefer?"
152     }
153
154     while ( $index_type ne 'sphinx' and $index_type ne 'mysql' ) {
155         $index_type = lc prompt(
156             message => $msg,
157             default => 'mysql',
158             silent  => !$OPT{'ask'},
159         );
160     };
161     $DB{'type'} = $index_type;
162 }
163
164 if ( $DB{'type'} eq 'mysql' ) {
165     # MySQL 5.6 has FTS on InnoDB "text" columns -- which the
166     # Attachments table doesn't have, but we can make it have.
167     my $table = $OPT{'table'} || prompt(
168         message => "Enter the name of a new MySQL table that will be used to store the\n"
169                  . "full-text content and indexes:",
170         default => $DEFAULT{'table'},
171         silent  => !$OPT{'ask'},
172     );
173     do_error_is_ok( dba_handle() => "DROP TABLE $table" )
174         unless $OPT{'dryrun'};
175
176     my $engine = $RT::Handle->dbh->{mysql_serverversion} < 50600 ? "MyISAM" : "InnoDB";
177     my $schema = "CREATE TABLE $table ( "
178         ."id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,"
179         ."Content LONGTEXT ) ENGINE=$engine CHARACTER SET utf8";
180     insert_schema( $schema );
181
182     insert_data( Table => $table, Engine => $engine );
183
184     insert_schema( "CREATE FULLTEXT INDEX $table ON $table(Content)" );
185
186     print_rt_config( Table => $table );
187 } elsif ($DB{'type'} eq 'sphinx') {
188     check_sphinx();
189     my $table = $OPT{'table'} || prompt(
190         message => "Enter name of a new MySQL table that will be used to connect to the\n"
191                  . "Sphinx server:",
192         default => $DEFAULT{'table'},
193         silent  => !$OPT{'ask'},
194     );
195
196     my $url = 'sphinx://localhost:3312/rt';
197     my $version = ($dbh->selectrow_array("show variables like 'version'"))[1];
198     $url = 'sphinx://127.0.0.1:3312/rt'
199         if $version and $version =~ /^(\d+\.\d+)/ and $1 >= 5.5;
200
201     $url = $OPT{'url'} || prompt(
202         message => "Enter URL of the sphinx search server; this should be of the form\n"
203                  . "sphinx://<server>:<port>/<index name>",
204         default => $url,
205         silent  => !$OPT{'ask'},
206     );
207     my $maxmatches = $OPT{'maxmatches'} || prompt(
208         message => "Maximum number of matches to return; this is the maximum number of\n"
209                  . "attachment records returned by the search, not the maximum number\n"
210                  . "of tickets.  Both your RT_SiteConfig.pm and your sphinx.conf must\n"
211                  . "agree on this value.  Larger values cause your Sphinx server to\n"
212                  . "consume more memory and CPU time per query.",
213         default => 10000,
214         silent  => !$OPT{'ask'},
215     );
216
217     my $schema = <<END;
218 CREATE TABLE $table (
219     id     BIGINT NOT NULL,
220     weight INTEGER NOT NULL,
221     query  VARCHAR(3072) NOT NULL,
222     INDEX(query)
223 ) ENGINE=SPHINX CONNECTION="$url" CHARACTER SET utf8
224 END
225
226     do_error_is_ok( dba_handle() => "DROP TABLE $table" )
227         unless $OPT{'dryrun'};
228     insert_schema( $schema );
229
230     print_rt_config( Table => $table, MaxMatches => $maxmatches );
231
232     require URI;
233     my $urlo = URI->new( $url );
234     my ($host, $port)  = split /:/, $urlo->authority;
235     my $index = $urlo->path;
236     $index =~ s{^/+}{};
237
238     my $var_path = $RT::VarPath;
239
240     my %sphinx_conf = ();
241     $sphinx_conf{'host'} = RT->Config->Get('DatabaseHost');
242     $sphinx_conf{'db'}   = RT->Config->Get('DatabaseName');
243     $sphinx_conf{'user'} = RT->Config->Get('DatabaseUser');
244     $sphinx_conf{'pass'} = RT->Config->Get('DatabasePassword');
245
246     print <<END
247
248 Below is a simple Sphinx configuration which can be used to index all
249 text/plain attachments in your database.  This configuration is not
250 ideal; you should read the Sphinx documentation to understand how to
251 configure it to better suit your needs.  It assumes that you create the
252 $var_path/sphinx/ directory, and that is is writable by the sphinx
253 user.
254
255 source rt {
256     type            = mysql
257
258     sql_host        = $sphinx_conf{'host'}
259     sql_db          = $sphinx_conf{'db'}
260     sql_user        = $sphinx_conf{'user'}
261     sql_pass        = $sphinx_conf{'pass'}
262
263     sql_query_pre   = SET NAMES utf8
264     sql_query       = \\
265         SELECT a.id, a.content FROM Attachments a \\
266         JOIN Transactions txn ON a.TransactionId = txn.id AND txn.ObjectType = 'RT::Ticket' \\
267         JOIN Tickets t ON txn.ObjectId = t.id \\
268         WHERE a.ContentType = 'text/plain' AND t.Status != 'deleted'
269
270     sql_query_info  = SELECT * FROM Attachments WHERE id=\$id
271 }
272
273 index $index {
274     source                  = rt
275     path                    = $var_path/sphinx/index
276     docinfo                 = extern
277     charset_type            = utf-8
278 }
279
280 indexer {
281     mem_limit               = 32M
282 }
283
284 searchd {
285     port                    = $port
286     log                     = $var_path/sphinx/searchd.log
287     query_log               = $var_path/sphinx/query.log
288     read_timeout            = 5
289     max_children            = 30
290     pid_file                = $var_path/sphinx/searchd.pid
291     max_matches             = $maxmatches
292     seamless_rotate         = 1
293     preopen_indexes         = 0
294     unlink_old              = 1
295     # For sphinx >= 1.10:
296     binlog_path             = $var_path/sphinx/
297 }
298
299 END
300
301 }
302 elsif ( $DB{'type'} eq 'Pg' ) {
303     check_tsvalue();
304     my $table = $OPT{'table'} || prompt(
305         message => "Enter the name of a DB table that will be used to store the Pg tsvector.\n"
306                  . "You may either use the existing Attachments table, or create a new\n"
307                  . "table.  Creating a new table makes initial indexing faster.",
308         default => $DEFAULT{'table'},
309         silent  => !$OPT{'ask'},
310     );
311     my $column = $OPT{'column'} || prompt(
312         message => 'Enter the name of a column that will be used to store the Pg tsvector:',
313         default => $DEFAULT{'column'},
314         silent  => !$OPT{'ask'},
315     );
316
317     my @schema;
318     my $drop;
319     if ( lc($table) eq 'attachments' ) {
320         $drop = "ALTER TABLE $table DROP COLUMN $column";
321         push @schema, "ALTER TABLE $table ADD COLUMN $column tsvector";
322     } else {
323         $drop = "DROP TABLE $table";
324         push @schema, split /;\n+/, <<SCHEMA;
325 CREATE TABLE $table (
326     id SERIAL,
327     $column tsvector
328 );
329 GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO "$DB{user}"
330 SCHEMA
331     }
332
333     my $index_type = lc($OPT{'index-type'} || '');
334     while ( $index_type ne 'gist' and $index_type ne 'gin' ) {
335         $index_type = lc prompt(
336             message => "You may choose between GiST or GIN indexes; the GiST takes less space on\n"
337                      . "disk and is faster to update, but is an order of magnitude slower to query.",
338             default => 'GIN',
339             silent  => !$OPT{'ask'},
340         );
341     }
342
343     do_error_is_ok( dba_handle() => $drop )
344         unless $OPT{'dryrun'};
345     insert_schema( $_ ) for @schema;
346
347     insert_data( Table => $table, Column => $column );
348
349     insert_schema( "CREATE INDEX ${column}_idx ON $table USING $index_type($column)" );
350
351     print_rt_config( Table => $table, Column => $column );
352 }
353 elsif ( $DB{'type'} eq 'Oracle' ) {
354     {
355         my $dbah = dba_handle();
356         do_print_error( $dbah => 'GRANT CTXAPP TO '. $DB{'user'} );
357         do_print_error( $dbah => 'GRANT EXECUTE ON CTXSYS.CTX_DDL TO '. $DB{'user'} );
358     }
359
360     my %PREFERENCES = (
361         datastore => {
362             type => 'DIRECT_DATASTORE',
363         },
364         filter => {
365             type => 'AUTO_FILTER',
366 #        attributes => {
367 #            timeout => 120, # seconds
368 #            timeout_type => 'HEURISTIC', # or 'FIXED'
369 #        },
370         },
371         lexer => {
372             type => 'WORLD_LEXER',
373         },
374         word_list => {
375             type => 'BASIC_WORDLIST',
376             attributes => {
377                 stemmer => 'AUTO',
378                 fuzzy_match => 'AUTO',
379 #            fuzzy_score => undef,
380 #            fuzzy_numresults => undef,
381 #            substring_index => undef,
382 #            prefix_index => undef,
383 #            prefix_length_min => undef,
384 #            prefix_length_max => undef,
385 #            wlidcard_maxterms => undef,
386             },
387         },
388         'section_group' => {
389             type => 'NULL_SECTION_GROUP',
390         },
391
392         storage => {
393             type => 'BASIC_STORAGE',
394             attributes => {
395                 R_TABLE_CLAUSE => 'lob (data) store as (cache)',
396                 I_INDEX_CLAUSE => 'compress 2',
397             },
398         },
399     );
400
401     my @params = ();
402     push @params, ora_create_datastore( %{ $PREFERENCES{'datastore'} } );
403     push @params, ora_create_filter( %{ $PREFERENCES{'filter'} } );
404     push @params, ora_create_lexer( %{ $PREFERENCES{'lexer'} } );
405     push @params, ora_create_word_list( %{ $PREFERENCES{'word_list'} } );
406     push @params, ora_create_stop_list();
407     push @params, ora_create_section_group( %{ $PREFERENCES{'section_group'} } );
408     push @params, ora_create_storage( %{ $PREFERENCES{'storage'} } );
409
410     my $index_params = join "\n", @params;
411     my $index_name = $DEFAULT{prefix} .'index';
412     do_error_is_ok( $dbh => "DROP INDEX $index_name" )
413         unless $OPT{'dryrun'};
414     $dbh->do(
415         "CREATE INDEX $index_name ON Attachments(Content)
416         indextype is ctxsys.context parameters('
417             $index_params
418         ')",
419     ) unless $OPT{'dryrun'};
420
421     print_rt_config( IndexName => $index_name );
422 }
423 else {
424     die "Full-text indexes on $DB{type} are not yet supported";
425 }
426
427 sub check_tsvalue {
428     my $dbh = $RT::Handle->dbh;
429     my $fts = ($dbh->selectrow_array(<<EOQ))[0];
430 SELECT 1 FROM information_schema.routines WHERE routine_name = 'plainto_tsquery'
431 EOQ
432     unless ($fts) {
433         print STDERR <<EOT;
434
435 Your PostgreSQL server does not include full-text support.  You will
436 need to upgrade to PostgreSQL version 8.3 or higher to use full-text
437 indexing.
438
439 EOT
440         exit 1;
441     }
442 }
443
444 sub check_sphinx {
445     return if $RT::Handle->CheckSphinxSE;
446
447     print STDERR <<EOT;
448
449 Your MySQL server has not been compiled with the Sphinx storage engine
450 (sphinxse).  You will need to recompile MySQL according to the
451 instructions in Sphinx's documentation at
452 http://sphinxsearch.com/docs/current.html#sphinxse-installing
453
454 EOT
455     exit 1;
456 }
457
458 sub ora_create_datastore {
459     return sprintf 'datastore %s', ora_create_preference(
460         @_,
461         name => 'datastore',
462     );
463 }
464
465 sub ora_create_filter {
466     my $res = '';
467     $res .= sprintf "format column %s\n", ora_create_format_column();
468     $res .= sprintf 'filter %s', ora_create_preference(
469         @_,
470         name => 'filter',
471     );
472     return $res;
473 }
474
475 sub ora_create_lexer {
476     return sprintf 'lexer %s', ora_create_preference(
477         @_,
478         name => 'lexer',
479     );
480 }
481
482 sub ora_create_word_list {
483     return sprintf 'wordlist %s', ora_create_preference(
484         @_,
485         name => 'word_list',
486     );
487 }
488
489 sub ora_create_stop_list {
490     my $file = shift || 'etc/stopwords/en.txt';
491     return '' unless -e $file;
492
493     my $name = $DEFAULT{'prefix'} .'stop_list';
494     unless ($OPT{'dryrun'}) {
495         do_error_is_ok( $dbh => 'begin ctx_ddl.drop_stoplist(?); end;', $name );
496
497         $dbh->do(
498             'begin ctx_ddl.create_stoplist(?, ?);  end;',
499             undef, $name, 'BASIC_STOPLIST'
500         );
501
502         open( my $fh, '<:utf8', $file )
503             or die "couldn't open file '$file': $!";
504         while ( my $word = <$fh> ) {
505             chomp $word;
506             $dbh->do(
507                 'begin ctx_ddl.add_stopword(?, ?); end;',
508                 undef, $name, $word
509             );
510         }
511         close $fh;
512     }
513     return sprintf 'stoplist %s', $name;
514 }
515
516 sub ora_create_section_group {
517     my %args = @_;
518     my $name = $DEFAULT{'prefix'} .'section_group';
519     unless ($OPT{'dryrun'}) {
520         do_error_is_ok( $dbh => 'begin ctx_ddl.drop_section_group(?); end;', $name );
521         $dbh->do(
522             'begin ctx_ddl.create_section_group(?, ?);  end;',
523             undef, $name, $args{'type'}
524         );
525     }
526     return sprintf 'section group %s', $name;
527 }
528
529 sub ora_create_storage {
530     return sprintf 'storage %s', ora_create_preference(
531         @_,
532         name => 'storage',
533     );
534 }
535
536 sub ora_create_format_column {
537     my $column_name = 'ContentOracleFormat';
538     return $column_name if $OPT{'dryrun'};
539     unless (
540         $dbh->column_info(
541             undef, undef, uc('Attachments'), uc( $column_name )
542         )->fetchrow_array
543     ) {
544         $dbh->do(qq{
545             ALTER TABLE Attachments ADD $column_name VARCHAR2(10)
546         });
547     }
548
549     my $detect_format = qq{
550         CREATE OR REPLACE FUNCTION $DEFAULT{prefix}detect_format_simple(
551             parent IN NUMBER,
552             type IN VARCHAR2,
553             encoding IN VARCHAR2,
554             fname IN VARCHAR2
555         )
556         RETURN VARCHAR2
557         AS
558             format VARCHAR2(10);
559         BEGIN
560             format := CASE
561     };
562     unless ( $OPT{'attachments'} ) {
563         $detect_format .= qq{
564                 WHEN fname IS NOT NULL THEN 'ignore'
565         };
566     }
567     $detect_format .= qq{
568                 WHEN type = 'text' THEN 'text'
569                 WHEN type = 'text/rtf' THEN 'ignore'
570                 WHEN type LIKE 'text/%' THEN 'text'
571                 WHEN type LIKE 'message/%' THEN 'text'
572                 ELSE 'ignore'
573             END;
574             RETURN format;
575         END;
576     };
577     ora_create_procedure( $detect_format );
578
579     $dbh->do(qq{
580         UPDATE Attachments
581         SET $column_name = $DEFAULT{prefix}detect_format_simple(
582             Parent,
583             ContentType, ContentEncoding,
584             Filename
585         )
586         WHERE $column_name IS NULL
587     });
588     $dbh->do(qq{
589         CREATE OR REPLACE TRIGGER $DEFAULT{prefix}set_format
590         BEFORE INSERT
591         ON Attachments
592         FOR EACH ROW
593         BEGIN
594             :new.$column_name := $DEFAULT{prefix}detect_format_simple(
595                 :new.Parent,
596                 :new.ContentType, :new.ContentEncoding,
597                 :new.Filename
598             );
599         END;
600     });
601     return $column_name;
602 }
603
604 sub ora_create_preference {
605     my %info = @_;
606     my $name = $DEFAULT{'prefix'} . $info{'name'};
607     return $name if $OPT{'dryrun'};
608     do_error_is_ok( $dbh => 'begin ctx_ddl.drop_preference(?); end;', $name );
609     $dbh->do(
610         'begin ctx_ddl.create_preference(?, ?);  end;',
611         undef, $name, $info{'type'}
612     );
613     return $name unless $info{'attributes'};
614
615     while ( my ($attr, $value) = each %{ $info{'attributes'} } ) {
616         $dbh->do(
617             'begin ctx_ddl.set_attribute(?, ?, ?);  end;',
618             undef, $name, $attr, $value
619         );
620     }
621
622     return $name;
623 }
624
625 sub ora_create_procedure {
626     my $text = shift;
627
628     return if $OPT{'dryrun'};
629     my $status = $dbh->do($text, { RaiseError => 0 });
630
631     # Statement succeeded
632     return if $status;
633
634     if ( 6550 != $dbh->err ) {
635         # Utter failure
636         die $dbh->errstr;
637     }
638     else {
639         my $msg = $dbh->func( 'plsql_errstr' );
640         die $dbh->errstr if !defined $msg;
641         die $msg if $msg;
642     }
643 }
644
645 sub dba_handle {
646     if ( $DB{'type'} eq 'Oracle' ) {
647         $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8";
648         $ENV{'NLS_NCHAR'} = "AL32UTF8";
649     }
650     my $dsn = do { my $h = new RT::Handle; $h->BuildDSN; $h->DSN };
651     my $dbh = DBI->connect(
652         $dsn, $DB{admin}, $DB{admin_password},
653         { RaiseError => 1, PrintError => 1 },
654     );
655     unless ( $dbh ) {
656         die "Failed to connect to $dsn as user '$DB{admin}': ". $DBI::errstr;
657     }
658     return $dbh;
659 }
660
661 sub do_error_is_ok {
662     my $dbh = shift;
663     local $dbh->{'RaiseError'} = 0;
664     local $dbh->{'PrintError'} = 0;
665     return $dbh->do(shift, undef, @_);
666 }
667
668 sub do_print_error {
669     my $dbh = shift;
670     local $dbh->{'RaiseError'} = 0;
671     local $dbh->{'PrintError'} = 1;
672     return $dbh->do(shift, undef, @_);
673 }
674
675 sub prompt {
676     my %args = ( @_ );
677     return $args{'default'} if $args{'silent'};
678
679     local $| = 1;
680     print $args{'message'};
681     if ( $args{'default'} ) {
682         print "\n[". $args{'default'} .']: ';
683     } else {
684         print ":\n";
685     }
686
687     my $res = <STDIN>;
688     chomp $res;
689     print "\n";
690     return $args{'default'} if !$res && $args{'default'};
691     return $res;
692 }
693
694 sub verbose  { print @_, "\n" if $OPT{verbose} || $OPT{verbose}; 1 }
695 sub debug    { print @_, "\n" if $OPT{debug}; 1 }
696 sub error    { $RT::Logger->error( @_ ); verbose(@_); 1 }
697 sub warning  { $RT::Logger->warning( @_ ); verbose(@_); 1 }
698
699 sub show_help {
700     my $error = shift;
701     RT::Interface::CLI->ShowHelp(
702         ExitValue => $error,
703         Sections => 'NAME|DESCRIPTION',
704     );
705 }
706
707 sub print_rt_config {
708     my %args = @_;
709     my $config = <<END;
710
711 You can now configure RT to use the newly-created full-text index by
712 adding the following to your RT_SiteConfig.pm:
713
714 Set( %FullTextSearch,
715     Enable     => 1,
716     Indexed    => 1,
717 END
718
719     $config .= sprintf("    %-10s => '$args{$_}',\n",$_)
720         foreach grep defined $args{$_}, keys %args;
721     $config .= ");\n";
722
723     print $config;
724 }
725
726 sub insert_schema {
727     my $dbh = dba_handle();
728     my $message = "Going to run the following in the DB:";
729     my $schema = shift;
730     print "$message\n";
731     my $disp = $schema;
732     $disp =~ s/^/    /mg;
733     print "$disp\n\n";
734     return if $OPT{'dryrun'};
735
736     my $res = $dbh->do( $schema );
737     unless ( $res ) {
738         die "Couldn't run DDL query: ". $dbh->errstr;
739     }
740 }
741
742 sub insert_data {
743     return if $OPT{dryrun};
744
745     print "Indexing existing data...\n";
746
747     $ENV{RT_FTS_CONFIG} = JSON::to_json( {Enable => 1, Indexed => 1, @_});
748     system( "$RT::SbinPath/rt-fulltext-indexer", "--all",
749             ($DB{'batch-size'} ? ("--limit", $DB{'batch-size'}) : ()));
750 }
751
752 =head1 NAME
753
754 rt-setup-fulltext-index - Create indexes for full text search
755
756 =head1 DESCRIPTION
757
758 This script creates the appropriate tables, columns, functions, and / or
759 views necessary for full-text searching for your database type.  It will
760 drop any existing indexes in the process.
761
762 Please read F<docs/full_text_indexing.pod> for complete documentation on
763 full-text indexing for your database type.
764
765 If you have a non-standard database administrator user or password, you
766 may use the C<--dba> and C<--dba-password> parameters to set them
767 explicitly:
768
769     rt-setup-fulltext-index --dba sysdba --dba-password 'secret'
770
771 To test what will happen without running any DDL, pass the C<--dryrun>
772 flag.
773
774 The Oracle index determines which content-types it will index at
775 creation time. By default, textual message bodies and textual uploaded
776 attachments (attachments with filenames) are indexed; to ignore textual
777 attachments, pass the C<--no-attachments> flag when the index is
778 created.
779
780
781 =head1 AUTHOR
782
783 Ruslan Zakirov E<lt>ruz@bestpractical.comE<gt>,
784 Alex Vandiver E<lt>alexmv@bestpractical.comE<gt>
785
786 =cut
787