How natural sorting a varchar column with Doctrine - postgresql

I have a table with a column that contains names (varchar) but some names have numbers inside and the ordering is not the expected.
I have something like:
Courlis
D11 Espadon
D13 Balbuzard
D1 empacher
D2
But I expect:
Courlis
D1 empacher
D2
D11 Espadon
D13 Balbuzard
I've found lot of tips about it, but it's always on ordering numbers only stored as string: add a 0 to convert it in numbers, check the length of the string to place 1 before 10, etc... But in my case it can't work.
I can't use SQL query, because I use it in a form of a Symfony application that need a QueryBuilder.

Here is a way to do this with ICU collations in PostgreSQL (available from v10 on):
CREATE COLLATION en_natural (
LOCALE = 'en-US-u-kn-true',
PROVIDER = 'icu'
);
CREATE TABLE atable (textcol text COLLATE en_natural);
COPY atable FROM STDIN;
Enter data to be copied followed by a newline.
End with a backslash and a period on a line by itself, or an EOF signal.
>> Courlis
>> D11 Espadon
>> D13 Balbuzard
>> D1 empacher
>> D2
>> \.
test=# SELECT textcol FROM atable ORDER BY textcol;
textcol
---------------
Courlis
D1 empacher
D2
D11 Espadon
D13 Balbuzard
(5 rows)

Thanks to Laurentz Albe for your answer,for a Step by step in a Symfony application:
Create a Migration file that create the custom collation
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20200221215657 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE COLLATION fr_natural (provider = "icu", locale = "fr-u-kn-true");');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('DROP COLLATION fr_natural');
}
}
Create a Doctrine Custom Function (Collate DQL)
<?php
namespace App\DQL;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
class Collate extends FunctionNode
{
public $expressionToCollate = null;
public $collation = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->expressionToCollate = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$parser->match(Lexer::T_IDENTIFIER);
$lexer = $parser->getLexer();
$this->collation = $lexer->token['value'];
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return sprintf( '%s COLLATE %s', $sqlWalker->walkStringPrimary($this->expressionToCollate), $this->collation );
}
}
Register the new Function in the Doctrine config
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
server_version: '12'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
dql:
string_functions:
collate: App\DQL\Collate
And finally juste use it in the query when needed
$query = $this->createQueryBuilder('shell')
->orderBy('COLLATE(shell.name, fr_natural)', 'ASC')
->getQuery();
return $query->getResult();

Related

Unexpected data found during update on eloquent / Laravel

I am using ajax to update a model that contains timestamps, but it throw me an exception:
{message: "Unexpected data found.", exception: "InvalidArgumentException",…}
message: "Unexpected data found."
exception: "InvalidArgumentException"
file: "/home/asus/Devagnos/almada/vendor/nesbot/carbon/src/Carbon/Traits/Creator.php"
line: 623
trace: [,…]
i have disabled the timestamps and i set the dateformat like this:
protected $dateFormat = 'Y-m-d H:i:s.u';
public $timestamps = false;
protected $dates = [
'created_at',
'updated_at'
];
alse I added these methods
/**
* #param $val
*/
public function setCreatedAtAttribute($val)
{
return Carbon::createFromFormat('Y-m-d H:i:s.u', $val);
}
/**
* #param $val
*/
public function setUpdatedAtAttribute($val)
{
return Carbon::createFromFormat('Y-m-d H:i:s.u', $val);
}
but I am always getting the same error, What am I doing wrong ?
I'm using laravel 6.8 and postgresql
If you're trying to use microseconds, then you should refer to this guide from the documentation:
https://carbon.nesbot.com/laravel/
I don't get what you tried with setCreatedAtAttribute and setUpdatedAtAttribute, setters are supposed to change the inner property, not to return a value.
Then check you gave to your DB columns enough precision (such as TIMESTAMP(6)) in your migration schemas.

Namespace missing

