/** * @dataProvider dataProviderTestWriteRead * * @param string $name * @param mixed $value */ public function testMariaEncoding($name, $value) { self::$resetFixture = false; $bar = DynamicActiveRecord::encodeForMaria($value); $bar = json_encode($bar); $bar = json_decode($bar); $bar = DynamicActiveRecord::decodeForMaria($bar); $this->assertSame($value, DynamicActiveRecord::decodeForMaria(DynamicActiveRecord::encodeForMaria($value))); }
/** * Generate DB command from ActiveQuery with Maria-specific SQL for dynamic columns. * * User of DynamicActiveQuery should not normally need to use this method. * * #### History * * This implementation is the best I could manage. A dynamic attribute name * can appear anywhere that a schema attribute name could appear (select, join, where, ...). * It needs to be converted to the Maria SQL using COLUMN_CREATE('name', value, …) * for accessing dynamic columns. * Because SQL is statically-typed and there is no schema to refer to for dynamic * attributes, the accessor SQL must specify the the dyn-col's type, e.g. * * ```sql * WHERE COLUMN_GET(details, 'color' AS CHAR) = 'black' * ``` * * In which details is the blob column containing all the dynamic columns, 'color' is the * name of a dynamic column that may or may not appear in any given table record, and * CHAR means the value should be cast to CHAR before it is compared with 'black'. * `COLUMN_GET(details, 'color' AS CHAR)` is the "accessor SQL". * * So I faced two problems: * * 1. How to identify a dynamic attribute name in an ActiveQuery? * 2. How to choose the type to which it should be cast in the SQL? * * The design prociple of DynamicAR is "an attribute that isn't an instance variable, * a column and doesn't have a magic get-/setter is assumed to be a dynamic attribute". * So, in order to infer from the properties of an AQ instance the attribute names * that need to be converted to dynamic column accessor SQL, I need to go through * the AQ to identify * all the column names and remove those in the schema. But I don't know how to * identify column names in an AQ instance. Even if I did, there's problem 2. * * The only way I can imagine to infer datatype from an AQ instance is to look * at the context. If the attribute is compared with a bound parameter, that's a clue. * If it is being used in an SQL function, e.g. CONCAT(), or being compared with a * schema column, that suggests something. But if it is on its own in a SELECT then I am * stuck. Also stuck if it is compared with another dynamic attribute. This seems * fundamentally intractible. * * So I decided that the user needs to help DynamicActiveQuery by distinguishing the names * of dynamic attributes and by explicitly specifying the type. The format for this: * * (!name|type!) * * Omitting type implies the default type: CHAR. Children of dynamic attributes, i.e. * array elements, are separated from parents with `.` (period), e.g. * `(!address.country|CHAR!)`. (Spaces are not alowed around the `|`.) So a user can do: * * $blueShirts = Product::find() * ->where(['category' => Product::SHIRT, '(!color!)' => 'blue']) * ->all(); * * $cheapShirts = Product::find() * ->select( * ['sale' => 'MAX((!cost|decimal(6,2)!), 0.75 * (!price.wholesale.12|decimal(6,2)!))'] * ) * ->where(['category' => Product::SHIRT]) * ->andWhere('(!price.retail.unit|decimal(6,2)!) < 20.00') * ->all(); * * The implementation is like db\Connection's quoting of [[string]] and {{string}}. Once * the full SQL string is ready, `preg_repalce()` it. The regex pattern is a bit complex * and the replacement callback isn't pretty either. Is there a better way to add to * `$params` in the callback than this? And for the parameter placeholder counter `$i`? * * @param null|\yii\db\Connection $db The database connection * * @return \yii\db\Command the modified SQL statement */ public function createCommand($db = null) { /** @var DynamicActiveRecord $modelClass */ $modelClass = $this->modelClass; if ($db === null) { $db = $modelClass::getDb(); } if ($this->sql === null) { list($sql, $params) = $db->getQueryBuilder()->build($this); } else { $sql = $this->sql; $params = $this->params; } $dynamicColumn = $modelClass::dynamicColumn(); $callback = function ($matches) use(&$params, $dynamicColumn) { $type = !empty($matches[3]) ? $matches[3] : 'CHAR'; $sql = $dynamicColumn; foreach (explode('.', $matches[2]) as $column) { $placeholder = DynamicActiveRecord::placeholder(); $params[$placeholder] = $column; $sql = "COLUMN_GET({$sql}, {$placeholder} AS {$type})"; } return $sql; }; $pattern = <<<'REGEXP' % (`?) \(! \s*? ( [a-z_\x7f-\xff][a-z0-9_\x7f-\xff]* (?: \. [^.|\s]+)* ) (?: \| (binary (?:\(\d+\))? | char (?:\(\d+\))? | time (?:\(\d+\))? | datetime (?:\(\d+\))? | date | decimal (?:\(\d\d?(?:,\d\d?)?\))? | double (?:\(\d\d?,\d\d?\))? | int(eger)? | (?:un)? signed (?:\s+int(eger)?)?) )? \s*? !\) \1 %ix REGEXP; $sql = preg_replace_callback($pattern, $callback, $sql); return $db->createCommand($sql, $params); }