#!perl

## no critic: ControlStructures::ProhibitMutatingListFunctions

use 5.010001;
use strict;
use warnings;
use Log::ger::Screen;

use Getopt::Long qw(:config gnu_getopt no_ignore_case);
use Text::CSV_XS qw(csv);

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2023-12-29'; # DATE
our $DIST = 'App-fsql'; # DIST
our $VERSION = '0.232'; # VERSION

my %Opts = (
    format => undef,
    action => 'query',
    hash   => 0,
    functions => [],
    function_defs => [],
);
my %Tables;

sub _prepare_tempdir {
   require File::Temp;

    state $tempdir;

    return $tempdir if $tempdir;
    $tempdir = File::Temp::tempdir(
        CLEANUP => $ENV{DEBUG_KEEP_TEMPDIR} ? 0:1);
    log_debug("Created tempdir: $tempdir");
    return $tempdir;
}

# parse filename and tablename from arg, copy stdin to tempfile
sub _preprocess_arg {
    my ($arg, $funcopts) = @_;

    $funcopts->{copy_stdin_to_tempfile} //= 1;

    state $stdin_specified;

    my $file;
    my $fileopts = {};
    if ($arg eq '-') {
        $file  = '-';
        $fileopts->{table} = 'stdin';
    } elsif ($arg =~ /\A(?:(\w+(?:::\w+)*)|([^:]+))(?::(.*))?\z/) {
        my $tabledata_mod = $1;
        $file  = $2;
        my $table_or_fileopts = $3;
        log_trace "tabledata_mod=<%s>, file=<%s>, table_or_fileopts=<%s>", $tabledata_mod, $file, $table_or_fileopts;

        # try a TableData module first
      TRY_TABLEDATA:
        {
            last unless defined $tabledata_mod;
            require Module::Load::Util;
            require TableDataRole::Util::CSV; # to help prereq scanner detect this prereq
            my $tabledata_obj;
            my $tabledata_pkg;
            eval {
                $tabledata_obj = Module::Load::Util::instantiate_class_with_optional_args(
                    {ns_prefixes => ['', 'TableData']},
                    $tabledata_mod,
                );
                $tabledata_pkg = ref($tabledata_obj);
                require Role::Tiny;
                eval { Role::Tiny->apply_roles_to_object($tabledata_obj, "TableDataRole::Util::CSV") };
                log_warn "Can't apply role TableDataRole::Util::CSV to $tabledata_obj: $@" if $@;
            };
            if ($@) {
                log_warn "Can't load TableData module '$tabledata_mod': $@";
                $file = $tabledata_mod;
                last;
            }
            # dump the tabledata content as CSV
            my $tempdir = _prepare_tempdir();
            my $i = 0;
            while (1) {
                $file = $tabledata_pkg . ($i ? ".$i" : "") . ".csv";
                $file =~ s/::/_/g;
                last unless -f "$tempdir/$file";
                $i++;
            }
            $file = "$tempdir/$file";
            log_debug "Writing tabledata module '$tabledata_mod' to CSV file '$file' ...";
            open my $fh, ">", $file;
            print $fh $tabledata_obj->as_csv;
            close $fh;
        }

        if ($table_or_fileopts =~ /=/) {
            for (split /:/, $table_or_fileopts) {
                if (/(.+)=(.*)/) {
                    $fileopts->{$1} = $2;
                } else {
                    die "Invalid option '$_', please use 'OPT=VAL' syntax";
                }
            }
        } else {
            $fileopts->{table} = $table_or_fileopts;
        }
    } else {
        die "Invalid argument '$arg', please use '-' or FILENAME[:TABLENAME|OPT1=VAL1:...]\n";
    }

  L1:
    unless (defined $fileopts->{table}) {
        $fileopts->{table} = $file;
        for ($fileopts->{table}) {
            s!.+[/\\]!!; # strip path
            s!\.\w+\z!!; # strip filename extension
            s/[^A-Za-z_0-9]+/_/g;
        }
    }

    unless ($fileopts->{table} =~ /\A[A-Za-z_][A-Za-z_0-9]*\z/) {
        warn "fsql: Invalid table name $fileopts->{table}, ".
            "please use letter+alphanums only\n";
        exit 99;
    }
    if (exists $Tables{$fileopts->{table}}) {
        warn "fsql: Duplicate table name $fileopts->{table}, ".
            "please use another name\n";
        exit 99;
    }

    my ($fh, $tempfile);
    if ($file eq '-') {
        if ($stdin_specified++) {
            warn "fsql: stdin cannot be specified more than once\n";
            exit 99;
        }
        if ($funcopts->{copy_stdin_to_tempfile}) {
            my $tempdir = _prepare_tempdir();
            $tempfile = "$tempdir/$fileopts->{table}";
            open $fh, ">", $tempfile
                or die "fsql: Can't write to tempfile $tempfile: $!\n";
            print $fh $_ while ($_ = <STDIN>);
            open $fh, "<", $tempfile
                or die "fsql: BUG: Can't reopen tempfile $tempfile: $!\n";
        } else {
            $fh = *STDIN;
        }
    } else {
        open $fh, "<", $file
            or die "fsql: Can't open $file: $!\n";
    }

    return {
        %$fileopts,
        orig_file => $file,
        file      => $tempfile // $file,
        fh        => $fh,
    };
}

