# ####################
# Parsing SQL commands
package XBase::SQL::Expr;
package XBase::SQL;
use strict;
use vars qw( $VERSION %COMMANDS );
$VERSION = '0.129';
# #################################
# Type conversions for create table
my %TYPES = ( 'char' => 'C', 'varchar' => 'C',
'num' => 'N', 'numeric' => 'N', 'int' => 'N',
'integer' => 'N', 'float' => 'F', 'boolean' => 'L',
'blob' => 'M', 'memo' => 'M', 'date' => 'D',
'time' => 'T', 'datetime' => 'T' );
# ##################
# Regexp definitions
%COMMANDS = (
# Top level SQL commands
'COMMANDS' => 'SELECT | INSERT | DELETE | UPDATE | CREATE | DROP',
'SELECT' => 'select ( SELECTALL | SELECTFIELDS ) from TABLE WHERE ?
ORDERBY ?',
'INSERT' => 'insert into TABLE ( \( INSERTFIELDS \) ) ? values
\( INSERTCONSTANTS \)',
'DELETE' => 'delete from TABLE WHERE ?',
'UPDATE' => 'update TABLE set SETCOLUMNS WHERE ?',
'CREATE' => 'create table TABLE \( COLUMNDEF ( , COLUMNDEF ) * \)',
'DROP' => 'drop table TABLE',
# table, field name, number, string
'TABLE' => '\\S+',
'FIELDNAME' => '[a-z_][a-z0-9_]*',
'NUMBER' => q'-?\d*\.?\d+',
'STRING' => q! \\" STRINGDBL \\" | \\' STRINGSGL \\' !,
'STRINGDBL' => q' STRINGDBLPART ( \\\\. STRINGDBLPART ) * ',
'STRINGSGL' => q' STRINGSGLPART ( \\\\. STRINGSGLPART ) * ',
'STRINGDBLPART' => q' [^\\\\"]* ',
'STRINGSGLPART' => q! [^\\\\']* !,
# select fields
'SELECTFIELDS' => 'SELECTFIELD ( , SELECTFIELD ) *',
'SELECTFIELD' => 'FIELDNAME',
'SELECTALL' => q'\*',
# where clause
'WHERE' => 'where WHEREEXPR',
'WHEREEXPR' => 'BOOLEAN',
'BOOLEAN' => q'\( BOOLEAN \) | RELATION ( ( AND | OR ) BOOLEAN ) *',
'RELATION' => 'EXPFIELDNAME ( is not ? null | LIKE CONSTANT_NOT_NULL | RELOP ARITHMETIC )',
'EXPFIELDNAME' => 'FIELDNAME',
'AND' => 'and',
'OR' => 'or',
'LIKE' => 'not ? like',
'RELOP' => [ qw{ == | = | <= | >= | <> | != | < | > } ],
'ARITHMETIC' => [ qw{ \( ARITHMETIC \)
| ( CONSTANT | EXPFIELDNAME ) ( ( \+ | \- | \* | \/ | \% ) ARITHMETIC ) ? } ],
'CONSTANT' => ' CONSTANT_NOT_NULL | NULL ',
'CONSTANT_NOT_NULL' => ' BINDPARAM | NUMBER | STRING ',
'BINDPARAM' => q'\?',
'NULL' => 'null',
'ORDERBY' => 'order by ORDERFIELDNAME ( asc | ORDERDESC ) ?',
'ORDERDESC' => 'desc',
'ORDERFIELDNAME' => 'FIELDNAME',
# insert definitions
'INSERTFIELDS' => 'FIELDNAME ( , FIELDNAME ) *',
'INSERTCONSTANTS' => 'CONSTANT ( , CONSTANT ) *',
# update definitions
'SETCOLUMNS' => 'SETCOLUMN ( , SETCOLUMN ) *',
'SETCOLUMN' => 'FIELDNAME = ARITHMETIC',
# create definitions
'COLUMNDEF' => 'COLUMNKEY | COLUMNNAMETYPE ( not null ) ?',
'COLUMNKEY' => 'primary key \( FIELDNAME \)',
'COLUMNNAMETYPE' => 'FIELDNAME FIELDTYPE',
'FIELDTYPE' => 'TYPECHAR | TYPENUM | TYPEBOOLEAN | TYPEMEMO | TYPEDATE',
'TYPECHAR' => ' ( varchar | char ) ( \( TYPELENGTH \) ) ?',
'TYPENUM' => '( num | numeric | float | int | integer ) ( \( TYPELENGTH ( , TYPEDEC ) ? \) ) ?',
'TYPEDEC' => '\d+',
'TYPELENGTH' => '\d+',
'TYPEBOOLEAN' => 'boolean | logical',
'TYPEMEMO' => 'memo | blob',
'TYPEDATE' => 'date | time | datetime',
);
# #####################################
# "Expected" messages for various types
my %ERRORS = (
'TABLE' => 'Table name',
'RELATION' => 'Relation',
'ARITHMETIC' => 'Arithmetic expression',
'from' => 'From specification',
'into' => 'Into specification',
'values' => 'Values specification',
'\\(' => 'Left paren',
'\\)' => 'Right paren',
'\\*' => 'Star',
'\\"' => 'Double quote',
"\\'" => 'Single quote',
'STRING' => 'String',
'SELECTFIELDS' => 'Columns to select',
'FIELDTYPE' => 'Field type',
);
# ########################################
# Simplifying conversions during the match
my %SIMPLIFY = (
'STRINGDBL' => sub { join '', '"', get_strings(@_), '"'; },
'STRINGSGL' => sub { join '', '\'', get_strings(@_), '\''; },
'STRING' => sub { my $e = (get_strings(@_))[1];
## $e =~ s/([\\'])/\\$1/g;
"XBase::SQL::Expr->string($e)"; },
'NUMBER' => sub { my $e = (get_strings(@_))[0];
"XBase::SQL::Expr->number($e)"; },
'EXPFIELDNAME' => sub { my $e = (get_strings(@_))[0];
"XBase::SQL::Expr->field('$e', \$TABLE, \$VALUES)"; },
'BINDPARAM' => 'XBase::SQL::Expr->string($BIND->[$startbind++])',
'FIELDNAME' => sub { uc ((get_strings(@_))[0]); },
'WHEREEXPR' => sub { join ' ', get_strings(@_); },
'RELOP' => sub { my $e = (get_strings(@_))[0];
if ($e eq '=') { $e = '=='; }
elsif ($e eq '<>') { $e = '!=';} $e; },
'TABLE' => sub { (get_strings(@_))[0]; },
'ARITHMETIC' => sub { join ' ', get_strings(@_); },
'RELATION' => sub { my @values = get_strings(@_);
local $^W = 0;
my $testnull = join ' ', @values[1 .. 3];
if ($testnull =~ /^is (not )?null ?$/i)
{ return "not $1 defined(($values[0])->value)"; }
elsif ($values[1] =~ /^(not )?like$/i)
{ return "$1(XBase::SQL::Expr->likematch($values[0], $values[2])) " }
else { return join ' ', @values; } },
'NULL' => 'XBase::SQL::Expr->null()',
'AND' => 'and',
'OR' => 'or',
'LIKE' => sub { join ' ', get_strings(@_); },
);
#
#
my %STORE = (
'SELECT' => sub { shift->{'command'} = 'select'; },
'SELECTALL' => 'selectall',
'SELECTFIELD' => 'fields',
### 'SELECTFIELDS' => sub { my ($self, @fields) = @_;
### while (@fields) { push @{$self->{'fields'}}, shift @fields; shift @fields; }},
'INSERT' => sub { shift->{'command'} = 'insert'; },
'INSERTCONSTANTS' => sub { my $self = shift;
my $fntext = 'sub { my ($TABLE, $BIND, $startbind) = @_; map { $_->value() } ' . join(' ', @_) . ' }';
my $fn = eval $fntext;
if ($@) { $self->{'inserterror'} = $@; }
else { $self->{'insertfn'} = $fn; }
},
'INSERTFIELDS' => sub { my ($self, @fields) = @_;
while (@fields) { push @{$self->{'fields'}}, shift @fields; shift @fields; }},
'DELETE' => sub { shift->{'command'} = 'delete'; },
'UPDATE' => sub { shift->{'command'} = 'update'; },
'SETCOLUMNS' => sub { my $self = shift;
my $list = '';
while (@_)
{
push @{$self->{'fields'}}, shift @_;
shift @_;
$list .= shift(@_) . ', ';
shift @_;
}
my $fntext = 'sub { my ($TABLE, $VALUES, $BIND, $startbind) = @_; map { $_->value() } ' . $list . ' }';
my $fn = eval $fntext;
if ($@) { $self->{'updateerror'} = $@; }
else { $self->{'updatefn'} = $fn; }
},
'CREATE' => sub { shift->{'command'} = 'create'; },
'COLUMNNAMETYPE' => sub { my $self = shift;
push @{$self->{'createfields'}}, $_[0];
push @{$self->{'createtypes'}}, $TYPES{lc $_[1]};
push @{$self->{'createlengths'}}, $_[3];
push @{$self->{'createdecimals'}}, $_[5]; },
'DROP' => sub { shift->{'command'} = 'drop'; },
'TABLE' => 'table',
'WHEREEXPR' => sub { my ($self, $expr) = @_;
### print STDERR "Evalling: $expr\n";
my $fn = eval 'sub { my ($TABLE, $VALUES, $BIND, $startbind) = @_; ' . $expr . '; }';
if ($@) { $self->{'whereerror'} = $@; }
else { $self->{'wherefn'} = $fn; }
},
'FIELDNAME' => 'usedfields',
'BINDPARAM' => sub { my $self = shift; $self->{'numofbinds'}++ },
'where' => sub { my $self = shift;
$self->{'bindsbeforewhere'} = $self->{'numofbinds'}; },
'ORDERFIELDNAME' => 'orderfield',
'ORDERDESC' => 'orderdesc',
);
sub parse
{
my ($class, $string) = @_;
my $self = bless {}, $class;
### print STDERR "Parsing $string\n";
# try to match the $string against $COMMANDS{'COMMANDS'}
my ($srest, $error, $errstr, @result) = match($string, 'COMMANDS');
$srest =~ s/^\s+//s;
if ($srest ne '' and not $error)
{ $error = 1; $errstr = 'Extra characters in SQL command'; }
if ($error)
{
if (not defined $errstr) { $errstr = 'Error in SQL command'; }
substr($srest, 40) = '...' if length $srest > 44;
$self->{'errstr'} = "$errstr near `$srest'";
}
else
{
# take the results and store them to $self
### use Data::Dumper; print STDERR Dumper @result;
$self->store_results(\@result, \%STORE);
if (defined $self->{'whereerror'})
{ $self->{'errstr'} = "Some deeper problem: eval failed: $self->{'whereerror'}"; }
### print STDERR Dumper $self;
}
$self;
}
sub match
{
my $string = shift;
my @regexps = @_;
my $origstring = $string;
my $title;
if (@regexps == 1 and defined $COMMANDS{$regexps[0]})
{
$title = $regexps[0];
my $c = $COMMANDS{$regexps[0]};
@regexps = expand( ( ref $c ) ? @$c :
grep { $_ ne '' } split /\s+/, $c);
}
my $modif;
if (@regexps and $regexps[0] eq '?' or $regexps[0] eq '*')
{ $modif = shift @regexps; }
my @result;
my $i = 0;
while ($i < @regexps)
{
my $regexp = $regexps[$i];
my ($error, $errstr, @r);
if (ref $regexp)
{ ($string, $error, $errstr, @r) = match($string, @$regexp); }
elsif ($regexp eq '|')
{ $i = $#regexps; next; }
elsif (defined $COMMANDS{$regexp})
{ ($string, $error, $errstr, @r) = match($string, $regexp); }
elsif ($string =~ s/^\s*?($regexp)(?:$|\b|(?=\W))//si)
{ @r = $1; }
else
{ $error = 1; }
if (defined $error)
{
if ($origstring eq $string)
{
while ($i < @regexps)
{ last if $regexps[$i] eq '|'; $i++; }
next if $i < @regexps;
last if defined $modif;
}
if (not defined $errstr)
{
if (defined $ERRORS{$regexp})
{ $errstr = $ERRORS{$regexp}; }
elsif (defined $title and defined $ERRORS{$title})
{ $errstr = $ERRORS{$title}; }
$errstr .= ' expected' if defined $errstr;
}
return ($string, 1, $errstr, @result);
}
if (ref $regexp)
{ push @result, @r; }
elsif (@r > 1)
{ push @result, $regexp, [ @r ]; }
else
{ push @result, $regexp, $r[0]; }
}
continue
{
$i++;
if (defined $modif and $modif eq '*' and $i >= @regexps)
{ $origstring = $string; $i = 0; }
}
return ($string, undef, undef, @result);
}
sub expand
{
my @result;
my $i = 0;
while ($i < @_)
{
my $t = $_[$i];
if ($t eq '(')
{
$i++;
my $begin = $i;
my $nest = 1;
while ($i < @_ and $nest)
{
my $t = $_[$i];
if ($t eq '(') { $nest++; }
elsif ($t eq ')') { $nest--; }
$i++;
}
$i--;
push @result, [ expand(@_[$begin .. $i - 1]) ];
}
elsif ($t eq '?' or $t eq '*')
{
my $prev = pop @result;
push @result, [ $t, ( ref $prev ? @$prev : $prev ) ];
}
else
{ push @result, $t; }
$i++;
}
@result;
}
sub store_results
{
my ($self, $result) = @_;
my $i = 0;
while ($i < @$result)
{
my ($key, $match) = @{$result}[$i, $i + 1];
my $stval = $STORE{$key};
my $m = $SIMPLIFY{$key};
if (ref $match)
{ $self->store_results($match); }
if (defined $m)
{
my @result = (( ref $m eq 'CODE' ) ? &{$m}( ref $match ? @$match : $match) : $m);
if (@result == 1)
{ $match = $result[0]; }
else
{ $match = [ @result ]; }
$result->[$i + 1] = $match;
}
if (defined $stval)
{
my @result;
if (ref $match) { @result = get_strings($match); }
else { @result = $match; }
if (ref $stval eq 'CODE')
{ &{$stval}($self, @result); }
else
{ push @{$self->{$stval}}, @result; }
}
$i += 2;
}
}
#
#
sub get_strings
{
my @strings = @_;
if (@strings == 1 and ref $strings[0])
{ @strings = @{$strings[0]}; }
my @result; my $i = 1;
while ($i < @strings)
{
if (ref $strings[$i])
{ push @result, get_strings($strings[$i]); }
else
{ push @result, $strings[$i]; }
$i += 2;
}
@result;
}
sub print_result
{
my $result = shift;
my @result = @$result;
my @before = @_;
my $i = 0;
while ($i < @result)
{
my ($regexp, $string) = @result[$i, $i + 1];
if (ref $string)
{ print_result($string, @before, $regexp); }
else
{ print "$string:\t @before $regexp\n"; }
$i += 2;
}
}
# #######################################
# Implementing methods in SQL expressions
package XBase::SQL::Expr;
use strict;
use overload
'+' => sub { XBase::SQL::Expr->number($_[0]->value + $_[1]->value); },
'-' => sub { my $a = $_[0]->value - $_[1]->value; $a = -$a if $_[2];
XBase::SQL::Expr->number($a); },
'/' => sub { my $a = ( $_[2] ? $_[0]->value / $_[1]->value
: $_[1]->value / $_[0]->value );
XBase::SQL::Expr->number($a); },
'%' => sub { my $a = ( $_[2] ? $_[0]->value % $_[1]->value
: $_[1]->value % $_[0]->value );
XBase::SQL::Expr->number($a); },
'<' => \&less,
'<=' => \&lesseq,
'>' => sub { $_[1]->less(@_[0, 2]); },
'>=' => sub { $_[1]->lesseq(@_[0, 2]); },
'!=' => \¬equal,
'<>' => \¬equal,
'==' => sub { my $a = shift->notequal(@_); return ( $a ? 0 : 1); },
'""' => sub { ref shift; },
;
sub new { bless {}, shift; }
sub value { shift->{'value'}; }
sub field {
my ($class, $field, $table, $values) = @_;
my $self = $class->new;
$self->{'field'} = $field;
$self->{'value'} = $values->{$field};
my $type = $table->field_type($field);
if ($type eq 'N') { $self->{'number'} = 1; }
else { $self->{'string'} = 1; }
$self;
}
sub string {
my $self = shift->new;
$self->{'value'} = shift;
$self->{'string'} = 1;
$self;
}
sub number {
my $self = shift->new;
$self->{'value'} = shift;
$self->{'number'} = 1;
$self;
}
sub null {
my $self = shift->new;
$self->{'value'} = undef;
$self;
}
sub other {
my $class = shift;
my $other = shift;
$other;
}
#
# Function working on Expr objects
#
sub less
{
my ($self, $other, $reverse) = @_;
my $answer;
if (defined $self->{'string'} or defined $other->{'string'})
{ $answer = ($self->value lt $other->value); }
else
{ $answer = ($self->value < $other->value); }
return -$answer if $reverse;
$answer;
}
sub lesseq
{
my ($self, $other, $reverse) = @_;
my $answer;
if (defined $self->{'string'} or defined $other->{'string'})
{ $answer = ($self->value le $other->value); }
else
{ $answer = ($self->value <= $other->value); }
return -$answer if $reverse;
$answer;
}
sub notequal
{
my ($self, $other) = @_;
local $^W = 0;
if (defined $self->{'string'} or defined $other->{'string'})
{ ($self->value ne $other->value); }
else
{ ($self->value != $other->value); }
}
sub likematch
{
my $class = shift;
my ($field, $string) = @_;
my $regexp = $string->value;
$regexp =~ s/(\\\\[%_]|.)/ ($1 eq '%') ? '.*' : ($1 eq '_') ? '.' : "\Q$1" /seg;
$field->value =~ /^$regexp$/i;
}
1;