package Doodle::Grammar;
use 5.014;
use strict;
use warnings;
use registry 'Doodle::Library';
use routines;
use Data::Object::Class;
use Data::Object::ClassHas;
use Doodle::Statement;
use Scalar::Util ();
our $VERSION = '0.08'; # VERSION
has name => (
is => 'ro',
isa => 'Str',
def => 'unknown'
);
# METHODS
method wrap(Str $arg) {
return $arg;
}
method exception(Str $msg) {
my $engine = ucfirst $self->name;
require Carp;
Carp::confess sprintf "[%s] %s", $engine, ucfirst $msg;
}
method execute(Command $cmd) {
my $sub = $cmd->name;
my $sql = $self->$sub($cmd);
my $statement = Doodle::Statement->new(cmd => $cmd, sql => $sql);
return $statement;
}
method create_schema(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the create_schema behaviour");
}
method delete_schema(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the delete_schema behaviour");
}
method create_table(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the create_table behaviour");
}
method delete_table(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the delete_table behaviour");
}
method rename_table(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the rename_table behaviour");
}
method create_column(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the create_column behaviour");
}
method update_column(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the update_column behaviour");
}
method delete_column(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the delete_column behaviour");
}
method rename_column(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the rename_column behaviour");
}
method create_index(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the create_index behaviour");
}
method delete_index(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the delete_index behaviour");
}
method create_constraint(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the create_constraint behaviour");
}
method delete_constraint(Command $cmd) {
# this class is meant to be subclassed
return $self->exception("does not support the delete_constraint behaviour");
}
method render(Str $str, Command $cmd) {
my $output = $str;
# compress multiline statements
$output =~ s/\n+\s*/ /gm;
# process template/data
my @tokens = $output =~ /\{\W*(\w+)\W*\}/g;
for my $token (@tokens) {
my $render = "render_$token";
next if !$self->can($render);
my $value = $self->$render($cmd);
next if !$value;
$output =~ s/\{(\W*)$token(\W*)\}/$1$value$2/g;
}
# clean up remaining tokens
$output =~ s/\s*\{[^\}]+\}//g;
# return the compiled sql statement
return $output;
}
method render_unique(Command $cmd) {
# render index "unique" clause
return $cmd->indices->[0]->data->{unique} ? 'unique' : undef;
}
method render_temporary(Command $cmd) {
# render table "temporary" clause
return $cmd->table->data->{temporary} ? 'temporary' : undef;
}
method render_if_exists(Command $cmd) {
# render schema/table "if exists" clause
my $source = $cmd->name =~ /schema/ ? $cmd->schema : $cmd->table;
return 'if exists' if $source->data->{if_exists};
return undef;
}
method render_if_not_exists(Command $cmd) {
# render schema/table "if exists" clause
my $source = $cmd->name =~ /schema/ ? $cmd->schema : $cmd->table;
return 'if not exists' if $source->data->{if_not_exists};
return undef;
}
method render_schema_name(Command $cmd) {
# render schema name
return $self->wrap($cmd->schema->name);
}
method render_table(Command $cmd) {
# render table expression
return $self->wrap($cmd->table->name);
}
method render_new_table(Command $cmd) {
# render table expression
return $self->wrap($cmd->table->data->{to});
}
method render_new_column(Command $cmd) {
# render alter table add column expression
return $self->render_column($cmd->columns->[0]);
}
method render_new_column_name(Command $cmd) {
# render alter table rename column to expression
return $self->wrap($cmd->columns->[0]->data->{to});
}
method render_column_name(Command $cmd) {
# render alter table alter column name
return $self->wrap($cmd->columns->[0]->name);
}
method render_column_change(Command $cmd) {
# render alter table alter column changes
my $col = $cmd->columns->[0];
return join ' ', 'set', $col->data->{set} if $col->data->{set};
return join ' ', 'drop', $col->data->{drop} if $col->data->{drop};
return join ' ', 'type', $col->type;
}
method render_columns(Command $cmd) {
# render create table column expressions
return join ', ', map $self->render_column($_), @{$cmd->table->columns};
}
method render_constraints(Command $cmd) {
# render create table constraints
return join ', ', map $self->render_constraint($_), @{$cmd->table->relations};
}
method render_type(Column $col) {
# render column data type expression
my $name = $col->type;
my $type = "type_$name";
$self->exception("can not handle a $name column") if !$self->can($type);
return $self->$type($col);
}
method render_column(Column $col) {
# render column spec expression
my @renders = (
'render_type',
'render_nullable',
'render_default',
'render_increments',
'render_primary'
);
return join ' ', $self->wrap($col->name), map $self->$_($col), @renders;
}
method render_constraint(Relation $rel) {
# render create table constraint expression
my $column = $self->wrap($rel->column);
my $ftable = $self->wrap($rel->foreign_table);
my $fcolumn = $self->wrap($rel->foreign_column);
return "foreign key ($column) references $ftable ($fcolumn)";
}
method render_default(Column $col) {
# render column default value
my $data = $col->data;
return () if !exists $data->{default};
my $default = $data->{default};
my $type = $default->[0];
my $value = $default->[1];
if ($type eq 'function') {
$value = uc $value;
return "default $value";
}
if ($type eq 'string') {
return "default '$value'";
}
if ($type eq 'integer') {
return "default $value";
}
if ($type eq 'deduce') {
if (Scalar::Util::looks_like_number($value)) {
return "default $value";
}
else {
return "default '$value'";
}
}
return ();
}
method render_nullable(Column $col) {
# render column null/not-null expression
my $data = $col->data;
return () if !exists $data->{nullable};
return $data->{nullable} ? 'null' : 'not null';
}
method render_increments(Column $col) {
# render column auto-increment expression
my $data = $col->data;
return $data->{increments} ? 'auto_increment' : ();
}
method render_primary(Column $col) {
# render column primary key expression
my $data = $col->data;
return $data->{primary} ? 'primary key' : ();
}
method render_index_name(Command $cmd) {
# render table create index expression
return $self->wrap($cmd->indices->[0]->name);
}
method render_index_columns(Command $cmd) {
# render table create index columns expression
return join ', ', map $self->wrap($_), @{$cmd->indices->[0]->columns};
}
method render_relation(Command $cmd) {
# render table create foreign constraint expression
my $relation = $cmd->relation;
my $name = $self->wrap($relation->name);
my $expr = $self->render_constraint($relation);
my $on_delete = $relation->data->{on_delete};
my $on_update = $relation->data->{on_update};
my @args = ($name, $expr);
push @args, "on delete $on_delete" if $on_delete;
push @args, "on update $on_update" if $on_update;
return join ' ', @args;
}
method render_relation_name(Command $cmd) {
# render table create foreign key name
return $self->wrap($cmd->relation->name);
}
method render_engine(Command $cmd) {
# render engine
my $engine = $cmd->table->engine;
return return $engine ? "engine = $engine" : undef;
}
method render_charset(Command $cmd) {
# render charset
my $charset = $cmd->table->charset;
return $charset ? "character set $charset" : undef;
}
method render_collation(Command $cmd) {
# render collation
my $collation = $cmd->table->collation;
return $collation ? "collate $collation" : undef;
}
1;
=encoding utf8
=head1 NAME
Doodle::Grammar
=cut
=head1 ABSTRACT
Doodle Grammar Base Class
=cut
=head1 SYNOPSIS
use Doodle::Grammar;
my $self = Doodle::Grammar->new;
=cut
=head1 DESCRIPTION
This package determines how command objects should be interpreted to produce
the correct DDL statements.
=cut
=head1 LIBRARIES
This package uses type constraints from:
L<Doodle::Library>
=cut
=head1 ATTRIBUTES
This package has the following attributes:
=cut
=head2 name
name(Str)
This attribute is read-only, accepts C<(Str)> values, and is optional.
=cut
=head1 METHODS
This package implements the following methods:
=cut
=head2 create_column
create_column(Command $command) : Str
Generate SQL statement for column-create Command.
=over 4
=item create_column example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $column = $ddl->table('users')->column('id');
my $command = $column->create;
my $create_column = $self->create_column($command);
=back
=cut
=head2 create_constraint
create_constraint(Column $column) : Str
Returns the SQL statement for the create constraint command.
=over 4
=item create_constraint example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $relation = $ddl->table('emails')->relation('user_id', 'users', 'id');
my $command = $relation->create;
$self->create_constraint($command);
=back
=cut
=head2 create_index
create_index(Command $command) : Str
Generate SQL statement for index-create Command.
=over 4
=item create_index example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $index = $ddl->table('users')->index(columns => ['is_admin']);
my $command = $index->create;
my $create_index = $self->create_index($command);
=back
=cut
=head2 create_schema
create_schema(Command $command) : Str
Generate SQL statement for schema-create Command.
=over 4
=item create_schema example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $schema = $ddl->schema('app');
my $command = $schema->create;
my $create_schema = $self->create_schema($command);
=back
=cut
=head2 create_table
create_table(Command $command) : Str
Generate SQL statement for table-create Command.
=over 4
=item create_table example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $table = $ddl->table('users');
my $command = $table->create;
my $create_table = $self->create_table($command);
=back
=cut
=head2 delete_column
delete_column(Command $command) : Str
Generate SQL statement for column-delete Command.
=over 4
=item delete_column example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $column = $ddl->table('users')->column('id');
my $command = $column->delete;
my $delete_column = $self->delete_column($command);
=back
=cut
=head2 delete_constraint
delete_constraint(Column $column) : Str
Returns the SQL statement for the delete constraint command.
=over 4
=item delete_constraint example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $relation = $ddl->table('emails')->relation('user_id', 'users', 'id');
my $command = $relation->delete;
$self->delete_constraint($command);
=back
=cut
=head2 delete_index
delete_index(Command $command) : Str
Generate SQL statement for index-delete Command.
=over 4
=item delete_index example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $index = $ddl->table('users')->index(columns => ['is_admin']);
my $command = $index->delete;
my $delete_index = $self->delete_index($command);
=back
=cut
=head2 delete_schema
delete_schema(Command $command) : Str
Generate SQL statement for schema-delete Command.
=over 4
=item delete_schema example #1
# given: synopsis
my $ddl = Doodle->new;
my $schema = $ddl->schema('app');
my $command = $schema->delete;
my $delete_schema = $self->delete_schema($command);
=back
=cut
=head2 delete_table
delete_table(Command $command) : Str
Generate SQL statement for table-delete Command.
=over 4
=item delete_table example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $table = $ddl->table('users');
my $command = $table->delete;
my $delete_table = $self->delete_table($command);
=back
=cut
=head2 exception
exception(Str $message) : Any
Throws an exception using L<Carp> confess.
=over 4
=item exception example #1
# given: synopsis
$self->exception('Oops');
=back
=cut
=head2 execute
execute(Command $command) : Statement
Processed the Command and returns a Statement object.
=over 4
=item execute example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $column = $ddl->table('users')->column('id');
my $command = $column->create;
my $statement = $self->execute($command);
=back
=cut
=head2 rename_column
rename_column(Command $command) : Str
Generate SQL statement for column-rename Command.
=over 4
=item rename_column example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $column = $ddl->table('users')->column('id');
my $command = $column->rename('uuid');
my $rename_column = $self->rename_column($command);
=back
=cut
=head2 rename_table
rename_table(Command $command) : Str
Generate SQL statement for table-rename Command.
=over 4
=item rename_table example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $table = $ddl->table('users');
my $command = $table->rename('people');
my $rename_table = $self->rename_table($command);
=back
=cut
=head2 render
render(Command $command) : Str
Returns the SQL statement for the given Command.
=over 4
=item render example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $schema = $ddl->schema('app');
my $command = $schema->create;
my $template = 'create schema {schema_name}';
my $sql = $self->render($template, $command);
=back
=cut
=head2 update_column
update_column(Any @args) : Str
Generate SQL statement for column-update Command.
=over 4
=item update_column example #1
# given: synopsis
use Doodle;
my $ddl = Doodle->new;
my $column = $ddl->table('users')->column('id')->integer_small;
my $command = $column->update;
my $update_column = $self->update_column($command);
=back
=cut
=head1 AUTHOR
Al Newkirk, C<awncorp@cpan.org>
=head1 LICENSE
Copyright (C) 2011-2019, Al Newkirk, et al.
This is free software; you can redistribute it and/or modify it under the terms
of the The Apache License, Version 2.0, as elucidated in the L<"license
file"|https://github.com/iamalnewkirk/doodle/blob/master/LICENSE>.
=head1 PROJECT
L<Wiki|https://github.com/iamalnewkirk/doodle/wiki>
L<Project|https://github.com/iamalnewkirk/doodle>
L<Initiatives|https://github.com/iamalnewkirk/doodle/projects>
L<Milestones|https://github.com/iamalnewkirk/doodle/milestones>
L<Contributing|https://github.com/iamalnewkirk/doodle/blob/master/CONTRIBUTE.md>
L<Issues|https://github.com/iamalnewkirk/doodle/issues>
=cut