sub _register_table {
    my ($arginfo, %overrides) = @_;
    $Tables{$arginfo->{table}} = {
        file      => $arginfo->{file},
        orig_file => $arginfo->{orig_file},
        fmt       => 'csv',
        %overrides,
    };
}

sub _add {
    my ($arg, $arginfo) = @_;
    log_debug "Adding $arg ...";
    $arginfo //= _preprocess_arg($arg);

    # try to detect type
    my $type;
    {
        if ($arginfo->{orig_file} =~ /\.(\w+)\z/) {
            my $ext = lc $1;
            if ($ext =~ /^csv$/) {
                $type = 'csv'; last;
            } elsif ($ext =~ /^tsv$/) {
                $type = 'tsv'; last;
            } elsif ($ext =~ /^ltsv$/) {
                $type = 'ltsv'; last;
            } elsif ($ext =~ /^(json|js)$/) {
                $type = 'json'; last;
            } elsif ($ext =~ /^ya?ml$/) {
                $type = 'yaml'; last;
            } elsif ($ext =~ /^(perl|pl|pm)$/) {
                $type = 'perl'; last;
            }
        }

        # read a few lines from file
        my $fh = $arginfo->{fh};
        my $i = 1;
        my $content = '';
        my $firstline;
        while (<$fh>) {
            $firstline = $_ if $i == 1;
            $content .= $_;
            last if ++$i > 5;
        }

        last unless defined $firstline;
        if ($firstline =~ /\t/) {
            if ($firstline =~ /:/) {
                $type = 'ltsv';
            } else {
                $type = 'tsv';
            }
        } elsif ($content =~ /=>/) {
            $type = 'perl';
        } elsif ($content =~ /[A-Za-z_]\w*, / || # bareword element, e.g. [foo, bar]
                     $content =~ /[A-Za-z_]\w*:\s/i) { # bareword key, e.g. [foo: bar]
            $type = 'yaml';
        } elsif ($content =~ /"[^"]*":\S/) { # yaml requires space
            $type = 'json';
        } else {
            $type = 'csv';
        }
        # put file pointer back at the beginning
        seek $arginfo->{fh}, 0, 0;
    }

    $type //= "";
    if ($type eq 'csv') {
        _add_csv($arg, $arginfo);
    } elsif ($type eq 'tsv') {
        _add_tsv($arg, $arginfo);
    } elsif ($type eq 'ltsv') {
        _add_ltsv($arg, $arginfo);
    } elsif ($type eq 'json') {
        _add_json($arg, $arginfo);
    } elsif ($type eq 'yaml') {
        _add_yaml($arg, $arginfo);
    } elsif ($type eq 'perl') {
        _add_perl($arg, $arginfo);
    } else {
        die "fsql: Can't determine table type for '$arg'\n";
    }
}