I have following class in folder frontend/migrations
use yii\db\Schema;
class m170727_180101_Bewerbungen extends \yii\db\Migration
{
public function safeUp()
{
$tableOptions = null;
if ($this->db->driverName === 'mysql') {
$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
}
$this->createTable('bewerbungen', [
'bew_id' => $this->primaryKey(),
'datum' => $this->date()->notNull(),
'firma' => $this->string(100)->notNull(),
'rechtsart' => $this->integer(11),
'stadt' => $this->string(100)->notNull(),
'plz' => $this->integer(11)->notNull(),
'strasse_nr' => $this->string(100),
'ansprech_person' => $this->string(100),
'email' => $this->string(50)->notNull(),
'feedback' => $this->integer(11),
'bemerkungen' => $this->string(150),
'FOREIGN KEY ([[feedback]]) REFERENCES nachricht ([[id_message]]) ON DELETE CASCADE ON UPDATE CASCADE',
'FOREIGN KEY ([[rechtsart]]) REFERENCES rechtsform ([[id_recht]]) ON DELETE CASCADE ON UPDATE CASCADE',
], $tableOptions);
}
public function safeDown()
{
$this->dropTable('bewerbungen');
}
}
Each try to read out method safeUp() throws out error:
Unable to find 'frontend\migrations\m170727_180101_Bewerbungen' in file: E:\xampp\htdocs\Yii2_Mail/frontend/migrations/m170727_180101_Bewerbungen.php. Namespace missing?**
Here is my script:
namespace frontend\migrations; ...
$connect=new m170727_180101_Bewerbungen();
$connect->safeUp(); ...
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
What the hell is that?
The same error using like this:
$connect=new \frontend\migrations\m170727_180101_Bewerbungen();
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Try using the full path
$connect=new \frontend\migration\m170727_180101_Bewerbungen();
You have got this error because there is no namespace in your file so autoloader can not find it.
But this is not the real problem here - you are not using Yii 2 migration properly. Follow the Yii2 Migration Guide.
In addition, since you placed this migration in frontend you might want to take a look at Namespaced Migrations to actually add namespace there and to run it properly.

Magento 2 - error creating db table from custom schema

I'm trying to create a custom module for Magento 2 and I've got to the point of defining the schema in the /Setup/InstallSchema.php
When running 'php bin/magento setup:upgrade' I get the error:
Call to undefined function Test/Connector/Setup/getConnection()
The module is enabled and correctly showing in the config file. The schema file I'm trying to run is:
<?php
namespace Test\Connector\Setup;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;
class InstallSchema implements InstallSchemaInterface
{
public function install(SchemaSetupInterface $setup, ModuleContextInterface
$context) {
$installer = $setup;
$installer->startSetup();
$tableName = $installer->getTable('test_connector_settings');
if ($installer->getConnection()->isTableExists($tableName) != true) {
$table = $installer->getConnection()
->newTable($installer->getTable('ipos_connector_settings'))
->addColumn('id', Table::TYPE_SMALLINT, null, ['identity'=> true, 'nullable'=>false, 'primary'=>true], 'ID')
->addColumn('api_url', Table::TYPE_TEXT, 255, ['nullable'=>true], 'API URL')
->addColumn('api_user', Table::TYPE_TEXT, 100, ['nullable'=>false], 'API User Name')
->addColumn('api_password', Table::TYPE_TEXT, 100, ['nullable'=>false], 'API Password');
$installer-getConnection()->createTable($table);
}
$installer->endSetup();
}
}
Thanks in advance,
Please change this line
$installer-getConnection()->createTable($table); // your code line.
With
$installer->getConnection()->createTable($table);

SQL Parse Error: Parameter name expected using firebird

I'm using the script in firebird:
SET TERM ^ ;
execute block as
declare a_cursor CURSOR FOR(select contaSelect.cdcontacontabil, spedcontavincSelect.nuversao, spedcontavincSelect.cdcontasped
from ectbconta contaSelect join ectbspedcontavinc spedcontavincSelect on contaSelect.cdempresa=spedcontavincSelect.cdempresa and contaSelect.cdconta=spedcontavincSelect.cdconta where contaSelect.cdempresa = 1);
​
declare variable contacontabil varchar(40);
declare variable versao integer;
declare variable contasped integer;
​
begin
open a_cursor;
while(1=1) do
begin
fetch a_cursor INTO contacontabil, versao, contasped;
if (row_count = 0) then leave;
insert into ectbspedcontavinc (cdempresa, cdconta, nuversao, cdcontasped)
select contaInsert.cdempresa, contaInsert.cdconta, :versao, :contasped from ectbconta contaInsert
where contaInsert.cdcontacontabil =:contacontabil and contaInsert.cdempresa in(2, 3, 4)
and not exists (select * from ectbspedcontavinc spedcontavincInsert
where spedcontavincInsert.cdempresa=contaInsert.cdempresa and spedcontavincInsert.cdconta=contaInsert.cdconta and spedcontavincInsert.nuversao=:versao and spedcontavincInsert.cdcontasped=:contasped);
end
end
^
​
SET TERM ; ^
​
COMMIT WORK;
Ocurred this message:
Error Message:
---------------------------------------- SQL Parse Error:
Parameter name expected
D:\Java\Firebird_2_5\bin>isql.exe -z ISQL Version: WI-V2.5.5.26952
Firebird 2.5 Use CONNECT or CREATE DATABASE to specify a database SQL>
I don't know what do.
public class CriaScriptSpedContaVincService {
private static final Integer GRUPO_EMPRESA_CTB = 3;
private static final Integer CD_EMPRESA_CONSOLIDADORA = 102;
public void criaCriaScriptSpedContaVinc() {
StringBuilder script = new StringBuilder();
IContabEmpresa contabEmpresa = new ContabEmpresaProvider();
List<Integer> empresas = contabEmpresa.retornaEmpresasDoGrupoContabil(GRUPO_EMPRESA_CTB);
for (Integer cdEmpresa : empresas) {
script.append("INSERT INTO ECTBSPEDCONTAVINC (CDEMPRESA, CDCONTA, NUVERSAO, CDCONTASPED) ");
script.append("SELECT ");
script.append(cdEmpresa);
script.append(" AS CDEMPRESA, C.CDCONTA, C.NUVERSAO, C.CDCONTASPED ");
script.append("FROM ECTBSPEDCONTAVINC C WHERE C.CDEMPRESA = ");
script.append(CD_EMPRESA_CONSOLIDADORA);
script.append(" AND NOT EXISTS (SELECT * FROM ECTBSPEDCONTAVINC CV WHERE CV.CDEMPRESA = ");
script.append(cdEmpresa);
script.append(" AND CV.CDCONTA = C.CDCONTA);");
script.append(TextUtil.NOVA_LINHA);
}
String caminhaCompletoArquivo = "t:/xande/script2.sql";
FileUtil.escreveArquivo(script, caminhaCompletoArquivo);
System.out.println("Fechou mano... ;)");
}
}

