Практикум по языку Lua
Если вы только осваиваете Lua, рекомендуем выполнить практическое
задание, встроенное в Tarantool. Чтобы начать работу с этим заданием,
выполните команду tutorial() в консоли Tarantool:
tarantool> tutorial()---- |Tutorial -- Screen #1 -- Hello, Moon====================================Welcome to the Tarantool tutorial.It will introduce you to Tarantool’s Lua application serverand database server, which is what’s running what you’re seeing.This is INTERACTIVE -- you’re expected to enter requestsbased on the suggestions or examples in the screen’s text.<...>
Задание по данному практикуму: "Вставьте 1 миллион кортежей. В каждом кортеже должно быть поле, которое соответствует ключу в первичном индексе, в виде постоянно возрастающего числа, а также поле в виде буквенной строки со случайным значением из 10 символов."
Цель данного упражнения состоит в том, чтобы показать, как выглядят
Lua-функции в Tarantool. Необходимо будет работать с математической
библиотекой Lua, библиотекой для работы со строками интерпретатора Lua,
Tarantool-библиотекой box, Tarantool-библиотекой box.tuple, циклами
и конкатенацией. Инструкции легко будет выполнять даже тем, кто никогда
не использовал раньше Lua или Tarantool. Единственное требование –
знание того, как работают другие языки программирования, и изучение
первых двух глав данного руководства. Но для лучшего понимания можно
следовать по комментариям и ссылкам на руководство по Lua или другим
пунктам в данном руководстве по Tarantool. А чтобы облегчить изучение,
читайте инструкции параллельно с вводом операторов в Tarantool-клиент.
Будем использовать Tarantool-песочницу, которую создавали для упражнений раздела "Руководство для начинающих". Таким образом, у нас есть один спейс и числовой ключ первичного индекса, а также экземпляр Tarantool, который также выступает в виде клиента.
В более ранних версиях Tarantool многострочные функции обрамляются символами-разделителями. Сейчас в них нет необходимости, поэтому в данном практическом задании они использоваться не будут. Однако они все еще поддерживаются. Если вы хотите использовать разделители или используете более раннюю версию Tarantool, перед работой проверьте описание синтаксиса для объявления разделителя.
Начнем с создания функции, которая возвращает заданную строку – "Hello world".
function string_function()return "hello world"end
Слово "function" (функция) – ключевое слово в языке Lua.
Рассмотрим подробно работу с языком Lua. Имя функции – string_function
(строковая_функция). В функции есть один исполняемый оператор,
return "hello world" (вернуть "hello world"). Строка "hello world"
здесь заключена в двойные кавычки, хотя в Lua это не имеет значения,
можно использовать одинарные кавычки. Слово "end" означает, что "это
конец объявления Lua-функции." Чтобы проверить работу функции, можем
выполнить команду
string_function()
Отправка <function_name>() означает команду вызова
Lua-функции. В результате возвращаемая функцией строка появится на
экране.
Для получения подробной информации о строках в языке Lua, см. Главу 2.4 "Строки") в руководстве по языку Lua. Для получения подробной информации о функциях см. Главу 5 "Функции" в руководстве по языку Lua.
Теперь вывод на экране выглядит следующим образом:
tarantool> function string_function()> return "hello world"> end---...tarantool> string_function()---- hello world...tarantool>
Теперь у нас есть функция string_function, и можно вызвать ее с помощью другой функции.
function main_function()local string_valuestring_value = string_function()return string_valueend
Сначала объявим переменную "string_value" (значение_строки). Слово
"local" (локально) означает, что string_value появится только в
main_function (основная_функция). Если бы мы не использовали
"local", то string_value увидели бы даже пользователи других
клиентов, которые подключились к данному экземпляру! Иногда это может
быть очень полезно при взаимодействии клиентов, но не в нашем случае.
Затем определим значение для string_value, а именно, результат функции
string_function(). Сейчас вызовем main_function(), чтобы проверить,
что значение определено.
Для получения подробной информации о переменных в языке Lua, см. Главу 4.2 "Локальные переменные и блоки в руководстве по языку Lua.
Теперь экран выглядит так:
tarantool> function main_function()> local string_value> string_value = string_function()> return string_value> end---...tarantool> main_function()---- hello world...tarantool>
Сейчас стало понятно, как задавать переменную, поэтому можно изменить
функцию string_function() так, чтобы вместо возврата заданной фразы
"Hello world", она возвращала случайным образом выбранную букву от
'A' до 'Z'.
function string_function()local random_numberlocal random_stringrandom_number = math.random(65, 90)random_string = string.char(random_number)return random_stringend
Нет необходимости стирать содержание старой функции string_function(),
оно просто перезаписывается. Первый оператор вызывает функцию из
математической библиотеки Lua, которая возвращает случайное число;
параметры означают, что число должно быть целым от 65 до 90. Второй
оператор вызывает функцию из библиотеки Lua для работы со строками,
которая преобразует число в символ; параметр представляет собой кодовую
точку символа. К счастью, в кодировке ASCII символу 'A' соответствует
значение 65, а 'Z' – 90, так что в результате всегда получим букву
от A до Z.
Подробнее о функциях математической библиотеки Lua см. на сайте сообщества пользователей Lua Руководство по математической библиотеке.
Подробнее о функциях строковой библиотеки Lua см. на сайте сообщества пользователей Lua Руководство по строковой библиотеке.
И снова функцию string_function() можно вызвать из main_function(),
которую можно вызвать с помощью main_function().
Теперь экран выглядит так:
tarantool> function string_function()> local random_number> local random_string> random_number = math.random(65, 90)> random_string = string.char(random_number)> return random_string> end---...tarantool> main_function()---- C...tarantool>
На самом деле, вывод не всегда будет именно таким, поскольку
функция math.random() вызывает случайные числа. Но для наглядности
случайные значения в строке не важны.
Сейчас стало понятно, как вызывать строки из одной случайной буквы, поэтому можно перейти к нашей цели – возврату строки из десяти букв с помощью конкатенации десяти строк из одной случайной буквы в цикле.
function string_function()local random_numberlocal random_stringrandom_string = ""for x = 1,10,1 dorandom_number = math.random(65, 90)random_string = random_string .. string.char(random_number)endreturn random_stringend
Слова "for x = 1,10,1" означают: "начать с x, равного 1, зацикливать
до тех пор, пока x не будет равен 10, увеличивать x на 1 на каждом шаге
цикла". Символ ".." означает "конкатенацию", то есть добавление
строки справа от знака ".." к строке слева от знака "..". Поскольку
в начале определяется, что random_string (случайная_строка) представляет
собой "" (пустую строку), в результате получим, что в random_string 10
случайных букв. И снова функцию string_function() можно вызвать из
main_function(), которую можно вызвать с помощью main_function().
Для получения подробной информации о циклах в языке Lua, см. Главу 4.3.4 "Числовой оператор for" в руководстве по языку Lua.
Теперь экран выглядит так:
tarantool> function string_function()> local random_number> local random_string> random_string = ""> for x = 1,10,1 do> random_number = math.random(65, 90)> random_string = random_string .. string.char(random_number)> end> return random_string> end---...tarantool> main_function()---- 'ZUDJBHKEFM'...tarantool>
Сейчас стало понятно, как создать строку из 10 случайных букв, поэтому можно создать кортеж, который будет содержать число и строку из 10 случайных букв, с помощью функции в Tarantool-библиотеке Lua-функций.
function main_function()local string_value, tstring_value = string_function()t = box.tuple.new({1, string_value})return tend
После этого, "t" будет представлять собой значение нового кортежа с
двумя полями. Первое поле является числовым: "1". Второе поле
представляет собой случайную строку. И снова функцию string_function()
можно вызвать из main_function(), которую можно вызвать с помощью
main_function().
Для получения подробной информации о кортежах в Tarantool, см. раздел Встроенный модуль box.tuple в руководстве по Tarantool.
Теперь экран выглядит так:
tarantool> function main_function()> local string_value, t> string_value = string_function()> t = box.tuple.new({1, string_value})> return t> end---...tarantool> main_function()---- [1, 'PNPZPCOOKA']...tarantool>
Сейчас стало понятно, как создавать кортеж, который содержит число и строку из десяти случайных букв, поэтому осталось только поместить этот кортеж в спейс tester. Следует отметить, что tester – это первый спейс, определенный в песочнице, поэтому он представляет собой таблицу в базе данных.
function main_function()local string_value, tstring_value = string_function()t = box.tuple.new({1,string_value})box.space.tester:replace(t)end
Здесь появляется новая строка – box.space.tester:replace(t).
Имя содержит слово 'tester', потому что вставка будет осуществляться в спейс tester.
Второй параметр представляет собой значение в кортеже. Для абсолютной
точности мы могли ввести команду box.space.tester:insert(t), а не
box.space.tester:replace(t), но слово "replace" (заменить) означает
"вставить, даже если уже существует кортеж, у которого значение
первичного ключа совпадает", и это облегчит повтор упражнения, даже если
песочница не пуста. После того, как это будет выполнено, спейс tester
будет содержать кортеж с двумя полями. Первое поле будет 1. Второе поле
будет представлять собой строку из десяти случайных букв. И снова
функцию string_function() можно вызвать из main_function(), которую
можно вызвать с помощью main_function(). Но функция main_function()
не может полностью отразить ситуацию, поскольку она не возвращает t, она
только размещает t в базе данных. Чтобы убедиться, что произошла
вставка, используем SELECT-запрос.
main_function()box.space.tester:select{1}
Для получения подробной информации о вызовах insert и replace в Tarantool, см. разделы Встроенный модуль box.space, space_object:insert() и space_object:replace() в руководстве по Tarantool.
Теперь экран выглядит так:
tarantool> function main_function()> local string_value, t> string_value = string_function()> t = box.tuple.new({1,string_value})> box.space.tester:replace(t)> end---...tarantool> main_function()---...tarantool> box.space.tester:select{1}---- - [1, 'EUJYVEECIL']...tarantool>
Сейчас стало понятно, как вставить кортеж в базу данных, поэтому несложно догадаться, как можно увеличить масштаб: вместо того, чтобы вставлять значение 1 для первичного ключа, вставьте значение переменной от 1 до миллиона в цикле. Поскольку уже рассматривалось, как заводить цикл, это будет несложно. Мы лишь добавим небольшой штрих – функцию распределения во времени.
function main_function()local string_value, tfor i = 1,1000000,1 dostring_value = string_function()t = box.tuple.new({i,string_value})box.space.tester:replace(t)endendstart_time = os.clock()main_function()end_time = os.clock()'insert done in ' .. end_time - start_time .. ' seconds'
Стандартная Lua-функция os.clock()
вернет время ЦП в секундах с момента начала программы. Таким образом,
выводя start_time = number of seconds (время_начала = число секунд)
прямо перед вставкой, а затем выводя end_time = number of seconds
(время_окончания = число секунд) сразу после вставки, можно рассчитать
(время_окончания - время_начала) = затраченное время в секундах.
Отобразим это значение путем ввода в запрос без операторов, что приведет
к тому, что Tarantool отправит значение на клиент, который выведет это
значение. (Ответ Lua на C-функцию printf(), а именно print(), также
сработает.)
Для получения подробной информации о функции os.clock()
см. Главу 22.1 "Дата и время" в руководстве по языку Lua.
Для получения подробной информации о функции print() см. Главу 5 "Функции" в
руководстве по языку Lua.
Здесь мы подходим к заключительной части нашего практикума. Повторно введем окончательные
варианты всех необходимых запросов: запрос, который создает
string_function(), запрос, который создает main_function(), и
запрос, который вызывает main_function().
function string_function()local random_numberlocal random_stringrandom_string = ""for x = 1,10,1 dorandom_number = math.random(65, 90)random_string = random_string .. string.char(random_number)endreturn random_stringendfunction main_function()local string_value, tfor i = 1,1000000,1 dostring_value = string_function()t = box.tuple.new({i,string_value})box.space.tester:replace(t)endendstart_time = os.clock()main_function()end_time = os.clock()'insert done in ' .. end_time - start_time .. ' seconds'
Теперь экран выглядит так:
tarantool> function string_function()> local random_number> local random_string> random_string = ""> for x = 1,10,1 do> random_number = math.random(65, 90)> random_string = random_string .. string.char(random_number)> end> return random_string> end---...tarantool> function main_function()> local string_value, t> for i = 1,1000000,1 do> string_value = string_function()> t = box.tuple.new({i,string_value})> box.space.tester:replace(t)> end> end---...tarantool> start_time = os.clock()---...tarantool> main_function()---...tarantool> end_time = os.clock()---...tarantool> 'insert done in ' .. end_time - start_time .. ' seconds'---- insert done in 37.62 seconds...tarantool>
Итак, мы доказали, что возможности Lua-функций довольно многообразны (на самом деле, с помощью хранимых процедур на языке Lua в Tarantool можно сделать больше, чем с помощью хранимых процедур в некоторых SQL СУБД), и несложно комбинировать функции Lua-библиотек и функции Tarantool-библиотек.
Также мы показали, что вставка миллиона кортежей заняла 37 секунд. Хостом выступил ноутбук с ОС Linux. А изменив значение wal_mode на 'none' перед запуском теста, можно уменьшить затраченное время до 4 секунд.
Задание по данному практикуму: "Предположим, что в каждом кортеже есть строка в формате JSON. В каждой строке есть числовое поле формата JSON. Для каждого кортежа необходимо найти значение числового поля и прибавить его к переменной 'sum' (сумма). В конце функция должна вернуть переменную 'sum'." Цель данного упражнения – получить опыт в прочтении и обработке кортежей одновременно.
json = require('json')function sum_json_field(field_name)local v, t, sum, field_value, is_valid_json, lua_tablesum = 0for v, t in box.space.tester:pairs() dois_valid_json, lua_table = pcall(json.decode, t[2])if is_valid_json thenfield_value = lua_table[field_name]if type(field_value) == "number" then sum = sum + field_value endendendreturn sumend
СТРОКА 3: ЗАЧЕМ НУЖЕН "LOCAL". Эта строка объявляет все переменные, которые будут использоваться в функции. На самом деле, нет необходимости в начале объявлять все переменные, а в длинной функции лучше объявить переменные прямо перед их использованием. Фактически объявлять переменные вообще необязательно, но необъявленная переменная будет "глобальной". Это представляется нежелательным для всех переменных, объявленных в строке 1, поскольку все они используются только в рамках функции.
СТРОКА 5: ЗАЧЕМ НУЖЕН "PAIRS()". Наша задача – пройти по всем
строкам, что можно сделать двумя способами: с помощьюbox.space.space_object:pairs() или
с помощью variable = select(...) с указанием for i, {n}, 1 do {некая-функция}(variable[i]) end.
Для данного примера мы предпочли использовать pairs().
СТРОКА 5: НАЧАЛО ОСНОВНОГО ЦИКЛА. Всё внутри цикла "for" будет
повторяться до тех пор, пока не кончатся индекс-ключи. На полученный
кортеж можно сослаться с помощью переменной t.
СТРОКА 6: ЗАЧЕМ НУЖЕН "PCALL". Если бы мы просто написали
lua_table = json.decode(t[2])), функция прервалась бы с ошибкой при
обнаружении проблемы в JSON-строке –- например, при отсутствии
двоеточия. Помещая функцию внутрь "pcall" (защищённый
вызов), мы указываем, что хотим
перехватывать такие ошибки: при возникновении проблемы просто установить
is_valid_json = false, а дальнейшие действия определить позже.
СТРОКА 6: ЗНАЧЕНИЕ. Функция json.decode выполняет
декодирование JSON-строки, а параметр t[2] представляет собой ссылку
на JSON-строку. Здесь есть заранее заданные значения, а мы предполагаем,
что JSON-строка была вставлена во второе поле кортежа. Например,
предположим, что кортеж выглядит следующим образом:
field[1]: 444field[2]: '{"Hello": "world", "Quantity": 15}'
Это означает, что первое поле кортежа, первичное поле, представляет
собой число, а второе поле кортежа, JSON-строка, является строкой. Таким
образом, значение оператора будет следующим: "декодировать t[2]
(второе поле кортежа) как JSON-строку; если обнаружится ошибка, то
указать is_valid_json = false; если ошибок нет, указать
is_valid_json = true и lua_table = Lua-таблица, в которой находится
декодированная строка".
СТРОКА 8. Наконец, мы готовы получить значение JSON-поля из
Lua-таблицы, взятое из JSON-строки. Значение в field_name (имя_поля),
которое является параметром всей функции, должно представлять собой
JSON-поле. Например, в JSON-строке
'{"Hello": "world", "Quantity": 15}' есть два JSON-поля: "Hello" и
"Quantity". Если вся функция вызывается с помощью
sum_json_field("Quantity"), тогда
field_value = lua_table[field_name] (значение_поля =
Lua_таблица[имя_поля]) по сути аналогично
field_value = lua_table["Quantity"] или даже
field_value = lua_table.Quantity. Итак, этими тремя способами можно
ввести следующую команду: получить значение поля Quantity в Lua-таблице
и поместить его в переменную field_value.
СТРОКА 9: ЗАЧЕМ НУЖЕН "IF". Предположим, что JSON-строка не
содержит синтаксических ошибок, но JSON-поле не является числовым или
вовсе отсутствует. В таком случае выполнение функции прервется при
попытке прибавить значение к сумме. Если сначала проверить,
type(field_value) == "number" (тип(значение_поля) == "число"), можно
избежать прерывания функции. Если вы уверены, что база данных в
идеальном состоянии, этот шаг можно пропустить.
И функция готова. Пора протестировать ее. Начинаем с пустой базы данных так же, как с песочницы в упражнении из "Руководства для начинающих":
-- если спейс tester остался от предыдущего задания, удалите егоbox.space.tester:drop()box.schema.space.create('tester')box.space.tester:create_index('primary', {parts = {1, 'unsigned'}})
Затем добавим несколько кортежей, где первое поле является числовым, а второе поле представляет собой строку.
box.space.tester:insert{444, '{"Item": "widget", "Quantity": 15}'}box.space.tester:insert{445, '{"Item": "widget", "Quantity": 7}'}box.space.tester:insert{446, '{"Item": "golf club", "Quantity": "sunshine"}'}box.space.tester:insert{447, '{"Item": "waffle iron", "Quantit": 3}'}
Для целей практики здесь допущены ошибки. В "golf club" и "waffle iron" поля Quantity не являются числовыми, поэтому будут игнорироваться. Таким образом, итоговая сумма для полей Quantity в JSON-строках должна быть следующей: 15 + 7 = 22.
Вызовите функцию с помощью sum_json_field("Quantity").
tarantool> sum_json_field("Quantity")---- 22...
Сработало. Для дополнительной отработки материала можно убрать заранее заданные значения, добавить проверку потенциально возможного арифметического переполнения при наличии больших значений некоторых полей, а также команду передачи управления (yield) при огромном количестве кортежей.
Ниже приведена универсальная функция, которая принимает идентификатор поля и шаблон поиска, и возвращает все совпадающие кортежи.
- Поле должно быть первым полем TREE-индекса.
- В функции используется сопоставление с шаблонами Lua, допускающее «магические символы» в регулярных выражениях.
- Начальные символы шаблона вплоть до первого магического символа используются в качестве ключа поиска по индексу. Для каждого кортежа, найденного через индекс, выполняется сопоставление с полным шаблоном.
- Для обеспечения кооперативности функция должна передавать управление после каждых 10 кортежей, если нет причин для отсрочки передачи управления. С помощью этой функции можно совместить скорость индексов Tarantool с гибкостью сопоставления с шаблонами Lua. Она выполняет всё то же, что и SQL-поиск LIKE, и многое другое.
Прочитайте следующий Lua-код, чтобы понять, как он работает. Комментарии, которые начинаются с "СМ. ПРИМЕЧАНИЕ ...", ссылаются на подробные объяснения, приведенные ниже.
function indexed_pattern_search(space_name, field_no, pattern)-- СМ. ПРИМЕЧАНИЕ №1 "ПОИСК НУЖНОГО ИНДЕКСА"if (box.space[space_name] == nil) thenprint("Error: Failed to find the specified space")return nilendlocal index_no = -1for i=0,box.schema.INDEX_MAX,1 doif (box.space[space_name].index[i] == nil) then break endif (box.space[space_name].index[i].type == "TREE"and box.space[space_name].index[i].parts[1].fieldno == field_noand (box.space[space_name].index[i].parts[1].type == "scalar"or box.space[space_name].index[i].parts[1].type == "string")) thenindex_no = ibreakendendif (index_no == -1) thenprint("Error: Failed to find an appropriate index")return nilend-- СМ. ПРИМЕЧАНИЕ №2 "ПОЛУЧЕНИЕ КЛЮЧА ИНДЕКСНОГО ПОИСКА ИЗ ШАБЛОНА"local index_search_key = ""local index_search_key_length = 0local last_character = ""local c = ""local c2 = ""for i=1,string.len(pattern),1 doc = string.sub(pattern, i, i)if (last_character ~= "%") thenif (c == '^' or c == "$" or c == "(" or c == ")" or c == "."or c == "[" or c == "]" or c == "*" or c == "+"or c == "-" or c == "?") thenbreakendif (c == "%") thenc2 = string.sub(pattern, i + 1, i + 1)if (string.match(c2, "%p") == nil) then break endindex_search_key = index_search_key .. c2elseindex_search_key = index_search_key .. cendendlast_character = cendindex_search_key_length = string.len(index_search_key)if (index_search_key_length < 3) thenprint("Error: index search key " .. index_search_key .. " is too short")return nilend-- СМ. ПРИМЕЧАНИЕ №3 "ВНЕШНИЙ ЦИКЛ: НАЧАЛО"local result_set = {}local number_of_tuples_in_result_set = 0local previous_tuple_field = ""while true dolocal number_of_tuples_since_last_yield = 0local is_time_for_a_yield = false-- СМ. ПРИМЕЧАНИЕ №4 "ВНУТРЕННИЙ ЦИКЛ: ИТЕРАТОР"for _,tuple in box.space[space_name].index[index_no]:pairs(index_search_key,{iterator = box.index.GE}) do-- СМ. ПРИМЕЧАНИЕ №5 "ВНУТРЕННИЙ ЦИКЛ: ПРЕРЫВАНИЕ, ЕСЛИ КЛЮЧ ИНДЕКСА СЛИШКОМ БОЛЬШОЙ"if (string.sub(tuple[field_no], 1, index_search_key_length)> index_search_key) thenbreakend-- СМ. ПРИМЕЧАНИЕ №6 "ВНУТРЕННИЙ ЦИКЛ: ПРЕРЫВАНИЕ ПОСЛЕ КАЖДЫХ ДЕСЯТИ КОРТЕЖЕЙ -- ВОЗМОЖНО"number_of_tuples_since_last_yield = number_of_tuples_since_last_yield + 1if (number_of_tuples_since_last_yield >= 10and tuple[field_no] ~= previous_tuple_field) thenindex_search_key = tuple[field_no]is_time_for_a_yield = truebreakendprevious_tuple_field = tuple[field_no]-- СМ. ПРИМЕЧАНИЕ №7 "ВНУТРЕННИЙ ЦИКЛ: ДОБАВЛЕНИЕ В РЕЗУЛЬТАТ, ЕСЛИ ШАБЛОН СОВПАДЕТ"if (string.match(tuple[field_no], pattern) ~= nil) thennumber_of_tuples_in_result_set = number_of_tuples_in_result_set + 1result_set[number_of_tuples_in_result_set] = tupleendend-- СМ. ПРИМЕЧАНИЕ №8 "ВНЕШНИЙ ЦИКЛ: ПРЕРЫВАНИЕ ИЛИ ПЕРЕДАЧА УПРАВЛЕНИЯ И ПРОДОЛЖЕНИЕ"if (is_time_for_a_yield ~= true) thenbreakendrequire('fiber').yield()endreturn result_setend
ПРИМЕЧАНИЕ №1 "ПОИСК НУЖНОГО ИНДЕКСА"
Вызывающий клиент передал space_name (имя_спейса – строка) и field_no (номер_поля – число).
Требования здесь следующие:
(a) тип индекса должен быть "TREE", поскольку для других типов индекса (HASH, BITSET, RTREE) поиск с iterator не вернет строки, упорядоченные по строковому значению;
(b) field_no должен представлять собой первую часть индекса;
(c) поле должно содержать строки, потому что для других типов данных (как "unsigned") шаблоны поиска не применяются.
Если индекс не удовлетворяет этим требованиям, выдать сообщение об ошибке и вернуть нулевое значение nil.
ПРИМЕЧАНИЕ №2 "ПОЛУЧЕНИЕ КЛЮЧА ИНДЕКСНОГО ПОИСКА ИЗ ШАБЛОНА"
Вызывающий клиент передал pattern (шаблон – строку). Ключом поиска по индексу
являются символы в шаблоне до первого магического символа. Магические
символы в Lua: % \^ \$ ( ) . [ ] * + - ?. Например, если задан шаблон
"ABC.E", точка будет магическим символом, и ключом поиска по индексу
будет "ABC". Однако есть затруднение ... Если символ "%" будет идти
следом за знаком препинания, этот знак препинания экранируется, поэтому
следует убрать "%" из ключа поиска по индексу. Например, если задан
шаблон "AB%$E", знак доллара экранируется, поэтому ключом поиска по
индексу будет "AB$E". Наконец, есть проверка длины ключа поиска по
индексу – не менее трех символов, причем это число выбрано
произвольно, и даже ноль здесь подойдет, но по короткому ключу поиск
займет длительное время.
ПРИМЕЧАНИЕ №3 "ВНЕШНИЙ ЦИКЛ: НАЧАЛО"
Назначение функции – вернуть результирующий набор данных, как вернул бы запрос box.space...select <box_space-select>.
Мы внесем ее во внешний цикл, который включает в себя внутренний цикл. Назначение внешнего цикла – выполнять
внутренний цикл и, при необходимости, передачу управления, пока
поиск не будет завершен. Назначение внутреннего цикла – находить кортежи по индексу и
включать их в результирующий набор данных, если они подходят под шаблон.
ПРИМЕЧАНИЕ №4 "ВНУТРЕННИЙ ЦИКЛ: ИТЕРАТОР"
Цикл for здесь использует pairs(), см. LINK_NOT_DEFINED(../../../reference/reference_lua/box/box_index/pairs).
Во внутреннем цикле будет локальная переменная под
названием "tuple" (кортеж), которая содержит последний кортеж,
обнаруженный в ходе поиска по индексу.
ПРИМЕЧАНИЕ №5 "ВНУТРЕННИЙ ЦИКЛ: ПРЕРЫВАНИЕ, ЕСЛИ КЛЮЧ ИНДЕКСА СЛИШКОМ БОЛЬШОЙ" Используется итератор GE (Greater or Equal - больше или равно), поэтому необходимо уточнить: если ключ поиска по индексу включает в себя N символов, то крайние N символов слева от найденного поля индекса не должны быть больше ключа поиска. Например, если ключом поиска является 'ABC', то 'ABCDE' потенциально подходит, а 'ABD' означает, что в дальнейшем совпадений не будет.
ПРИМЕЧАНИЕ №6 "ВНУТРЕННИЙ ЦИКЛ: ПРЕРЫВАНИЕ ПОСЛЕ КАЖДЫХ ДЕСЯТИ КОРТЕЖЕЙ – ВОЗМОЖНО" Эта часть кода предназначена для кооперативной многозадачности. Число 10 выбрано произвольно, и как правило, большее число также подойдет. Простое правило гласит: "после проверки 10 кортежей передать управление, а затем возобновить поиск (то есть снова выполнять внутренний цикл), начиная с последнего обнаруженного значения". Однако, если индекс не уникален, или в индексе более одного поля, можно получить дублирующиеся результаты, например, {"ABC",1}, {"ABC", 2}, {"ABC", 3}" – и будет трудно решить, с какого кортежа "ABC" возобновлять поиск. Таким образом, если найденное поле индекса совпадает с предыдущим найденным полем индекса, цикл не прерывается.
ПРИМЕЧАНИЕ №7 "ВНУТРЕННИЙ ЦИКЛ: ДОБАВЛЕНИЕ В РЕЗУЛЬТАТ, ЕСЛИ ШАБЛОН СОВПАДЕТ" Сравнение найденного поля индекса с шаблоном. Например, предположим, что вызывающий клиент передает шаблон "ABC.E", и существует поле индекса, содержащее "ABCDE". В таком случае, начальный ключ поиска будет "ABC". Таким образом, кортеж, содержащий поле индекса с "ABCDE" будет обнаружен итератором, поскольку "ABCDE" > "ABC". В этом случае, string.match вернет значение, отличное от нулевого nil. В итоге, этот кортеж можно добавить в результирующий набор данных.
ПРИМЕЧАНИЕ №8 "ВНЕШНИЙ ЦИКЛ: ПРЕРЫВАНИЕ ИЛИ ПЕРЕДАЧА УПРАВЛЕНИЯ И ПРОДОЛЖЕНИЕ" Существуют три условия, которые вызовут прерывание из внутреннего цикла:
(1) цикл for заканчивается закономерно, потому что отсутствуют ключи индекса, которые больше или равны ключу поиска по индексу, (2) ключ индекса слишком большой, как описано в ПРИМЕЧАНИИ №5, (3) пора передавать управление, как описано в ПРИМЕЧАНИИ №6.
Если условие (1) или условие (2) соблюдается, другие действия не требуются, и внешний цикл также заканчивается. Только в том случае, если справедливо условие (3), внешний цикл должен передать управление, а затем продолжить выполнение. Если он продолжит выполнение, то внутренний цикл – поиск с итератором – будет выполняться снова с новым значением для ключа поиска по индексу.
ПРИМЕР:
Запустите Tarantool, скопируйте и вставьте код для функции
indexed_pattern_search() и попробуйте выполнить следующее:
box.space.t:drop() box.schema.space.create('t')box.space.t:create_index('primary',{})box.space.t:create_index('secondary',{unique=false,parts={2,'string',3,'string'}})box.space.t:insert{1,'A','a'} box.space.t:insert{2,'AB',''}box.space.t:insert{3,'ABC','a'} box.space.t:insert{4,'ABCD',''}box.space.t:insert{5,'ABCDE','a'}box.space.t:insert{6,'ABCDE',''}box.space.t:insert{7,'ABCDEF','a'}box.space.t:insert{8,'ABCDF',''} indexed_pattern_search("t", 2, "ABC.E.")
Получим следующий результат:
tarantool> indexed_pattern_search("t", 2, "ABC.E.")---- - [7, 'ABCDEF', 'a']...