2019-05-09 18:54:39 +12:00
< ? php
2020-03-25 06:56:32 +13:00
namespace Appwrite\Database\Adapter ;
2019-05-09 18:54:39 +12:00
use Utopia\Registry\Registry ;
2020-03-25 06:56:32 +13:00
use Appwrite\Database\Adapter ;
use Appwrite\Database\Exception\Duplicate ;
use Appwrite\Database\Validator\Authorization ;
2019-05-09 18:54:39 +12:00
use Exception ;
use PDO ;
use Redis as Client ;
class MySQL extends Adapter
{
2019-09-07 05:04:26 +12:00
const DATA_TYPE_STRING = 'string' ;
const DATA_TYPE_INTEGER = 'integer' ;
const DATA_TYPE_FLOAT = 'float' ;
const DATA_TYPE_BOOLEAN = 'boolean' ;
const DATA_TYPE_OBJECT = 'object' ;
const DATA_TYPE_DICTIONARY = 'dictionary' ;
const DATA_TYPE_ARRAY = 'array' ;
const DATA_TYPE_NULL = 'null' ;
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
const OPTIONS_LIMIT_ATTRIBUTES = 1000 ;
2019-05-09 18:54:39 +12:00
/**
2020-02-17 20:16:11 +13:00
* @ var Registry
2019-05-09 18:54:39 +12:00
*/
protected $register ;
/**
2019-09-07 05:04:26 +12:00
* Last modified .
2019-05-09 18:54:39 +12:00
*
* Read node with most recent changes
*
* @ var int
*/
protected $lastModified = - 1 ;
/**
* @ var array
*/
protected $debug = [];
/**
2019-09-07 05:04:26 +12:00
* Constructor .
2019-05-09 18:54:39 +12:00
*
* Set connection and settings
*
* @ param Registry $register
*/
public function __construct ( Registry $register )
{
2019-09-07 05:04:26 +12:00
$this -> register = $register ;
2019-05-09 18:54:39 +12:00
}
/**
2019-09-07 05:04:26 +12:00
* Get Document .
2019-05-09 18:54:39 +12:00
*
* @ param string $id
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
public function getDocument ( $id )
{
// Get fields abstraction
2019-09-07 05:04:26 +12:00
$st = $this -> getPDO () -> prepare ( 'SELECT * FROM `' . $this -> getNamespace () . ' . database . documents ` a
2019-05-09 18:54:39 +12:00
WHERE a . uid = : uid AND a . status = 0
ORDER BY a . updatedAt DESC LIMIT 10 ;
' );
$st -> bindValue ( ':uid' , $id , PDO :: PARAM_STR );
$st -> execute ();
$document = $st -> fetch ();
if ( empty ( $document )) { // Not Found
return [];
}
// Get fields abstraction
2019-09-07 05:04:26 +12:00
$st = $this -> getPDO () -> prepare ( 'SELECT * FROM `' . $this -> getNamespace () . ' . database . properties ` a
2019-05-09 18:54:39 +12:00
WHERE a . documentUid = : documentUid AND a . documentRevision = : documentRevision
ORDER BY `order`
' );
$st -> bindParam ( ':documentUid' , $document [ 'uid' ], PDO :: PARAM_STR );
$st -> bindParam ( ':documentRevision' , $document [ 'revision' ], PDO :: PARAM_STR );
$st -> execute ();
$properties = $st -> fetchAll ();
$output = [
2020-02-17 20:16:11 +13:00
'$id' => null ,
2019-05-09 18:54:39 +12:00
'$collection' => null ,
2020-06-20 23:05:43 +12:00
'$permissions' => ( ! empty ( $document [ 'permissions' ])) ? \json_decode ( $document [ 'permissions' ], true ) : [],
2019-05-09 18:54:39 +12:00
];
foreach ( $properties as & $property ) {
2020-06-20 23:05:43 +12:00
\settype ( $property [ 'value' ], $property [ 'primitive' ]);
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
if ( $property [ 'array' ]) {
2019-05-09 18:54:39 +12:00
$output [ $property [ 'key' ]][] = $property [ 'value' ];
2019-09-07 05:04:26 +12:00
} else {
2019-05-09 18:54:39 +12:00
$output [ $property [ 'key' ]] = $property [ 'value' ];
}
}
// Get fields abstraction
2019-09-07 05:04:26 +12:00
$st = $this -> getPDO () -> prepare ( 'SELECT * FROM `' . $this -> getNamespace () . ' . database . relationships ` a
2019-05-09 18:54:39 +12:00
WHERE a . start = : start AND revision = : revision
ORDER BY `order`
' );
$st -> bindParam ( ':start' , $document [ 'uid' ], PDO :: PARAM_STR );
$st -> bindParam ( ':revision' , $document [ 'revision' ], PDO :: PARAM_STR );
$st -> execute ();
$output [ 'temp-relations' ] = $st -> fetchAll ();
return $output ;
}
/**
2019-09-07 05:04:26 +12:00
* Create Document .
2019-05-09 18:54:39 +12:00
*
* @ param array $data
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws \Exception
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
*/
2020-03-22 10:10:06 +13:00
public function createDocument ( array $data = [], array $unique = [])
2019-05-09 18:54:39 +12:00
{
2019-09-07 05:04:26 +12:00
$order = 0 ;
2020-06-20 23:05:43 +12:00
$data = \array_merge ([ '$id' => null , '$permissions' => []], $data ); // Merge data with default params
$signature = \md5 ( \json_encode ( $data , true ));
$revision = \uniqid ( '' , true );
2020-03-09 08:25:31 +13:00
$data [ '$id' ] = ( empty ( $data [ '$id' ])) ? null : $data [ '$id' ];
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
/*
2019-05-09 18:54:39 +12:00
* When updating node , check if there are any changes to update
* by comparing data md5 signatures
*/
2020-02-17 20:16:11 +13:00
if ( null !== $data [ '$id' ]) {
2019-09-07 05:04:26 +12:00
$st = $this -> getPDO () -> prepare ( 'SELECT signature FROM `' . $this -> getNamespace () . ' . database . documents ` a
2019-05-09 18:54:39 +12:00
WHERE a . uid = : uid AND a . status = 0
ORDER BY a . updatedAt DESC LIMIT 1 ;
' );
2020-02-17 20:16:11 +13:00
$st -> bindValue ( ':uid' , $data [ '$id' ], PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
$st -> execute ();
2020-04-09 01:14:16 +12:00
$result = $st -> fetch ();
2019-05-09 18:54:39 +12:00
2020-06-25 09:02:27 +12:00
if ( $result && isset ( $result [ 'signature' ])) {
2020-04-09 01:14:16 +12:00
$oldSignature = $result [ 'signature' ];
if ( $signature === $oldSignature ) {
return $data ;
}
2019-05-09 18:54:39 +12:00
}
}
2020-03-22 10:10:06 +13:00
/**
* Check Unique Keys
*/
2020-06-25 09:02:27 +12:00
foreach ( $unique as $key => $value ) {
2020-03-22 10:10:06 +13:00
$st = $this -> getPDO () -> prepare ( 'INSERT INTO `' . $this -> getNamespace () . ' . database . unique `
SET `key` = : key ;
' );
2020-06-20 23:05:43 +12:00
$st -> bindValue ( ':key' , \md5 ( $data [ '$collection' ] . ':' . $key . '=' . $value ), PDO :: PARAM_STR );
2020-03-22 10:10:06 +13:00
2020-06-25 09:02:27 +12:00
if ( ! $st -> execute ()) {
2020-03-22 10:10:06 +13:00
throw new Duplicate ( 'Duplicated Property: ' . $key . '=' . $value );
}
}
2019-05-09 18:54:39 +12:00
// Add or update fields abstraction level
2019-09-07 05:04:26 +12:00
$st1 = $this -> getPDO () -> prepare ( 'INSERT INTO `' . $this -> getNamespace () . ' . database . documents `
2019-05-09 18:54:39 +12:00
SET uid = : uid , createdAt = : createdAt , updatedAt = : updatedAt , signature = : signature , revision = : revision , permissions = : permissions , status = 0
ON DUPLICATE KEY UPDATE uid = : uid , updatedAt = : updatedAt , signature = : signature , revision = : revision , permissions = : permissions ;
' );
// Adding fields properties
2020-02-17 20:16:11 +13:00
if ( null === $data [ '$id' ] || ! isset ( $data [ '$id' ])) { // Get new fields UID
$data [ '$id' ] = $this -> getId ();
2019-05-09 18:54:39 +12:00
}
2020-02-17 20:16:11 +13:00
$st1 -> bindValue ( ':uid' , $data [ '$id' ], PDO :: PARAM_STR );
2019-09-30 19:13:40 +13:00
$st1 -> bindValue ( ':revision' , $revision , PDO :: PARAM_STR );
$st1 -> bindValue ( ':signature' , $signature , PDO :: PARAM_STR );
2020-06-20 23:05:43 +12:00
$st1 -> bindValue ( ':createdAt' , \date ( 'Y-m-d H:i:s' , \time ()), PDO :: PARAM_STR );
$st1 -> bindValue ( ':updatedAt' , \date ( 'Y-m-d H:i:s' , \time ()), PDO :: PARAM_STR );
$st1 -> bindValue ( ':permissions' , \json_encode ( $data [ '$permissions' ]), PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
$st1 -> execute ();
// Delete old properties
2019-09-07 05:04:26 +12:00
$rms1 = $this -> getPDO () -> prepare ( 'DELETE FROM `' . $this -> getNamespace () . '.database.properties` WHERE documentUid = :documentUid AND documentRevision != :documentRevision' );
2020-02-17 20:16:11 +13:00
$rms1 -> bindValue ( ':documentUid' , $data [ '$id' ], PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
$rms1 -> bindValue ( ':documentRevision' , $revision , PDO :: PARAM_STR );
$rms1 -> execute ();
// Delete old relationships
2019-09-07 05:04:26 +12:00
$rms2 = $this -> getPDO () -> prepare ( 'DELETE FROM `' . $this -> getNamespace () . '.database.relationships` WHERE start = :start AND revision != :revision' );
2020-02-17 20:16:11 +13:00
$rms2 -> bindValue ( ':start' , $data [ '$id' ], PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
$rms2 -> bindValue ( ':revision' , $revision , PDO :: PARAM_STR );
$rms2 -> execute ();
// Create new properties
2019-09-07 05:04:26 +12:00
$st2 = $this -> getPDO () -> prepare ( 'INSERT INTO `' . $this -> getNamespace () . ' . database . properties `
2019-05-09 18:54:39 +12:00
( `documentUid` , `documentRevision` , `key` , `value` , `primitive` , `array` , `order` )
VALUES ( : documentUid , : documentRevision , : key , : value , : primitive , : array , : order ) ' );
$props = [];
2019-09-07 05:04:26 +12:00
foreach ( $data as $key => $value ) { // Prepare properties data
2019-05-09 18:54:39 +12:00
2020-06-20 23:05:43 +12:00
if ( \in_array ( $key , [ '$permissions' ])) {
2019-05-09 18:54:39 +12:00
continue ;
}
$type = $this -> getDataType ( $value );
// Handle array of relations
2019-09-07 05:04:26 +12:00
if ( self :: DATA_TYPE_ARRAY === $type ) {
foreach ( $value as $i => $child ) {
if ( self :: DATA_TYPE_DICTIONARY !== $this -> getDataType ( $child )) { // not dictionary
2019-05-09 18:54:39 +12:00
$props [] = [
2019-09-07 05:04:26 +12:00
'type' => $this -> getDataType ( $child ),
'key' => $key ,
2019-05-09 18:54:39 +12:00
'value' => $child ,
'array' => true ,
'order' => $order ++ ,
];
continue ;
}
$data [ $key ][ $i ] = $this -> createDocument ( $child );
2020-02-17 20:16:11 +13:00
$this -> createRelationship ( $revision , $data [ '$id' ], $data [ $key ][ $i ][ '$id' ], $key , true , $i );
2019-05-09 18:54:39 +12:00
}
continue ;
}
// Handle relation
2019-09-07 05:04:26 +12:00
if ( self :: DATA_TYPE_DICTIONARY === $type ) {
2019-05-09 18:54:39 +12:00
$value = $this -> createDocument ( $value );
2020-02-17 20:16:11 +13:00
$this -> createRelationship ( $revision , $data [ '$id' ], $value [ '$id' ], $key ); //xxx
2019-05-09 18:54:39 +12:00
continue ;
}
// Handle empty values
2019-09-07 05:04:26 +12:00
if ( self :: DATA_TYPE_NULL === $type ) {
2019-05-09 18:54:39 +12:00
continue ;
}
$props [] = [
2019-09-07 05:04:26 +12:00
'type' => $type ,
'key' => $key ,
2019-05-09 18:54:39 +12:00
'value' => $value ,
'array' => false ,
'order' => $order ++ ,
];
}
foreach ( $props as $prop ) {
2020-06-20 23:05:43 +12:00
if ( \is_array ( $prop [ 'value' ])) {
throw new Exception ( 'Value can\'t be an array: ' . \json_encode ( $prop [ 'value' ]));
2019-05-09 18:54:39 +12:00
}
2020-02-17 20:16:11 +13:00
$st2 -> bindValue ( ':documentUid' , $data [ '$id' ], PDO :: PARAM_STR );
2019-09-30 19:13:40 +13:00
$st2 -> bindValue ( ':documentRevision' , $revision , PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
2019-09-30 19:13:40 +13:00
$st2 -> bindValue ( ':key' , $prop [ 'key' ], PDO :: PARAM_STR );
$st2 -> bindValue ( ':value' , $prop [ 'value' ], PDO :: PARAM_STR );
$st2 -> bindValue ( ':primitive' , $prop [ 'type' ], PDO :: PARAM_STR );
$st2 -> bindValue ( ':array' , $prop [ 'array' ], PDO :: PARAM_BOOL );
$st2 -> bindValue ( ':order' , $prop [ 'order' ], PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
$st2 -> execute ();
}
//TODO remove this dependency (check if related to nested documents)
2020-02-17 20:16:11 +13:00
$this -> getRedis () -> expire ( $this -> getNamespace () . ':document-' . $data [ '$id' ], 0 );
$this -> getRedis () -> expire ( $this -> getNamespace () . ':document-' . $data [ '$id' ], 0 );
2019-05-09 18:54:39 +12:00
return $data ;
}
/**
2019-09-07 05:04:26 +12:00
* Update Document .
2019-05-09 18:54:39 +12:00
*
* @ param array $data
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
public function updateDocument ( array $data = [])
{
return $this -> createDocument ( $data );
}
/**
2019-09-07 05:04:26 +12:00
* Delete Document .
2019-05-09 18:54:39 +12:00
*
* @ param int $id
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
public function deleteDocument ( $id )
{
2019-09-07 05:04:26 +12:00
$st1 = $this -> getPDO () -> prepare ( 'DELETE FROM `' . $this -> getNamespace () . ' . database . documents `
2019-05-09 18:54:39 +12:00
WHERE uid = : id
' );
$st1 -> bindValue ( ':id' , $id , PDO :: PARAM_STR );
$st1 -> execute ();
2019-09-07 05:04:26 +12:00
$st2 = $this -> getPDO () -> prepare ( 'DELETE FROM `' . $this -> getNamespace () . ' . database . properties `
2019-05-09 18:54:39 +12:00
WHERE documentUid = : id
' );
$st2 -> bindValue ( ':id' , $id , PDO :: PARAM_STR );
$st2 -> execute ();
2019-09-07 05:04:26 +12:00
$st3 = $this -> getPDO () -> prepare ( 'DELETE FROM `' . $this -> getNamespace () . ' . database . relationships `
2019-05-09 18:54:39 +12:00
WHERE start = : id OR end = : id
' );
$st3 -> bindValue ( ':id' , $id , PDO :: PARAM_STR );
$st3 -> execute ();
return [];
}
/**
2019-09-07 05:04:26 +12:00
* Create Relation .
2019-05-09 18:54:39 +12:00
*
* Adds a new relationship between different nodes
*
* @ param string $revision
2019-09-07 05:04:26 +12:00
* @ param int $start
* @ param int $end
2019-05-09 18:54:39 +12:00
* @ param string $key
2019-09-07 05:04:26 +12:00
* @ param bool $isArray
* @ param int $order
*
2019-05-09 18:54:39 +12:00
* @ return array
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
protected function createRelationship ( $revision , $start , $end , $key , $isArray = false , $order = 0 )
{
2019-09-07 05:04:26 +12:00
$st2 = $this -> getPDO () -> prepare ( 'INSERT INTO `' . $this -> getNamespace () . ' . database . relationships `
2019-05-09 18:54:39 +12:00
( `revision` , `start` , `end` , `key` , `array` , `order` )
VALUES ( : revision , : start , : end , : key , : array , : order ) ' );
2019-09-30 19:13:40 +13:00
$st2 -> bindValue ( ':revision' , $revision , PDO :: PARAM_STR );
$st2 -> bindValue ( ':start' , $start , PDO :: PARAM_STR );
$st2 -> bindValue ( ':end' , $end , PDO :: PARAM_STR );
$st2 -> bindValue ( ':key' , $key , PDO :: PARAM_STR );
$st2 -> bindValue ( ':array' , $isArray , PDO :: PARAM_INT );
$st2 -> bindValue ( ':order' , $order , PDO :: PARAM_INT );
2019-05-09 18:54:39 +12:00
$st2 -> execute ();
return [];
}
/**
2019-09-07 05:04:26 +12:00
* Create Namespace .
2019-05-09 18:54:39 +12:00
*
* @ param $namespace
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return bool
*/
public function createNamespace ( $namespace )
{
2019-09-07 05:04:26 +12:00
if ( empty ( $namespace )) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Empty namespace' );
}
2019-09-07 05:04:26 +12:00
$documents = 'app_' . $namespace . '.database.documents' ;
$properties = 'app_' . $namespace . '.database.properties' ;
$relationships = 'app_' . $namespace . '.database.relationships' ;
2020-03-22 10:10:06 +13:00
$unique = 'app_' . $namespace . '.database.unique' ;
2019-09-07 05:04:26 +12:00
$audit = 'app_' . $namespace . '.audit.audit' ;
$abuse = 'app_' . $namespace . '.abuse.abuse' ;
2019-05-09 18:54:39 +12:00
try {
2019-09-07 05:04:26 +12:00
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $documents . '` LIKE `template.database.documents`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $properties . '` LIKE `template.database.properties`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $relationships . '` LIKE `template.database.relationships`;' ) -> execute ();
2020-03-22 10:10:06 +13:00
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $unique . '` LIKE `template.database.unique`;' ) -> execute ();
2019-09-07 05:04:26 +12:00
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $audit . '` LIKE `template.audit.audit`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'CREATE TABLE `' . $abuse . '` LIKE `template.abuse.abuse`;' ) -> execute ();
} catch ( Exception $e ) {
2019-05-09 18:54:39 +12:00
throw $e ;
}
return true ;
}
2019-08-23 08:42:06 +12:00
/**
2019-09-07 05:04:26 +12:00
* Delete Namespace .
2019-08-23 08:42:06 +12:00
*
* @ param $namespace
2019-09-07 05:04:26 +12:00
*
2019-08-23 08:42:06 +12:00
* @ throws Exception
2019-09-07 05:04:26 +12:00
*
2019-08-23 08:42:06 +12:00
* @ return bool
*/
public function deleteNamespace ( $namespace )
{
2019-09-07 05:04:26 +12:00
if ( empty ( $namespace )) {
2019-08-23 08:42:06 +12:00
throw new Exception ( 'Empty namespace' );
}
2019-09-07 05:04:26 +12:00
$documents = 'app_' . $namespace . '.database.documents' ;
$properties = 'app_' . $namespace . '.database.properties' ;
$relationships = 'app_' . $namespace . '.database.relationships' ;
$audit = 'app_' . $namespace . '.audit.audit' ;
$abuse = 'app_' . $namespace . '.abuse.abuse' ;
2019-08-23 08:42:06 +12:00
try {
2019-09-07 05:04:26 +12:00
$this -> getPDO () -> prepare ( 'DROP TABLE `' . $documents . '`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'DROP TABLE `' . $properties . '`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'DROP TABLE `' . $relationships . '`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'DROP TABLE `' . $audit . '`;' ) -> execute ();
$this -> getPDO () -> prepare ( 'DROP TABLE `' . $abuse . '`;' ) -> execute ();
} catch ( Exception $e ) {
2019-08-23 08:42:06 +12:00
throw $e ;
}
return true ;
}
2019-05-09 18:54:39 +12:00
/**
2019-09-07 05:04:26 +12:00
* Get Collection .
2019-05-09 18:54:39 +12:00
*
* @ param array $options
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
*/
public function getCollection ( array $options )
{
2020-06-20 23:05:43 +12:00
$start = \microtime ( true );
2019-09-07 05:04:26 +12:00
$orderCastMap = [
'int' => 'UNSIGNED' ,
'string' => 'CHAR' ,
'date' => 'DATE' ,
'time' => 'TIME' ,
'datetime' => 'DATETIME' ,
2019-05-09 18:54:39 +12:00
];
2019-09-07 05:04:26 +12:00
$orderTypeMap = [ 'DESC' , 'ASC' ];
2019-05-09 18:54:39 +12:00
2020-02-17 20:16:11 +13:00
$options [ 'orderField' ] = ( empty ( $options [ 'orderField' ])) ? '$id' : $options [ 'orderField' ]; // Set default order field
2019-05-09 18:54:39 +12:00
$options [ 'orderCast' ] = ( empty ( $options [ 'orderCast' ])) ? 'string' : $options [ 'orderCast' ]; // Set default order field
2020-06-20 23:05:43 +12:00
if ( ! \array_key_exists ( $options [ 'orderCast' ], $orderCastMap )) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid order cast' );
}
2020-06-20 23:05:43 +12:00
if ( ! \in_array ( $options [ 'orderType' ], $orderTypeMap )) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid order type' );
}
2019-09-07 05:04:26 +12:00
$where = [];
$join = [];
$sorts = [];
2019-05-09 18:54:39 +12:00
$search = '' ;
// Filters
2019-09-07 05:04:26 +12:00
foreach ( $options [ 'filters' ] as $i => $filter ) {
$filter = $this -> parseFilter ( $filter );
$key = $filter [ 'key' ];
$value = $filter [ 'value' ];
$operator = $filter [ 'operator' ];
2019-05-09 18:54:39 +12:00
2020-06-20 23:05:43 +12:00
$path = \explode ( '.' , $key );
2019-09-07 05:04:26 +12:00
$original = $path ;
2019-05-09 18:54:39 +12:00
2020-06-20 23:05:43 +12:00
if ( 1 < \count ( $path )) {
$key = \array_pop ( $path );
2019-09-07 05:04:26 +12:00
} else {
2019-05-09 18:54:39 +12:00
$path = [];
}
//$path = implode('.', $path);
2019-09-07 05:04:26 +12:00
$key = $this -> getPDO () -> quote ( $key , PDO :: PARAM_STR );
$value = $this -> getPDO () -> quote ( $value , PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
//$path = $this->getPDO()->quote($path, PDO::PARAM_STR);
2019-09-07 05:04:26 +12:00
$options [ 'offset' ] = ( int ) $options [ 'offset' ];
$options [ 'limit' ] = ( int ) $options [ 'limit' ];
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
if ( empty ( $path )) {
//if($path == "''") { // Handle direct attributes queries
$where [] = 'JOIN `' . $this -> getNamespace () . " .database.properties` b { $i } ON a.uid IS NOT NULL AND b { $i } .documentUid = a.uid AND (b { $i } .key = { $key } AND b { $i } .value { $operator } { $value } ) " ;
} else { // Handle direct child attributes queries
2020-06-20 23:05:43 +12:00
$len = \count ( $original );
2019-09-07 05:04:26 +12:00
$prev = 'c' . $i ;
2019-05-09 18:54:39 +12:00
foreach ( $original as $y => $part ) {
$part = $this -> getPDO () -> quote ( $part , PDO :: PARAM_STR );
2019-09-07 05:04:26 +12:00
if ( 0 === $y ) { // First key
$join [ $i ] = 'JOIN `' . $this -> getNamespace () . " .database.relationships` c { $i } ON a.uid IS NOT NULL AND c { $i } .start = a.uid AND c { $i } .key = { $part } " ;
} elseif ( $y == $len - 1 ) { // Last key
$join [ $i ] .= 'JOIN `' . $this -> getNamespace () . " .database.properties` e { $i } ON e { $i } .documentUid = { $prev } .end AND e { $i } .key = { $part } AND e { $i } .value { $operator } { $value } " ;
} else {
$join [ $i ] .= 'JOIN `' . $this -> getNamespace () . " .database.relationships` d { $i } { $y } ON d { $i } { $y } .start = { $prev } .end AND d { $i } { $y } .key = { $part } " ;
$prev = 'd' . $i . $y ;
2019-05-09 18:54:39 +12:00
}
}
//$join[] = "JOIN `" . $this->getNamespace() . ".database.relationships` c{$i} ON a.uid IS NOT NULL AND c{$i}.start = a.uid AND c{$i}.key = {$path}
// JOIN `" . $this->getNamespace() . ".database.properties` d{$i} ON d{$i}.documentUid = c{$i}.end AND d{$i}.key = {$key} AND d{$i}.value {$operator} {$value}";
}
}
// Sorting
2020-06-20 23:05:43 +12:00
$orderPath = \explode ( '.' , $options [ 'orderField' ]);
$len = \count ( $orderPath );
2019-09-07 05:04:26 +12:00
$orderKey = 'order_b' ;
2020-06-20 23:05:43 +12:00
$part = $this -> getPDO () -> quote ( \implode ( '' , $orderPath ), PDO :: PARAM_STR );
2019-09-07 05:04:26 +12:00
$orderSelect = " CASE WHEN { $orderKey } .key = { $part } THEN CAST( { $orderKey } .value AS { $orderCastMap [ $options [ 'orderCast' ]] } ) END AS sort_ff " ;
if ( 1 === $len ) {
//if($path == "''") { // Handle direct attributes queries
$sorts [] = 'LEFT JOIN `' . $this -> getNamespace () . " .database.properties` order_b ON a.uid IS NOT NULL AND order_b.documentUid = a.uid AND (order_b.key = { $part } ) " ;
} else { // Handle direct child attributes queries
2019-05-09 18:54:39 +12:00
$prev = 'c' ;
$orderKey = 'order_e' ;
foreach ( $orderPath as $y => $part ) {
2019-09-07 05:04:26 +12:00
$part = $this -> getPDO () -> quote ( $part , PDO :: PARAM_STR );
$x = $y - 1 ;
if ( 0 === $y ) { // First key
$sorts [] = 'JOIN `' . $this -> getNamespace () . " .database.relationships` order_c { $y } ON a.uid IS NOT NULL AND order_c { $y } .start = a.uid AND order_c { $y } .key = { $part } " ;
} elseif ( $y == $len - 1 ) { // Last key
$sorts [] .= 'JOIN `' . $this -> getNamespace () . " .database.properties` order_e ON order_e.documentUid = order_ { $prev } { $x } .end AND order_e.key = { $part } " ;
} else {
$sorts [] .= 'JOIN `' . $this -> getNamespace () . " .database.relationships` order_d { $y } ON order_d { $y } .start = order_ { $prev } { $x } .end AND order_d { $y } .key = { $part } " ;
2019-05-09 18:54:39 +12:00
$prev = 'd' ;
}
}
}
2019-09-07 05:04:26 +12:00
/*
2019-07-29 16:50:10 +12:00
* Workaround for a MySQL bug as reported here :
* https :// bugs . mysql . com / bug . php ? id = 78485
*/
$options [ 'search' ] = ( $options [ 'search' ] === '*' ) ? '' : $options [ 'search' ];
// Search
2019-09-07 05:04:26 +12:00
if ( ! empty ( $options [ 'search' ])) { // Handle free search
$where [] = 'LEFT JOIN `' . $this -> getNamespace () . " .database.properties` b_search ON a.uid IS NOT NULL AND b_search.documentUid = a.uid AND b_search.primitive = 'string'
2019-07-29 16:50:10 +12:00
LEFT JOIN
2019-09-07 05:04:26 +12:00
`".$this->getNamespace().'.database.relationships` c_search ON c_search . start = b_search . documentUid
2019-07-29 16:50:10 +12:00
LEFT JOIN
2019-09-07 05:04:26 +12:00
`'.$this->getNamespace().".database.properties` d_search ON d_search . documentUid = c_search . end AND d_search . primitive = 'string'
2019-07-29 16:50:10 +12:00
\n " ;
$search = " AND (MATCH (b_search.value) AGAINST ( { $this -> getPDO () -> quote ( $options [ 'search' ], PDO :: PARAM_STR ) } IN BOOLEAN MODE)
OR MATCH ( d_search . value ) AGAINST ({ $this -> getPDO () -> quote ( $options [ 'search' ], PDO :: PARAM_STR )} IN BOOLEAN MODE )
) " ;
}
2019-05-09 18:54:39 +12:00
$select = 'DISTINCT a.uid' ;
2020-06-20 23:05:43 +12:00
$where = \implode ( " \n " , $where );
$join = \implode ( " \n " , $join );
$sorts = \implode ( " \n " , $sorts );
2019-09-07 05:04:26 +12:00
$range = " LIMIT { $options [ 'offset' ] } , { $options [ 'limit' ] } " ;
$roles = [];
2019-05-09 18:54:39 +12:00
foreach ( Authorization :: getRoles () as $role ) {
2019-09-07 05:04:26 +12:00
$roles [] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"' . $role . '"\', \'$.read\')' ;
2019-05-09 18:54:39 +12:00
}
2019-09-07 05:04:26 +12:00
if ( false === Authorization :: $status ) { // FIXME temporary solution (hopefully)
2019-05-09 18:54:39 +12:00
$roles = [ '1=1' ];
}
2019-09-07 05:04:26 +12:00
$query = " SELECT %s, { $orderSelect }
FROM `".$this->getNamespace().".database.documents` a { $where }{ $join }{ $sorts }
2019-05-09 18:54:39 +12:00
WHERE status = 0
{ $search }
2020-06-20 23:05:43 +12:00
AND ( " . \ implode('||', $roles ). " )
2019-05-09 18:54:39 +12:00
ORDER BY sort_ff { $options [ 'orderType' ]} % s " ;
2020-06-20 23:05:43 +12:00
$st = $this -> getPDO () -> prepare ( \sprintf ( $query , $select , $range ));
2019-05-09 18:54:39 +12:00
$st -> execute ();
$results = [ 'data' => []];
// Get entire fields data for each id
2019-09-07 05:04:26 +12:00
foreach ( $st -> fetchAll () as $node ) {
2019-05-09 18:54:39 +12:00
$results [ 'data' ][] = $node [ 'uid' ];
}
2020-06-20 23:05:43 +12:00
$count = $this -> getPDO () -> prepare ( \sprintf ( $query , 'count(DISTINCT a.uid) as sum' , '' ));
2019-05-09 18:54:39 +12:00
$count -> execute ();
$count = $count -> fetch ();
$this -> resetDebug ();
$this
2020-06-20 23:05:43 +12:00
-> setDebug ( 'query' , \preg_replace ( '/\s+/' , ' ' , \sprintf ( $query , $select , $range )))
-> setDebug ( 'time' , \microtime ( true ) - $start )
-> setDebug ( 'filters' , \count ( $options [ 'filters' ]))
-> setDebug ( 'joins' , \substr_count ( $query , 'JOIN' ))
-> setDebug ( 'count' , \count ( $results [ 'data' ]))
2019-09-07 05:04:26 +12:00
-> setDebug ( 'sum' , ( int ) $count [ 'sum' ])
2019-05-09 18:54:39 +12:00
;
return $results [ 'data' ];
}
/**
2019-09-07 05:04:26 +12:00
* Get Collection .
2019-05-09 18:54:39 +12:00
*
* @ param array $options
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return int
*/
public function getCount ( array $options )
{
2020-06-20 23:05:43 +12:00
$start = \microtime ( true );
2019-09-07 05:04:26 +12:00
$where = [];
$join = [];
2019-05-09 18:54:39 +12:00
2020-07-17 21:48:43 +12:00
$options = array_merge ([
'attribute' => '' ,
'filters' => [],
], $options );
2019-05-09 18:54:39 +12:00
// Filters
2019-09-07 05:04:26 +12:00
foreach ( $options [ 'filters' ] as $i => $filter ) {
$filter = $this -> parseFilter ( $filter );
$key = $filter [ 'key' ];
$value = $filter [ 'value' ];
$operator = $filter [ 'operator' ];
2020-06-20 23:05:43 +12:00
$path = \explode ( '.' , $key );
2019-09-07 05:04:26 +12:00
$original = $path ;
2020-06-20 23:05:43 +12:00
if ( 1 < \count ( $path )) {
$key = \array_pop ( $path );
2019-09-07 05:04:26 +12:00
} else {
2019-05-09 18:54:39 +12:00
$path = [];
}
2019-09-07 05:04:26 +12:00
$key = $this -> getPDO () -> quote ( $key , PDO :: PARAM_STR );
$value = $this -> getPDO () -> quote ( $value , PDO :: PARAM_STR );
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
if ( empty ( $path )) {
//if($path == "''") { // Handle direct attributes queries
$where [] = 'JOIN `' . $this -> getNamespace () . " .database.properties` b { $i } ON a.uid IS NOT NULL AND b { $i } .documentUid = a.uid AND (b { $i } .key = { $key } AND b { $i } .value { $operator } { $value } ) " ;
} else { // Handle direct child attributes queries
2020-06-20 23:05:43 +12:00
$len = \count ( $original );
2019-09-07 05:04:26 +12:00
$prev = 'c' . $i ;
2019-05-09 18:54:39 +12:00
foreach ( $original as $y => $part ) {
$part = $this -> getPDO () -> quote ( $part , PDO :: PARAM_STR );
2019-09-07 05:04:26 +12:00
if ( 0 === $y ) { // First key
$join [ $i ] = 'JOIN `' . $this -> getNamespace () . " .database.relationships` c { $i } ON a.uid IS NOT NULL AND c { $i } .start = a.uid AND c { $i } .key = { $part } " ;
} elseif ( $y == $len - 1 ) { // Last key
$join [ $i ] .= 'JOIN `' . $this -> getNamespace () . " .database.properties` e { $i } ON e { $i } .documentUid = { $prev } .end AND e { $i } .key = { $part } AND e { $i } .value { $operator } { $value } " ;
} else {
$join [ $i ] .= 'JOIN `' . $this -> getNamespace () . " .database.relationships` d { $i } { $y } ON d { $i } { $y } .start = { $prev } .end AND d { $i } { $y } .key = { $part } " ;
$prev = 'd' . $i . $y ;
2019-05-09 18:54:39 +12:00
}
}
}
}
2020-06-20 23:05:43 +12:00
$where = \implode ( " \n " , $where );
$join = \implode ( " \n " , $join );
2020-07-17 21:48:43 +12:00
$attribute = $this -> getPDO () -> quote ( $options [ 'attribute' ], PDO :: PARAM_STR );
2019-09-07 05:04:26 +12:00
$func = 'JOIN `' . $this -> getNamespace () . " .database.properties` b_func ON a.uid IS NOT NULL
2019-05-09 18:54:39 +12:00
AND a . uid = b_func . documentUid
2020-07-17 21:48:43 +12:00
AND ( b_func . key = { $attribute }) " ;
2019-09-07 05:04:26 +12:00
$roles = [];
2019-05-09 18:54:39 +12:00
foreach ( Authorization :: getRoles () as $role ) {
2019-09-07 05:04:26 +12:00
$roles [] = 'JSON_CONTAINS(REPLACE(a.permissions, \'{self}\', a.uid), \'"' . $role . '"\', \'$.read\')' ;
2019-05-09 18:54:39 +12:00
}
2019-09-07 05:04:26 +12:00
if ( false === Authorization :: $status ) { // FIXME temporary solution (hopefully)
2019-05-09 18:54:39 +12:00
$roles = [ '1=1' ];
}
2020-07-26 23:21:58 +12:00
$query = " SELECT SUM(b_func.value) as result
FROM `".$this->getNamespace().".database.documents` a { $where }{ $join }{ $func }
2019-05-09 18:54:39 +12:00
WHERE status = 0
2020-06-20 23:05:43 +12:00
AND ( " . \ implode('||', $roles ).')';
2019-05-09 18:54:39 +12:00
2020-06-20 23:05:43 +12:00
$st = $this -> getPDO () -> prepare ( \sprintf ( $query ));
2019-05-09 18:54:39 +12:00
$st -> execute ();
$result = $st -> fetch ();
$this -> resetDebug ();
$this
2020-06-20 23:05:43 +12:00
-> setDebug ( 'query' , \preg_replace ( '/\s+/' , ' ' , \sprintf ( $query )))
-> setDebug ( 'time' , \microtime ( true ) - $start )
-> setDebug ( 'filters' , \count ( $options [ 'filters' ]))
-> setDebug ( 'joins' , \substr_count ( $query , 'JOIN' ))
2019-05-09 18:54:39 +12:00
;
2020-07-09 02:55:56 +12:00
return ( isset ( $result [ 'result' ])) ? ( int ) $result [ 'result' ] : 0 ;
2019-05-09 18:54:39 +12:00
}
/**
2019-09-07 05:04:26 +12:00
* Get Unique Document ID .
2019-05-09 18:54:39 +12:00
*/
2020-02-17 20:16:11 +13:00
public function getId ()
2019-05-09 18:54:39 +12:00
{
2020-06-20 23:05:43 +12:00
$unique = \uniqid ();
2019-09-07 05:04:26 +12:00
$attempts = 5 ;
2019-05-09 18:54:39 +12:00
2019-09-07 05:04:26 +12:00
for ( $i = 1 ; $i <= $attempts ; ++ $i ) {
2019-05-09 18:54:39 +12:00
$document = $this -> getDocument ( $unique );
2020-02-17 20:16:11 +13:00
if ( empty ( $document ) || $document [ '$id' ] !== $unique ) {
2019-05-09 18:54:39 +12:00
return $unique ;
}
}
2019-09-07 05:04:26 +12:00
throw new Exception ( 'Failed to create a unique ID (' . $attempts . ' attempts)' );
2019-05-09 18:54:39 +12:00
}
/**
2019-09-07 05:04:26 +12:00
* Last Modified .
2019-05-09 18:54:39 +12:00
*
2020-06-24 23:18:33 +12:00
* Return Unix timestamp of last time a node queried in corrent session has been changed
2019-05-09 18:54:39 +12:00
*
* @ return int
*/
public function lastModified ()
{
return $this -> lastModified ;
}
/**
2019-09-07 05:04:26 +12:00
* Parse Filter .
2019-05-09 18:54:39 +12:00
*
* @ param string $filter
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return array
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
protected function parseFilter ( $filter )
{
2019-09-07 05:04:26 +12:00
$operatorsMap = [ '!=' , '>=' , '<=' , '=' , '>' , '<' ]; // Do not edit order of this array
2019-05-09 18:54:39 +12:00
//FIXME bug with >= <= operators
$operator = null ;
foreach ( $operatorsMap as $node ) {
2020-06-20 23:05:43 +12:00
if ( \strpos ( $filter , $node ) !== false ) {
2019-05-09 18:54:39 +12:00
$operator = $node ;
break ;
}
}
2019-09-07 05:04:26 +12:00
if ( empty ( $operator )) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid operator' );
}
2020-06-20 23:05:43 +12:00
$filter = \explode ( $operator , $filter );
2019-05-09 18:54:39 +12:00
2020-06-20 23:05:43 +12:00
if ( \count ( $filter ) != 2 ) {
2019-05-09 18:54:39 +12:00
throw new Exception ( 'Invalid filter expression' );
}
return [
'key' => $filter [ 0 ],
'value' => $filter [ 1 ],
'operator' => $operator ,
];
}
/**
2019-09-07 05:04:26 +12:00
* Get Data Type .
2019-05-09 18:54:39 +12:00
*
* Check value data type . return value can be on of the following :
* string , integer , float , boolean , object , list or null
*
* @ param $value
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return string
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws \Exception
*/
protected function getDataType ( $value )
{
2020-06-20 23:05:43 +12:00
switch ( \gettype ( $value )) {
2019-05-09 18:54:39 +12:00
case 'string' :
return self :: DATA_TYPE_STRING ;
break ;
case 'integer' :
return self :: DATA_TYPE_INTEGER ;
break ;
case 'double' :
return self :: DATA_TYPE_FLOAT ;
break ;
case 'boolean' :
return self :: DATA_TYPE_BOOLEAN ;
break ;
case 'array' :
2020-06-20 23:05:43 +12:00
if (( bool ) \count ( \array_filter ( \array_keys ( $value ), 'is_string' ))) {
2019-05-09 18:54:39 +12:00
return self :: DATA_TYPE_DICTIONARY ;
}
return self :: DATA_TYPE_ARRAY ;
break ;
case 'NULL' :
return self :: DATA_TYPE_NULL ;
break ;
}
2020-06-20 23:05:43 +12:00
throw new Exception ( 'Unknown data type: ' . $value . ' (' . \gettype ( $value ) . ')' );
2019-05-09 18:54:39 +12:00
}
/**
* @ param $key
* @ param $value
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return $this
*/
public function setDebug ( $key , $value )
{
$this -> debug [ $key ] = $value ;
return $this ;
}
/**
* @ return array
*/
public function getDebug ()
{
return $this -> debug ;
}
/**
2019-09-07 05:04:26 +12:00
* return $this ; .
2019-05-09 18:54:39 +12:00
*/
public function resetDebug ()
{
2019-09-07 05:04:26 +12:00
$this -> debug = [];
2019-05-09 18:54:39 +12:00
}
/**
* @ return PDO
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ throws Exception
*/
2020-07-02 10:34:05 +12:00
protected function getPDO ()
2019-05-09 18:54:39 +12:00
{
return $this -> register -> get ( 'db' );
}
/**
* @ throws Exception
2019-09-07 05:04:26 +12:00
*
2019-05-09 18:54:39 +12:00
* @ return Client
*/
protected function getRedis () : Client
{
return $this -> register -> get ( 'cache' );
}
2019-09-07 05:04:26 +12:00
}