The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl
# -*- mode: perl; coding: utf-8 -*-
#----------------------------------------
use strict;
use warnings FATAL => qw(all);
use FindBin;
BEGIN { do "$FindBin::Bin/t_lib.pl" }
#----------------------------------------

use Test::More;
use YATT::Lite::Test::TestUtil;
use YATT::Lite::Util qw(terse_dump);

foreach my $req (qw(DBD::SQLite SQL::Abstract)) {
  unless (eval qq{require $req}) {
    plan skip_all => "$req is not installed."; exit;
  }
}
plan qw(no_plan);

my $CLASS = 'YATT::Lite::WebMVC0::DBSchema';
use_ok($CLASS);

my $DBNAME = shift || ':memory:';

my @schema1
  = [Author => undef
	, author_id => [int => -primary_key, -autoincrement
			, ['has_many:books:author_id'
			   => [Book => undef
			       , book_id => [int => -primary_key
					     , -autoincrement]
			       , author_id => [int => -indexed
					       , [belongs_to => 'Author']]
			       , name => 'text']]]
	, name => 'text'];

{
  my $THEME = "[schema only]";
  my $schema = $CLASS->new(@schema1);
  is_deeply [$schema->list_tables], [qw(Author Book)]
    , "$THEME list_tables";

  is_deeply [map {ref $_} $schema->list_tables(raw => 1)]
    , [("${CLASS}::Table") x 2]
      , "$THEME list_tables raw=>1";

  is ref $schema->get_table('Author'), "${CLASS}::Table"
    , "$THEME get_table Author";

  is_deeply [map {[$_, $schema->list_table_columns($_)]} $schema->list_tables]
    , [[Author => qw(author_id name)], [Book => qw(book_id author_id name)]]
      , "$THEME list_table_columns";

  $schema->extend_table(Book => undef
			, price => 'int'
			, isbn => [text => -unique]);

  is_deeply [$schema->list_table_columns('Book')]
    , [qw(book_id author_id name price isbn)]
      , "$THEME extend_table";

  is_deeply [$schema->list_relations('Author')]
    , [[has_many => 'books', author_id => 'Book']]
      , "$THEME relations: Author has_many Book";

  is_deeply [$schema->list_relations('Book')]
    , [[belongs_to => 'author', author_id => 'Author']]
      , "$THEME relations: Book belongs_to Author";
}

{
  my $THEME = "[sqlite create]";
  my $dbh = DBI->connect("dbi:SQLite:dbname=$DBNAME", undef, undef
			 , {PrintError => 0, RaiseError => 1, AutoCommit => 0});

  ok(my $schema = $CLASS->new(DBH => $dbh, @schema1)
     , "$THEME Can new without connection spec");

  eq_or_diff join("", map {chomp;"$_;\n"} $schema->sql_create), <<'END'
CREATE TABLE Author
(author_id integer primary key
, name text);
CREATE TABLE Book
(book_id integer primary key
, author_id int
, name text);
CREATE INDEX Book_author_id on Book(author_id);
END
    , "$THEME SQL returned by sql_create";

  $schema->extend_table(Book => undef
			, price => 'int'
			, isbn => [text => -unique]);

  eq_or_diff join("", map {chomp;"$_;\n"} $schema->sql_create), <<'END'
CREATE TABLE Author
(author_id integer primary key
, name text);
CREATE TABLE Book
(book_id integer primary key
, author_id int
, name text
, price int
, isbn text unique);
CREATE INDEX Book_author_id on Book(author_id);
END
    , "$THEME (after extend_table) SQL returned by sql_create";


  is_deeply $dbh->selectall_arrayref
    (q|select name from sqlite_master where type = 'table'|)
      , [], "$THEME no table before create";

  $schema->create(sqlite => $DBNAME);

  is_deeply $schema->dbh->selectall_arrayref
    (q|select name from sqlite_master where type = 'table'|)
      , [['Author'], ['Book']], "$THEME dbschema create worked";

  # to_insert, to_find returns sub which directly returns interest.
  my $ins_auth = $schema->to_insert(Author => 'name');
  my $ins_book = $schema->to_insert(Book => qw(author_id name));
  my $sel_auth = $schema->to_find(Author => 'name');
  is $ins_auth->('Foo'), 1, 'Foo is inserted';
  is $ins_book->(1, "Foo's 1st book"), 1, "Foo's 1st book is inserted";
  is $ins_book->(1, "Foo's 2nd book"), 2, "Foo's 2nd book is inserted";
  is $sel_auth->('Foo'), 1, 'Foo is found in Authors';

  # to_fetch returns sub which returns sth.
  is_deeply $schema->to_fetch(Book => author_id => 'name')
    ->(1)->fetchall_arrayref
      , [["Foo's 1st book"], ["Foo's 2nd book"]], "fetchall arrayref";
  is_deeply $schema->to_fetch(Book => author_id => [book_id => 'name']
			      , order_by => 'book_id')
    ->(1)->fetchall_arrayref
      , [[1, "Foo's 1st book"]
	 , [2, "Foo's 2nd book"]], "fetchall arrayref, many cols";

  my $enc_auth = $schema->to_encode(Author => 'name');

  is $enc_auth->('Bar'), 2, "enc_auth(Foo) == 1";
  is $enc_auth->('Foo'), 1, "enc_auth(Bar) == 2";
}