doctrine migrations bundle and postgres schema: diff does not work properly

I'm using doctrine in my Symfony project, by connecting to an already existent postgres database.
The DB has several schemas, but the symfony app will use nothing but its own schema. The first Entity class I created is the following one:
namespace Belka\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="app_auth.User", schema="app_auth")
*/
class User {
/**
* #ORM\Column(type="string")
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $username;
/**
* #ORM\Column(type="string")
*/
private $email;
/**
* #ORM\Column(type="string")
*/
private $password;
}
as you can see, the Entity is specifying its own schema app_auth.
Next, I tried to use the migrations bundle. So, I installed and configured it, in order not to consider anything but my schema:
Extract of config.yml:
doctrine:
dbal:
driver: "%database_driver%"
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
schema_filter: ~^app_auth\..*~
Extract of config_dev.yml:
doctrine_migrations:
dir_name: "%kernel.root_dir%/../.container/update/DoctrineMigrations"
namespace: Application\Migrations
table_name: "app_auth.migration_versions"
name: Application Migrations
And I run the diff:
php app/console doctrine:migrations:diff
Unfortunately, the migration class generated is the following one:
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160422171409 extends AbstractMigration
{
/**
* #param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
}
/**
* #param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SCHEMA app_auth');
}
}
what's wrong with my configuration then?
Dirty but working solution:
Tested with doctrine/migrations: 3.0.1
We can patch out the schema removing functionality of the DiffGenerator. I personally used this https://github.com/cweagans/composer-patches
Install the package, and then in composer.json "extra" section add:
"patches": {
"doctrine/migrations": {
"Change doctrine behavior to correctly pass PostgreSQL schema to schema_filter while diffing": "patches/doctrine-diff-generator.patch"
}
}
The patch file in patches/doctrine-diff-generator.patch:
--- lib/Doctrine/Migrations/Generator/DiffGenerator.php 2020-06-21 10:55:42.000000000 +0200
+++ lib/Doctrine/Migrations/Generator/DiffGenerator.patched.php 2020-12-23 12:33:02.689405221 +0100
## -142,8 +142,6 ##
*/
private function resolveTableName(string $name) : string
{
- $pos = strpos($name, '.');
-
- return $pos === false ? $name : substr($name, $pos + 1);
+ return $name;
}
}
Of course you have to bare in mind, that updates to doctrine could break the patch. And you are changing this functionality globally for your doctrine, so beware of breaking changes.
Personally, for my use case, which is adding doctrine entities into legacy database, without breaking it, this works nicely, and saves me from adding every new table managed by doctrine into the schema_filter.
Explanation:
When we look into DiffGenerator implementation it actually decodes the table names with schema correctly, but only when collecting the current database schema. This happens in PostgreSqlSchemaManager.php:
/**
* {#inheritdoc}
*/
protected function _getPortableTableDefinition($table)
{
$schemas = $this->getExistingSchemaSearchPaths();
$firstSchema = array_shift($schemas);
if ($table['schema_name'] === $firstSchema) {
return $table['table_name'];
}
return $table['schema_name'] . '.' . $table['table_name'];
}
But then then when calculating the target schema, this information is lost here in DiffGenerator.php:
/**
* Resolve a table name from its fully qualified name. The `$name` argument
* comes from Doctrine\DBAL\Schema\Table#getName which can sometimes return
* a namespaced name with the form `{namespace}.{tableName}`. This extracts
* the table name from that.
*/
private function resolveTableName(string $name) : string
{
$pos = strpos($name, '.');
return $pos === false ? $name : substr($name, $pos + 1);
}
And only afterwards the return value of this function is passed into the schema_filter. Unfortunate, but i guess there was a reason for this :)