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";