From f56de9b05394390a6b7896141352902352519a77 Mon Sep 17 00:00:00 2001 From: mirzaev Date: Mon, 27 Jan 2025 17:10:20 +0700 Subject: [PATCH] DONE: resolved #1, resolved #3, resolved #4, resolved #5, resolved #7, resolved #9, resolved #10, resolved #12 --- README.md | 2 +- composer.json | 21 +- composer.lock | 2 +- mirzaev/csv/system/database.php | 240 ------- mirzaev/csv/system/record.php | 237 ------- mirzaev/csv/system/traits/file.php | 103 --- mirzaev/csv/tests/deserialize.php | 92 --- mirzaev/ebaboba/system/column.php | 123 ++++ mirzaev/ebaboba/system/database.php | 617 ++++++++++++++++++ .../ebaboba/system/enumerations/encoding.php | 76 +++ mirzaev/ebaboba/system/enumerations/type.php | 66 ++ mirzaev/ebaboba/system/record.php | 119 ++++ mirzaev/ebaboba/tests/.gitignore | 1 + mirzaev/ebaboba/tests/record.php | 278 ++++++++ 14 files changed, 1293 insertions(+), 684 deletions(-) delete mode 100644 mirzaev/csv/system/database.php delete mode 100644 mirzaev/csv/system/record.php delete mode 100755 mirzaev/csv/system/traits/file.php delete mode 100644 mirzaev/csv/tests/deserialize.php create mode 100644 mirzaev/ebaboba/system/column.php create mode 100644 mirzaev/ebaboba/system/database.php create mode 100644 mirzaev/ebaboba/system/enumerations/encoding.php create mode 100644 mirzaev/ebaboba/system/enumerations/type.php create mode 100644 mirzaev/ebaboba/system/record.php create mode 100644 mirzaev/ebaboba/tests/.gitignore create mode 100644 mirzaev/ebaboba/tests/record.php diff --git a/README.md b/README.md index 48c3c00..b235e2e 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Ebaboba database -A lightweight database in pure PHP
+A lightweight database by pure PHP
At the moment the project is a modified RFC 4180 diff --git a/composer.json b/composer.json index e11a811..8bc5727 100755 --- a/composer.json +++ b/composer.json @@ -1,11 +1,12 @@ { - "name": "mirzaev/csv", - "description": "Lightweight library for creating CSV databases", - "homepage": "https://git.mirzaev.sexy/mirzaev/csv", - "type": "library", + "name": "mirzaev/ebaboba", + "description": "Lightweight binary database by pure PHP", + "homepage": "https://git.svoboda.works/mirzaev/ebaboba", + "type": "database", "keywords": [ - "csv", - "database" + "binary", + "plain", + "lightweight" ], "readme": "README.md", "license": "WTFPL", @@ -19,8 +20,8 @@ ], "support": { "email": "arsen@mirzaev.sexy", - "wiki": "https://git.mirzaev.sexy/mirzaev/csv/wiki", - "issues": "https://git.mirzaev.sexy/mirzaev/csv/issues" + "wiki": "https://git.svoboda.works/mirzaev/ebaboba/wiki", + "issues": "https://git.svoboda.works/mirzaev/ebaboba/issues" }, "minimum-stability": "stable", "require": { @@ -28,12 +29,12 @@ }, "autoload": { "psr-4": { - "mirzaev\\csv\\": "mirzaev/csv/system/" + "mirzaev\\ebaboba\\": "mirzaev/ebaboba/system/" } }, "autoload-dev": { "psr-4": { - "mirzaev\\csv\\tests\\": "mirzaev/csv/tests" + "mirzaev\\ebaboba\\tests\\": "mirzaev/ebaboba/tests" } } diff --git a/composer.lock b/composer.lock index fde7627..388fbe1 100755 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "70ef8045ba581d96d3a68483b6031a33", + "content-hash": "d05285edd5fdf816383617183f6d6c38", "packages": [], "packages-dev": [], "aliases": [], diff --git a/mirzaev/csv/system/database.php b/mirzaev/csv/system/database.php deleted file mode 100644 index 46ef22e..0000000 --- a/mirzaev/csv/system/database.php +++ /dev/null @@ -1,240 +0,0 @@ - - */ - -class database -{ - use file { - file::read as protected file; - } - - /** - * File - * - * Path directories to the file will not be created automatically to avoid - * checking the existence of all directories on every read or write operation. - * - * @var string FILE Path to the database file - */ - public const string FILE = 'database.csv'; - - /** - * Columns - * - * This property is used instead of adding a check for the presence of the first row - * with the designation of the column names, as well as reading these columns, - * which would significantly slow down the library. - * - * @see https://www.php.net/manual/en/function.array-combine.php Used when creating a record instance - * - * @var array $columns Database columns - */ - public protected(set) array $columns; - - /** - * Constructor - * - * @param string ...$columns Columns - * - * @return void - */ - public function __construct(string ...$columns) - { - // Initializing columns - if (!empty($columns)) $this->columns = $columns; - } - - /** - * Initialize - * - * Checking for existance of the database file and creating it - * - * @return bool Is the database file exists? - */ - public static function initialize(): bool - { - if (file_exists(static::FILE)) { - // The database file exists - - // Exit (success) - return true; - } else { - // The database file is not exists - - // Creating the database file and exit (success/fail) - return touch(static::FILE); - } - } - - /** - * Create - * - * Create records in the database file - * - * @param record $record The record - * @param array &$errors Buffer of errors - * - * @return void - */ - public static function write(record $record, array &$errors = []): void - { - try { - // Opening the database file - $file = fopen(static::FILE, 'c'); - - if (flock($file, LOCK_EX)) { - // The file was locked - - // Writing the serialized record to the database file - fwrite($file, $record->serialize()); - - // Applying changes - fflush($file); - - // Unlocking the file - flock($file, LOCK_UN); - } - - // Deinitializing unnecessary variables - unset($serialized, $record, $before); - - // Closing the database file - fclose($file); - } catch (exception $e) { - // Write to the buffer of errors - $errors[] = [ - 'text' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'stack' => $e->getTrace() - ]; - } - } - - /** - * Read - * - * Read records in the database file - * - * @param int $amount Amount of records - * @param int $offset Offset of rows for start reading - * @param bool $backwards Read from end to beginning? - * @param callable|null $filter Filter for records function($record, $records): bool - * @param array &$errors Buffer of errors - * - * @return array|null Readed records - */ - public static function read(int $amount = 1, int $offset = 0, bool $backwards = false, ?callable $filter = null, array &$errors = []): ?array - { - try { - // Opening the database file - $file = fopen(static::FILE, 'r'); - - // Initializing the buffer of readed records - $records = []; - - // Continuing reading - offset: - - foreach (static::file(file: $file, offset: $offset, rows: $amount, position: 0, step: $backwards ? -1 : 1) as $row) { - // Iterating over rows - - if ($row === null) { - // Reached the end or the beginning of the file - - // Deinitializing unnecessary variables - unset($row, $record, $offset); - - // Closing the database file - fclose($file); - - // Exit (success) - return $records; - } - - // Initializing record - $record = new record($row)->combine($this); - - if ($record) { - // Initialized record - - if ($filter === null || $filter($record, $records)) { - // Filter passed - - // Writing to the buffer of readed records - $records[] = $record; - } - } - } - - // Deinitializing unnecessary variables - unset($row, $record); - - if (count($records) < $amount) { - // Fewer rows were read than requested - - // Writing offset for reading - $offset += $amount; - - // Continuing reading (enter to the recursion) - goto offset; - } - - // Deinitializing unnecessary variables - unset($offset); - - - // Closing the database file - fclose($file); - - // Exit (success) - return $records; - } catch (exception $e) { - // Write to the buffer of errors - $errors[] = [ - 'text' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'stack' => $e->getTrace() - ]; - } - - // Exit (fail) - return null; - } -} diff --git a/mirzaev/csv/system/record.php b/mirzaev/csv/system/record.php deleted file mode 100644 index 7da1692..0000000 --- a/mirzaev/csv/system/record.php +++ /dev/null @@ -1,237 +0,0 @@ - - */ -class record -{ - /** - * Parameters - * - * Mapped with database::COLUMN - * - * @var array $parameters Parameters of the record - */ - public protected(set) array $parameters = []; - - /** - * Constructor - * - * @param mixed $parameters Parameter of the record - * - * @return void - */ - public function __construct(mixed ...$parameters) - { - // Initializing parameters - if (!empty($parameters)) $this->parameters = $parameters; - } - - /** - * Columns - * - * Combine parameters of the record with columns of the database - * The array of parameters of the record will become associative - * - * @return static The instance from which the method was called (fluent interface) - */ - public function columns(database $database): static - { - // Combining database columns with record parameters - $this->parameters = array_combine($database->columns, $this->parameters); - - // Exit (success) - return $this; - } - - /** - * Serialize - * - * Convert record instance to values for writing into the database - * - * @return string Serialized record - */ - public function serialize(): string - { - // Declaring the buffer of generated row - $serialized = ''; - - foreach ($this->parameters as $value) { - // Iterating over parameters - - // Generating row by RFC 4180 - $serialized .= ',' . preg_replace('/(?<=[^^])"(?=[^$])/', '""', preg_replace('/(?<=[^^]),(?=[^$])/', '\,', $value ?? '')); - } - - // Trimming excess first comma in the buffer of generated row - $serialized = mb_substr($serialized, 1, mb_strlen($serialized)); - - // Exit (success) - return $serialized; - } - - /** - * Deserialize - * - * Convert values from the database and write to the record instance - * - * @param string $row Row from the database - * - * @return array Deserialized record - */ - public static function deserialize(string $row): array - { - // Separating row by commas - preg_match_all('/(.*)(?>(?parameters[$name] = $value; - } - - /** - * Read - * - * Read the parameter - * - * @param string $name Name of the parameter - * - * @return mixed Content of the parameter - */ - public function __get(string $name): mixed - { - // Reading the parameter and exit (success) - return $this->parameters[$name]; - } - - /** - * Delete - * - * Delete the parameter - * - * @param string $name Name of the parameter - * - * @return void - */ - public function __unset(string $name): void - { - // Deleting the parameter and exit (success) - unset($this->parameter[$name]); - } - - /** - * Check for initializing - * - * Check for initializing the parameter - * - * @param string $name Name of the parameter - * - * @return bool Is the parameter initialized? - */ - public function __isset(string $name): bool - { - // Checking for initializing the parameter and exit (success) - return isset($this->parameters[$name]); - } - -} diff --git a/mirzaev/csv/system/traits/file.php b/mirzaev/csv/system/traits/file.php deleted file mode 100755 index 31b4a2b..0000000 --- a/mirzaev/csv/system/traits/file.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ -trait file -{ - /** - * Read - * - * Read the file - * - * @param resource $file Pointer to the file (fopen()) - * @param int $rows Amount of rows for reading - * @param int $offset Offset of rows for start reading - * @param int $position Initial cursor position on a row - * @param int $step Reading step - * @param array &$errors Buffer of errors - * - * @return generator|null|false - */ - private static function read($file, int $rows = 10, int $offset = 0, int $position = 0, int $step = 1, array &$errors = []): generator|null|false - { - try { - while ($offset-- > 0) { - do { - // Iterate over symbols of the row - - // The end (or the beginning) of the file reached (success) - if (feof($file)) break; - - // Moving the cursor to next position on the row - fseek($file, $position += $step, SEEK_END); - - // Reading a character of the row - $character = fgetc($file); - - // Is the character a carriage return? (end or start of the row) - } while ($character !== PHP_EOL); - } - - while ($rows-- > 0) { - // Reading rows - - // Initializing of the buffer of row - $row = ''; - - // Initializing the character buffer to generate $row - $character = ''; - - do { - // Iterate over symbols of the row - - // The end (or the beginning) of the file reached (success) - if (feof($file)) break; - - // Building the row - $row = $step > 0 ? $row . $character : $character . $row; - - // Moving the cursor to next position on the row - fseek($file, $position += $step, SEEK_END); - - // Reading a character of the row - $character = fgetc($file); - - // Is the character a carriage return? (end or start of the row) - } while ($character !== PHP_EOL); - - // Exit (success) - yield empty($row) ? null : $row; - } - - // Exit (success) - return null; - } catch (exception $e) { - // Write to the buffer of errors - $errors[] = [ - 'text' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), - 'stack' => $e->getTrace() - ]; - } - - // Exit (fail) - return false; - } -} diff --git a/mirzaev/csv/tests/deserialize.php b/mirzaev/csv/tests/deserialize.php deleted file mode 100644 index 4e1c070..0000000 --- a/mirzaev/csv/tests/deserialize.php +++ /dev/null @@ -1,92 +0,0 @@ -parameters[0] === null ? 'SUCCESS' : 'FAIL') . "] The empty value at the beginning is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[1] === 'Arsen' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Arsen\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[2] === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Mirzaev\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[3] === '23' ? 'SUCCESS' : 'FAIL') . "] The age between quotes value is need to be \"23\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[4] === true ? 'SUCCESS' : 'FAIL') . "] The value is need to be true\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[5] === null ? 'SUCCESS' : 'FAIL') . "] The empty value is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[6] === '' ? 'SUCCESS' : 'FAIL') . "] The empty value between quotes is need to be \"\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[7] === 100 ? 'SUCCESS' : 'FAIL') . "] The value is need to be 100 integer\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[8] === null ? 'SUCCESS' : 'FAIL') . "] The null value is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[9] === 'null' ? 'SUCCESS' : 'FAIL') . "] The null value between quotes is need to be \"null\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[10] === '102.1' ? 'SUCCESS' : 'FAIL') . "] The float value between quotes is need to be \"102.1\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[11] === 300.34 ? 'SUCCESS' : 'FAIL') . "] The float value is need to be 300.34 float \n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[12] === 1001.23145 ? 'SUCCESS' : 'FAIL') . "] The long float value is need to be 1001.23145 float\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[13] === '5000.400.400' ? 'SUCCESS' : 'FAIL') . "] The float value with two dots is need to be \"5000.400.400\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[14] === 'test "value"' ? 'SUCCESS' : 'FAIL') . "] The value with quotes is need to be \"test \"value\"\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[15] === 'another" test " value with "two double quotes pairs" yeah' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"another\" test \" value with \"two double quotes pairs\" yeah\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[16] === ' starts with space' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \" starts with space\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[17] === 'has, an escaped comma inside' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"has, an escaped comma inside\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->parameters[18] === 'unicode символы' ? 'SUCCESS' : 'FAIL') . "] The valueis need to be \"unicode символы\" string\n"; - -// Combining database columns with record parameters -$record->columns($database); - -echo '[' . ++$action . "] Combined database columns with record parameters\n"; - - -// Reinitializing the counter of tests -$test = 0; - -echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value_at_the_beginning === null ? 'SUCCESS' : 'FAIL') . "] The empty value at the beginning is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->name === 'Arsen' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Arsen\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"Mirzaev\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->age_between_quotes === '23' ? 'SUCCESS' : 'FAIL') . "] The age between quotes value is need to be \"23\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->true === true ? 'SUCCESS' : 'FAIL') . "] The value is need to be true\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value === null ? 'SUCCESS' : 'FAIL') . "] The empty value is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->empty_value_between_quotes === '' ? 'SUCCESS' : 'FAIL') . "] The empty value between quotes is need to be \"\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->number === 100 ? 'SUCCESS' : 'FAIL') . "] The value is need to be 100 integer\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->null === null ? 'SUCCESS' : 'FAIL') . "] The null value is need to be null\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->null_between_quotes === 'null' ? 'SUCCESS' : 'FAIL') . "] The null value between quotes is need to be \"null\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_between_quotes === '102.1' ? 'SUCCESS' : 'FAIL') . "] The float value between quotes is need to be \"102.1\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->float === 300.34 ? 'SUCCESS' : 'FAIL') . "] The float value is need to be 300.34 float \n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_long === 1001.23145 ? 'SUCCESS' : 'FAIL') . "] The long float value is need to be 1001.23145 float\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->float_with_two_dots === '5000.400.400' ? 'SUCCESS' : 'FAIL') . "] The float value with two dots is need to be \"5000.400.400\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_doubled_quotes === 'test "value"' ? 'SUCCESS' : 'FAIL') . "] The value with quotes is need to be \"test \"value\"\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_doubled_quotes_twice === 'another" test " value with "two double quotes pairs" yeah' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"another\" test \" value with \"two double quotes pairs\" yeah\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_space_at_the_beginning === ' starts with space' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \" starts with space\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_escaped_comma === 'has, an escaped comma inside' ? 'SUCCESS' : 'FAIL') . "] The value is need to be \"has, an escaped comma inside\" string\n"; -echo '[' . ++$action . '][' . ++$test . '][' . ($record->string_with_unicode_symbols === 'unicode символы' ? 'SUCCESS' : 'FAIL') . "] The valueis need to be \"unicode символы\" string\n"; diff --git a/mirzaev/ebaboba/system/column.php b/mirzaev/ebaboba/system/column.php new file mode 100644 index 0000000..d80eec3 --- /dev/null +++ b/mirzaev/ebaboba/system/column.php @@ -0,0 +1,123 @@ + + */ +class column +{ + /** + * Name + * + * @var string $name Name of the column + */ + public readonly protected(set) string $name; + + /** + * Type + * + * @see https://www.php.net/manual/en/function.pack.php Pack (types are shown here) + * @see https://www.php.net/manual/en/function.unpack.php Unpack + * + * @var type $type Type of the column values + */ + public readonly protected(set) type $type; + + /** + * Length + * + * Length of every binary value that will be written to the database file + * + * @throws exception_logic if the length property is already initialized + * @throws exception_logic if the type is not initialized + * @throws exception_domain if the type can not has length + * + * @var int $length Length of every binary values + */ + public protected(set) int $length { + // Write + set (int $value) { + if (isset($this->length)) { + // Already been initialized + + // Exit (fail) + throw new exception_logic('The length property is already initialized'); + } else if (!isset($this->type)) { + // The type is not initialized + + // Exit (fail) + throw new exception_logic('The type of the column values is not initialized'); + } else if (match ($this->type) { + type::string => true, + default => false + }) { + // The type has length + + // Writing into the property + $this->length = $value; + } else { + // The type has no length + + // Exit (fail) + throw new exception_domain('The "' . $this->type->name . '" type can not has length'); + } + } + } + + /** + * Constructor + * + * @param string $name Name of the column + * @param type $type Type of the column values + * @param array $parameters Parameters of the column + * + * @return void + */ + public function __construct(string $name, type $type, array $parameters = []) + { + // Writing into the property + $this->name = $name; + + // Writing into the property + $this->type = $type; + + foreach ($parameters as $name => $value) { + // Iterating over parameters + + if (property_exists($this, $name)) { + // Found the property + + // Writing into the property + $this->{$name} = $value; + } else { + // Not found the property + + // Exit (fail) + throw new exceptiin_invalid_argument("Not found the property: $name"); + } + } + } +} diff --git a/mirzaev/ebaboba/system/database.php b/mirzaev/ebaboba/system/database.php new file mode 100644 index 0000000..2373450 --- /dev/null +++ b/mirzaev/ebaboba/system/database.php @@ -0,0 +1,617 @@ + + */ +class database +{ + /** + * Database + * + * Path to the database file + * + * @var string $database Path to the database file + */ + public protected(set) string $database = __DIR__ . DIRECTORY_SEPARATOR . 'database.ba'; + + /** + * Backups + * + * Path to the backups files directory + * + * @var string $backups Path to the backups files directory + */ + public protected(set) string $backups = __DIR__ . DIRECTORY_SEPARATOR . 'backups'; + + /** + * Encoding + * + * @var encoding $encoding Encoding of records in the database file + */ + public protected(set) encoding $encoding; + + /** + * Columns + * + * @var record[] $columns The database columns + */ + public protected(set) array $columns; + + /** + * Length + * + * @var int $length Binary size of every record in the database file + */ + public protected(set) int $length; + + /** + * Encoding + * + * Write encoding into the database instance property + * + * @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface + * + * @param encoding $encoding The database file encoding + * + * @return self The database instance (fluent interface) + */ + public function encoding(encoding $encoding): self + { + // Writing into the database instance property + $this->encoding = $encoding; + + // Exit (success) + return $this; + } + + /** + * Columns + * + * Write columns into the database instance property + * + * @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface + * + * @param column[] ...$columns The database columns + * + * @return self The database instance (fluent interface) + */ + public function columns(column ...$columns): self + { + // Writing into the database instance property + $this->columns = $columns; + + // Initializing the database instance property + $this->length ??= 0; + + foreach ($this->columns as $column) { + // Iterating over columns + + if ($column->type === type::string) { + // String + + // Adding the column string maximum length to the database instance property + $this->length += $column->length; + } else { + // Other types + + // Adding the column type size to the database instance property + $this->length += $column->type->size(); + } + } + + // Exit (success) + return $this; + } + + /** + * Connect + * + * Initialize the database files + * + * @see https://en.wikipedia.org/wiki/Fluent_interface#PHP Fluent Interface + * + * @param string $database Path to the database file + * + * @return self The database instance (fluent interface) + */ + public function connect(string $database): self + { + // Writing into the database instance property + $this->database = $database; + + // Exit (success) + return $this; + } + + /** + * Record + * + * Initialize the record by the database columns + * + * @param mixed[] $values Values of the record + * + * @throws exceptiin_invalid_argument if the balue type not matches the column values types + * @throws exception_logic if amount of columns not matches the amount of values + * + * @return record|null The record instance + */ + public function record(string|int|float ...$values): ?record + { + if (count($values) === count($this->columns)) { + // Amount of values matches amount of columns + + // Declaring the buffer of combined values + $combined = []; + + foreach ($this->columns as $index => $column) { + // Iterating over columns + + if (gettype($values[$index]) === $column->type->type()) { + // The value type matches the column values type + + // Writing named index value into the buffer of combined values + $combined[$column->name] = $values[$index]; + } else { + // The value type not matches the column values type + + // Exit (fail) + throw new exception_invalid_argument('The value type not matches the column values type'); + } + } + + // Initializing the record by the buffer of combined values + $record = new record(...$combined); + + // Exit (success) + return $record; + } else { + // Amount of values not matches amount of columns + + // Exit (fail) + throw new exception_logic('Amount of values not matches amount of columns'); + } + + // Exit (fail) + return null; + } + + /** + * Pack + * + * Pack the record values + * + * @param record $record The record + * + * @return string Packed values + */ + public function pack(record $record): string + { + // Declaring buffer of packed values + $packed = ''; + + foreach ($this->columns as $column) { + // Iterating over columns + + if ($column->type === type::string) { + // String + + // Converting to the database encoding + $value = mb_convert_encoding($record->values()[$column->name], $this->encoding->value); + + // Packung the value and writing into the buffer of packed values + $packed .= pack($column->type->value . $column->length, $value); + } else { + // Other types + + // Packung the value and writing into the buffer of packed values + $packed .= pack($column->type->value, $record->values()[$column->name]); + } + } + + // Exit (success) + return $packed; + } + + /** + * Unpack + * + * Unpack binary values and implement them as a `record` instance + * + * @param array $binaries Binary values in the same order as the columns + * + * @return record The unpacked record from binary values + */ + public function unpack(array $binaries): record + { + if (count($binaries) === count($this->columns)) { + // Amount of binery values matches amount of columns + + // Declaring the buffer of unpacked values + $unpacked = []; + + foreach (array_combine($binaries, $this->columns) as $binary => $column) { + // Iterating over columns + + if ($column->type === type::string) { + // String + + // Unpacking the value + $value = unpack($column->type->value . $column->length, $binary)[1]; + + // Deleting NULL-characters + $unnulled = str_replace("\0", '', $value); + + // Encoding the unpacked value + $encoded = mb_convert_encoding($unnulled, $this->encoding->value); + + // Writing into the buffer of readed values + $unpacked[] = $encoded; + } else { + // Other types + + // Writing into the buffer of readed values + $unpacked[] = unpack($column->type->value, $binary)[1]; + } + } + + // Implementing the record + $record = $this->record(...$unpacked); + + // Exit (success) + return $record; + } else { + // Amount of binery values not matches amount of columns + + // Exit (fail) + throw new exception_invalid_argument('Amount of binary values not matches amount of columns'); + } + } + + /** + * Write + * + * Write the record into the database file + * + * @param record $record The record + * + * @throws exception_runtime If failed to lock the file + * @throws exception_runtime If failed to unlock the file + * + * @return bool Is the record was writed into the end of the database file + */ + public function write(record $record): bool + { + try { + // Opening the database file + $file = fopen($this->database, 'ab'); + + if (flock($file, LOCK_EX)) { + // The file was locked + + // Packing the record values + $packed = $this->pack($record); + + // Writing the packed values to the database file + fwrite($file, $packed); + + // Applying changes + fflush($file); + + if (flock($file, LOCK_UN)) { + // The file was unlocked + + // Exit (success) + return true; + } else { + // Failed to unlock the file + + // Exit (fail) + throw new exception_runtime('Failed to unlock the file'); + } + } else { + // Failed to lock the file + + // Exit (fail) + throw new exception_runtime('Failed to lock the file'); + } + } finally { + // Closing the database file + fclose($file); + } + + // Exit (fail) + return false; + } + + /** + * Read + * + * Read records from the database file + * + * Order: `$filter` -> `$offset` -> (`$delete` -> read deleted || `$update` -> read updated || read) -> `$amount` + * + * @param callable|null $filter Filtering records `function($record, $records): bool` + * @param callable|null $update Updating records `function(&$record): void` + * @param callable|null $delete Deleting records + * @param int $amount Amount iterator + * @param int $offset Offset iterator + * + * @return array|null Readed records + */ + public function read(?callable $filter = null, ?callable $update = null, bool $delete = false, int $amount = 1, int $offset = 0): ?array + { + // Opening the database file + $file = fopen($this->database, 'r+b'); + + if (flock($file, LOCK_EX)) { + // The file was locked + + // Declaring the buffer of readed records + $records = []; + + // Declaring the buffer of failed to reading records + /* $failed = []; */ + + while ($amount > 0) { + // Reading records + + // Declaring the buffer of binary values + $binaries = []; + + foreach ($this->columns as $column) { + // Iterating over columns + + if ($column->type === type::string) { + // String + + // Reading the binary value from the database + $binaries[] = fread($file, $column->length); + } else { + // Other types + + // Reading the binary value from the database + $binaries[] = fread($file, $column->type->size()); + } + } + + // Terminate loop when end of file is reached + if (feof($file)) break; + + try { + // Unpacking the record + $record = $this->unpack($binaries); + + if (is_null($filter) || $filter($record, $records)) { + // Passed the filter + + if ($offset-- <= 0) { + // Offsetted + + if ($delete) { + // Requested deleting + + // Moving to the beginning of the row + fseek($file, -$this->length, SEEK_CUR); + + // Writing NUL-characters instead of the record to the database file + fwrite($file, str_repeat("\0", $this->length)); + + // Moving to the end of the row + fseek($file, $this->length, SEEK_CUR); + } else if ($update) { + // Requested updating + + // Updating the record + $update($record); + + // Packing the updated record + $packed = $this->pack($record); + + // Moving to the beginning of the row + fseek($file, -$this->length, SEEK_CUR); + + // Writing to the database file + fwrite($file, $packed); + + // Moving to the end of the row + fseek($file, $this->length, SEEK_CUR); + } + + // Writing into the buffer of records + $records[] = $record; + + // Decreasing the amount iterator + --$amount; + } + } + } catch (exception_logic | exception_invalid_argument $exception) { + // Writing into the buffer of failed to reading records + /* $failed[] = $record; */ + } + } + + // Unlocking the file + flock($file, LOCK_UN); + + // Closing the database file + fclose($file); + + // Exit (success) + return $records; + } + + // Exit (fail) + return null; + } + + /** + * Backups + * + * Initialize the backups files directory + * + * @throws exception_runtime if failed to create the backups files directory + * + * @return bool Is the backups files directory created? + */ + public function backups(): bool + { + if (is_dir($this->backups) || is_writable($this->backups)) { + // The backups files directory exists + + // Exit (success) + return true; + } else { + // The backups files directory is not exists + + if (mkdir(directory: $this->backups, permissions: 0775, recursive: true)) { + // The backups files directory created + + // Exit (success) + return true; + } else { + // The backups files directory is still not exists + + // Exit (fail) + throw new exception_runtime('Failed to create the backups files directory: "' . $this->backups . '"'); + } + } + + // Exit (fail) + return false; + } + + /** + * Save + * + * Create an unique backup file from the database file + * + * @throws exception_runtime if failed to copying the database file to the backup file + * @throws exception_runtime if failed to initialize the backups files directory + * + * @return int|false Unique identifier of the created backup file + */ + public function save(): int|false + { + if ($this->backups()) { + // Initialized the backups files directory + + // Generation of unique identifier + generate: + + // Generating unique identifier for the backup file + $identifier = uniqid(); + + // Initializing path to the backup file with generated identifier + $file = $this->backups . DIRECTORY_SEPARATOR . $identifier; + + if (file_exists($file)) { + // File with this identifier is already exists + + // Repeating generation (entering into recursion) + goto generate; + } else { + // Generated unique identifier for the backup file + + if (copy($this->database, $file)) { + // Copied the database file to the backup file + + // Exit (success) + return $identifier; + } else { + // Not copied the database file to the backup file + + // Exit (fail) + throw new exception_runtime('Failed to copying the database file to the backup file'); + } + } + } else { + // Not initialized the backups files directory + + // Exit (fail) + throw new exception_runtime('Failed to initialize the backups files directory'); + } + + // Exit (fail) + return false; + } + + /** + * Load + * + * Restore the database file from the backup file + * + * @throws exception_runtime if not found the backup file + * @throws exception_runtime if failed to initialize the backups files directory + * + * @return int|false Unique identifier of the created backup file + */ + public function load(int $identifier): bool + { + if ($this->backups()) { + // Initialized the backups files directory + + // Initializing path to the backup file + $file = $this->backups . DIRECTORY_SEPARATOR . $identifier; + + if (file_exists($file)) { + // Initialized the backup file + + if (rename($file, $this->database)) { + // Loaded the database file from the backup file + + // Exit (success) + return true; + } + } else { + // Not initialized the backup file + + // Exit (fail) + throw new exception_runtime('Not found the backup file'); + } + } else { + // Not initialized the backups files directory + + // Exit (fail) + throw new exception_runtime('Failed to initialize the backups files directory'); + } + + // Exit (fail) + return false; + } +} diff --git a/mirzaev/ebaboba/system/enumerations/encoding.php b/mirzaev/ebaboba/system/enumerations/encoding.php new file mode 100644 index 0000000..c4990c1 --- /dev/null +++ b/mirzaev/ebaboba/system/enumerations/encoding.php @@ -0,0 +1,76 @@ + + */ +enum encoding: string +{ + case ascii = 'ASCII'; + case cp1251 = 'CP1251'; // Windows 1251 + case cp1252 = 'CP1252'; // Windows-1252 + case cp1253 = 'CP1253'; // Windows-1253 + case cp1254 = 'CP1254'; // Windows-1254 + case cp1255 = 'CP1255'; // Windows-1255 + case cp1256 = 'CP1256'; // Windows-1256 + case cp1257 = 'CP1257'; // Windows-1257 + case cp1258 = 'CP1258'; // Windows-1258 + + case utf8 = 'UTF-8'; + case utf16 = 'UTF-16'; + case utf32 = 'UTF-32'; + + /** + * Length + * + * @return int Number of bits for a symbol + */ + public function maximum(): int + { + // Exit (success) + return match ($this) { + encoding::ascii => 7, + encoding::cp1251, encoding::cp1252, encoding::cp1253, encoding::cp1254, encoding::cp1255, encoding::cp1256, encoding::cp1257, encoding::cp1258 => 8, + encoding::utf8 => 8, + encoding::utf16 => 16, + encoding::utf32 => 32, + default => throw new exception_unexpected_value('Not found the encoding') + }; + } +} diff --git a/mirzaev/ebaboba/system/enumerations/type.php b/mirzaev/ebaboba/system/enumerations/type.php new file mode 100644 index 0000000..1c15930 --- /dev/null +++ b/mirzaev/ebaboba/system/enumerations/type.php @@ -0,0 +1,66 @@ + + */ +enum type: string +{ + case string = 'a'; + case char = 'c'; + case char_unsigned = 'C'; + case short = 's'; + case short_unsigned = 'S'; + case integer = 'i'; + case integer_unsigned = 'I'; + case long = 'l'; + case long_unsigned = 'L'; + case long_long = 'q'; + case long_long_unsigned = 'Q'; + case float = 'f'; + case double = 'd'; + case null = 'x'; + + /** + * Type + * + * @see https://www.php.net/manual/en/function.gettype.php (here is why "double" instead of "float" and "NULL" instead of "null") + * + * @return string Type + */ + public function type(): string + { + // Exit (success) + return match ($this) { + type::char, type::string, type::short => 'string', + type::char_unsigned, type::short_unsigned, type::integer, type::integer_unsigned, type::long, type::long_unsigned, type::long_long, type::long_long_unsigned => 'integer', + type::float, type::double => 'double', + type::null => 'NULL', + default => throw new exception_unexpected_value('Not found the type') + }; + } + + /** + * Size + * + * @return int Size in bytes + */ + public function size(): int + { + // Exit (success) + return strlen(pack($this->value, 0)); + } +} diff --git a/mirzaev/ebaboba/system/record.php b/mirzaev/ebaboba/system/record.php new file mode 100644 index 0000000..4641ab1 --- /dev/null +++ b/mirzaev/ebaboba/system/record.php @@ -0,0 +1,119 @@ + + */ +class record +{ + /** + * Values + * + * @var array $values The record values + */ + protected array $values = []; + + /** + * Constructor + * + * @param string[]|int[]|float[] $values Values of the record + * + * @return void + */ + public function __construct(string|int|float ...$values) + { + // Initializing values + if (!empty($values)) $this->values = $values; + } + + /** + * Values + * + * Read all values of the record + * + * @return array All values of the record + */ + public function values(): array + { + return $this->values ?? []; + } + + /** + * Write + * + * Write the value + * + * @param string $name Name of the parameter + * @param mixed $value Content of the parameter + * + * @return void + */ + public function __set(string $name, mixed $value = null): void + { + // Writing the value and exit + $this->values[$name] = $value; + } + + /** + * Read + * + * Read the value + * + * @param string $name Name of the value + * + * @return mixed Content of the value + */ + public function __get(string $name): mixed + { + // Reading the value and exit (success) + return $this->values[$name] ?? null; + } + + /** + * Delete + * + * Delete the value + * + * @param string $name Name of the value + * + * @return void + */ + public function __unset(string $name): void + { + // Deleting the value + unset($this->values[$name]); + } + + /** + * Check for initializing + * + * Check for initializing the value + * + * @param string $name Name of the value + * + * @return bool Is the value initialized? + */ + public function __isset(string $name): bool + { + // Checking for initializing the value and exit (success) + return isset($this->values[$name]); + } +} diff --git a/mirzaev/ebaboba/tests/.gitignore b/mirzaev/ebaboba/tests/.gitignore new file mode 100644 index 0000000..ed041f1 --- /dev/null +++ b/mirzaev/ebaboba/tests/.gitignore @@ -0,0 +1 @@ +temporary diff --git a/mirzaev/ebaboba/tests/record.php b/mirzaev/ebaboba/tests/record.php new file mode 100644 index 0000000..b6e9930 --- /dev/null +++ b/mirzaev/ebaboba/tests/record.php @@ -0,0 +1,278 @@ +encoding(encoding::utf8) + ->columns( + new column('name', type::string, ['length' => 32]), + new column('second_name', type::string, ['length' => 64]), + new column('age', type::integer), + new column('height', type::float) + ) + ->connect(__DIR__ . DIRECTORY_SEPARATOR . 'temporary' . DIRECTORY_SEPARATOR . 'database.ba'); + +echo '[' . ++$action . "] Initialized the database\n"; + +// Initializing the record +$record = $database->record( + 'Arsen', + 'Mirzaev', + 24, + 165.5 +); + +echo '[' . ++$action . "] Initialized the record\n"; + +// Initializing the counter of tests +$test = 0; + +echo '[' . ++$action . '][' . ++$test . '][' . ($record->name === 'Arsen' ? 'SUCCESS' : 'FAIL') . "][\"name\"] Expected: \"Arsen\" (string). Actual: \"$record->name\" (" . gettype($record->name) . ")\n"; +echo '[' . $action . '][' . ++$test . '][' . ($record->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . "][\"second_name\"] Expected: \"Mirzaev\" (string). Actual: \"$record->second_name\" (" . gettype($record->second_name) . ")\n"; +echo '[' . $action . '][' . ++$test . '][' . ($record->age === 24 ? 'SUCCESS' : 'FAIL') . "][\"age\"] Expected: \"24\" (integer). Actual: \"$record->age\" (" . gettype($record->age) . ")\n"; +echo '[' . $action . '][' . ++$test . '][' . ($record->height === 165.5 ? 'SUCCESS' : 'FAIL') . "][\"height\"] Expected: \"165.5\" (double). Actual: \"$record->height\" (" . gettype($record->height) . ")\n"; + +echo '[' . $action . "] The record parameters checks have been completed\n"; + +// Reinitializing the counter of tests +$test = 0; + +// Writing the record into the database +$database->write($record); + +echo '[' . ++$action . "] Writed the record into the database\n"; + +// Initializing the second record +$record_ivan = $database->record( + 'Ivan', + 'Ivanov', + 24, + (float) 210, +); + +echo '[' . ++$action . "] Initialized the record\n"; + +// Writing the second record into the databasse +$database->write($record_ivan); + +echo '[' . ++$action . "] Writed the record into the database\n"; + +// Initializing the second record +$record_ivan = $database->record( + 'Margarita', + 'Esenina', + 19, + (float) 165, +); + +echo '[' . ++$action . "] Initialized the record\n"; + +// Writing the second record into the databasse +$database->write($record_ivan); + +echo '[' . ++$action . "] Writed the record into the database\n"; + +// Reading all records from the database +$records_readed_all = $database->read(amount: 99999); + +echo '[' . ++$action . "] Readed all records from the database\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_all) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_all) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_all) === 3 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 3 records. Actual: ' . count($records_readed_all) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_all[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_all[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_all[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_all[0]::class . "\"\n"; + + echo '[' . $action . "] The readed all records checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed all records checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Reading the first record from the database +$record_readed_first = $database->read(amount: 1); + +echo '[' . ++$action . "] Readed the first record from the database\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_first) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_first) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_first) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_first) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_first[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_first[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_first[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_first[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_first[0]->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Mirzaev" (string). Actual: "' . $record_readed_first[0]->second_name . '" (' . gettype($record_readed_first[0]->second_name) . ")\n"; + + echo '[' . $action . "] The readed first record checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed first record checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Reading the second record from the database +$record_readed_second = $database->read(amount: 1, offset: 1); + +echo '[' . ++$action . "] Readed the second record from the database\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_second) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_second) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_second) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_second) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_second[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_second[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_second[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_second[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_second[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $record_readed_second[0]->second_name . '" (' . gettype($record_readed_second[0]->second_name) . ")\n"; + + echo '[' . $action . "] The readed second record checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed second record checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Reading the record from the database by filter +$record_readed_filter = $database->read(filter: fn($record) => $record?->second_name === 'Ivanov', amount: 1); + +echo '[' . ++$action . "] Readed the record from the database by filter\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($record_readed_filter) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($record_readed_filter) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($record_readed_filter) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($record_readed_filter) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($record_readed_filter[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($record_readed_filter[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_filter[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $record_readed_filter[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($record_readed_filter[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $record_readed_filter[0]->second_name . '" (' . gettype($record_readed_filter[0]->second_name) . ")\n"; + + echo '[' . $action . "] The readed record by filter checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed record by filter checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Reading the record from the database by filter with amount limit +$records_readed_filter_amount = $database->read(filter: fn($record) => $record?->age === 24, amount: 1); + +echo '[' . ++$action . "] Readed the record from the database by filter with amount limit\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_amount) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_amount) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($records_readed_filter_amount) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_amount[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_amount[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0]->age === 24 ? 'SUCCESS' : 'FAIL') . ']["age"] Expected: "24" (integer). Actual: "' . $records_readed_filter_amount[0]->age . '" (' . gettype($records_readed_filter_amount[0]->age) . ")\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount[0]->second_name === 'Mirzaev' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Mirzaev" (string). Actual: "' . $records_readed_filter_amount[0]->second_name . '" (' . gettype($records_readed_filter_amount[0]->second_name) . ")\n"; + + echo '[' . $action . "] The readed record by filter with amount limit checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed record by filter with amount limit checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Reading the record from the database by filter with amount limit and offset +$records_readed_filter_amount_offset = $database->read(filter: fn($record) => $record?->age === 24, amount: 1, offset: 1); + +echo '[' . ++$action . "] Readed the record from the database by filter with amount limit and offset\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount_offset) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_amount_offset) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_amount_offset) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of readed records] Expected: 1 records. Actual: ' . count($records_readed_filter_amount_offset) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_amount_offset[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_amount_offset[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_amount_offset[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0]->age === 24 ? 'SUCCESS' : 'FAIL') . ']["age"] Expected: "24" (integer). Actual: "' . $records_readed_filter_amount_offset[0]->age . '" (' . gettype($records_readed_filter_amount_offset[0]->age) . ")\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_amount_offset[0]->second_name === 'Ivanov' ? 'SUCCESS' : 'FAIL') . ']["second_name"] Expected: "Ivanov" (string). Actual: "' . $records_readed_filter_amount_offset[0]->second_name . '" (' . gettype($records_readed_filter_amount_offset[0]->second_name) . ")\n"; + + echo '[' . $action . "] The readed record by filter with amount limit and offset checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The readed record by filter with amount limit and offset checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Deleting the record in the database by filter +$records_readed_filter_delete = $database->read(filter: fn($record) => $record?->name === 'Ivan', delete: true, amount: 1); + +echo '[' . ++$action . "] Deleted the record from the database by filter\n"; + +// Reading records from the database after deleting +$records_readed_filter_delete_readed = $database->read(amount: 100); + +echo '[' . ++$action . "] Readed records from the database after deleting the record\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_delete) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_delete) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_delete) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of deleted records] Expected: 1 records. Actual: ' . count($records_readed_filter_delete) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_delete[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_delete[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_delete[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_delete[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_delete[0]->name === 'Ivan' ? 'SUCCESS' : 'FAIL') . ']["name"] Expected: "Ivan" (string). Actual: "' . $records_readed_filter_delete[0]->second_name . '" (' . gettype($records_readed_filter_delete[0]->second_name) . ")\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_delete_readed) === 2 ? 'SUCCESS' : 'FAIL') . '][amount of readed records after deleting] Expected: 2 records. Actual: ' . count($records_readed_filter_delete_readed) . " records\n"; + + echo '[' . $action . "] The deleted record by filter checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The deleted record by filter checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +// Updating the record in the database +$records_readed_filter_update = $database->read(filter: fn($record) => $record?->name === 'Margarita', update: function (&$record) { $record->height += 0.5; }, amount: 1); + +echo '[' . ++$action . "] Updated the record in the database by filter\n"; + +// Reading records from the database after updating +$records_readed_filter_update_readed = $database->read(amount: 100); + +echo '[' . ++$action . "] Readed records from the database after updating the record\n"; + +try { + echo '[' . ++$action . '][' . ++$test . '][' . (gettype($records_readed_filter_update) === 'array' ? 'SUCCESS' : 'FAIL') . '][type of returned value] Expected: "array". Actual: "' . gettype($records_readed_filter_update) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_update) === 1 ? 'SUCCESS' : 'FAIL') . '][amount of updated records] Expected: 1 records. Actual: ' . count($records_readed_filter_update) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . (gettype($records_readed_filter_update[0]) === 'object' ? 'SUCCESS' : 'FAIL') . '][type of readed values] Expected: "object". Actual: "' . gettype($records_readed_filter_update[0]) . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update[0] instanceof record ? 'SUCCESS' : 'FAIL') . '][class of readed object values] Expected: "' . record::class . '". Actual: "' . $records_readed_filter_update[0]::class . "\"\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update[0]->height === 165.5 ? 'SUCCESS' : 'FAIL') . ']["height"] Expected: "165.5" (double). Actual: "' . $records_readed_filter_update[0]->height . '" (' . gettype($records_readed_filter_update[0]->height) . ")\n"; + echo '[' . $action . '][' . ++$test . '][' . (count($records_readed_filter_update_readed) === 2 ? 'SUCCESS' : 'FAIL') . '][amount of readed records after updating] Expected: 2 records. Actual: ' . count($records_readed_filter_update_readed) . " records\n"; + echo '[' . $action . '][' . ++$test . '][' . ($records_readed_filter_update_readed[1]->height === $records_readed_filter_update[0]->height ? 'SUCCESS' : 'FAIL') . "] Height from `update` process response matched height from the `read` preocess response\n"; + + echo '[' . $action . "] The updated record by filter checks have been completed\n"; +} catch (exception $e) { + echo '[' . $action . "][WARNING] The updated record by filter checks have been completed with errors\n"; +} + +// Reinitializing the counter of tests +$test = 0; + +echo "\n\nCompleted testing";