sub _add_csv {
    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg);
    if (defined $arginfo->{header} && !$arginfo->{header}) {
        # copy csv, add header row
        my $tempdir = _prepare_tempdir();
        my $csv = Text::CSV_XS->new ({ binary => 1, auto_diag => 1 });
        open my $fhi, "<", $arginfo->{file}
            or die "fsql: Can't open $arginfo->{file}: $!\n";
        my $row = $csv->getline($fhi) // ["col0"];
        my $fno = "$tempdir/$arginfo->{table}";
        open my $fho, ">", $fno or die "fsql: Can't write to $fno: $!\n";
        $arginfo->{file} = $fno;
        print $fho join(",", map {"col$_"} 0..$#{$row}), "\n";
        seek $fhi, 0, 0;
        print $fho $_ while <$fhi>;
        close $fhi;
        close $fho;
    }
    _register_table($arginfo);
}

sub _add_tsv {
    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg);

    my $tempdir = _prepare_tempdir();
    my $aoa = csv(in => $arginfo->{file}, sep_char=>"\t");
    if (defined $arginfo->{header} && !$arginfo->{header}) {
        # add header row
        my $numcols = @$aoa ? @{$aoa->[0]} : 0;
        $numcols = 1 if $numcols < 1;
        unshift @$aoa, [map {"col$_"} 0..$numcols-1];
    }
    my $outfile = "$tempdir/$arginfo->{table}";
    csv(in => $aoa, out=>$outfile);
    _register_table($arginfo, file=>$outfile, fmt=>'tsv');
}

sub _add_ltsv {
    require Text::LTSV;

    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg);

    my $tempdir = _prepare_tempdir();
    my $ltsv = Text::LTSV->new;
    my $aoh = $ltsv->parse_file($arginfo->{fh});
    my $outfile = "$tempdir/$arginfo->{table}";
    csv(in => $aoh, out => $outfile);
    _register_table($arginfo, file=>$outfile, fmt=>'ltsv');
}

sub _res_to_csv {
    require Perinci::Result::Util;

    my ($res, $arginfo, $fmt) = @_;
    my $tf;
    if (Perinci::Result::Util::is_env_res($res)) {
        $tf = $res->[3]{"table.fields"}
            if $res->[3] && $res->[3]{"table.fields"};
        $res = $res->[2];
    }

    unless (ref($res) eq 'ARRAY') {
        warn "fsql: Data is not an array: $arginfo->{file}\n";
        exit 99;
    }

    my $tempdir = _prepare_tempdir();
    my $outfile = "$tempdir/$arginfo->{table}";

    # handle special case of zero rows
    unless (@$res) {
        csv(in => [], headers => $tf ? $tf : ["column0"],
            out => $outfile);
        goto DONE;
    }

    my $row0 = $res->[0];

    # handle another special case of array of scalars
    unless (ref($row0) eq 'ARRAY' || ref($row0) eq 'HASH') {
        csv(in => [map {[$_]} @$res], headers=>["column0"],
            out => $outfile);
        goto DONE;
    }

    # produce headers for aoa without tf
    if (ref($row0) eq 'ARRAY' && !$tf) {
        $tf = [map {"column$_"} 0..@$row0-1];
    }

    csv(in=>$res, headers=>$tf, out=>$outfile);

  DONE:
    _register_table($arginfo, file => $outfile, fmt => $fmt);
}

sub _add_json {
    require JSON::MaybeXS;

    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg, {copy_stdin_to_tempfile=>0});

    state $json = JSON::MaybeXS->new->allow_nonref;
    my $res;
    {
        local $/;
        my $fh = $arginfo->{fh}; # perl's diamond syntax limitation
        my $content = <$fh>;
        $res = $json->decode($content);
    }
    _res_to_csv($res, $arginfo, 'json');
}

sub _add_yaml {
    require YAML::XS;

    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg);

    # YAML::XS::LoadFile doesn't accept filehandle
    my $res = YAML::XS::LoadFile($arginfo->{file});
    _res_to_csv($res, $arginfo, 'yaml');
}

sub _add_perl {
    my ($arg, $arginfo) = @_;
    $arginfo //= _preprocess_arg($arg, {copy_stdin_to_tempfile=>0});

    my $res;
    {
        local $/;
        my $fh = $arginfo->{fh};
        my $content = <$fh>;
        $res = eval $content;
        die if $@;
    }
    _res_to_csv($res, $arginfo, 'perl');
}

