├── .github └── workflows │ ├── code_analysis.yaml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── ecs.php ├── helpers ├── dataTypes.php └── json.php ├── phpstan.neon ├── phpunit.xml ├── src └── JSONDB.php └── tests ├── JSONDBTest.php ├── PerformanceTest.php ├── WhereTest.php ├── pets.json └── users.json /.github/workflows/code_analysis.yaml: -------------------------------------------------------------------------------- 1 | name: Code Analysis 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | code_analysis: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | actions: 15 | - 16 | name: 'Coding Standard' 17 | run: composer check-cs 18 | 19 | name: ${{ matrix.actions.name }} 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | # see https://github.com/shivammathur/setup-php 25 | - uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: 7.3 28 | coverage: none 29 | 30 | # composer install cache - https://github.com/ramsey/composer-install 31 | - uses: "ramsey/composer-install@v1" 32 | 33 | - run: ${{ matrix.actions.run }} 34 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: null 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | unit_tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php-versions: ['7.3', '7.4', '8.0', '8.1'] 15 | 16 | name: PHP ${{ matrix.php-versions }} Tests 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | # see https://github.com/shivammathur/setup-php 22 | - uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php-versions }} 25 | coverage: none 26 | 27 | # composer install cache - https://github.com/ramsey/composer-install 28 | - uses: "ramsey/composer-install@v1" 29 | 30 | - run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jajo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## php-jsondb 2 | A PHP Class that reads JSON file as a database. Use for sample DBs. 3 | 4 | ### Usage 5 | Install package `composer require jajo/jsondb` 6 | #### Initialize 7 | ```php 8 | insert( 'users.json', 21 | [ 22 | 'name' => 'Thomas', 23 | 'state' => 'Nigeria', 24 | 'age' => 22 25 | ] 26 | ); 27 | ``` 28 | 29 | #### Get 30 | Get back data, just like MySQL in PHP 31 | 32 | ##### All columns: 33 | ```php 34 | select( '*' ) 36 | ->from( 'users.json' ) 37 | ->get(); 38 | print_r( $users ); 39 | ``` 40 | 41 | ##### Custom Columns: 42 | ```php 43 | select( 'name, state' ) 45 | ->from( 'users.json' ) 46 | ->get(); 47 | print_r( $users ); 48 | 49 | ``` 50 | 51 | ##### Where Statement: 52 | This WHERE works as AND Operator at the moment or OR 53 | ```php 54 | select( 'name, state' ) 56 | ->from( 'users.json' ) 57 | ->where( [ 'name' => 'Thomas' ] ) 58 | ->get(); 59 | print_r( $users ); 60 | 61 | // Defaults to Thomas OR Nigeria 62 | $users = $json_db->select( 'name, state' ) 63 | ->from( 'users.json' ) 64 | ->where( [ 'name' => 'Thomas', 'state' => 'Nigeria' ] ) 65 | ->get(); 66 | print_r( $users ); 67 | 68 | // Now is THOMAS AND Nigeria 69 | $users = $json_db->select( 'name, state' ) 70 | ->from( 'users.json' ) 71 | ->where( [ 'name' => 'Thomas', 'state' => 'Nigeria' ], 'AND' ) 72 | ->get(); 73 | print_r( $users ); 74 | 75 | 76 | ``` 77 | ##### Where Statement with regex: 78 | By passing`JSONDB::regex` to where statement, you can apply regex searching. It can be used for implementing `LIKE` or `REGEXP_LIKE` clause in SQL. 79 | 80 | ```php 81 | $users = $json_db->select( 'name, state' ) 82 | ->from( "users" ) 83 | ->where( array( "state" => JSONDB::regex( "/ria/" )), JSONDB::AND ) 84 | ->get(); 85 | print_r( $users ); 86 | // Outputs are rows which contains "ria" string in "state" column. 87 | ``` 88 | 89 | ##### Order By: 90 | Thanks to [Tarun Shanker](http://in.linkedin.com/in/tarunshankerpandey) for this feature. By passing the `order_by()` method, the result is sorted with 2 arguments of the column name and sort method - `JSONDB::ASC` and `JSONDB::DESC` 91 | ```php 92 | select( 'name, state' ) 94 | ->from( 'users.json' ) 95 | ->where( [ 'name' => 'Thomas' ] ) 96 | ->order_by( 'age', JSONDB::ASC ) 97 | ->get(); 98 | print_r( $users ); 99 | ``` 100 | 101 | #### Updating Row 102 | You can also update same JSON file with these methods 103 | ```php 104 | update( [ 'name' => 'Oji', 'age' => 10 ] ) 106 | ->from( 'users.json' ) 107 | ->where( [ 'name' => 'Thomas' ] ) 108 | ->trigger(); 109 | 110 | ``` 111 | *Without the **where()** method, it will update all rows* 112 | 113 | #### Deleting Row 114 | ```php 115 | delete() 117 | ->from( 'users.json' ) 118 | ->where( [ 'name' => 'Thomas' ] ) 119 | ->trigger(); 120 | 121 | ``` 122 | *Without the **where()** method, it will deletes all rows* 123 | 124 | #### Exporting to MySQL 125 | You can export the JSON back to SQL file by using this method and providing an output 126 | ```php 127 | to_mysql( 'users.json', 'users.sql' ); 129 | ``` 130 | Disable CREATE TABLE 131 | ```php 132 | to_mysql( 'users.json', 'users.sql', false ); 134 | ``` 135 | 136 | #### Exporting to XML 137 | [Tarun Shanker](http://in.linkedin.com/in/tarunshankerpandey) also provided a feature to export data to an XML file 138 | ```php 139 | to_xml( 'users.json', 'users.xml' ) ) { 141 | echo 'Saved!'; 142 | } 143 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jajo/jsondb", 3 | "description": "A PHP Class that reads JSON file as a database. Use for sample DBs", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Jajo", 9 | "email": "me@donjajo.com" 10 | } 11 | ], 12 | "autoload" : { 13 | "psr-4" : { 14 | "Jajo\\" : "src/" 15 | }, 16 | "files" : [ 17 | "helpers/dataTypes.php", 18 | "helpers/json.php" 19 | ] 20 | }, 21 | "require": { 22 | "php": ">=7.3" 23 | }, 24 | "require-dev": { 25 | "symplify/easy-coding-standard": "dev-main", 26 | "phpunit/phpunit": "^9.5", 27 | "phpstan/phpstan": "^1.4" 28 | }, 29 | "scripts": { 30 | "check-cs": "vendor/bin/ecs check --ansi", 31 | "fix-cs": "vendor/bin/ecs check --fix --ansi", 32 | "phpstan": "vendor/bin/phpstan --ansi" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | parameters(); 12 | $parameters->set(Option::PATHS, [ 13 | __DIR__ . '/src', 14 | __DIR__ . '/tests', 15 | ]); 16 | 17 | $parameters->set(Option::PARALLEL, true); 18 | 19 | $parameters->set(Option::SKIP, [ 20 | \PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer::class, 21 | ]); 22 | 23 | $services = $containerConfigurator->services(); 24 | $services->set(ArraySyntaxFixer::class) 25 | ->call('configure', [[ 26 | 'syntax' => 'short', 27 | ]]); 28 | 29 | // run and fix, one by one 30 | $containerConfigurator->import(SetList::SPACES); 31 | $containerConfigurator->import(SetList::ARRAY); 32 | $containerConfigurator->import(SetList::DOCBLOCK); 33 | $containerConfigurator->import(SetList::COMMENTS); 34 | $containerConfigurator->import(SetList::CONTROL_STRUCTURES); 35 | 36 | $containerConfigurator->import(SetList::PSR_12); 37 | }; 38 | -------------------------------------------------------------------------------- /helpers/dataTypes.php: -------------------------------------------------------------------------------- 1 | $value ) { 17 | $arr[ $key ] = obj_to_array( $value ); 18 | } 19 | 20 | return $arr; 21 | } -------------------------------------------------------------------------------- /helpers/json.php: -------------------------------------------------------------------------------- 1 | $bufsz ) ? $bufsz : $size; 28 | $size -= $read_count; 29 | 30 | if ( false === $start ) { 31 | // Find first occurence of the curly bracket 32 | $start = strpos( $buffer, '{' ); 33 | if ( false === $start ) { 34 | $total_bytes_read += $read_count; 35 | continue; 36 | } else { 37 | $i = $start+1; 38 | $start += $total_bytes_read; 39 | $total_bytes_read += $read_count; 40 | } 41 | } else { 42 | $total_bytes_read += $read_count; 43 | } 44 | 45 | for ( ; isset( $buffer[ $i ] ); $i++ ) { 46 | if ( "'" == $buffer[ $i ] && ! $quotes[1] ) { 47 | // If quote is escaped, ignore 48 | if ( ! empty( $buffer[ $i - 1 ] ) && '\\' == $buffer[ $i - 1 ] ) { 49 | continue; 50 | } 51 | 52 | $quotes[0] = ! $quotes[0]; 53 | continue; 54 | } 55 | 56 | if ( '"' == $buffer[ $i ] && ! $quotes[0] ) { 57 | // If quote is escaped, ignore 58 | if ( ! empty( $buffer[ $i - 1 ] ) && '\\' == $buffer[ $i - 1 ] ) { 59 | continue; 60 | } 61 | 62 | $quotes[1] = ! $quotes[1]; 63 | continue; 64 | } 65 | 66 | $is_quoted = in_array( true, $quotes, true ); 67 | 68 | if ( '{' == $buffer[ $i ] && ! $is_quoted ) { 69 | if ( $depth == $start_depth ) { 70 | $start = $total_bytes_read - $read_count + $i; 71 | } 72 | $depth++; 73 | } 74 | 75 | if ( '}' == $buffer[ $i ] && ! $is_quoted ) { 76 | $depth--; 77 | if ( $depth == $start_depth ) { 78 | $end = $total_bytes_read - $read_count + $i +1; 79 | break 2; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | $chunk = ''; 87 | 88 | if ( false !== $start && false !== $end ) { 89 | fseek( $fp, $start, SEEK_SET ); 90 | $chunk = fread( $fp, $end - $start ); 91 | } 92 | 93 | fseek( $fp, $cur_pos, SEEK_SET ); 94 | 95 | return $chunk; 96 | } -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 4 3 | 4 | paths: 5 | - src 6 | - helpers 7 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | 18 | ./ 19 | 20 | ./test 21 | ./vendor 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/JSONDB.php: -------------------------------------------------------------------------------- 1 | dir = $dir; 45 | $this->json_opts['encode'] = $json_encode_opt; 46 | } 47 | 48 | public function check_fp_size() 49 | { 50 | $size = 0; 51 | $cur_size = 0; 52 | 53 | if ($this->fp) { 54 | $cur_size = ftell($this->fp); 55 | fseek($this->fp, 0, SEEK_END); 56 | $size = ftell($this->fp); 57 | fseek($this->fp, $cur_size, SEEK_SET); 58 | } 59 | 60 | return $size; 61 | } 62 | 63 | private function check_file() 64 | { 65 | /** 66 | * Checks and validates if JSON file exists 67 | * 68 | * @return bool 69 | */ 70 | 71 | // Checks if DIR exists, if not create 72 | if (! is_dir($this->dir)) { 73 | mkdir($this->dir, 0700); 74 | } 75 | // Checks if JSON file exists, if not create 76 | if (! file_exists($this->file)) { 77 | touch($this->file); 78 | // $this->commit(); 79 | } 80 | 81 | if ($this->load == 'partial') { 82 | $this->fp = fopen($this->file, 'r+'); 83 | if (! $this->fp) { 84 | throw new \Exception('Unable to open json file'); 85 | } 86 | 87 | $size = $this->check_fp_size(); 88 | if ($size) { 89 | $content = get_json_chunk($this->fp); 90 | 91 | // We could not get the first chunk of JSON. Lets try to load everything then 92 | if (! $content) { 93 | $content = fread($this->fp, $size); 94 | } else { 95 | // We got the first chunk, we still need to put it into an array 96 | $content = sprintf('[%s]', $content); 97 | } 98 | 99 | $content = json_decode($content, true); 100 | } else { 101 | // Empty file. File was just created 102 | $content = []; 103 | } 104 | } else { 105 | // Read content of JSON file 106 | $content = file_get_contents($this->file); 107 | $content = json_decode($content, true); 108 | } 109 | 110 | // Check if its arrays of jSON 111 | if (! is_array($content) && is_object($content)) { 112 | throw new \Exception('An array of json is required: Json data enclosed with []'); 113 | } 114 | // An invalid jSON file 115 | if (! is_array($content) && ! is_object($content)) { 116 | throw new \Exception('json is invalid'); 117 | } 118 | $this->content = $content; 119 | return true; 120 | } 121 | 122 | public function select($args = '*') 123 | { 124 | /** 125 | * Explodes the selected columns into array 126 | * 127 | * @param type $args Optional. Default * 128 | * @return type object 129 | */ 130 | 131 | // Explode to array 132 | $this->select = explode(',', $args); 133 | // Remove whitespaces 134 | $this->select = array_map('trim', $this->select); 135 | // Remove empty values 136 | $this->select = array_filter($this->select); 137 | 138 | return $this; 139 | } 140 | 141 | public function from($file, $load = 'full') 142 | { 143 | /** 144 | * Loads the jSON file 145 | * 146 | * @param type $file. Accepts file path to jSON file 147 | * @return type object 148 | */ 149 | 150 | $this->file = sprintf('%s/%s.json', $this->dir, str_replace('.json', '', $file)); // Adding .json extension is no longer necessary 151 | 152 | // Reset where 153 | $this->where([]); 154 | $this->content = ''; 155 | $this->load = $load; 156 | 157 | // Reset order by 158 | $this->order_by = []; 159 | 160 | if ($this->check_file()) { 161 | //$this->content = ( array ) json_decode( file_get_contents( $this->file ) ); 162 | } 163 | return $this; 164 | } 165 | 166 | public function where(array $columns, $merge = 'OR') 167 | { 168 | $this->where = $columns; 169 | $this->merge = $merge; 170 | return $this; 171 | } 172 | 173 | /** 174 | * Implements regex search on where statement. 175 | * 176 | * @param string $pattern Regex pattern 177 | * @param int $preg_match_flags Flags for preg_grep(). See - https://www.php.net/manual/en/function.preg-match.php 178 | */ 179 | public static function regex(string $pattern, int $preg_match_flags = 0): object 180 | { 181 | $c = new \stdClass(); 182 | $c->is_regex = true; 183 | $c->value = $pattern; 184 | $c->options = $preg_match_flags; 185 | 186 | return $c; 187 | } 188 | 189 | public function delete() 190 | { 191 | $this->delete = true; 192 | return $this; 193 | } 194 | 195 | public function update(array $columns) 196 | { 197 | $this->update = $columns; 198 | return $this; 199 | } 200 | 201 | /** 202 | * Inserts data into json file 203 | * 204 | * @param string $file json filename without extension 205 | * @param array $values Array of columns as keys and values 206 | */ 207 | public function insert($file, array $values) 208 | { 209 | $this->from($file, 'partial'); 210 | 211 | $first_row = current($this->content); 212 | $this->content = []; 213 | 214 | if (! empty($first_row)) { 215 | $unmatched_columns = 0; 216 | 217 | foreach ($first_row as $column => $value) { 218 | if (! isset($values[$column])) { 219 | $values[$column] = null; 220 | } 221 | } 222 | 223 | foreach ($values as $col => $val) { 224 | if (! array_key_exists($col, $first_row)) { 225 | $unmatched_columns = 1; 226 | break; 227 | } 228 | } 229 | 230 | if ($unmatched_columns) { 231 | throw new \Exception('Columns must match as of the first row'); 232 | } 233 | } 234 | 235 | $this->content[] = $values; 236 | $this->commit(); 237 | } 238 | 239 | public function commit() 240 | { 241 | if ($this->fp && is_resource($this->fp)) { 242 | $f = $this->fp; 243 | } else { 244 | $f = fopen($this->file, 'w+'); 245 | } 246 | 247 | if ($this->load === 'full') { 248 | // Write everything back into the file 249 | fwrite($f, (! $this->content ? '[]' : json_encode($this->content, $this->json_opts['encode']))); 250 | } elseif ($this->load === 'partial') { 251 | // Append it 252 | $this->append(); 253 | } else { 254 | // Unknown load type 255 | fclose($f); 256 | throw new \Exception('Write fail: Unkown load type provided', 'write_error'); 257 | } 258 | 259 | fclose($f); 260 | } 261 | 262 | private function append() 263 | { 264 | $size = $this->check_fp_size(); 265 | $per_read = $size > 64 ? 64 : $size; 266 | $read_size = -$per_read; 267 | $lstblkbrkt = false; 268 | $lastinput = false; 269 | $i = $size; 270 | $data = json_encode($this->content, $this->json_opts['encode']); 271 | 272 | if ($size) { 273 | fseek($this->fp, $read_size, SEEK_END); 274 | 275 | while (($read = fread($this->fp, $per_read))) { 276 | $per_read = $i - $per_read < 0 ? $i : $per_read; 277 | if ($lstblkbrkt === false) { 278 | $lstblkbrkt = strrpos($read, ']', 0); 279 | if ($lstblkbrkt !== false) { 280 | $lstblkbrkt = ($i - $per_read) + $lstblkbrkt; 281 | } 282 | } 283 | 284 | if ($lstblkbrkt !== false) { 285 | $lastinput = strrpos($read, '}'); 286 | if ($lastinput !== false) { 287 | $lastinput = ($i - $per_read) + $lastinput; 288 | break; 289 | } 290 | } 291 | 292 | $i -= $per_read; 293 | $read_size += -$per_read; 294 | if (abs($read_size) >= $size) { 295 | break; 296 | } 297 | fseek($this->fp, $read_size, SEEK_END); 298 | } 299 | } 300 | 301 | if ($lstblkbrkt !== false) { 302 | // We found existing json data, don't write extra [ 303 | $data = substr($data, 1); 304 | if ($lastinput !== false) { 305 | $data = sprintf(',%s', $data); 306 | } 307 | } else { 308 | if ($size > 0) { 309 | throw new \Exception('Append error: JSON file looks malformed'); 310 | } 311 | 312 | $lstblkbrkt = 0; 313 | } 314 | 315 | fseek($this->fp, $lstblkbrkt, SEEK_SET); 316 | fwrite($this->fp, $data); 317 | } 318 | 319 | private function _update() 320 | { 321 | if (! empty($this->last_indexes) && ! empty($this->where)) { 322 | foreach ($this->content as $i => $v) { 323 | if (in_array($i, $this->last_indexes)) { 324 | $content = (array) $this->content[$i]; 325 | if (! array_diff_key($this->update, $content)) { 326 | $this->content[$i] = (object) array_merge($content, $this->update); 327 | } else { 328 | throw new \Exception('Update method has an off key'); 329 | } 330 | } else { 331 | continue; 332 | } 333 | } 334 | } elseif (! empty($this->where) && empty($this->last_indexes)) { 335 | return; 336 | } else { 337 | foreach ($this->content as $i => $v) { 338 | $content = (array) $this->content[$i]; 339 | if (! array_diff_key($this->update, $content)) { 340 | $this->content[$i] = (object) array_merge($content, $this->update); 341 | } else { 342 | throw new \Exception('Update method has an off key '); 343 | } 344 | } 345 | } 346 | } 347 | 348 | /** 349 | * Prepares data and written to file 350 | * 351 | * @return object $this 352 | */ 353 | public function trigger() 354 | { 355 | $content = (! empty($this->where) ? $this->where_result() : $this->content); 356 | $return = false; 357 | if ($this->delete) { 358 | if (! empty($this->last_indexes) && ! empty($this->where)) { 359 | $this->content = array_filter($this->content, function ($index) { 360 | return ! in_array($index, $this->last_indexes); 361 | }, ARRAY_FILTER_USE_KEY); 362 | 363 | $this->content = array_values($this->content); 364 | } elseif (empty($this->where) && empty($this->last_indexes)) { 365 | $this->content = []; 366 | } 367 | 368 | $return = true; 369 | $this->delete = false; 370 | } elseif (! empty($this->update)) { 371 | $this->_update(); 372 | $this->update = []; 373 | } else { 374 | $return = false; 375 | } 376 | $this->commit(); 377 | return $this; 378 | } 379 | 380 | /** 381 | * Flushes indexes they won't be reused on next action 382 | */ 383 | private function flush_indexes($flush_where = false) 384 | { 385 | $this->last_indexes = []; 386 | if ($flush_where) { 387 | $this->where = []; 388 | } 389 | 390 | if ($this->fp && is_resource($this->fp)) { 391 | fclose($this->fp); 392 | } 393 | } 394 | 395 | private function intersect_value_check($a, $b) 396 | { 397 | if ($b instanceof \stdClass) { 398 | if ($b->is_regex) { 399 | return ! preg_match($b->value, (string) $a, $_, $b->options); 400 | } 401 | 402 | return -1; 403 | } 404 | 405 | if ($a instanceof \stdClass) { 406 | if ($a->is_regex) { 407 | return ! preg_match($a->value, (string) $b, $_, $a->options); 408 | } 409 | 410 | return -1; 411 | } 412 | 413 | return strcasecmp((string) $a, (string) $b); 414 | } 415 | 416 | /** 417 | * Validates and fetch out the data for manipulation 418 | * 419 | * @return array $r Array of rows matching WHERE 420 | */ 421 | private function where_result() 422 | { 423 | $this->flush_indexes(); 424 | 425 | if ($this->merge == 'AND') { 426 | return $this->where_and_result(); 427 | } 428 | // Filter array 429 | $r = array_filter($this->content, function ($row, $index) { 430 | $row = (array) $row; // Convert first stage to array if object 431 | 432 | // Check for rows intersecting with the where values. 433 | if (array_uintersect_uassoc($row, $this->where, [$this, 'intersect_value_check'], 'strcasecmp') /*array_intersect_assoc( $row, $this->where )*/) { 434 | $this->last_indexes[] = $index; 435 | return true; 436 | } 437 | 438 | return false; 439 | }, ARRAY_FILTER_USE_BOTH); 440 | 441 | // Make sure every object is turned to array here. 442 | return array_values(obj_to_array($r)); 443 | } 444 | 445 | /** 446 | * Validates and fetch out the data for manipulation for AND 447 | * 448 | * @return array $r Array of fetched WHERE statement 449 | */ 450 | private function where_and_result() 451 | { 452 | /* 453 | Validates the where statement values 454 | */ 455 | $r = []; 456 | 457 | // Loop through the db rows. Ge the index and row 458 | foreach ($this->content as $index => $row) { 459 | 460 | // Make sure its array data type 461 | $row = (array) $row; 462 | 463 | //check if the row = where['col'=>'val', 'col2'=>'val2'] 464 | if (! array_udiff_uassoc($this->where, $row, [$this, 'intersect_value_check'], 'strcasecmp')) { 465 | $r[] = $row; 466 | // Append also each row array key 467 | $this->last_indexes[] = $index; 468 | } else { 469 | continue; 470 | } 471 | } 472 | return $r; 473 | } 474 | 475 | public function to_xml($from, $to) 476 | { 477 | $this->from($from); 478 | if ($this->content) { 479 | $element = pathinfo($from, PATHINFO_FILENAME); 480 | $xml = ' 481 | 482 | <' . $element . '> 483 | '; 484 | 485 | foreach ($this->content as $index => $value) { 486 | $xml .= ' 487 | '; 488 | foreach ($value as $col => $val) { 489 | $xml .= sprintf(' 490 | <%s>%s', $col, $val, $col); 491 | } 492 | $xml .= ' 493 | 494 | '; 495 | } 496 | $xml .= ''; 497 | 498 | $xml = trim($xml); 499 | file_put_contents($to, $xml); 500 | return true; 501 | } 502 | return false; 503 | } 504 | 505 | /** 506 | * Generates SQL from JSON 507 | * 508 | * @param string $from JSON file to get data from 509 | * @param string $to Filename to write SQL into 510 | * @param bool $create_table If to include create table in this export 511 | * 512 | * @return bool Returns true if file was created, else false 513 | */ 514 | public function to_mysql(string $from, string $to, bool $create_table = true): bool 515 | { 516 | $this->from($from); // Reads the JSON file 517 | if ($this->content) { 518 | $table = pathinfo($to, PATHINFO_FILENAME); // Get filename to use as table 519 | 520 | $sql = "-- PHP-JSONDB JSON to MySQL Dump\n--\n\n"; 521 | if ($create_table) { 522 | // Should create table, generate a CREATE TABLE statement using the column of the first row 523 | $first_row = (array) $this->content[0]; 524 | $columns = array_map(function ($column) use ($first_row) { 525 | return sprintf("\t`%s` %s", $column, $this->_to_mysql_type(gettype($first_row[$column]))); 526 | }, array_keys($first_row)); 527 | 528 | $sql = sprintf("%s-- Table Structure for `%s`\n--\n\nCREATE TABLE `%s` \n(\n%s\n);\n", $sql, $table, $table, implode(",\n", $columns)); 529 | } 530 | 531 | foreach ($this->content as $row) { 532 | $row = (array) $row; 533 | $values = array_map(function ($vv) { 534 | $vv = (is_array($vv) || is_object($vv) ? serialize($vv) : $vv); 535 | return sprintf("'%s'", addslashes((string) $vv)); 536 | }, array_values($row)); 537 | 538 | $cols = array_map(function ($col) { 539 | return sprintf('`%s`', $col); 540 | }, array_keys($row)); 541 | $sql .= sprintf("INSERT INTO `%s` ( %s ) VALUES ( %s );\n", $table, implode(', ', $cols), implode(', ', $values)); 542 | } 543 | file_put_contents($to, $sql); 544 | return true; 545 | } 546 | return false; 547 | } 548 | 549 | private function _to_mysql_type($type) 550 | { 551 | if ($type == 'bool') { 552 | $return = 'BOOLEAN'; 553 | } elseif ($type == 'integer') { 554 | $return = 'INT'; 555 | } elseif ($type == 'double') { 556 | $return = strtoupper($type); 557 | } else { 558 | $return = 'VARCHAR( 255 )'; 559 | } 560 | return $return; 561 | } 562 | 563 | public function order_by($column, $order = self::ASC) 564 | { 565 | $this->order_by = [$column, $order]; 566 | return $this; 567 | } 568 | 569 | private function _process_order_by($content) 570 | { 571 | if ($this->order_by && $content && in_array($this->order_by[0], array_keys((array) $content[0]))) { 572 | /* 573 | * Check if order by was specified 574 | * Check if there's actually a result of the query 575 | * Makes sure the column actually exists in the list of columns 576 | */ 577 | 578 | list($sort_column, $order_by) = $this->order_by; 579 | $sort_keys = []; 580 | $sorted = []; 581 | 582 | foreach ($content as $index => $value) { 583 | $value = (array) $value; 584 | // Save the index and value so we can use them to sort 585 | $sort_keys[$index] = $value[$sort_column]; 586 | } 587 | 588 | // Let's sort! 589 | if ($order_by == self::ASC) { 590 | asort($sort_keys); 591 | } elseif ($order_by == self::DESC) { 592 | arsort($sort_keys); 593 | } 594 | 595 | // We are done with sorting, lets use the sorted array indexes to pull back the original content and return new content 596 | foreach ($sort_keys as $index => $value) { 597 | $sorted[$index] = (array) $content[$index]; 598 | } 599 | 600 | $content = $sorted; 601 | } 602 | 603 | return $content; 604 | } 605 | 606 | public function get() 607 | { 608 | if ($this->where != null) { 609 | $content = $this->where_result(); 610 | } else { 611 | $content = $this->content; 612 | } 613 | 614 | if ($this->select && ! in_array('*', $this->select)) { 615 | $r = []; 616 | foreach ($content as $id => $row) { 617 | $row = (array) $row; 618 | foreach ($row as $key => $val) { 619 | if (in_array($key, $this->select)) { 620 | $r[$id][$key] = $val; 621 | } else { 622 | continue; 623 | } 624 | } 625 | } 626 | $content = $r; 627 | } 628 | 629 | // Finally, lets do sorting :) 630 | $content = $this->_process_order_by($content); 631 | 632 | $this->flush_indexes(true); 633 | return $content; 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /tests/JSONDBTest.php: -------------------------------------------------------------------------------- 1 | db = new JSONDB(__DIR__); 15 | } 16 | 17 | public function tearDown(): void 18 | { 19 | @unlink(__DIR__ . '/users.sql'); 20 | } 21 | 22 | public function testInsert(): void 23 | { 24 | $names = ['James£', 'John£', 'Oji£', 'Okeke', 'Bola', 'Thomas', 'Ibrahim', 'Smile']; 25 | $states = ['Abia', 'Lagos', 'Benue', 'Kano', 'Kastina', 'Abuja', 'Imo', 'Ogun']; 26 | shuffle($names); 27 | shuffle($states); 28 | 29 | $state = current($states); 30 | $name = current($names); 31 | $age = mt_rand(20, 100); 32 | printf("Inserting: \n\nName\tAge\tState\n%s\t%d\t%s", $name, $age, $state); 33 | 34 | $indexes = $this->db->insert('users', [ 35 | 'name' => $name, 36 | 'state' => $state, 37 | 'age' => $age, 38 | ]); 39 | 40 | $user = $this->db->select('*') 41 | ->from('users') 42 | ->where([ 43 | 'name' => $name, 44 | 'state' => $state, 45 | 'age' => $age, 46 | ], 'AND') 47 | ->get(); 48 | 49 | $this->db->insert('users', [ 50 | 'name' => 'Dummy', 51 | 'state' => 'Lagos', 52 | 'age' => 12, 53 | ]); 54 | 55 | $this->assertEquals($name, $user[0]['name']); 56 | } 57 | 58 | public function testGet(): void 59 | { 60 | printf("\nCheck exist\n"); 61 | $users = ($this->db->select('*') 62 | ->from('users') 63 | ->get()); 64 | $this->assertNotEmpty($users); 65 | } 66 | 67 | public function testWhere(): void 68 | { 69 | $result = ( 70 | $this->db->select('*') 71 | ->from('users') 72 | ->where([ 73 | 'name' => 'Okeke', 74 | ]) 75 | ->get() 76 | ); 77 | 78 | // Probably has not inserted. Lets do it then 79 | if (! $result) { 80 | $this->db->insert('users', [ 81 | 'name' => 'Okeke', 82 | 'age' => 21, 83 | 'state' => 'Enugu', 84 | ]); 85 | 86 | $result = ( 87 | $this->db->select('*') 88 | ->from('users') 89 | ->where([ 90 | 'name' => 'Okeke', 91 | ]) 92 | ->get() 93 | ); 94 | } 95 | 96 | $this->assertEquals('Okeke', $result[0]['name']); 97 | } 98 | 99 | public function testMultiWhere(): void 100 | { 101 | $this->db->insert('users', [ 102 | 'name' => 'Jajo', 103 | 'age' => null, 104 | 'state' => 'Lagos', 105 | ]); 106 | 107 | $this->db->insert('users', [ 108 | 'name' => 'Johnny', 109 | 'age' => 30, 110 | 'state' => 'Ogun', 111 | ]); 112 | 113 | $result = $this->db->select('*')->from('users')->where([ 114 | 'age' => null, 115 | 'name' => 'Jajo', 116 | ])->get(); 117 | $this->assertEquals('Jajo', $result[0]['name']); 118 | } 119 | 120 | public function testAND(): void 121 | { 122 | $this->db->insert('users', [ 123 | 'name' => 'Jajo', 124 | 'age' => 50, 125 | 'state' => 'Lagos', 126 | ]); 127 | 128 | $this->db->insert('users', [ 129 | 'name' => 'Johnny', 130 | 'age' => 50, 131 | 'state' => 'Ogun', 132 | ]); 133 | 134 | $result = $this->db->select('*')->from('users')->where([ 135 | 'age' => 50, 136 | 'name' => 'Jajo', 137 | ], JSONDB::AND)->get(); 138 | 139 | $this->assertEquals(1, count($result)); 140 | $this->assertEquals('Jajo', $result[0]['name']); 141 | } 142 | 143 | public function testRegexAND(): void 144 | { 145 | $this->db->insert('users', [ 146 | 'name' => 'Paulo', 147 | 'age' => 50, 148 | 'state' => 'Algeria', 149 | ]); 150 | 151 | $this->db->insert('users', [ 152 | 'name' => 'Nina', 153 | 'age' => 50, 154 | 'state' => 'Nigeria', 155 | ]); 156 | 157 | $this->db->insert('users', [ 158 | 'name' => 'Ogwo', 159 | 'age' => 49, 160 | 'state' => 'Nigeria', 161 | ]); 162 | 163 | $result = ( 164 | $this->db->select('*') 165 | ->from('users') 166 | ->where([ 167 | 'state' => JSONDB::regex('/ria/'), 168 | 'age' => JSONDB::regex('/5[0-9]/'), 169 | ], JSONDB::AND) 170 | ->get() 171 | ); 172 | 173 | $this->assertEquals(2, count($result)); 174 | $this->assertEquals('Paulo', $result[0]['name']); 175 | $this->assertEquals('Nina', $result[1]['name']); 176 | } 177 | 178 | public function testRegex(): void 179 | { 180 | $this->db->insert('users', [ 181 | 'name' => 'Jajo', 182 | 'age' => 89, 183 | 'state' => 'Abia', 184 | ]); 185 | 186 | $this->db->insert('users', [ 187 | 'name' => 'Mitchell', 188 | 'age' => 45, 189 | 'state' => 'Zamfara', 190 | ]); 191 | 192 | $result = ($this->db->select('*') 193 | ->from('users') 194 | ->where([ 195 | 'state' => JSONDB::regex('/Zam/'), 196 | ]) 197 | ->get()); 198 | 199 | $this->assertEquals('Mitchell', $result[0]['name']); 200 | } 201 | 202 | public function testUpdate(): void 203 | { 204 | $this->db->update([ 205 | 'name' => 'Jammy', 206 | 'state' => 'Sokoto', 207 | ]) 208 | ->from('users') 209 | ->where([ 210 | 'name' => 'Okeke', 211 | ]) 212 | ->trigger(); 213 | 214 | $this->db->update([ 215 | 'state' => 'Rivers', 216 | ]) 217 | ->from('users') 218 | ->where([ 219 | 'name' => 'Dummy', 220 | ]) 221 | ->trigger(); 222 | 223 | $result = $this->db->select('*') 224 | ->from('users') 225 | ->where([ 226 | 'name' => 'Jammy', 227 | ]) 228 | ->get(); 229 | 230 | $this->assertTrue($result[0]['state'] == 'Sokoto' && $result[0]['name'] == 'Jammy'); 231 | } 232 | 233 | public function testSQLExport(): void 234 | { 235 | $this->db->to_mysql('users', 'tests/users.sql'); 236 | 237 | $this->assertTrue(file_exists('tests/users.sql')); 238 | } 239 | 240 | public function testDelete(): void 241 | { 242 | $this->db->delete() 243 | ->from('users') 244 | ->where([ 245 | 'name' => 'Jammy', 246 | ]) 247 | ->trigger(); 248 | 249 | $result = $this->db->select('*') 250 | ->from('users') 251 | ->where([ 252 | 'name' => 'Jammy', 253 | ]) 254 | ->get(); 255 | 256 | $this->assertEmpty($result); 257 | } 258 | 259 | public function testDeleteAll(): void 260 | { 261 | /* I add a select action with where statement */ 262 | $result_before = $this->db->select('*') 263 | ->from('users') 264 | ->where([ 265 | 'state' => 'Rivers', 266 | ]) 267 | ->get(); 268 | 269 | /* Select action works fine */ 270 | printf("\nCount of select action's result : %d", count($result_before)); 271 | $this->assertTrue($result_before[0]['name'] == 'Dummy'); 272 | 273 | /* Original test code by donjajo */ 274 | $this->db->delete() 275 | ->from('users') 276 | ->trigger(); 277 | 278 | $result = $this->db->select('*') 279 | ->from('users') 280 | ->get(); 281 | 282 | /* But delete all action not working and assertion fail*/ 283 | $this->assertEmpty($result); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /tests/PerformanceTest.php: -------------------------------------------------------------------------------- 1 | jsondb = new JSONDB(__DIR__, JSON_UNESCAPED_UNICODE); 15 | } 16 | 17 | protected function tearDown(): void 18 | { 19 | unlink(__DIR__ . '/food.json'); 20 | } 21 | 22 | public function testInsert(): void 23 | { 24 | $i = 0; 25 | while ($i < 5000) { 26 | $sum = 0; 27 | for ($j = 0; $j < 1000; $j++) { 28 | $start = hrtime(true); 29 | $this->jsondb->insert('food', [ 30 | 'name' => 'Rice', 31 | 'class' => 'Carbohydrate', 32 | ]); 33 | $stop = hrtime(true); 34 | $sum += ($stop - $start) / 1000000; 35 | } 36 | $i += $j; 37 | fprintf(STDOUT, "\nTook average of %fms to insert 1000 records - BATCH %d", $sum, $i / 1000); 38 | fflush(STDOUT); 39 | } 40 | 41 | $foods = $this->jsondb->select('name')->from('food')->get(); 42 | $this->assertCount(5000, $foods); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/WhereTest.php: -------------------------------------------------------------------------------- 1 | db = new JSONDB(__DIR__); 15 | } 16 | 17 | // Both 'setUp' and 'tearDown' function is called by phpunit per every test function before test function is called 18 | public function setUp(): void 19 | { 20 | $this->load_db(); 21 | 22 | $names = ['hamster', 'chinchilla', 'dog', 'cat', 'rat', 'chamaeleon', 'turtle', 'chupacabra', 'catoblepas', 'catoblepas']; 23 | $kinds = ['rodentia', 'rodentia', 'canivora', 'carnivora', 'rodentia', 'squamata', 'testudines', null, null, 'game-character']; 24 | 25 | for ($i = 0; $i < count($names); $i++) { 26 | $this->db->insert('pets', [ 27 | 'name' => $names[$i], 28 | 'kind' => $kinds[$i], 29 | 'age' => $i % 3, 30 | ]); 31 | } 32 | } 33 | 34 | public function tearDown(): void 35 | { 36 | $this->db->delete() 37 | ->from('pets') 38 | ->trigger(); 39 | } 40 | 41 | public function testWhereOr() 42 | { 43 | $result = ( 44 | $this->db->select('*') 45 | ->from('pets') 46 | ->where([ 47 | 'kind' => 'rodentia', 48 | 'age' => 0, 49 | ])->get() 50 | ); 51 | $this->assertCount(6, $result); 52 | 53 | $result = ( 54 | $this->db->select('*') 55 | ->from('pets') 56 | ->where([ 57 | 'kind' => 'squamata', 58 | 'age' => 2, 59 | ])->get() 60 | ); 61 | $this->assertCount(3, $result); 62 | } 63 | 64 | public function testWhereNullOr() 65 | { 66 | $result = ( 67 | $this->db->select('*') 68 | ->from('pets') 69 | ->where([ 70 | 'kind' => null, 71 | ])->get() 72 | ); 73 | $this->assertCount(2, $result); 74 | } 75 | 76 | public function testWhereNullAnd() 77 | { 78 | $result = ( 79 | $this->db->select('*') 80 | ->from('pets') 81 | ->where([ 82 | 'name' => 'catoblepas', 83 | 'kind' => null, 84 | ], 'AND')->get() 85 | ); 86 | $this->assertCount(1, $result); 87 | $this->assertEquals('catoblepas', $result[0]['name']); 88 | $this->assertSame(null, $result[0]['kind']); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/pets.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /tests/users.json: -------------------------------------------------------------------------------- 1 | [] --------------------------------------------------------------------------------