{
  my $THEME = "[auto connect/create]";
  ok(my $schema = $CLASS->new(connection_spec => [sqlite => $DBNAME], @schema1)
     , "$THEME Can create");

  is_deeply $schema->dbh->selectall_arrayref
    (q|select name from sqlite_master where type = 'table'|)
      , [['Author'], ['Book']]
	, "$THEME dbschema can connect";
}

{
  my $THEME = "[Relation in column_list]";
  my $schema = $CLASS->new
    ([Author => undef
      , author_id => [int => -primary_key, -autoincrement]
      , name => 'text'
      , [has_many => [Book => undef
		      , book_id => [int => -primary_key, -autoincrement]
		      , author_id => [int => -indexed
				     , [belongs_to => 'Author']]
		      , name => 'text']
	, 'author_id', {join_type => 'left'}]
     ]);

  is_deeply [$schema->list_tables], [qw(Author Book)]
    , "$THEME list_tables";

  is_deeply [map {ref $_} $schema->list_tables(raw => 1)]
    , [("${CLASS}::Table") x 2]
      , "$THEME list_tables raw=>1";

  is ref $schema->get_table('Author'), "${CLASS}::Table"
    , "$THEME get_table Author";

  is_deeply [map {[$_, $schema->list_table_columns($_)]} $schema->list_tables]
    , [[Author => qw(author_id name)], [Book => qw(book_id author_id name)]]
      , "$THEME list_table_columns";

  is_deeply [$schema->list_relations('Author')]
    , [[has_many => 'book', author_id => 'Book']]
      , "$THEME relations: Author has_many Book";

  is_deeply [$schema->list_relations('Book')]
    , [[belongs_to => 'author', author_id => 'Author']]
      , "$THEME relations: Book belongs_to Author";
}

{
  my $THEME = "[Encoded relation]";
  my $schema = $CLASS->new
    ([Book => undef
      , book_id => [int => -primary_key, -autoincrement]
      , author_id => [int => -indexed
		      # XXX: [-encoded_by] == [-belongs_to <=> -has_many]
		      , [-belongs_to
			 , [Author => undef
			    , author_id => [int => -primary_key, -autoincrement]
			    , name => 'text'
			    , [-has_many => 'Book', 'author_id']
			   ]]]
      , name => 'text']
     );

  is_deeply [$schema->list_tables], [qw(Book Author)]
    , "$THEME list_tables";

  is_deeply [map {ref $_} $schema->list_tables(raw => 1)]
    , [("${CLASS}::Table") x 2]
      , "$THEME list_tables raw=>1";

  is ref $schema->get_table('Author'), "${CLASS}::Table"
    , "$THEME get_table Author";

  is_deeply [map {[$_, $schema->list_table_columns($_)]} $schema->list_tables]
    , [[Book => qw(book_id author_id name)], [Author => qw(author_id name)]]
      , "$THEME list_table_columns";

  is_deeply [$schema->list_relations('Author')]
    , [[has_many => 'book', author_id => 'Book']]
      , "$THEME relations: Author has_many Book";

  is_deeply [$schema->list_relations('Book')]
    , [[belongs_to => 'author', author_id => 'Author']]
      , "$THEME relations: Book belongs_to Author";
}

{
  my $THEME = "[many_to_many]";
  my $schema = $CLASS->new
    ([user => undef
	, id => [integer => -primary_key, -autoincrement]
	, name => 'text'
	, ['has_many:user_address'
	   => [user_address => undef
	       , user => [int => [belongs_to => 'user']]
	       , address => [int => [belongs_to =>
				     [address => undef
				      , id => [int => -primary_key]
				      , street => 'text'
				      , town => 'text'
				      , area_code => 'text'
				      , country => 'text'
				      , ['has_many:user_address' => 'user']
				      , ['many_to_many:users'
					 => 'user_address', 'user']
				     ]]]
	       , [primary_key => qw(user address)]]]
	, ['many_to_many:addresses'
	   => 'user_address', 'address']
       ]);

  # print join("", map {chomp;"$_;\n"} $schema->sql_create), "\n";
  foreach my $tabName (qw(user user_address address)) {
    # print terse_dump($schema->list_relations($tabName)), "\n";
  }
}

{
  my $THEME = "[Misc]";
  my $schema = $CLASS->new
    ([Account => undef
      , aid => [int => -primary_key, -autoincrement]
      , aname => [text => -unique]
      , atype => [text => -indexed]]
     # XXX: Enum(Asset, Liability, Income, Expense)

     , [Description => undef
	, did => [int => -primary_key, -autoincrement]
	, dname => [text => -unique]]

     , [Transaction => undef
	, tid => [int => -primary_key, -autoincrement]
	, at =>  [date => -indexed]
	, debit_id => [int => -indexed, ['-belongs_to:debit', 'Account']]
	, amt => 'int'
	, credit_id => [int => -indexed, ['-belongs_to:credit', 'Account']]
	, desc => [int => -indexed, [-belongs_to, 'Description']]
	, note => 'text'
       ]);

  # print join("", map {chomp;"$_;\n"} $schema->sql_create), "\n";
  # print terse_dump($schema->list_relations('Transaction')), "\n";
  # print terse_dump($schema->list_relations('Account')), "\n";
}

