Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support array columns types #59

Closed
max13fr opened this issue May 25, 2023 · 8 comments
Closed

Support array columns types #59

max13fr opened this issue May 25, 2023 · 8 comments
Labels
enhancement New feature or request

Comments

@max13fr
Copy link

max13fr commented May 25, 2023

Hello,

First, thanks for your really useful package !

I would like to add a field with the type integer[] (array of integer) but the type doesn't seems to exist in the columns types supported, I'm missing something ?

Thanks in advance,
Max

@tpetry
Copy link
Owner

tpetry commented May 25, 2023

Array types are not supported. @jaulz worked on it with #29, but adding it is very complicated.

But I am right now working on adding integerArray to the package, this will be the only supported array type. For every other type, JSON should be used.

For now, you can add the column by using the domain type:

$query->domain('mycol', 'int[]');

Can you share why you need the integer array type? What do you want to do with it? I could directly add some functionality for your use case.

@max13fr
Copy link
Author

max13fr commented May 25, 2023

I would like to store the list of zipcodes on each french cities. The equivalent of :

CREATE TABLE cities (
    id serial primary key;
    name VARCHAR(255) NOT NULL;
    insee_code VARCHAR(10) NOT NULL UNIQUE;
    postal_codes integer[] NOT NULL;
);

I'm not sure to completely understand #29 but just add main types as following should do the job, no ?

vendor/tpetry/laravel-postgresql-enhanced/src/Schema/BlueprintTypes.php

    /**
     * Create a new integer array column on the table.
     */
    public function integerArray(string $column): ColumnDefinition
    {
        return $this->addColumn('integerArray', $column);
    }

    /**
     * Create a new string array column on the table.
     */
    public function stringArray(string $column, ?int $length = null)
    {
        $length = $length ?: Builder::$defaultStringLength;

        return $this->addColumn('stringArray', $column, compact('length'));
    }

    /**
     * Create a new integer array column on the table.
     */
    public function textArray(string $column): ColumnDefinition
    {
        return $this->addColumn('textArray', $column);
    }
}

vendor/tpetry/laravel-postgresql-enhanced/src/Schema/Grammars/GrammarTypes.php

    /**
     * Create the column definition for a integer array type.
     */
    protected function typeIntegerArray(Fluent $column): string
    {
        return 'int[]';
    }

    /**
     * Create the column definition for a string array type.
     */
    protected function typeStringArray(Fluent $column): string
    {
        return blank($column['length']) ? 'varchar' : "varchar({$column['length']})";
    }

    /**
     * Create the column definition for a text array type.
     */
    protected function typeTextArray(Fluent $column): string
    {
        return 'text[]';
    }    

One advantage of the array type vs the jsonb type is that you have a all constraints already set from your type (ex: integer[] allows only integer inside). With a JSON you can have all types inside (object, array, integer, string, boolean, ...) even with a constraint :

CREATE TABLE test (
    tab jsonb NOT NULL CHECK (jsonb_typeof(tab) = 'array')
);

INSERT INTO test VALUES ('[1, "2", true, {}, null]');

select * from test;
=> [1, "2", true, {}, null]

With an integer array, you can only insert integer inside (number as string are automatically converted in addition) :

CREATE TABLE test (
    tab integer[] NOT NULL
);

INSERT INTO test VALUES ('{1, "2", 3}');

select * from test;
=> {1,2,3}

Max

@max13fr
Copy link
Author

max13fr commented May 25, 2023

Ah ok I understood, the issue : doctrine don't know how to manage the integer array field :

Exception: Unknown database type _int4 requested, Doctrine\DBAL\Platforms\PostgreSQL100Platform may not support it.
Could not analyze class App\Models\City.

@tpetry
Copy link
Owner

tpetry commented May 25, 2023

Yeah, that's the problem. I need to build doctrine types for everything. For now, you can do the domain(...) trick but you may got problems with some stuff.

I may be able to work on it sometime in the next 2 weeks. I'll need that by myself currently.

@max13fr
Copy link
Author

max13fr commented May 25, 2023

It's a doctrine issue, I will try to create an issue on doctrine repo.

vendor/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php

    protected function initializeDoctrineTypeMappings()
    {
        $this->doctrineTypeMapping = [
            'bigint'           => 'bigint',
            'bigserial'        => 'bigint',
            'bool'             => 'boolean',
            'boolean'          => 'boolean',
            'bpchar'           => 'string',
            'bytea'            => 'blob',
            'char'             => 'string',
            'date'             => 'date',
            'datetime'         => 'datetime',
            'decimal'          => 'decimal',
            'double'           => 'float',
            'double precision' => 'float',
            'float'            => 'float',
            'float4'           => 'float',
            'float8'           => 'float',
            'inet'             => 'string',
            'int'              => 'integer',
            'int2'             => 'smallint',
            'int4'             => 'integer',
            'int8'             => 'bigint',
            'integer'          => 'integer',
            'interval'         => 'string',
            'json'             => 'json',
            'jsonb'            => 'json',
            'money'            => 'decimal',
            'numeric'          => 'decimal',
            'serial'           => 'integer',
            'serial4'          => 'integer',
            'serial8'          => 'bigint',
            'real'             => 'float',
            'smallint'         => 'smallint',
            'text'             => 'text',
            'time'             => 'time',
            'timestamp'        => 'datetime',
            'timestamptz'      => 'datetimetz',
            'timetz'           => 'time',
            'tsvector'         => 'text',
            'uuid'             => 'guid',
            'varchar'          => 'string',
            'year'             => 'date',
            '_varchar'         => 'string',
            // new line =>
            '_int4'            => 'string',
        ];
    }

With that patch integer[] are working fine (except we get a string {1, 2, 3} instead of an array in eloquent).
It's seem that they already fix it for varchar array apparently.

All array are the type with underscore as prefix (even multidimensional array, for example: integer[][] => _int4)

@max13fr
Copy link
Author

max13fr commented May 25, 2023

Just added a specific cast and it's working 🎉

app/Casts/PostgresArray.php (the implementation is ugly mainly for string but it's ok for integer array, it's just as demo) :

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;

class PostgresArray implements CastsAttributes
{
    /**
     * Cast the given value.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return json_decode(str_replace(['{', '}'], ['[', ']'], $value));
    }

    /**
     * Prepare the given value for storage.
     *
     * @param  array<string, mixed>  $attributes
     */
    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
    {
        return str_replace(['[', ']'], ['{', '}'], json_encode($value));;
    }
}

app/Models/City.php

<?php

namespace App\Models;

use App\Casts\PostgresArray;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class City extends Model
{
    use HasFactory;

    protected $casts = [
        'postal_codes' => PostgresArray::class,
    ];
}

routes/web.php

Route::get('/test', function () {
    $city = City::first();
    dump($city->postal_codes);
    $city->postal_codes = [...$city->postal_codes, 12345];
    $city->save();
    dump($city->postal_codes);
});

@max13fr
Copy link
Author

max13fr commented May 25, 2023

@tpetry tpetry added the enhancement New feature or request label Jun 3, 2023
@tpetry tpetry closed this as completed in 13d3cad Jun 26, 2023
@tpetry
Copy link
Owner

tpetry commented Jun 26, 2023

The integer array type is now supported with 0.31.0. There is additionally a cast for easier usage and a whereIntegerArrayMatches query builder function to use all of the potentials of this marvelous data type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants