Skip to content
This repository was archived by the owner on Jul 16, 2021. It is now read-only.
This repository was archived by the owner on Jul 16, 2021. It is now read-only.

Custom Cast/convert in Eloquent #992

@gogromat

Description

@gogromat

I am trying to figure out a way to save binary/varbinary MSSQL types with Eloquent (sqlsrv driver + doctrine/dbal v2.5.5).
So far I figured out a way to prepare statement with conversion and a placeholder, but it involves manually calling DB::statement, instead of $model->save(), which is much more desirable.

DB::statement(
    "INSERT INTO files (file_name, content, ...) VALUES (?, CONVERT(VARBINARY(MAX) ?), ...));", [
	    $file->file_name,
	    base64_encode(file_get_contents($file->file_path))
]);

Notice this is a prepared statement, I call CONVERT(VARBINARY(MAX), ?) and then I feed my values afterwards.
If I do it using something like setContentAttribute on a model and have it defined as

function setContentAttribute ($value) {
	$this->attributes['content'] = DB::raw("CONVERT(VARBINARY(MAX), '" . base64_encode($value) . "')");
}

it has two drawbacks:

  • will not be a prepared statement value anymore,
  • for some reason it is being executed extremely slow, (like a second per 100kB slow).

Looking at the code I found this:

Query\Builder :: insert

        // Finally, we will run this query against the database connection and return
        // the results. We will need to also flatten these bindings before running
        // the query so they are all in one huge, flattened array for execution.
        return $this->connection->insert(
            $this->grammar->compileInsert($this, $values),
            $this->cleanBindings(Arr::flatten($values, 1))
        );

Grammars\Grammar :: compileInsert

        // We need to build a list of parameter place-holders of values that are bound
        // to the query. Each insert should have the exact same amount of parameter
        // bindings so we will loop through the record and parameterize them all.
        $parameters = collect($values)->map(function ($record) {
            return '('.$this->parameterize($record).')';
        })->implode(', ');

        return "insert into $table ($columns) values $parameters";

Grammars\Grammar :: parameterize and parameter

  /**
     * Create query parameter place-holders for an array.
     *
     * @param  array   $values
     * @return string
     */
    public function parameterize(array $values)
    {
        return implode(', ', array_map([$this, 'parameter'], $values));
    }

    /**
     * Get the appropriate query parameter place-holder for a value.
     *
     * @param  mixed   $value
     * @return string
     */
    public function parameter($value)
    {
        return $this->isExpression($value) ? $this->getValue($value) : '?';
    }

So parameters can be easily parametarised, I can actually have my setContentAttribute, which should set $this->attributes['content'] = DB::raw("CONVERT(VARBINARY(MAX), ?)");,
but then I would need to get the value from the Expression as well. So far the

Query\Builder :: cleanBindings just ignores Expression instance.

    /**
     * Remove all of the expressions from a list of bindings.
     *
     * @param  array  $bindings
     * @return array
     */
    protected function cleanBindings(array $bindings)
    {
        return array_values(array_filter($bindings, function ($binding) {
            return ! $binding instanceof Expression;
        }));
    }

From what I sort of understand we can modify Expression to not only have getValue, but also have something like getParameter, switch the logic around and make Expression->getParamter return CONVERT(VARBINARY(MAX), ?) and a value as Expression->getValue return actual string value.

What do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions