#!/usr/pkg/bin/perl -w
#-*-perl-*-

#  Copyright 2008 Google Inc.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

use strict;
use Getopt::Long;
use DBI;
use FileHandle;

my ($dsn, $uid, $pwd);
$dsn = $ENV{'DB_DSN'};
$uid = $ENV{'DB_UID'};
$pwd = $ENV{'DB_PWD'};

my $CRUSH_DEBUG = $ENV{'CRUSH_DEV_DEBUG'};

# --file or --sql contain the query.
# --input will be a stream of values for insert or update queries
# --delimiter is provided for spliting input lines and/or joining output.

my ($help, $sqlfile, $sql, $infile, $outfile, $d, $show_version, $include_header);
$d = $ENV{"DELIMITER"} || chr(0xfe);
Getopt::Long::Configure( "no_ignore_case" );
GetOptions('help' => \$help,
           'file=s' => \$sqlfile,
           'sql=s' => \$sql,
           'input=s' => \$infile,
           'output=s' => \$outfile,
           'delimiter=s' => \$d,
           'dsn=s' => \$dsn,
           'uid=s' => \$uid,
           'pwd=s' => \$pwd,
           'Version' => \$show_version,
           'Header'  => \$include_header,
          );

if ($help) {
  usage();
  exit(1);
}
if ($show_version) {
  crush_version();
  exit(0);
}

$d = expand_chars($d);

if (! (defined($dsn) && defined($uid) && defined($pwd) )) {
  print STDERR "$0: need db connect info in environment vars DB_[DSN|UID|PWD]\n";
  exit(1);
}

my $query;
if (defined($sql)) {
  $query = $sql;
} elsif (defined($sqlfile)) {
  open(SFILE, $sqlfile) or die "$0: $sqlfile: $!\n";
  $query = join '', <SFILE>;
  close(SFILE);
} else {
  usage();
  print STDERR "$0: -s or -f must be specified.\n";
  exit(1);
}

my $fhout;
if (defined($outfile) and $outfile ne '-') {
  $fhout = new FileHandle($outfile, "w");
  die "$0: $outfile: $!\n" unless $fhout;
} else {
  $fhout = new FileHandle;
  $fhout->fdopen("STDOUT", "w");
  die "$0: stdout: $!\n" unless $fhout;
}

my $fhin;
if (defined($infile) and $infile ne '-') {
  $fhin = new FileHandle($infile, "r");
  die "$0: $infile: $!\n" unless $fhin;
} else {
  $fhin = new FileHandle;
  $fhin->fdopen("STDIN", "r");
  die "$0: stdin: $!\n" unless $fhin;
}

my $dbh = DBI->connect($dsn, $uid, $pwd, {RaiseError => 1, AutoCommit => 1} );
my $dbq = $dbh->prepare($query);
my $header_printed = 0;

# NOTE: Some DB Drivers require an execute() before the Statement handle has
# access to NUM_OF_FIELDS.
if ($dbq->{NUM_OF_PARAMS}) {
  # This query has placeholders.
  my $input_line;
  while ($input_line = <$fhin>) {
    $input_line =~ s/[\r\n]*//;
    print STDERR "$0: Executing with placeholder values: $input_line\n" if $CRUSH_DEBUG;
    chomp(my @vals = split(/\Q$d\E/o, $input_line));
    $dbq->execute(@vals);
    if ($dbq->{NUM_OF_FIELDS}) {
      # DB-read query.
      if (! $header_printed && $include_header) {
        print $fhout join($d, @{ $dbq->{NAME} }), qq(\n);
        $header_printed = 1;
      }
      fetch_and_print_results($dbq, $fhout);
    }
  }
} else {
  print STDERR "$0: Executing with no placeholder values\n" if $CRUSH_DEBUG;
  $dbq->execute();
  if ($dbq->{NUM_OF_FIELDS}) {
    # DB-read query.
    if (! $header_printed && $include_header) {
      print $fhout join($d, @{ $dbq->{NAME} }), qq(\n);
      $header_printed = 1;
    }
    fetch_and_print_results($dbq, $fhout);
  }
}
 
$dbh->disconnect();
$fhin->close;
$fhout->close;

exit(0);


sub fetch_and_print_results {
  my ($qh, $fh) = @_;
  while (my $results = $qh->fetchrow_arrayref()) {
    for (my $ri = 0; $ri < scalar(@{ $results }); $ri++) {
      if (! defined($results->[$ri])) {
        $results->[$ri] = '';
      }
    }
    print $fh join($d, @{ $results }) , qq(\n);
  }
}


sub usage {
  print STDERR << "ENDUSAGE";

runs a query.

usage: $0 <-f <sql-file> | -s <query>> [options]

options:

  -h, --help           print this message and exit
  -H, --Header         output the sql field names
  -f, --file <file>    take sql from specified file
  -s, --sql <query>    take sql on the commandline
  -i, --input <file>   input of placeholder values (default: stdin)
  -o, --output <file>  write to specified file (default: stdout)
  --delimiter <delim>  use specified field separator for input and output
  --dsn <dsn>          use DBI-style data-source name
  --uid <username>     database username
  --pwd <password>     database password

If --dsn, --uid, or --pwd aren't specified, environment variables DB_DSN,
DB_UID, and DB_PWD will be used for the unprovided values.

If the query contains value placeholders ('?'), values for those placeholders
will be taken from the file named by --input, or from STDIN.

ENDUSAGE
}

=item * expand_chars

expand escape sequences like '\t' in a string to their expansions.

=cut
sub expand_chars {
  my $d = shift || return;
  eval("sprintf(\"$d\")");
}

=item * field_str()

returns the 0-based index of the first field in a delimited string equal to
the specified value, or undef if not found.

=cut
sub field_str {
  my $value = shift;
  my $string = shift;
  my $delim = shift;
  $string =~ s/[\r\n]//g;
  my @a = split(/\Q$delim\E/, $string);
  my $i;
  for $i (0 .. $#a) {
    if ($a[$i] eq $value) {
      return $i;
    }
  }
  return undef;
}

=item * fields_in_line()

Counts the number of fields in a delimited string.

=cut
sub fields_in_line {
  my $str = shift;
  my $delim = shift;
  my $n = 1;
  my $i = 0;
  while (($i = index($str, $delim, $i)) > 0) {
    $n++;
    $i += length($delim);
  }
  return $n;
}

=item * get_line_field($line, $field_index, $delim)

Get the data at position field from the delim deliminated string line.

$field_index is 0 based

=cut
sub get_line_field {
  my $pos = 0;
  for (my $i = 0; $i < $_[1]; $i++) {
    $pos = index($_[0], $_[2], $pos);
    $pos++;
  }
  my $end_pos = index($_[0], $_[2], $pos) - $pos;
  $end_pos = length($_[0]) - $pos if $end_pos <= 0;
  return substr($_[0], $pos, $end_pos);
}

=item * expand_nums($arg [, $adjust])

Turn a string of comma-separated numbers and number ranges into an array of
numbers. If specified, $adjust is added to each value after expansion. E.g.,
If turning 1-based field indexes into array indexes, pass -1 as the adjust
value.

=cut
sub expand_nums {
  my $arg = shift;
  my $adjust = shift || 0;
  my @fields = split(',', $arg);
  my @idxs = ();
  foreach my $f (@fields) {
    if ($f =~ /(\d+)-(\d+)/) {
      push(@idxs, $1 .. $2);
    } elsif ($f =~ /\d+/) {
      push(@idxs, $f);
    } else {
      use Carp;
      croak "Invalid value in numeric list: $f";
    }
  }
  if ($adjust) {
    foreach my $i (0 .. $#idxs) {
      $idxs[$i] += $adjust;
    }
  }
  return @idxs;
}


1;


sub crush_version {
  print STDERR "CRUSH tools release 2013-04 compiled at 2025-10-03-13:58:39\n";
}

1;