{
  my $THEME = "[View (real)]";
  my $schema = $CLASS->new
    ([ticket => undef
      , tn => [int => -primary_key, -autoincrement]
      , at => 'datetime'
      , title => 'text'
      , description => 'text'
      , [values => [qw(at title)]
	 , ['2010-01-01T09:00', '1st ticket']
	 , ['2010-01-02T15:00', '2nd ticket']
	 ]
     ]

     , [chng => undef
	, cn => [int => -primary_key, -autoincrement]
	, at => 'datetime'
	, comment => 'text'
	, [values => [qw(at comment)]
	   , ['2010-01-01T12:00', '1st chng']
	   , ['2010-01-02T18:00', '2nd chng']
	  ]
       ]

     , [timeline => {view => <<SQL}
select tn as num, 'ticket' as type, at, title from ticket
union all
select cn as num, 'chng' as type, at, comment as title from chng
SQL
	, num => 'int'
	, type => 'text'
	, at => 'datetime'
	, title => 'text'
       ]
    );

  is_deeply [$schema->list_tables], [qw(ticket chng)]
    , "$THEME list_tables";

  is_deeply [$schema->list_views], [qw(timeline)]
    , "$THEME list_views";

  $schema->create(sqlite => $DBNAME);

  is_deeply $schema->dbh->selectall_arrayref
    ('select * from timeline order by at')
      , [[1, ticket => '2010-01-01T09:00', '1st ticket']
	 , [1, chng => '2010-01-01T12:00', '1st chng']
	 , [2, ticket => '2010-01-02T15:00', '2nd ticket']
	 , [2, chng => '2010-01-02T18:00', '2nd chng']
	], "$THEME timeline";
}

# XXX: Should have mysql tests too.
{
  my $THEME = "[trigger_after_delete]";
  my $schema = $CLASS->new
    ([user => {trigger_after_delete => {user_del => <<SQL}}
delete from auth where uid = old.uid
SQL
   , uid => [integer => -primary_key]
   , email => [text => -unique]
   , fullname => [text => -indexed]
   , [-might_have
      , [auth => undef # XXX: {auto_delete => 1}
	 , uid => [integer => -primary_key, [belongs_to => 'user']]
	 , login => [text => -unique]
	 , encpass => 'text'
	 , tmppass => 'text'
	 , tmppass_expire => 'timestamp'
	 , confirm_token => [text => -unique]]]
  ]);

  $schema->create(sqlite => $DBNAME);

  my $dbh = $schema->dbh;

  my @tests
    = ([1, 'foo@example.com', 'Well known foo', 'foo', 'foopass']
       , [2, 'bar@example.com', 'Slightly minor bar', 'bar', 'barpass']);

  foreach my $user (@tests) {
    my ($uid, $email, $full, $login, $pass) = @$user;
    $dbh->do(<<END, undef, $uid, $email, $full);
insert into user(uid, email, fullname) values(?, ?, ?)
END

    $dbh->do(<<END, undef, $uid, $login, $pass); # Not encrypted:-)
insert into auth(uid, login, encpass) values(?, ?, ?)
END

  }

  is_deeply $dbh->selectall_arrayref(<<END)
select user.uid, email, fullname, login, encpass
from user left join auth using(uid) order by user.uid
END
    , \@tests, "Test data is correctly installed";

  $dbh->do('delete from user where uid = ?', undef, 1);

  is_deeply $dbh->selectall_arrayref(<<END)
select uid from user
END
    , [[2]], "uid=1 is removed.";

  is_deeply $dbh->selectall_arrayref(<<END)
select uid from auth
END
    , [[2]], "After deleting uid=1, auth rec is deleted too.";
}

{
  my $schema = $CLASS->new
    ([purchase => undef
      , compid => 'text'
      , prodid => 'text'
      , [primary_key => qw/compid prodid/]]);

  eq_or_diff join("", map {chomp;"$_;\n"} $schema->sql_create)
    , <<END, "multicolumn primary key";
CREATE TABLE purchase
(compid text
, prodid text
, PRIMARY KEY(compid, prodid));
END

  $schema->create(sqlite => $DBNAME);

  my $dbh = $schema->dbh;
  {
    my $ins = $dbh->prepare(<<END);
insert or ignore into purchase(compid, prodid) values(?,?)
END
    $ins->execute('foo', 'bar');
    $ins->execute('foo', 'bar');
    $ins->execute('baz', 'qux');
    $ins->execute('baz', 'qux');
  }

  is_deeply [sort {$$a[0] cmp $$b[0]}
	     @{$dbh->selectall_arrayref('select * from purchase')}]
    , [[qw/baz qux/], [qw/foo bar/]]
      , "multicol primary key insertion";
}