sub parse_cmdline {
    require Log::ger::Util;

    my $res = GetOptions(
        'format|f=s'     => \$Opts{format},
        'add|a=s'        => sub { _add($_[1]) },
        'add-csv=s'      => sub { _add_csv($_[1]) },
        'add-tsv=s'      => sub { _add_tsv($_[1]) },
        'add-ltsv=s'     => sub { _add_ltsv($_[1]) },
        'add-json=s'     => sub { _add_json($_[1]) },
        'add-yaml=s'     => sub { _add_yaml($_[1]) },
        'add-perl=s'     => sub { _add_perl($_[1]) },
        'add-tabledata=s' => sub { _add_tabledata($_[1]) },
        'load-function|F=s' => sub {
            die "Invalid function name syntax '$_[1]', please use alphanums only\n"
                unless $_[1] =~ /\A[A-Za-z_][A-Za-z_0-9]*(:[A-Za-z_][A-Za-z_0-9]*)?\z/;
            push @{ $Opts{functions} }, uc($_[1]);
        },
        'define-function|D=s' => sub {
            die "Invalid function definition syntax, please provide name:code\n"
                unless $_[1] =~ /(.+?):(.+)/;
            my ($name, $code) = ($1, $2);
            die "Invalid function name syntax '$name', please use alphanums only\n"
                unless $name =~ /\A[A-Za-z_][A-Za-z_0-9]*\z/;
            my $compiled_code = eval "sub { $code }";
            die "Invalid Perl code in function definition of '$name': $@ (code is: $code)\n" if $@;
            push @{ $Opts{function_defs} }, {name=>uc($name), code=>$compiled_code};
        },
        'show-schema|s'  => sub { $Opts{action} = 'show-schema' },
        'aoh'            => sub { $Opts{hash} = 1 },
        'aoa'            => sub { $Opts{hash} = 0 },
        'log-level=s'    => sub { Log::ger::Util::set_level($_[1]) },
        'trace'          => sub { Log::ger::Util::set_level('trace') },
        'quiet'          => sub { Log::ger::Util::set_level('quiet') },
        'verbose'        => sub { Log::ger::Util::set_level('info') },
        'debug'          => sub { Log::ger::Util::set_level('debug') },
        'help|h'         => sub {
            print <<USAGE;
Usage:
  fsql [OPTIONS]... [ <QUERY> | --show-schema|-s ]
  fsql --help|h
  fsql --version|-v
Options:
  --add=s
  --add-csv=s
  --add-tsv=s
  --add-ltsv=s
  --add-json=s
  --add-yaml=s
  --add-perl=s
  --load-function=s, -F
  --define-function=s, -D
  --format=s, -f
  --aoh, --aoa
For more details, see the manpage/documentation.
USAGE
            exit 0;
        },
        'version|v'      => sub {
            say "fsql version ", ($main::VERSION // "dev"),
                ($main::DATE ? " ($main::DATE)" : "");
            exit 0;
        },
    );
    exit 99 if !$res;

    unless (keys %Tables) {
        _add("-");
    }

    # pick default format from the most used input format
    unless ($Opts{format}) {
        my %fmts;
        $fmts{$Tables{$_}{fmt}}++ for keys %Tables;
        my @fmts = sort {$fmts{$b} <=> $fmts{$a} || $a cmp $b} keys %fmts;
        $Opts{format} = $fmts[0];
    }
}

sub run {
    require DBI;

    my $res;

    if ($Opts{action} eq 'show-schema') {

        my $tt = {};
        for my $t (sort keys %Tables) {
            open my($fh), "<", $Tables{$t}{file}
                or die "fsql: Can't open $Tables{$t}{file}: $!\n";
            my $line1 = <$fh>;
            $line1 =~ s/\r?\n//;
            $Tables{$t}{columns} = [
                # XXX we should perhaps ask DBD::CSV directly
                map {s/\A"(.*)"\z/$1/; s/\W/_/g; lc $_}
                    split /,/, $line1];
        }
        $res = [200, "OK", {tables => \%Tables}];
	$Opts{format} = 'text' if $Opts{format} =~ /^(c|t|lt)sv$/;

    } elsif ($Opts{action} eq 'query') {
        unless (@ARGV) {
            warn "fsql: Please specify query\n";
            exit 99;
        }
        if (@ARGV > 1) {
            warn "fsql: Too many arguments, ".
                "please specify only 1 argument (query)\n";
            exit 99;
        }
        my $query = $ARGV[0];

        my $tempdir = _prepare_tempdir();
        my $dbh = DBI->connect(
            "dbi:CSV:", undef, undef,
            {
                RaiseError => 1,
                csv_tables => {
                    map { $_=>{f_file=>$Tables{$_}{file}} }
                        keys %Tables,
                },
            });

        for my $func (@{ $Opts{functions} }) {
            my ($name, $newname);
            if ($func =~ /(.+):(.+)/) {
                $name = $1;
                $newname = $2;
            } else {
                $name = $newname = $func;
            }
            my $mod = "SQL::Statement::Function::ByName::$name";
            (my $mod_pm = $mod) =~ s!::!/!g; $mod_pm .= ".pm";
            require $mod_pm;
            if (defined &{"$mod\::SQL_FUNCTION_$name"}) {
                $dbh->do(qq{CREATE FUNCTION $newname NAME "$mod\::SQL_FUNCTION_$name"});
            } else {
                # old, deprecated, will be removed someday
                $dbh->do(qq{CREATE FUNCTION $newname NAME "$mod\::$name"});
            }
        }

        for my $entry (@{ $Opts{function_defs} }) {
            no strict 'refs';
            my $addr = "$entry->{code}"; $addr =~ s/CODE\(0x(.+)\)/$1/;
            my $name = "App::fsql::SQL_FUNCTIONS::f$addr";
            *{$name} = $entry->{code};
            $dbh->do(qq{CREATE FUNCTION $entry->{name} NAME "$name"});
        }

        my $sth = $dbh->prepare($query);
        $sth->execute;

        # not SELECT query, no need to display result
        unless ($query =~ /\A\s*SELECT\b/is) {
            return;
        }

        my @rows;
        if ($Opts{hash}) {
            while (my $row = $sth->fetchrow_hashref) {
                push @rows, $row;
            }
        } else {
            while (my @row = $sth->fetchrow_array) {
                push @rows, \@row;
            }
        }
        $res = [200, "OK", \@rows, {"table.fields" => $sth->{NAME}}];

    } else {

        die "BUG: Unknown action\n";

    }

    show_result($res);
}

sub show_result {
    my $res = shift;

    my $ff = $res->[3]{"table.fields"};
    if ($Opts{format} =~ /^[ct]sv$/) {
        csv(in => $res->[2], headers => $ff, out => *STDOUT,
	    sep_char => $Opts{format} eq 'tsv' ? "\t" : ',');
    } elsif ($Opts{format} eq 'ltsv') {
        # Text::LTSV expects a format of [[k=>v, k2=>v2, ...], ...]. we might as
        # well print it ourselves.
        for my $row (@{ $res->[2] }) {
            if (ref($row) eq 'HASH') {
                say join("\t", map {"$_:".($row->{$_} // '')} @$ff);
            } else {
                say join("\t", map {"$ff->[$_]:".($row->[$_] // '')} 0..@$ff-1);
            }
        }
    } elsif ($Opts{format} eq 'perl') {
        require Data::Format::Pretty::Perl;
        print Data::Format::Pretty::Perl::format_pretty($res->[2]);
    } elsif ($Opts{format} eq 'json') {
        require Data::Format::Pretty::JSON;
        print Data::Format::Pretty::JSON::format_pretty($res->[2]);
    } elsif ($Opts{format} eq 'text') {
        require Data::Format::Pretty::Console;
        print Data::Format::Pretty::Console::format_pretty($res->[2]);
    } elsif ($Opts{format} eq 'yaml') {
        require Data::Format::Pretty::YAML;
        print Data::Format::Pretty::YAML::format_pretty($res->[2]);
    } else {
        die "fsql: Invalid output format, please see documentation ".
            "for known output formats\n";
    }
}

# MAIN

parse_cmdline();
run();

1;
# ABSTRACT: Perform SQL queries against {CSV/TSV/LTSV/JSON/YAML files,TableData modules}
# PODNAME: fsql

__END__

=pod

=encoding UTF-8

=head1 NAME

fsql - Perform SQL queries against {CSV/TSV/LTSV/JSON/YAML files,TableData modules}

=head1 VERSION

This document describes version 0.232 of fsql (from Perl distribution App-fsql), released on 2023-12-29.

=head1 SYNOPSIS

 fsql [OPTIONS] [ <QUERY> | --show-schema|-s ]

=head1 DESCRIPTION

B<fsql> lets you perform SQL queries against one or several "flat" files of
various formats. Each file will be regarded as a SQL table. By using SQL
queries, you can do various calculations or manipulations that are otherwise
hard/cumbersome to do with traditional text-manipulating Unix commands like
B<cut>, B<sort>, B<head>, B<tail>, B<uniq>, and so on. Particularly: data
grouping, joining, or filtering with SQL expressions and functions.

As a bonus, you can also modify data (currently CSV only) via SQL INSERT or
DELETE commands.

The query result will then be printed out, in one of several available formats.

The magic of all this is performed by L<DBD::CSV> and L<SQL::Statement>.

To use C<fsql>, you must at least specify one file/table (with C<--add> or one
of the C<--add-TYPE> options). If none of those options are specified, a table
is assumed in STDIN with name C<stdin> and the format will be detected. You must
also specify a SQL query.

=head1 EXIT CODES

0 on success.

255 on I/O or SQL error.

99 on command-line options or input data error.

=head1 OPTIONS

=over

=item * --add=FILENAME[:TABLENAME|OPT1=VAL1:...] or --add=MODULENAME[:TABLENAME|OPT=VAL1:...], -a

Add a table from a file. Type will be detected from filename extension (and some
heuristics, if there is no file extension or extension is unrecognized). Die if
type cannot be detected.

Can also add a table from a L<TableData> module, in which case C<MODULENAME>
should be in the form of C<TableData::*>.

Sometimes the detection will miss. Alternatively, you can use one of the
C<--add-TYPE> options to add a specific table type.

If C<TABLENAME> is not specified, it will be taken from C<FILENAME> (e.g. with
filename C<foo-bar.csv>, table name will be C<foo_bar>). Will croak if duplicate
table name is detected. Table name must match regex
C</\A[A-Za-z_][A-Za-z_0-9]*\z/>.

C<FILENAME> can be C<-> to mean the standard input (the default table name will
be C<stdin>).

Alternatively, you can specify table name as well as other option using the
OPT1=VAL1:... syntax. Known options include:

=over

=item + table=TABLENAME

Specify table name.

=item + header=BOOL

Default is 1. If set to 0, specifies that CSV/TSV file does not contain header
row that contains column names. Column names will be C<col0>, C<col1>, C<col2>,
and so on. Has no effect on file types other than CSV/TSV.

=back

=item * --add-csv=FILENAME[:TABLENAME|OPT1=VAL1:...]

Add a table from a CSV file.

=item * --add-tsv=FILENAME[:TABLENAME|OPT1=VAL1:...]

Like C<--add-csv>, but will load file as TSV (tab-separated value).

=item * --add-ltsv=FILENAME[:TABLENAME|OPT1=VAL1:...]

Like C<--add-csv>, but will load file as LTSV (labeled tab separated value, see
L<Text::LTSV>). Names of columns will be taken from the first row.

=item * --add-json=FILENAME[:TABLENAME|OPT1=VAL1:...]

Like C<--add-csv>, but will load file as JSON.

Data can be array, or array of arrays, or array of hashes, or an enveloped
response (see L<Rinci::function>), so it is suitable to accept piped output of
L<Perinci::CmdLine>-based programs.

=item * --add-yaml=FILENAME[:TABLENAME|OPT1=VAL1:...]

Like C<--add-json>, but will load file as YAML.

=item * --add-perl=FILENAME[:TABLENAME|OPT1=VAL1:...]

Like C<--add-json>, but will load file as Perl.

=item * --add-tabledata=MODULENAME[:TABLENAME|OPT1=VAL1:...]

Add a table from a L<TableData> module. C<MODULENAME> is the module name
with/without the C<TableData::> prefix (both will be attempted).

=item * --load-function=NAME, -F

Load a SQL function. This will load Perl module
C<SQL::Statement::Function::ByName::NAME>. See CPAN for list of available
modules.

If you use C<NAME:NEWNAME>, you can load a SQL function as another name, for
example:

 -F DAYOFYEAR:DOY

=item * --define-function=NAME:CODE, -D

Define a SQL function. You need to specify name as well as perl code that
implements it. Perl code will be wrapped in a subroutine, it should expect the
function argument in C<$_[2]> (for more details see
L<SQL::Statement::Functions>). Example:

 --define-function 'MONTH:$_[2] =~ /^\d{4}-(\d{2})-/ or return undef; $1+0'

=item * --aoa

Return array of array (the default). Only relevant to outputs like C<perl>,
C<json>, C<yaml>, C<text>.

=item * --aoh

Return array of hashes instead of the default array of array, where each row is
represented as a hash (dictionary/associated array) instead of an array. Only
relevant to output formats like C<perl>, C<json>, C<yaml>, C<text>.

Returning a hash is convenient when you want column name information on each
row, but you can't specify the same column twice and order of columns are not
guaranteed.

=item * --format=FORMAT (default: text), -f

Set output format.

The value C<csv> or C<tsv> or C<ltsv> will cause query results to be output as a
comma-separated or TAB-separated list or labeled-TAB separated list,
respectively. As this isn't very useful for a schema listing, these values will
be silently converted to C<text> if C<--show-schema> (C<-s>) is also present.

The other values C<perl>, C<json>, C<yaml>, and C<text> will be formatted using
appropriate L<Data::Format::Pretty> formatter.

The default value is the most used table format. So if your tables are mostly
CSV, B<fsql> will also output CSV by default.

=item * --show-schema, -s

Instead of running a query, show schema instead. This is useful for debugging.

=back

=head1 FAQ

=head2 What SQL dialect is supported? Why is SQL feature "X" not supported?

See L<SQL::Statement::Syntax> for the range of supported SQL syntax. In short,
you can do select with several kinds of joins, almost/around 100 builtin
functions (with additional functions available from Perl modules, see next FAQ
entry), etc.

Also, sometimes if there is SQL parsing error, the error message is not
immediately obvious and can be a bit confusing. That is usually a
parsing limitation/bug within SQL::Statement.

=head2 How do I define more SQL functions? Why aren't there more date/time/X functions?

SQL::Statement allows loading Perl functions as SQL functions. There are several
CPAN modules (see the C<SQL::Statement::Function::ByName::> namespace) which
nicely package them so you can load them from B<fsql> simply by using the C<-F>
option, e.g. to load the YEAR() function:

 % fsql -F YEAR --add-csv sometable.csv 'SELECT c1, YEAR(c2) FROM sometable ...'

=head1 ENVIRONMENT

=head2 DEBUG => bool

If set to true, print debugging messages.

=head2 DEBUG_KEEP_TEMPDIR => bool

If set to true, will not cleanup tempdir.

=head1 EXAMPLES

Filter CSV (table from stdin is aptly named so):

 % prog-that-produces-csv | fsql 'SELECT id,name FROM stdin WHERE id <= 1000' > final.csv

Pick output format, produce array of hashes instead of the default array of
arrays:

 % fsql -a ~/book.pl 'SELECT title,name FROM book WHERE year >= 2010' --aoh -f json

You can perform joins, of course:

 % fsql -a t.json -a 2.csv:t2 'SELECT * FROM t1 LEFT JOIN t2 ON t1.uid=t2.id'

Show schema:

 % fsql -a table1.json -a 2.csv:table2 -s

Insert row to CSV:

 % fsql -a file.csv 'INSERT INTO file VALUES (1, 2, 3)'

Delete rows from CSV:

 % fsql -a file.csv 'DELETE FROM file WHERE c1 < 10'

Input is CSV or TSV without header row (columns will be named 'col0', 'col1',
and so on):

 % fsql -a file.csv:header=0:table=t 'SELECT col1,col3 FROM t WHERE col0 <= 1000' > final.csv

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/App-fsql>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-App-fsql>.

=head1 SEE ALSO

L<TableData>

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 CONTRIBUTING


To contribute, you can send patches by email/via RT, or send pull requests on
GitHub.

Most of the time, you don't need to build the distribution yourself. You can
simply modify the code, then test via:

 % prove -l

If you want to build the distribution (e.g. to try to install it locally on your
system), you can install L<Dist::Zilla>,
L<Dist::Zilla::PluginBundle::Author::PERLANCAR>,
L<Pod::Weaver::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
Dist::Zilla- and/or Pod::Weaver plugins. Any additional steps required beyond
that are considered a bug and can be reported to me.

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2023, 2021, 2019, 2016, 2015, 2014 by perlancar <perlancar@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-fsql>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=cut
