[![Actions Status](https://github.com/darviarush/perl-aion-query/actions/workflows/test.yml/badge.svg)](https://github.com/darviarush/perl-aion-query/actions) [![MetaCPAN Release](https://badge.fury.io/pl/Aion-Query.svg)](https://metacpan.org/release/Aion-Query)
# NAME

Aion::Query - функциональный интерфейс для доступа к базам данных SQL (MySQL, MariaDB, Postgres и SQLite)

# VERSION

0.0.5

# SYNOPSIS

File .config.pm:
```perl
package config;

config_module Aion::Query => {
    DRV  => "SQLite",
    BASE => "test-base.sqlite",
    BQ => 0,
};

1;
```

```perl
use Aion::Query;

query "CREATE TABLE author (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE
)";

insert "author", name => "Pushkin A.S." # -> 1

touch "author", name => "Pushkin A."    # -> 2
touch "author", name => "Pushkin A.S."  # -> 1
touch "author", name => "Pushkin A."    # -> 2

query_scalar "SELECT count(*) FROM author"  # -> 2

my @rows = query "SELECT *
FROM author
WHERE 1
    if_name>> AND name like :name
",
    if_name => Aion::Query::BQ == 0,
    name => "P%",
;

\@rows # --> [{id => 1, name => "Pushkin A.S."}, {id => 2, name => "Pushkin A."}]

$Aion::Query::DEBUG[1]  # => query: INSERT INTO author (name) VALUES ('Pushkin A.S.')
```

# DESCRIPTION

`Aion::Query` позволяет строить SQL-запрос используя простой механизм шаблонов.

Обычно SQL-запросы строятся с помощью условий, что нагружает код.

Вторая проблема — размещение символов Юникода в однобайтовых кодировках, что уменьшает размер базы данных. Пока проблема решена только для кодировки **cp1251**. Это контролируется параметром `use config BQ => 1`.

# SUBROUTINES

## query ($query, %params)

Предоставляет SQL-запросы (DCL, DDL, DQL и DML) к СУБД с квотированием параметров.

```perl
query "SELECT * FROM author WHERE name=:name", name => 'Pushkin A.S.' # --> [{id=>1, name=>"Pushkin A.S."}]
```

## LAST_INSERT_ID ()

Возвращает идентификатор последней вставки.

```perl
query "INSERT INTO author (name) VALUES (:name)", name => "Alice"  # -> 1
LAST_INSERT_ID  # -> 3
```

## quote ($scalar)

Квотирует скаляр для SQL-запроса.

```perl
quote undef     # => NULL
quote "abc"     # => 'abc'
quote 123       # => 123
quote "123"     # => '123'
quote(0+"123")  # => 123
quote(123 . "") # => '123'
quote 123.0       # => 123.0
quote(0.0+"126")  # => 126
quote("127"+0.0)  # => 127
quote("128"-0.0)  # => 128
quote("129"+1.e-100)  # => 129.0

# use for insert formula: SELECT :x as summ ⇒ x => \"xyz + 123"
quote \"without quote"  # => without quote

# use in: WHERE id in (:x)
quote [1,2,"5"] # => 1, 2, '5'

# use in: INSERT INTO author VALUES :x
quote [[1, 2], [3, "4"]]  # => (1, 2), (3, '4')

# use in multiupdate: UPDATE author SET name=CASE id :x ELSE null END
quote \[2=>'Pushkin A.', 1=>'Pushkin A.S.']  # => WHEN 2 THEN 'Pushkin A.' WHEN 1 THEN 'Pushkin A.S.'

# use for UPDATE SET :x or INSERT SET :x
quote {name => 'A.S.', id => 12}   # => id = 12, name = 'A.S.'

[map quote, -6, "-6", 1.5, "1.5"] # --> [-6, "'-6'", 1.5, "'1.5'"]

```

## query_prepare ($query, %param)

Заменяет параметры (`%param`) в запросе (`$query`) и возвращает его. Параметры заключаются в кавычки через подпрограмму `quote`.

Параметры вида `:x` будут квотироваться с учётом флагов скаляра, которые укажут, что в нём находится: строка, целое или число с плавающей запятой.

Чтобы явно указать тип скаляра используйте префиксы: `:^x` – целое, `:.x` – строка, `:~x` – плавающее.

```perl
query_prepare "INSERT author SET name IN (:name)", name => ["Alice", 1, 1.0]  # => INSERT author SET name IN ('Alice', 1, 1.0)

query_prepare ":x :^x :.x :~x", x => "10"  # => '10' 10 10.0 '10'

my $query = query_prepare "SELECT *
FROM author
    words*>> JOIN word:_
WHERE 1
    name>> AND name like :name
",
    name => "%Alice%",
    words => [1, 2, 3],
;

my $res = << 'END';
SELECT *
FROM author
    JOIN word1
    JOIN word2
    JOIN word3
WHERE 1
    AND name like '%Alice%'
END

$query # -> $res
```

## query_do ($query)

Выполняет запрос и возвращает его результат.

```perl
query_do "SELECT count(*) as n FROM author"  # --> [{n=>3}]
query_do "SELECT id FROM author WHERE id=2"  # --> [{id=>2}]
```

## query_ref ($query, %kw)

Как `query`, но всегда возвращает скаляр.

```perl
my @res = query_ref "SELECT id FROM author WHERE id=:id", id => 2;
\@res  # --> [[ {id=>2} ]]
```

## query_sth ($query, %kw)

Как `query`, но возвращает `$sth`.

```perl
my $sth = query_sth "SELECT * FROM author";
my @rows;
while(my $row = $sth->fetchrow_arrayref) {
    push @rows, $row;
}
$sth->finish;

0+@rows  # -> 3
```

## query_slice ($key, $val, $query, %kw)

Как query, плюс преобразует результат в нужную структуру данных.

Если нужен хеш вида идентификатор – значение:

```perl
my %author = query_slice name => "id", "SELECT id, name FROM author";
\%author  # --> {"Pushkin A.S." => 1, "Pushkin A." => 2, "Alice" => 3}
```

Если нужен хеш вида идентификатор – строка:

```perl
my %author = query_slice id => {}, "SELECT id, name FROM author";

my $rows = {
    1 => {name => "Pushkin A.S.", id => 1},
    2 => {name => "Pushkin A.",   id => 2},
    3 => {name => "Alice",        id => 3},
};

\%author  # --> $rows
```

Если одному идентификатору соответствует несколько строк, то логично собрать их в массивы:

```perl
query "CREATE TABLE book (
	id SERIAL PRIMARY KEY,
    author_id INT NOT NULL REFERENCES author(id),
    title TEXT NOT NULL
)";

stores book => [
    {author_id => 1, title => "Mir"},
    {author_id => 1, title => "Kiss in night"},
    {author_id => 3, title => "Mips as cpu"},
];

my %author = query_slice author_id => ["title"], "SELECT author_id, title FROM book ORDER BY title";

my $rows = {
    1 => ["Kiss in night", "Mir"],
    3 => ["Mips as cpu"],
};

\%author  # --> $rows
```

Ну и строки со всеми полями:

```perl
my %author = query_slice author_id => [], "SELECT author_id, title FROM book ORDER BY title";

my $rows = {
    1 => [
        {title => "Kiss in night", author_id => 1},
        {title => "Mir",           author_id => 1},
    ],
    3 => [
        {title => "Mips as cpu",   author_id => 3}
    ],
};

\%author  # --> $rows
```

## query_attach ($rows, $attach, $query, %kw)

Подсоединяет в результат запроса результат другого запроса.

`$attach` содержит три ключа через двоеточие: ключ для присоединяемых данных, столбец из `$rows` и столбец из `$query`. По столбцам происходит объединение строк.

Возвращает функция массив с результатом запроса (`$query`), в который можно приаттачить ещё что-то.

```perl
my $authors = query "SELECT id, name FROM author";

my $res = [
    {name => "Pushkin A.S.", id => 1},
    {name => "Pushkin A.",   id => 2},
    {name => "Alice",        id => 3},
];

$authors # --> $res

my @books = query_attach $authors => "books:id:author_id" => "SELECT author_id, title FROM book ORDER BY title";

my $attaches = [
    {name => "Pushkin A.S.", id => 1, books => [
        {title => "Kiss in night", author_id => 1},
        {title => "Mir",           author_id => 1},
    ]},
    {name => "Pushkin A.",   id => 2},
    {name => "Alice",        id => 3, books => [
        {title => "Mips as cpu", author_id => 3},
    ]},
];

$authors # --> $attaches

my $books = [
    {title => "Kiss in night", author_id => 1},
    {title => "Mips as cpu",   author_id => 3},
    {title => "Mir",           author_id => 1},
];

\@books  # --> $books
```

## query_col ($query, %params)

Возвращает один столбец.

```perl
query_col "SELECT name FROM author ORDER BY name" # --> ["Alice", "Pushkin A.", "Pushkin A.S."]

eval {query_col "SELECT id, name FROM author"}; $@  # ~> Only one column is acceptable!
```

## query_row ($query, %params)

Возвращает одну строку.

```perl
query_row "SELECT name FROM author WHERE id=2" # --> {name => "Pushkin A."}

my ($id, $name) = query_row "SELECT id, name FROM author WHERE id=2";
$id    # -> 2
$name  # => Pushkin A.

eval { query_row "SELECT id, name FROM author" }; $@ # ~> A few lines! 
```

## query_row_ref ($query, %params)

Как `query_row`, но всегда возвращает скаляр.

```perl
my @x = query_row_ref "SELECT name FROM author WHERE id=2";
\@x # --> [{name => "Pushkin A."}]

eval {query_row_ref "SELECT name FROM author"}; $@  # ~> A few lines!
```

## query_scalar ($query, %params)

Возвращает первое значение. Запрос должен возвращать одну строку, иначе – выбрасывает исключение.

```perl
query_scalar "SELECT name FROM author WHERE id=2" # => Pushkin A.
```

## make_query_for_order ($order, $next)

Создает условие запроса страницы не по смещению, а по **пагинации курсора**.

Для этого он получает `$order` SQL-запроса и `$next` — ссылку на следующую страницу.

```perl
my ($select, $where, $order_sel) = make_query_for_order "name DESC, id ASC", undef;

$select     # => name || ',' || id
$where      # -> 1
$order_sel  # -> undef

my @rows = query "SELECT $select as next FROM author WHERE $where LIMIT 2";

my $last = pop @rows;

($select, $where, $order_sel) = make_query_for_order "name DESC, id ASC", $last->{next};
$select     # => name || ',' || id
$where      # => (name < 'Pushkin A.'\nOR name = 'Pushkin A.' AND id >= '2')
$order_sel  # --> [qw/name id/]
```

Смотрите также:

1. Article [Paging pages on social networks
](https://habr.com/ru/articles/674714/).
2. [SQL::SimpleOps->SelectCursor](https://metacpan.org/dist/SQL-SimpleOps/view/lib/SQL/SimpleOps.pod#SelectCursor)

## settings ($id, $value)

Устанавливает или возвращает ключ из таблицы `settings`.

```perl
query "CREATE TABLE settings(
    id TEXT PRIMARY KEY,
	value TEXT NOT NULL
)";

settings "x1"       # -> undef
settings "x1", 10   # -> 1
settings "x1"       # -> 10
```

## load_by_id ($tab, $pk, $fields, @options)

Возвращает запись по ее идентификатору.

```perl
load_by_id author => 2  # --> {id=>2, name=>"Pushkin A."}
load_by_id author => 2, "name as n"  # --> {n=>"Pushkin A."}
load_by_id author => 2, "id+:x as n", x => 10  # --> {n=>12}
```

## insert ($tab, %x)

Добавляет запись и возвращает ее идентификатор.

```perl
insert 'author', name => 'Masha'  # -> 4
```

## update ($tab, $id, %params)

Обновляет запись по её идентификатору и возвращает этот идентификатор.

```perl
update author => 3, name => 'Sasha'  # -> 3
eval { update author => 5, name => 'Sasha' }; $@  # ~> Row author.id=5 is not!
```

## remove ($tab, $id)

Удалить строку из таблицы по её идентификатору и вернуть этот идентификатор.

```perl
remove "author", 4  # -> 4
eval { remove author => 4 }; $@  # ~> Row author.id=4 does not exist!
```

## query_id ($tab, %params)

Возвращает идентификатор на основе других полей.

```perl
query_id 'author', name => 'Pushkin A.' # -> 2
```

## stores ($tab, $rows, %opt)

Сохраняет данные (обновляет или вставляет). Возвращает подсчет успешных операций.

```perl
my @authors = (
    {id => 1, name => 'Pushkin A.S.'},
    {id => 2, name => 'Pushkin A.'},
    {id => 3, name => 'Sasha'},
);

query "SELECT * FROM author ORDER BY id" # --> \@authors

my $rows = stores 'author', [
    {name => 'Locatelli'},
    {id => 3, name => 'Kianu R.'},
    {id => 2, name => 'Pushkin A.'},
];
$rows  # -> 3

my $sql = "query: INSERT INTO author (id, name) VALUES (NULL, 'Locatelli'),
(3, 'Kianu R.'),
(2, 'Pushkin A.') ON CONFLICT DO UPDATE SET id = excluded.id, name = excluded.name";

$Aion::Query::DEBUG[$#Aion::Query::DEBUG]  # -> $sql


@authors = (
    {id => 1, name => 'Pushkin A.S.'},
    {id => 2, name => 'Pushkin A.'},
    {id => 3, name => 'Kianu R.'},
    {id => 5, name => 'Locatelli'},
);

query "SELECT * FROM author ORDER BY id" # --> \@authors
```

## store ($tab, %params)

Сохраняет данные (обновляет или вставляет) одну строку.

```perl
store 'author', name => 'Bishop M.' # -> 1
```

## touch ($tab, %params)

Супермощная функция: возвращает идентификатор строки, а если он не существует, создает или обновляет строку и всё равно возвращает.

```perl
touch 'author', name => 'Pushkin A.' # -> 2
touch 'author', name => 'Pushkin X.' # -> 7
```

## START_TRANSACTION ()

Возвращает переменную на которой необходимо выполнить фиксацию, иначе происходит откат.

```perl
my $transaction = START_TRANSACTION;

query "UPDATE author SET name='Pushkin N.' where id=7"  # -> 1

$transaction->commit;

query_scalar "SELECT name FROM author where id=7"  # => Pushkin N.


eval {
    my $transaction = START_TRANSACTION;

    query "UPDATE author SET name='Pushkin X.' where id=7" # -> 1

    die "!";  # rollback
    $transaction->commit;
};

query_scalar "SELECT name FROM author where id=7"  # => Pushkin N.
```

## default_dsn ()

DSN по умолчанию для `DBI->connect`.

```perl
default_dsn  # => DBI:SQLite:dbname=test-base.sqlite
```

## default_connect_options ()

DSN, пользователь, пароль и команды после подключения.

```perl
[default_connect_options]  # --> ['DBI:SQLite:dbname=test-base.sqlite', 'root', 123, []]
```

## base_connect ($dsn, $user, $password, $conn)

Подключаемся к базе и возвращаем соединение и идентифицируем.

```perl
my ($dbh, $connect_id) = base_connect("DBI:SQLite:dbname=base-2.sqlite", "toor", "toorpasswd", []);

ref $dbh     # => DBI::db
$connect_id  # -> -1
```

## connect_respavn ($base)

Проверка подключения и повторное подключение.

```perl
my $old_base = $Aion::Query::base;

$old_base->ping  # -> 1
connect_respavn $Aion::Query::base, $Aion::Query::base_connection_id;

$old_base  # -> $Aion::Query::base
```

## connect_restart ($base)

Перезапуск соединения.

```perl
my $connection_id = $Aion::Query::base_connection_id;
my $base = $Aion::Query::base;

connect_restart $Aion::Query::base, $Aion::Query::base_connection_id;

$base->ping  # -> 0
$Aion::Query::base->ping  # -> 1
```

## query_stop ()

Создает дополнительное соединение с базой и убивает основное.

Для этого используется `$Aion::Query::base_connection_id`.

SQLite работает в том же процессе, поэтому `$Aion::Query::base_connection_id` имеет `-1`. То есть для SQLite этот метод ничего не делает.

```perl
my @x = query_stop;
\@x  # --> []
```

## sql_debug ($fn, $query)

Сохраняет запросы к базе данных в `@Aion::Query::DEBUG`. Вызывается из `query_do`.

```perl
sql_debug label => "SELECT 123";

$Aion::Query::DEBUG[$#Aion::Query::DEBUG]  # => label: SELECT 123
```

# AUTHOR

Yaroslav O. Kosmina [dart@cpan.org](dart@cpan.org)

# LICENSE

âš– **GPLv3**

# COPYRIGHT

The Aion::Surf module is copyright © 2023 Yaroslav O. Kosmina. Rusland. All rights reserved.