[DC-1040] allow queries with table joins across different databases Created: 17/Nov/11 Updated: 17/Nov/11 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Blocker |
| Reporter: | Fabrice Agnello | Assignee: | Jonathan H. Wage |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Windows XP SP3, Apache 2, PHP 5.3, MySQL 5.1.36, Symfony 1.4.8, Doctrine 1.2.3 |
||
| Attachments: |
|
| Description |
|
I'm currently working on a project which relies upon several databases declared in databases.yml in symfony 1.4.8. I was facing an issue that has already been raised by other people, namely that you can't join tables which reference each other among different mysql databases. I've dug a bit in the Doctrine_Query class and came to a solution that is acceptable for us, and allows now to make joins accross databases. Description follows : first change is in the Doctrine_Core class, that gets stuffed with a new constant : this constant allows us to add a new attribute to the databases.yml file as in :
after that, a few changes have been done in the Doctrine_Query class which is attached to this issue for the sake of readability. This may not be optimal, and probably need some regression testing, but it is currently working fine on our test server. after this is done, I was able to issue queries like the following : where the referenced tables are in different databases. The necessary object binding has been done in every model class following the paradigm : Doctrine_Manager::getInstance()->bindComponent('CdcIndInt ', 'gescdc'); I don't know if this description is clear enough, so let me know if something is missing/wrong. |
[DC-1031] CLONE -Multiple connections and i18n (raised as unresolved - original ticket marked as resolved) Created: 23/Aug/11 Updated: 28/Mar/12 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | Connection, I18n |
| Affects Version/s: | 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Blocker |
| Reporter: | James Bell | Assignee: | Jonathan H. Wage |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | None | ||
| Environment: |
MySQL 5.1.37 symfony 1.4.13 |
||
| Description |
|
I used to work with a single database named "doctrine". The query was working properly. I then decided to use 2 databases so I got my schema like this:
I did setup my connections in config/databases.yml this way:
build-model, build-forms, build-filters and cc got ran. But now, I got an exception saying the "Translation" relation doesn't exist. The Base Models include correctly the bindComponent line:
For now, I managed to kind of fixing it with simply swapping the databases order in my config/databases.yml and it's now working again perfectly. I forgot to mention that in the CategoryTable when i call $this->getConnection()->getName(), it outputs "second" |
| Comments |
| Comment by James Bell [ 23/Aug/11 ] |
|
Original issue: There are some additional comments in there that explain the issue as currently being seen in released versions of Doctrine 1.2. Can I help verify that this is the same ticket? What is needed to help with debug (in addition to the extra information already provided)? |
| Comment by Andy.L [ 28/Mar/12 ] |
|
meet the same issue and it really blocked my project, seems no one will maintain 1.x. I'm considering whether to replace symfony 1.4 with 2.0. |
[DC-1004] ATTR_TBLNAME_FORMAT not used when creating models from database Created: 08/May/11 Updated: 08/May/11 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.3 |
| Fix Version/s: | 1.2.3, 1.2.4 |
| Type: | Bug | Priority: | Major |
| Reporter: | Robin Parker | Assignee: | Jonathan H. Wage |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
if you set prefix to "xyz_%s" and have the model "BackgroundColor" it will become the table => "xyz_background_color" The fix (diff):
|
| Comments |
| Comment by Robin Parker [ 08/May/11 ] |
|
The diff output as .diff |
[DC-941] Spatial index type for mysql Created: 29/Nov/10 Updated: 29/Nov/10 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Major |
| Reporter: | Mishal | Assignee: | Jonathan H. Wage |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
I'm using doctrine and some of mysql's spatial functions. I need to specify spatial index for my tables. Geometry:
Exporting this definitions throws an exception: Unknown type spatial for index geometry_idx |
[DC-924] type mismatch for keyfield in column aggregation Created: 11/Nov/10 Updated: 11/Nov/10 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | Inheritance |
| Affects Version/s: | 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Arnaud Morvan | Assignee: | Roman S. Borschel |
| Resolution: | Unresolved | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PostgreSQL |
||
| Description |
|
This is the doc exemple on column aggregation inheritance : Entity: User: Group: But the keyField (type) is created as VARCHAR(255) so PostgreSQL return an error on applying inheritance condition : SQLSTATE[42883]: Undefined function: 7 ERROR: operator does not exist: character varying = integer I found this with symfony sfFilebasePlugin on sfFilebase:create-root task. |
[DC-895] [I18n] Defining languages with locality (eg. en_GB) breaks functionality with SQL Integrity error - fix included Created: 20/Oct/10 Updated: 20/Oct/10 Resolved: 20/Oct/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | I18n |
| Affects Version/s: | 1.2.0, 1.2.1, 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.0, 1.2.1, 1.2.2, 1.2.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Erik Van Kelst | Assignee: | Jonathan H. Wage |
| Resolution: | Invalid | Votes: | 0 |
| Labels: | None | ||
| Environment: |
all |
||
| Description |
|
When defining languages as language_COUNTRY codes (supported by symfony by default), the functionality to work with I18n records breaks, resulting in "SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry" errors. The reason is very simple: Doctrine's I18n language column is defined as a CHAR(2), thus shortening eg. "en_GB" value to "en", thus causing the above SQL error when a "en" translation for a record already exists. The solution is even simpler: change the column's length to 7 in the Doctrine_I18n class's options: I've tested this and all runs great: the correct SQL is being generated, the models behave correct, ... |
| Comments |
| Comment by Erik Van Kelst [ 20/Oct/10 ] |
|
Length of the i18n column is configurable... |
[DC-832] PostgreSQL - lastInsertId fails because sequence name on table with column alias on primary key does not work [+patch] Created: 18/Aug/10 Updated: 24/Aug/10 Resolved: 24/Aug/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Query |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Enrico Stahn | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
PostgreSQL - lastInsertId fails because sequence name on table with column alias on primary key does not work. DDL created sequence name: <table>_<column name> |
| Comments |
| Comment by Enrico Stahn [ 18/Aug/10 ] |
|
Patch: |
| Comment by Jonathan H. Wage [ 24/Aug/10 ] |
|
Thanks, fixed by http://trac.doctrine-project.org/changeset/7684 |
[DC-826] Doctrine_Collection::replace() EASY PATCH! Created: 13/Aug/10 Updated: 24/Aug/10 Resolved: 24/Aug/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Major |
| Reporter: | Severin Puschkarski | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
linux, symfony-framework. I dont know the Version but Revision is 7490 |
||
| Description |
|
Record::replace() exists already. but Doctrine_Collection::replace() does not exist. I just copied the Doctrine_Collection::save() function, renamed it to replace and replaced WORKS GREAT! By the way ... why is there no component for collection in the above select-field? |
| Comments |
| Comment by Jonathan H. Wage [ 24/Aug/10 ] |
|
Fixed in http://trac.doctrine-project.org/changeset/7686 Thanks, Jon |
[DC-813] Add ability to register a custom hydrator as a class instance instead of a class name Created: 09/Aug/10 Updated: 11/Aug/10 Resolved: 11/Aug/10 |
|
| Status: | Closed |
| Project: | Doctrine 1 |
| Component/s: | Query |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | New Feature | Priority: | Major |
| Reporter: | Lukas Kahwe | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
Currently its only possible to register a custom hydrator as a string name. However in order to inject additional attributes it would be beneficial to be able to register an instance of the custom hydrator class. http://www.doctrine-project.org/documentation/manual/1_2/en/data-hydrators#writing-hydration-method So instead of: One could do: |
| Comments |
| Comment by Lukas Kahwe [ 09/Aug/10 ] |
|
patch with tests is in the works |
| Comment by Lukas Kahwe [ 09/Aug/10 ] |
|
patch |
| Comment by Lukas Kahwe [ 09/Aug/10 ] |
|
tests |
| Comment by Guilherme Blanco [ 11/Aug/10 ] |
|
Fixed |
[DC-800] PostgreSQL does not have LOCATE expressions Created: 28/Jul/10 Updated: 24/Aug/10 Resolved: 24/Aug/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Native SQL, Query |
| Affects Version/s: | None |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Ilya Sabelnikov | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP: v5.2.13 (cli) (built: May 6 2010 01:51:58) Zend Engine v2.2.0, Xdebug v2.0.5 OS: FreeBSD x 8.0-RELEASE-p2 FreeBSD 8.0-RELEASE-p2 #0: Thu May 6 03:37:19 EEST 2010 x@y.z:/usr/obj/usr/src/sys/CUSTOM_8_0 amd64 Database: postgres (PostgreSQL) 8.4.3 Symfony: 1.4.7-DEV (/web/vendor/symfony/1.4-svn/lib) Web-server: nginx/0.7.65 |
||
| Description |
IntroductionAs it's described in Doctrine documentation: http://www.doctrine-project.org/projects/orm/1.2/docs/manual/dql-doctrine-query-language/en#functional-expressions. In case I have correctly understood this documentation, I can use registered expressions (CONCAT,TRIM,LOCATE etc.) with supported database drivers. IssueThe problem is with PostgreSQL, - it does not have the string function "LOCATE" since v7.4 (i have no info about previous version) Here is my example: $q = PortalTable::getInstance()
->createQuery()
->addSelect(
'(1 <= LOCATE(host, ?) as is_host_matched)',
array('.google.com')
);
$q->execute();
And this code dies with an error: In PgSQL you can use POSITION for this needs (I want to mention, MySQL has this function too as an alias for LOCATE) btw, POSITION is SQL-92 standard http://owen.sj.ca.us/~rk/howto/sql92.html - maybe it is better to rename LOCATE with POSITION? PatchIndex: Doctrine/Expression/Pgsql.php
===================================================================
--- Doctrine/Expression/Pgsql.php (revision 7678)
+++ Doctrine/Expression/Pgsql.php (working copy)
@@ -230,4 +230,31 @@
$translate = 'TRANSLATE(' . $string . ', ' . $from . ', ' . $to . ')';
return $translate;
}
-}
\ No newline at end of file
+
+ /**
+ * transform locate to position
+ *
+ * @param string $substr string to find
+ * @param string $str to find where
+ * @return string
+ */
+ public function locate($substr, $str)
+ {
+ return $this->position($substr, $str);
+ }
+
+ /**
+ * position
+ *
+ * @param string $substr string to find
+ * @param string $str to find where
+ * @return string
+ */
+ public function position($substr, $str)
+ {
+ $substr = $this->getIdentifier($substr);
+ $str = $this->getIdentifier($str);
+
+ return sprintf('POSITION(%s IN %s)', $substr, $str);
+ }
+}
Solution without patch: # will work with PgSQL and MySQL (tested)
$exp = new Doctrine_Expression('POSITION(host IN ?)');
$q = PortalTable::getInstance()
->createQuery()
->addSelect(
"(1 <= {$exp} as is_host_matched)",
array('.google.com')
);
$q->execute();
|
| Comments |
| Comment by Jonathan H. Wage [ 24/Aug/10 ] |
|
Fixed in http://trac.doctrine-project.org/changeset/7685 Thanks, Jon |
[DC-794] findBy issue with field names containing "Or" Created: 20/Jul/10 Updated: 29/Sep/10 Resolved: 24/Aug/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Eduardo Gulias Davis | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 1 |
| Labels: | None | ||
| Environment: |
Tested with Windows Vista / Ubuntu 10.4, PHP 5.2.10, MySQL 5. |
||
| Description |
|
The problem is in Doctrine_Table::buildFindByWhere, line 2715. I put the code here for clarity: public function buildFindByWhere($fieldName) else { throw new Doctrine_Table_Exception('Invalid field name to find by: ' . $v2); } } In my proyect I have a table called OrigenesOportunidadCliente, which id field name is idOrigenOportunidadCliente. As you have probably noticed, the name contains Or: idOrigenOportunidadCliente. And there is where it fails, it gets as if there where an OR statement, not finding a valid field name in the below foreach as the field is "OrigenOportunidad". |
| Comments |
| Comment by Enrico Stahn [ 22/Aug/10 ] |
|
fixed http://github.com/estahn/doctrine1/compare/master...DC-794 |
| Comment by Jonathan H. Wage [ 24/Aug/10 ] |
|
Fixed in http://trac.doctrine-project.org/changeset/7681 Thanks |
| Comment by Eduardo Gulias Davis [ 29/Sep/10 ] |
|
Sorry for the delay in posting this comment!! thank you very very much for the quick response on this issue. Great work! |
[DC-764] Major->please.....Value of Primary key from sequence in Postgres table NOT being set (although sequence gets incremented) Created: 24/Jun/10 Updated: 25/Jun/10 Resolved: 25/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection, Record |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.0, 1.2.1, 1.2.2, 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Dennis Gearon | Assignee: | Jonathan H. Wage |
| Resolution: | Invalid | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Ubuntu9.10 / PHP 5.2.6-3ubuntu4.5 with Suhosin-Patch 0.9.6.2 / Postgres-8.4 / Symfony 1.4.1 |
||
| Attachments: |
|
| Description |
|
In the ERD/schema that I have set up, a couple levels down in hierarchal order, a table has 3 composite foreign keys, and one sequence of its own. That sequence does not get get set into the 'Table->sequence variable'. That means when the file 'UnitOfWork' executes the function '_assignSequence()', it finds no sequence name, and skips the assignment of the sequence value. This of course blows up my inserts. I have included the following documentation: A/ An installation and further description README.tx file. Please let me know what I can do to help get this troubleshot quicly. Thx |
| Comments |
| Comment by Dennis Gearon [ 25/Jun/10 ] |
|
Don't know if it's related, but I ran: ./symfony doctrine:build-sql on the database in this bug report, and none of the tables got sequences assigned to them, nor default values set coming from a sequence. This is Postgres. |
| Comment by Dennis Gearon [ 25/Jun/10 ] |
|
So much for getting around this problem easily, I tried doing this: $e_table=Doctrine::getTable('E'); before inserting a record into the 'E' table. The option value 'sequenceName' is in the option array and returns correctly. However, when doing an insert immediatley after the above code, I get: 'sequence name was Array' (from my troubleshooting 'echo' statements in the modified UnitOfWork.php file) and the following errors: (you have to be using my modified UOW.php file to get the same line number there.) Warning: Illegal offset type in /home/bugreport/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Connection/UnitOfWork.php on line 917 Warning: Illegal offset type in /home/bugreport/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Record.php on line 2222 Warning: Illegal offset type in /home/bugreport/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Record.php on line 2223 Warning: Invalid argument supplied for foreach() in /home/bugreport/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Record.php on line 1151 So I am wondering, does the public function table->setOption(); even work, correclty that is? |
| Comment by Dennis Gearon [ 25/Jun/10 ] |
|
If I instead just retrieve the next val of the sequence manually, change 'id' column manually, then it works. But It then fails on the insert into the 'J' table. Apparently, Doctrine does not like composite primary foreign keys with a sequence also part of the foreign key. I wonder if my file would work on the Oracle version? |
| Comment by Dennis Gearon [ 25/Jun/10 ] |
|
Showing simpler version of ERD/Schema converting Primary Foreign Keys to Foreign keys. The then required unique index on the former Primary Foreign Keys has not yet been coded. Just create it on all the keys in tables E and J, that were listed as primary in the first version in the zip file |
| Comment by Dennis Gearon [ 25/Jun/10 ] |
|
See last comment, but the short answer is . . . at this date, 2010-06-25, even Doctrine 2.0-DBAL can't do what I'm trying to get verson 1.2.1 to do. So I got around it by converting primary foreign keys to foreign keys and then putting a unique index on the formerly primary keys. However, the child of the table treated that way, E(parent), J(child), now only has one foreign key, for E. To get all the ancestors, I will have to do subselects and joins. Oh well. |
[DC-745] Exported foreign keys names are not formatted with attribute FKNAME_FORMAT Created: 16/Jun/10 Updated: 24/Aug/10 Resolved: 24/Aug/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export, Query, Record, Relations |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Guilliam X | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Doctrine SVN 1.2 r7676 |
||
| Attachments: |
|
| Description |
|
Class Doctrine_Formatter formats index names using Doctrine_Core::ATTR_IDXNAME_FORMAT Attached simple patch (copied from function getIndexName) |
| Comments |
| Comment by Jonathan H. Wage [ 24/Aug/10 ] |
|
Fixed in http://trac.doctrine-project.org/changeset/7682 Thanks, Jon |
[DC-715] Doctrine_Tree_NestedSet::fetchRoots does not return roots with no branches. Created: 04/Jun/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Behaviors, Nested Set |
| Affects Version/s: | 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Jacek Krysztofik | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
any |
||
| Attachments: |
|
| Description |
|
Doctrine_Tree_NestedSet::fetchRoots does not return roots with no branches. |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
This patch breaks our test suite: Doctrine_PessimisticLocking_TestCase............................................passed
Doctrine_NestedSet_SingleRoot_TestCase..........................................failed
Unexpected Doctrine_Query_Exception thrown in [Doctrine_NestedSet_SingleRoot_TestCase] with message [Unknown column ] in /Users/jwage/Sites/doctrine12/lib/Doctrine/Query.php on line 729
Trace
-------------
#0 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Where.php(93): Doctrine_Query->parseClause('base.')
#1 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Where.php(81): Doctrine_Query_Where->_buildSql('base.', 'IS', 'NULL')
#2 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Condition.php(92): Doctrine_Query_Where->load('base. IS NULL')
#3 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Abstract.php(2077): Doctrine_Query_Condition->parse('base. IS NULL')
#4 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query.php(1160): Doctrine_Query_Abstract->_processDqlQueryPart('where', Array)
#5 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query.php(1126): Doctrine_Query->buildSqlQuery(true)
#6 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Abstract.php(945): Doctrine_Query->getSqlQuery(Array)
#7 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Abstract.php(1026): Doctrine_Query_Abstract->_execute(Array)
#8 /Users/jwage/Sites/doctrine12/lib/Doctrine/Tree/NestedSet.php(132): Doctrine_Query_Abstract->execute()
#9 /Users/jwage/Sites/doctrine12/tests/NestedSet/SingleRootTestCase.php(60): Doctrine_Tree_NestedSet->fetchRoot()
#10 /Users/jwage/Sites/doctrine12/tests/DoctrineTest/UnitTestCase.php(158): Doctrine_NestedSet_SingleRoot_TestCase->testLftRgtValues()
#11 /Users/jwage/Sites/doctrine12/tests/DoctrineTest/GroupTest.php(75): UnitTestCase->run()
#12 /Users/jwage/Sites/doctrine12/tests/DoctrineTest.php(183): GroupTest->run(Object(DoctrineTest_Reporter_Cli), '')
#13 /Users/jwage/Sites/doctrine12/tests/run.php(320): DoctrineTest->run()
#14 {main}
Fatal error: Call to a member function getNode() on a non-object in /Users/jwage/Sites/doctrine12/tests/NestedSet/MultiRootTestCase.php on line 151
Call Stack:
0.0047 521504 1. {main}() /Users/jwage/Sites/doctrine12/tests/run.php:0
0.7401 50855368 2. DoctrineTest->run() /Users/jwage/Sites/doctrine12/tests/run.php:320
0.7410 50872656 3. GroupTest->run() /Users/jwage/Sites/doctrine12/tests/DoctrineTest.php:183
55.2363 158425064 4. UnitTestCase->run() /Users/jwage/Sites/doctrine12/tests/DoctrineTest/GroupTest.php:75
55.3227 158747088 5. Doctrine_NestedSet_MultiRoot_TestCase->testSaveMultipleRootsWithChildren() /Users/jwage/Sites/doctrine12/tests/DoctrineTest/UnitTestCase.php:158
|
| Comment by Jacek Krysztofik [ 08/Jun/10 ] |
|
Works for me (doctrine1-git): Tested: 440 test cases. |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
I apply your patch cleanly to a fresh checkout from SVN and it fails for me. I tried on my laptop and the CI server. Hmm |
| Comment by Jacek Krysztofik [ 08/Jun/10 ] |
|
Your failure log shows #3 /Users/jwage/Sites/doctrine12/lib/Doctrine/Query/Abstract.php(2077): Doctrine_Query_Condition->parse('base. IS NULL') which means the $this->getAttribute('rootColumnName') returns nothing. I think the problem is not in the patch but I cannot reproduce it, so I can't help. |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
rootColumnName can be empty in some cases, no? when you don't have multiple roots. |
[DC-714] Fatal error(Call to a member function quoteIdentifier() on a non-object), on batchUpdateIndex(), file search.php on line 246 Created: 04/Jun/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Searchable |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Alex Kucherenko | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Windows XP, php 5.2.4, Apache 2.0 |
||
| Description |
|
I am creating a search in such a way that's: after excute i have fatal error: Fatal error: Call to a member function quoteIdentifier() on a non-object in E:\home\tender.loc\www\engine\class\Doctrine\Doctrine\Search.php on line 246 When i am opened Search.php on 246 line, i saw : $conn->quoteIdentifier($this->_table->getTableName()) var_dump($conn) return NULL I climbed up the code and saw the error: $conn = $this->_options['connection']; (line 228) now works fine Sory for my english |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks for the issue and patch! |
[DC-712] [pgsql] missing quoteIdentifier() on "alter table" queries Created: 02/Jun/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Francesco Montefoschi | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PostgreSQL |
||
| Description |
|
In Doctrine_Export_Pgsql the table name of query like "ALTER TABLE mytable ..." are not quoted even if ATTR_QUOTE_IDENTIFIER is enabled. Please note in Mysql exporter the table name of ALTER TABLE queries is correctly quoted. |
| Comments |
| Comment by Francesco Montefoschi [ 02/Jun/10 ] |
|
Proposed test and fix on github: http://github.com/fmntf/doctrine1/tree/DC-712 |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks again for the issue and patch! |
[DC-709] Column comments are not escaped Created: 28/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Schema Files |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Claudio Nicora | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
MySQL 5.1.41 |
||
| Description |
|
Following schema.yml causes MySQL error because of the first column comment that contains the reserved ' char. Client:
columns:
serial: { type: string(50), primary: true, comment: "Seriale dell'hardware" }
The following works (note the double ''), but Doctrine schema should be automatically escaped Client:
columns:
serial: { type: string(50), primary: true, comment: "Seriale dell''hardware" }
|
| Comments |
| Comment by John Kary [ 01/Jun/10 ] |
|
Confirmed and fixed with test case: http://github.com/johnkary/doctrine1/commit/640d2a11866e49b4315419ba665c4069c122e6b5 |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks for the issue, test and patch! |
| Comment by Claudio Nicora [ 08/Jun/10 ] |
|
You're welcome. Mine was the easiest part |
[DC-706] Subqueries do not work in select Created: 27/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Blocker |
| Reporter: | will ferrer | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
XP, Xamp |
||
| Attachments: |
|
| Description |
|
Hi All I have found another bug in Doctrine 1.2.2. Subqueries are not working in select statements at the moment. The reason for this is that this line (found in the parseSelect function of the class Doctrine_Query) : $componentAlias = $this->getExpressionOwner($expression);
Returns an unusable value when passed an $expression which is a subquery. To fix this problem I have patched my version of the code to use an existing "$componentAlias" instead of the value that would be returned by this function when a subquery is encountered. My code now reads: if ($pos !== false && substr($term[0], 0, 1) !== "'" && substr($term[0], 0, $pos) == '') { $_queryComponents = $this->_queryComponents; reset($components); $componentAlias = key($_queryComponents); } else { $componentAlias = $this->getExpressionOwner($expression); } I have not rigorously tested this patch yet but it has been working for me in what tests I have done so far. I have posted several very large bugs into jira over the past few months and haven't heard back regarding them. This leads me to believe that the Doctrine team has moved on from Doctrine 1.2.2 and is focusing only on Doctrine 2 issues at this point. I still love version 1 and haven't had the heart (or the time) to migrate my code over to 2 yet. If this is the case then the job of patching bugs like the ones I have reported in 1.2.2 is probably up to us users at this point. As such I will post my patched version of Doctrine_Query in a comment to this bug (My patched version also fixes another bug I reported: Best Regards Will Ferrer |
| Comments |
| Comment by will ferrer [ 27/May/10 ] |
|
Here is the whole of my Doctrine_Query posted for any one who wants to use the bug patches I put into the file (they are both marked with a comment that says: "Patched By Will Ferrer" <?php /* * $Id: Query.php 7490 2010-03-29 19:53:27Z jwage $ * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * and is licensed under the LGPL. For more information, see * <http://www.doctrine-project.org>. */ /** * Doctrine_Query * A Doctrine_Query object represents a DQL query. It is used to query databases for * data in an object-oriented fashion. A DQL query understands relations and inheritance * and is dbms independant. * * @package Doctrine * @subpackage Query * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org * @since 1.0 * @version $Revision: 7490 $ * @author Konsta Vesterinen <kvesteri@cc.hut.fi> * @todo Proposal: This class does far too much. It should have only 1 task: Collecting * the DQL query parts and the query parameters (the query state and caching options/methods * can remain here, too). * The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?) * whose task it is to convert DQL into SQL. * Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?), * (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need * to know the tokenizer/parsers. There could be extending * implementations of SqlBuilder? that cover the specific SQL dialects. * This would release Doctrine_Connection and the Doctrine_Connection_xxx classes * from this tedious task. * This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract) * and better hide all these transformation internals from the public Query API. * * @internal The lifecycle of a Query object is the following: * After construction the query object is empty. Through using the fluent * query interface the user fills the query object with DQL parts and query parameters. * These get collected in {@link $_dqlParts} and {@link $_params}, respectively. * When the query is executed the first time, or when {@link getSqlQuery()} * is called the first time, the collected DQL parts get parsed and the resulting * connection-driver specific SQL is generated. The generated SQL parts are * stored in {@link $_sqlParts} and the final resulting SQL query is stored in * {@link $_sql}. */ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable { /** * @var array The DQL keywords. */ protected static $_keywords = array('ALL', 'AND', 'ANY', 'AS', 'ASC', 'AVG', 'BETWEEN', 'BIT_LENGTH', 'BY', 'CHARACTER_LENGTH', 'CHAR_LENGTH', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DELETE', 'DESC', 'DISTINCT', 'EMPTY', 'EXISTS', 'FALSE', 'FETCH', 'FROM', 'GROUP', 'HAVING', 'IN', 'INDEXBY', 'INNER', 'IS', 'JOIN', 'LEFT', 'LIKE', 'LOWER', 'MEMBER', 'MOD', 'NEW', 'NOT', 'NULL', 'OBJECT', 'OF', 'OR', 'ORDER', 'OUTER', 'POSITION', 'SELECT', 'SOME', 'TRIM', 'TRUE', 'UNKNOWN', 'UPDATE', 'WHERE'); /** * @var array */ protected $_subqueryAliases = array(); /** * @var array $_aggregateAliasMap an array containing all aggregate aliases, keys as dql aliases * and values as sql aliases */ protected $_aggregateAliasMap = array(); /** * @var array */ protected $_pendingAggregates = array(); /** * @param boolean $needsSubquery */ protected $_needsSubquery = false; /** * @param boolean $isSubquery whether or not this query object is a subquery of another * query object */ protected $_isSubquery; /** * @var array $_neededTables an array containing the needed table aliases */ protected $_neededTables = array(); /** * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since * they cannot be parsed directly (some queries might be correlated) */ protected $_pendingSubqueries = array(); /** * @var array $_pendingFields an array of pending fields (fields waiting to be parsed) */ protected $_pendingFields = array(); /** * @var array $_parsers an array of parser objects, each DQL query part has its own parser */ protected $_parsers = array(); /** * @var array $_pendingJoinConditions an array containing pending joins */ protected $_pendingJoinConditions = array(); /** * @var array */ protected $_expressionMap = array(); /** * @var string $_sql cached SQL query */ protected $_sql; /** * create * returns a new Doctrine_Query object * * @param Doctrine_Connection $conn optional connection parameter * @param string $class Query class to instantiate * @return Doctrine_Query */ public static function create($conn = null, $class = null) { if ( ! $class) { $class = Doctrine_Manager::getInstance() ->getAttribute(Doctrine_Core::ATTR_QUERY_CLASS); } return new $class($conn); } /** * Clears all the sql parts. */ protected function clear() { $this->_preQueried = false; $this->_pendingJoinConditions = array(); $this->_state = self::STATE_DIRTY; } /** * Resets the query to the state just after it has been instantiated. */ public function reset() { $this->_subqueryAliases = array(); $this->_aggregateAliasMap = array(); $this->_pendingAggregates = array(); $this->_pendingSubqueries = array(); $this->_pendingFields = array(); $this->_neededTables = array(); $this->_expressionMap = array(); $this->_subqueryAliases = array(); $this->_needsSubquery = false; $this->_isLimitSubqueryUsed = false; } /** * createSubquery * creates a subquery * * @return Doctrine_Hydrate */ public function createSubquery() { $class = get_class($this); $obj = new $class(); // copy the aliases to the subquery $obj->copySubqueryInfo($this); // this prevents the 'id' being selected, re ticket #307 $obj->isSubquery(true); return $obj; } /** * addPendingJoinCondition * * @param string $componentAlias component alias * @param string $joinCondition dql join condition * @return Doctrine_Query this object */ public function addPendingJoinCondition($componentAlias, $joinCondition) { if ( ! isset($this->_pendingJoinConditions[$componentAlias])) { $this->_pendingJoinConditions[$componentAlias] = array(); } $this->_pendingJoinConditions[$componentAlias][] = $joinCondition; } /** * fetchArray * Convenience method to execute using array fetching as hydration mode. * * @param string $params * @return array */ public function fetchArray($params = array()) { return $this->execute($params, Doctrine_Core::HYDRATE_ARRAY); } /** * fetchOne * Convenience method to execute the query and return the first item * of the collection. * * @param string $params Query parameters * @param int $hydrationMode Hydration mode: see Doctrine_Core::HYDRATE_* constants * @return mixed Array or Doctrine_Collection, depending on hydration mode. False if no result. */ public function fetchOne($params = array(), $hydrationMode = null) { $collection = $this->execute($params, $hydrationMode); if (is_scalar($collection)) { return $collection; } if (count($collection) === 0) { return false; } if ($collection instanceof Doctrine_Collection) { return $collection->getFirst(); } else if (is_array($collection)) { return array_shift($collection); } return false; } /** * isSubquery * if $bool parameter is set this method sets the value of * Doctrine_Query::$isSubquery. If this value is set to true * the query object will not load the primary key fields of the selected * components. * * If null is given as the first parameter this method retrieves the current * value of Doctrine_Query::$isSubquery. * * @param boolean $bool whether or not this query acts as a subquery * @return Doctrine_Query|bool */ public function isSubquery($bool = null) { if ($bool === null) { return $this->_isSubquery; } $this->_isSubquery = (bool) $bool; return $this; } /** * getSqlAggregateAlias * * @param string $dqlAlias the dql alias of an aggregate value * @return string */ public function getSqlAggregateAlias($dqlAlias) { if (isset($this->_aggregateAliasMap[$dqlAlias])) { // mark the expression as used $this->_expressionMap[$dqlAlias][1] = true; return $this->_aggregateAliasMap[$dqlAlias]; } else if ( ! empty($this->_pendingAggregates)) { $this->processPendingAggregates(); return $this->getSqlAggregateAlias($dqlAlias); } else if( ! ($this->_conn->getAttribute(Doctrine_Core::ATTR_PORTABILITY) & Doctrine_Core::PORTABILITY_EXPR)){ return $dqlAlias; } else { throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias); } } /** * Check if a dql alias has a sql aggregate alias * * @param string $dqlAlias * @return boolean */ public function hasSqlAggregateAlias($dqlAlias) { try { $this->getSqlAggregateAlias($dqlAlias); return true; } catch (Exception $e) { return false; } } /** * Adjust the processed param index for "foo.bar IN ?" support * */ public function adjustProcessedParam($index) { // Retrieve all params $params = $this->getInternalParams(); // Retrieve already processed values $first = array_slice($params, 0, $index); $last = array_slice($params, $index, count($params) - $index); // Include array as values splicing the params array array_splice($last, 0, 1, $last[0]); // Put all param values into a single index $this->_execParams = array_merge($first, $last); } /** * Retrieves a specific DQL query part. * * @see Doctrine_Query_Abstract::$_dqlParts * <code> * var_dump($q->getDqlPart('where')); * // array(2) { [0] => string(8) 'name = ?' [1] => string(8) 'date > ?' } * </code> * @param string $queryPart the name of the query part; can be: * array from, containing strings; * array select, containg string; * boolean forUpdate; * array set; * array join; * array where; * array groupby; * array having; * array orderby, containing strings such as 'id ASC'; * array limit, containing numerics; * array offset, containing numerics; * @return array */ public function getDqlPart($queryPart) { if ( ! isset($this->_dqlParts[$queryPart])) { throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart); } return $this->_dqlParts[$queryPart]; } /** * contains * * Method to check if a arbitrary piece of dql exists * * @param string $dql Arbitrary piece of dql to check for * @return boolean */ public function contains($dql) { return stripos($this->getDql(), $dql) === false ? false : true; } /** * processPendingFields * the fields in SELECT clause cannot be parsed until the components * in FROM clause are parsed, hence this method is called everytime a * specific component is being parsed. For instance, the wildcard '*' * is expanded in the list of columns. * * @throws Doctrine_Query_Exception if unknown component alias has been given * @param string $componentAlias the alias of the component * @return string SQL code * @todo Description: What is a 'pending field' (and are there non-pending fields, too)? * What is 'processed'? (Meaning: What information is gathered & stored away) */ public function processPendingFields($componentAlias) { $tableAlias = $this->getSqlTableAlias($componentAlias); $table = $this->_queryComponents[$componentAlias]['table']; if ( ! isset($this->_pendingFields[$componentAlias])) { if ($this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_NONE) { if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) { throw new Doctrine_Query_Exception("The root class of the query (alias $componentAlias) " . " must have at least one field selected."); } } return; } // At this point we know the component is FETCHED (either it's the base class of // the query (FROM xyz) or its a "fetch join"). // Check that the parent join (if there is one), is a "fetch join", too. if ( ! $this->isSubquery() && isset($this->_queryComponents[$componentAlias]['parent'])) { $parentAlias = $this->_queryComponents[$componentAlias]['parent']; if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias]) && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_NONE && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_SCALAR && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_SINGLE_SCALAR) { throw new Doctrine_Query_Exception("The left side of the join between " . "the aliases '$parentAlias' and '$componentAlias' must have at least" . " the primary key field(s) selected."); } } $fields = $this->_pendingFields[$componentAlias]; // check for wildcards if (in_array('*', $fields)) { $fields = $table->getFieldNames(); } else { $driverClassName = $this->_hydrator->getHydratorDriverClassName(); // only auto-add the primary key fields if this query object is not // a subquery of another query object or we're using a child of the Object Graph // hydrator if ( ! $this->_isSubquery && is_subclass_of($driverClassName, 'Doctrine_Hydrator_Graph')) { $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); } } $sql = array(); foreach ($fields as $fieldName) { $columnName = $table->getColumnName($fieldName); if (($owner = $table->getColumnOwner($columnName)) !== null && $owner !== $table->getComponentName()) { $parent = $this->_conn->getTable($owner); $columnName = $parent->getColumnName($fieldName); $parentAlias = $this->getSqlTableAlias($componentAlias . '.' . $parent->getComponentName()); $sql[] = $this->_conn->quoteIdentifier($parentAlias) . '.' . $this->_conn->quoteIdentifier($columnName) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); } else { $columnName = $table->getColumnName($fieldName); $sql[] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($columnName) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); } } $this->_neededTables[] = $tableAlias; return implode(', ', $sql); } /** * Parses a nested field * <code> * $q->parseSelectField('u.Phonenumber.value'); * </code> * * @param string $field * @throws Doctrine_Query_Exception if unknown component alias has been given * @return string SQL fragment * @todo Description: Explain what this method does. Is there a relation to parseSelect()? * This method is not used from any class or testcase in the Doctrine package. * */ public function parseSelectField($field) { $terms = explode('.', $field); if (isset($terms[1])) { $componentAlias = $terms[0]; $field = $terms[1]; } else { reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $fields = $terms[0]; } $tableAlias = $this->getSqlTableAlias($componentAlias); $table = $this->_queryComponents[$componentAlias]['table']; // check for wildcards if ($field === '*') { $sql = array(); foreach ($table->getColumnNames() as $field) { $sql[] = $this->parseSelectField($componentAlias . '.' . $field); } return implode(', ', $sql); } else { $name = $table->getColumnName($field); $this->_neededTables[] = $tableAlias; return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); } } /** * getExpressionOwner * returns the component alias for owner of given expression * * @param string $expr expression from which to get to owner from * @return string the component alias * @todo Description: What does it mean if a component is an 'owner' of an expression? * What kind of 'expression' are we talking about here? */ public function getExpressionOwner($expr) { if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches); $match = current($matches); if (isset($match[0])) { $terms = explode('.', $match[0]); return $terms[0]; } } return $this->getRootAlias(); } /** * parseSelect * parses the query select part and * adds selected fields to pendingFields array * * @param string $dql * @todo Description: What information is extracted (and then stored)? */ public function parseSelect($dql) { $refs = $this->_tokenizer->sqlExplode($dql, ','); $pos = strpos(trim($refs[0]), ' '); $first = substr($refs[0], 0, $pos); // check for DISTINCT keyword if ($first === 'DISTINCT') { $this->_sqlParts['distinct'] = true; $refs[0] = substr($refs[0], ++$pos); } $parsedComponents = array(); foreach ($refs as $reference) { $reference = trim($reference); if (empty($reference)) { continue; } $terms = $this->_tokenizer->sqlExplode($reference, ' '); $pos = strpos($terms[0], '('); if (count($terms) > 1 || $pos !== false) { $expression = array_shift($terms); $alias = array_pop($terms); if ( ! $alias) { $alias = substr($expression, 0, $pos); } //Patched By Will Ferrer to detect when a subquery was encountered and use an existing componentAlias if the expression is a subquery. (not well tested) Fixes Bug -- DC-706 if ($pos !== false && substr($term[0], 0, 1) !== "'" && substr($term[0], 0, $pos) == '') { $_queryComponents = $this->_queryComponents; reset($components); $componentAlias = key($_queryComponents); } else { $componentAlias = $this->getExpressionOwner($expression); } //End Patch $expression = $this->parseClause($expression); $tableAlias = $this->getSqlTableAlias($componentAlias); $index = count($this->_aggregateAliasMap); $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_expressionMap[$alias][0] = $expression; $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; $this->_neededTables[] = $tableAlias; } else { $e = explode('.', $terms[0]); if (isset($e[1])) { $componentAlias = $e[0]; $field = $e[1]; } else { reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $field = $e[0]; } $this->_pendingFields[$componentAlias][] = $field; } } } /** * parseClause * parses given DQL clause * * this method handles five tasks: * * 1. Converts all DQL functions to their native SQL equivalents * 2. Converts all component references to their table alias equivalents * 3. Converts all field names to actual column names * 4. Quotes all identifiers * 5. Parses nested clauses and subqueries recursively * * @return string SQL string * @todo Description: What is a 'dql clause' (and what not)? * Refactor: Too long & nesting level */ public function parseClause($clause) { $clause = $this->_conn->dataDict->parseBoolean(trim($clause)); if (is_numeric($clause)) { return $clause; } $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<=', '&', '|')); $str = ''; foreach ($terms as $term) { $pos = strpos($term[0], '('); if ($pos !== false && substr($term[0], 0, 1) !== "'") { $name = substr($term[0], 0, $pos); $term[0] = $this->parseFunctionExpression($term[0]); } else { if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") { if (strpos($term[0], '.') !== false) { if ( ! is_numeric($term[0])) { $e = explode('.', $term[0]); $field = array_pop($e); if ($this->getType() === Doctrine_Query::SELECT) { $componentAlias = implode('.', $e); if (empty($componentAlias)) { $componentAlias = $this->getRootAlias(); } $this->load($componentAlias); // check the existence of the component alias if ( ! isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); } $table = $this->_queryComponents[$componentAlias]['table']; $def = $table->getDefinitionOf($field); // get the actual field name from alias $field = $table->getColumnName($field); // check column existence if ( ! $def) { throw new Doctrine_Query_Exception('Unknown column ' . $field); } if (isset($def['owner'])) { $componentAlias = $componentAlias . '.' . $def['owner']; } $tableAlias = $this->getSqlTableAlias($componentAlias); // build sql expression $term[0] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($field); } else { // build sql expression $field = $this->getRoot()->getColumnName($field); $term[0] = $this->_conn->quoteIdentifier($field); } } } else { if ( ! empty($term[0]) && ! in_array(strtoupper($term[0]), self::$_keywords) && ! is_numeric($term[0]) && $term[0] !== '?' && substr($term[0], 0, 1) !== ':') { $componentAlias = $this->getRootAlias(); $found = false; if ($componentAlias !== false && $componentAlias !== null) { $table = $this->_queryComponents[$componentAlias]['table']; // check column existence if ($table->hasField($term[0])) { $found = true; $def = $table->getDefinitionOf($term[0]); // get the actual column name from field name $term[0] = $table->getColumnName($term[0]); if (isset($def['owner'])) { $componentAlias = $componentAlias . '.' . $def['owner']; } $tableAlias = $this->getSqlTableAlias($componentAlias); if ($this->getType() === Doctrine_Query::SELECT) { // build sql expression $term[0] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($term[0]); } else { // build sql expression $term[0] = $this->_conn->quoteIdentifier($term[0]); } } else { $found = false; } } if ( ! $found) { $term[0] = $this->getSqlAggregateAlias($term[0]); } } } } } $str .= $term[0] . $term[1]; } return $str; } public function parseIdentifierReference($expr) { } public function parseFunctionExpression($expr) { $pos = strpos($expr, '('); $name = substr($expr, 0, $pos); if ($name === '') { return $this->parseSubquery($expr); } $argStr = substr($expr, ($pos + 1), -1); $args = array(); // parse args foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) { $args[] = $this->parseClause($arg); } // convert DQL function to its RDBMS specific equivalent try { $expr = call_user_func_array(array($this->_conn->expression, $name), $args); } catch (Doctrine_Expression_Exception $e) { throw new Doctrine_Query_Exception('Unknown function ' . $name . '.'); } return $expr; } public function parseSubquery($subquery) { $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); // check for possible subqueries if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { // parse subquery $q = $this->createSubquery()->parseDqlQuery($trimmed); $trimmed = $q->getSqlQuery(); $q->free(); } else if (substr($trimmed, 0, 4) == 'SQL:') { $trimmed = substr($trimmed, 4); } else { $e = $this->_tokenizer->sqlExplode($trimmed, ','); $value = array(); $index = false; foreach ($e as $part) { $value[] = $this->parseClause($part); } $trimmed = implode(', ', $value); } return '(' . $trimmed . ')'; } /** * processPendingSubqueries * processes pending subqueries * * subqueries can only be processed when the query is fully constructed * since some subqueries may be correlated * * @return void * @todo Better description. i.e. What is a 'pending subquery'? What does 'processed' mean? * (parsed? sql is constructed? some information is gathered?) */ public function processPendingSubqueries() { foreach ($this->_pendingSubqueries as $value) { list($dql, $alias) = $value; $subquery = $this->createSubquery(); $sql = $subquery->parseDqlQuery($dql, false)->getQuery(); $subquery->free(); reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $tableAlias = $this->getSqlTableAlias($componentAlias); $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap); $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias); $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_queryComponents[$componentAlias]['agg'][] = $alias; } $this->_pendingSubqueries = array(); } /** * processPendingAggregates * processes pending aggregate values for given component alias * * @return void * @todo Better description. i.e. What is a 'pending aggregate'? What does 'processed' mean? */ public function processPendingAggregates() { // iterate trhough all aggregates foreach ($this->_pendingAggregates as $aggregate) { list ($expression, $components, $alias) = $aggregate; $tableAliases = array(); // iterate through the component references within the aggregate function if ( ! empty ($components)) { foreach ($components as $component) { if (is_numeric($component)) { continue; } $e = explode('.', $component); $field = array_pop($e); $componentAlias = implode('.', $e); // check the existence of the component alias if ( ! isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); } $table = $this->_queryComponents[$componentAlias]['table']; $field = $table->getColumnName($field); // check column existence if ( ! $table->hasColumn($field)) { throw new Doctrine_Query_Exception('Unknown column ' . $field); } $sqlTableAlias = $this->getSqlTableAlias($componentAlias); $tableAliases[$sqlTableAlias] = true; // build sql expression $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field); $expression = str_replace($component, $identifier, $expression); } } if (count($tableAliases) !== 1) { $componentAlias = reset($this->_tableAliasMap); $tableAlias = key($this->_tableAliasMap); } $index = count($this->_aggregateAliasMap); $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_expressionMap[$alias][0] = $expression; $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; $this->_neededTables[] = $tableAlias; } // reset the state $this->_pendingAggregates = array(); } /** * _buildSqlQueryBase * returns the base of the generated sql query * On mysql driver special strategy has to be used for DELETE statements * (where is this special strategy??) * * @return string the base of the generated sql query */ protected function _buildSqlQueryBase() { switch ($this->_type) { case self::DELETE: $q = 'DELETE FROM '; break; case self::UPDATE: $q = 'UPDATE '; break; case self::SELECT: $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : ''; $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM '; break; } return $q; } /** * _buildSqlFromPart * builds the from part of the query and returns it * * @return string the query sql from part */ protected function _buildSqlFromPart($ignorePending = false) { $q = ''; foreach ($this->_sqlParts['from'] as $k => $part) { $e = explode(' ', $part); if ($k === 0) { if ( ! $ignorePending && $this->_type == self::SELECT) { // We may still have pending conditions $alias = count($e) > 1 ? $this->getComponentAlias($e[1]) : null; $where = $this->_processPendingJoinConditions($alias); // apply inheritance to WHERE part if ( ! empty($where)) { if (count($this->_sqlParts['where']) > 0) { $this->_sqlParts['where'][] = 'AND'; } if (substr($where, 0, 1) === '(' && substr($where, -1) === ')') { $this->_sqlParts['where'][] = $where; } else { $this->_sqlParts['where'][] = '(' . $where . ')'; } } } $q .= $part; continue; } // preserve LEFT JOINs only if needed // Check if it's JOIN, if not add a comma separator instead of space if ( ! preg_match('/\bJOIN\b/i', $part) && ! isset($this->_pendingJoinConditions[$k])) { $q .= ', ' . $part; } else { if (substr($part, 0, 9) === 'LEFT JOIN') { $aliases = array_merge($this->_subqueryAliases, array_keys($this->_neededTables)); if ( ! in_array($e[3], $aliases) && ! in_array($e[2], $aliases) && ! empty($this->_pendingFields)) { continue; } } if ( ! $ignorePending && isset($this->_pendingJoinConditions[$k])) { if (strpos($part, ' ON ') !== false) { $part .= ' AND '; } else { $part .= ' ON '; } $part .= $this->_processPendingJoinConditions($k); } $componentAlias = $this->getComponentAlias($e[3]); $string = $this->getInheritanceCondition($componentAlias); if ($string) { $part = $part . ' AND ' . $string; } $q .= ' ' . $part; } $this->_sqlParts['from'][$k] = $part; } return $q; } /** * Processes the pending join conditions, used for dynamically add conditions * to root component/joined components without interfering in the main dql * handling. * * @param string $alias Component Alias * @return Processed pending conditions */ protected function _processPendingJoinConditions($alias) { $parts = array(); if ($alias !== null && isset($this->_pendingJoinConditions[$alias])) { $parser = new Doctrine_Query_JoinCondition($this, $this->_tokenizer); foreach ($this->_pendingJoinConditions[$alias] as $joinCondition) { $parts[] = $parser->parse($joinCondition); } // FIX #1860 and #1876: Cannot unset them, otherwise query cannot be reused later //unset($this->_pendingJoinConditions[$alias]); } return (count($parts) > 0 ? '(' . implode(') AND (', $parts) . ')' : ''); } /** * builds the sql query from the given parameters and applies things such as * column aggregation inheritance and limit subqueries if needed * * @param array $params an array of prepared statement params (needed only in mysql driver * when limit subquery algorithm is used) * @param bool $limitSubquery Whether or not to try and apply the limit subquery algorithm * @return string the built sql query */ public function getSqlQuery($params = array(), $limitSubquery = true) { // Assign building/execution specific params $this->_params['exec'] = $params; // Initialize prepared parameters array $this->_execParams = $this->getFlattenedParams(); if ($this->_state !== self::STATE_DIRTY) { $this->fixArrayParameterValues($this->getInternalParams()); // Return compiled SQL return $this->_sql; } return $this->buildSqlQuery($limitSubquery); } /** * Build the SQL query from the DQL * * @param bool $limitSubquery Whether or not to try and apply the limit subquery algorithm * @return string $sql The generated SQL string */ public function buildSqlQuery($limitSubquery = true) { // reset the state if ( ! $this->isSubquery()) { $this->_queryComponents = array(); $this->_pendingAggregates = array(); $this->_aggregateAliasMap = array(); } $this->reset(); // invoke the preQuery hook $this->_preQuery(); // process the DQL parts => generate the SQL parts. // this will also populate the $_queryComponents. foreach ($this->_dqlParts as $queryPartName => $queryParts) { // If we are parsing FROM clause, we'll need to diff the queryComponents later if ($queryPartName == 'from') { // Pick queryComponents before processing $queryComponentsBefore = $this->getQueryComponents(); } // FIX #1667: _sqlParts are cleaned inside _processDqlQueryPart. if ($queryPartName != 'forUpdate') { $this->_processDqlQueryPart($queryPartName, $queryParts); } // We need to define the root alias if ($queryPartName == 'from') { // Pick queryComponents aftr processing $queryComponentsAfter = $this->getQueryComponents(); // Root alias is the key of difference of query components $diffQueryComponents = array_diff_key($queryComponentsAfter, $queryComponentsBefore); $this->_rootAlias = key($diffQueryComponents); } } $this->_state = self::STATE_CLEAN; // Proceed with the generated SQL if (empty($this->_sqlParts['from'])) { return false; } $needsSubQuery = false; $subquery = ''; $map = $this->getRootDeclaration(); $table = $map['table']; $rootAlias = $this->getRootAlias(); if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery && $table->getAttribute(Doctrine_Core::ATTR_QUERY_LIMIT) == Doctrine_Core::LIMIT_RECORDS) { // We do not need a limit-subquery if DISTINCT is used // and the selected fields are either from the root component or from a localKey relation (hasOne) // (i.e. DQL: SELECT DISTINCT u.id FROM User u LEFT JOIN u.phonenumbers LIMIT 5). if(!$this->_sqlParts['distinct']) { $this->_isLimitSubqueryUsed = true; $needsSubQuery = true; } else { foreach( array_keys($this->_pendingFields) as $alias){ //no subquery for root fields if($alias == $this->getRootAlias()){ continue; } //no subquery for ONE relations if(isset($this->_queryComponents[$alias]['relation']) && $this->_queryComponents[$alias]['relation']->getType() == Doctrine_Relation::ONE){ continue; } $this->_isLimitSubqueryUsed = true; $needsSubQuery = true; } } } $sql = array(); if ( ! empty($this->_pendingFields)) { foreach ($this->_queryComponents as $alias => $map) { $fieldSql = $this->processPendingFields($alias); if ( ! empty($fieldSql)) { $sql[] = $fieldSql; } } } if ( ! empty($sql)) { array_unshift($this->_sqlParts['select'], implode(', ', $sql)); } $this->_pendingFields = array(); // build the basic query $q = $this->_buildSqlQueryBase(); $q .= $this->_buildSqlFromPart(); if ( ! empty($this->_sqlParts['set'])) { $q .= ' SET ' . implode(', ', $this->_sqlParts['set']); } $string = $this->getInheritanceCondition($this->getRootAlias()); // apply inheritance to WHERE part if ( ! empty($string)) { if (count($this->_sqlParts['where']) > 0) { $this->_sqlParts['where'][] = 'AND'; } if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') { $this->_sqlParts['where'][] = $string; } else { $this->_sqlParts['where'][] = '(' . $string . ')'; } } $modifyLimit = true; $limitSubquerySql = ''; if ( ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) && $needsSubQuery && $limitSubquery) { $subquery = $this->getLimitSubquery(); // what about composite keys? $idColumnName = $table->getColumnName($table->getIdentifier()); switch (strtolower($this->_conn->getDriverName())) { case 'mysql': $this->useQueryCache(false); // mysql doesn't support LIMIT in subqueries $list = $this->_conn->execute($subquery, $this->_execParams)->fetchAll(Doctrine_Core::FETCH_COLUMN); $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list)); break; case 'pgsql': $subqueryAlias = $this->_conn->quoteIdentifier('doctrine_subquery_alias'); // pgsql needs special nested LIMIT subquery $subquery = 'SELECT ' . $subqueryAlias . '.' . $this->_conn->quoteIdentifier($idColumnName) . ' FROM (' . $subquery . ') AS ' . $subqueryAlias; break; } $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName; // FIX #1868: If not ID under MySQL is found to be restricted, restrict pk column for null // (which will lead to a return of 0 items) $limitSubquerySql = $this->_conn->quoteIdentifier($field) . (( ! empty($subquery)) ? ' IN (' . $subquery . ')' : ' IS NULL') . ((count($this->_sqlParts['where']) > 0) ? ' AND ' : ''); $modifyLimit = false; } // FIX #DC-26: Include limitSubquerySql as major relevance in conditions $emptyWhere = empty($this->_sqlParts['where']); if ( ! ($emptyWhere && $limitSubquerySql == '')) { $where = implode(' ', $this->_sqlParts['where']); $where = ($where == '' || (substr($where, 0, 1) === '(' && substr($where, -1) === ')')) ? $where : '(' . $where . ')'; $q .= ' WHERE ' . $limitSubquerySql . $where; // . (($limitSubquerySql == '' && count($this->_sqlParts['where']) == 1) ? substr($where, 1, -1) : $where); } // Fix the orderbys so we only have one orderby per value foreach ($this->_sqlParts['orderby'] as $k => $orderBy) { $e = explode(', ', $orderBy); unset($this->_sqlParts['orderby'][$k]); foreach ($e as $v) { $this->_sqlParts['orderby'][] = $v; } } // Add the default orderBy statements defined in the relationships and table classes // Only do this for SELECT queries if ($this->_type === self::SELECT) { foreach ($this->_queryComponents as $alias => $map) { $sqlAlias = $this->getSqlTableAlias($alias); if (isset($map['relation'])) { $orderBy = $map['relation']->getOrderByStatement($sqlAlias, true); if ($orderBy == $map['relation']['orderBy']) { if (isset($map['ref'])) { $orderBy = $map['relation']['refTable']->processOrderBy($sqlAlias, $map['relation']['orderBy'], true); } else { $orderBy = null; } } } else { $orderBy = $map['table']->getOrderByStatement($sqlAlias, true); } if ($orderBy) { $e = explode(',', $orderBy); $e = array_map('trim', $e); foreach ($e as $v) { if ( ! in_array($v, $this->_sqlParts['orderby'])) { $this->_sqlParts['orderby'][] = $v; } } } } } $q .= ( ! empty($this->_sqlParts['groupby'])) ? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; $q .= ( ! empty($this->_sqlParts['having'])) ? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']): ''; $q .= ( ! empty($this->_sqlParts['orderby'])) ? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; if ($modifyLimit) { $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']); } $q .= $this->_sqlParts['forUpdate'] === true ? ' FOR UPDATE ' : ''; $this->_sql = $q; $this->clear(); return $q; } /** * getLimitSubquery * this is method is used by the record limit algorithm * * when fetching one-to-many, many-to-many associated data with LIMIT clause * an additional subquery is needed for limiting the number of returned records instead * of limiting the number of sql result set rows * * @return string the limit subquery * @todo A little refactor to make the method easier to understand & maybe shorter? */ public function getLimitSubquery() { $map = reset($this->_queryComponents); $table = $map['table']; $componentAlias = key($this->_queryComponents); // get short alias $alias = $this->getSqlTableAlias($componentAlias); // what about composite keys? $primaryKey = $alias . '.' . $table->getColumnName($table->getIdentifier()); $driverName = $this->_conn->getAttribute(Doctrine_Core::ATTR_DRIVER_NAME); // initialize the base of the subquery if (($driverName == 'oracle' || $driverName == 'oci') && $this->_isOrderedByJoinedColumn()) { $subquery = 'SELECT '; } else { $subquery = 'SELECT DISTINCT '; } $subquery .= $this->_conn->quoteIdentifier($primaryKey); // pgsql & oracle need the order by fields to be preserved in select clause if ($driverName == 'pgsql' || $driverName == 'oracle' || $driverName == 'oci' || $driverName == 'mssql' || $driverName == 'odbc') { foreach ($this->_sqlParts['orderby'] as $part) { // Remove identifier quoting if it exists $e = $this->_tokenizer->bracketExplode($part, ' '); foreach ($e as $f) { if ($f == 0 || $f % 2 == 0) { $partOriginal = str_replace(',', '', trim($f)); $callback = create_function('$e', 'return trim($e, \'[]`"\');'); $part = trim(implode('.', array_map($callback, explode('.', $partOriginal)))); if (strpos($part, '.') === false) { continue; } // don't add functions if (strpos($part, '(') !== false) { continue; } // don't add primarykey column (its already in the select clause) if ($part !== $primaryKey) { $subquery .= ', ' . $partOriginal; } } } } } $orderby = $this->_sqlParts['orderby']; $having = $this->_sqlParts['having']; if ($driverName == 'mysql' || $driverName == 'pgsql') { foreach ($this->_expressionMap as $dqlAlias => $expr) { if (isset($expr[1])) { $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias]; } } } else { foreach ($this->_expressionMap as $dqlAlias => $expr) { if (isset($expr[1])) { foreach ($having as $k => $v) { $having[$k] = str_replace($this->_aggregateAliasMap[$dqlAlias], $expr[0], $v); } foreach ($orderby as $k => $v) { $e = explode(' ', $v); if ($e[0] == $this->_aggregateAliasMap[$dqlAlias]) { $orderby[$k] = $expr[0]; } } } } } // Add having fields that got stripped out of select preg_match_all('/`[a-z0-9_]+`\.`[a-z0-9_]+`/i', implode(' ', $having), $matches, PREG_PATTERN_ORDER); if (count($matches[0]) > 0) { $subquery .= ', ' . implode(', ', array_unique($matches[0])); } $subquery .= ' FROM'; foreach ($this->_sqlParts['from'] as $part) { // preserve LEFT JOINs only if needed if (substr($part, 0, 9) === 'LEFT JOIN') { $e = explode(' ', $part); //Patched by Will Ferrer to also check for groupBys. Fixes Bug -- DC-594 if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where']) && empty($this->_sqlParts['having']) && empty($this->_sqlParts['groupby'])) { //End Patch continue; } } $subquery .= ' ' . $part; } // all conditions must be preserved in subquery $subquery .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' ', $this->_sqlParts['where']) : ''; $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; $subquery .= ( ! empty($having))? ' HAVING ' . implode(' AND ', $having) : ''; $subquery .= ( ! empty($orderby))? ' ORDER BY ' . implode(', ', $orderby) : ''; if (($driverName == 'oracle' || $driverName == 'oci') && $this->_isOrderedByJoinedColumn()) { // When using "ORDER BY x.foo" where x.foo is a column of a joined table, // we may get duplicate primary keys because all columns in ORDER BY must appear // in the SELECT list when using DISTINCT. Hence we need to filter out the // primary keys with an additional DISTINCT subquery. // #1038 $quotedIdentifierColumnName = $this->_conn->quoteIdentifier($table->getColumnName($table->getIdentifier())); $subquery = 'SELECT doctrine_subquery_alias.' . $quotedIdentifierColumnName . ' FROM (' . $subquery . ') doctrine_subquery_alias' . ' GROUP BY doctrine_subquery_alias.' . $quotedIdentifierColumnName . ' ORDER BY MIN(ROWNUM)'; } // add driver specific limit clause $subquery = $this->_conn->modifyLimitSubquery($table, $subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']); $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'"); foreach ($parts as $k => $part) { if (strpos($part, ' ') !== false) { continue; } $part = str_replace(array('"', "'", '`'), "", $part); if ($this->hasSqlTableAlias($part)) { $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part)); continue; } if (strpos($part, '.') === false) { continue; } preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m); foreach ($m[0] as $match) { $e = explode('.', $match); // Rebuild the original part without the newly generate alias and with quoting reapplied $e2 = array(); foreach ($e as $k2 => $v2) { $e2[$k2] = $this->_conn->quoteIdentifier($v2); } $match = implode('.', $e2); // Generate new table alias $e[0] = $this->generateNewSqlTableAlias($e[0]); // Requote the part with the newly generated alias foreach ($e as $k2 => $v2) { $e[$k2] = $this->_conn->quoteIdentifier($v2); } $replace = implode('.' , $e); // Replace the original part with the new part with new sql table alias $parts[$k] = str_replace($match, $replace, $parts[$k]); } } if ($driverName == 'mysql' || $driverName == 'pgsql') { foreach ($parts as $k => $part) { if (strpos($part, "'") !== false) { continue; } if (strpos($part, '__') == false) { continue; } preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m); foreach ($m[0] as $match) { $e = explode('__', $match); $e[0] = $this->generateNewSqlTableAlias($e[0]); $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]); } } } $subquery = implode(' ', $parts); return $subquery; } /** * Checks whether the query has an ORDER BY on a column of a joined table. * This information is needed in special scenarios like the limit-offset when its * used with an Oracle database. * * @return boolean TRUE if the query is ordered by a joined column, FALSE otherwise. */ private function _isOrderedByJoinedColumn() { if ( ! $this->_queryComponents) { throw new Doctrine_Query_Exception("The query is in an invalid state for this " . "operation. It must have been fully parsed first."); } $componentAlias = key($this->_queryComponents); $mainTableAlias = $this->getSqlTableAlias($componentAlias); foreach ($this->_sqlParts['orderby'] as $part) { $part = trim($part); $e = $this->_tokenizer->bracketExplode($part, ' '); $part = trim($e[0]); if (strpos($part, '.') === false) { continue; } list($tableAlias, $columnName) = explode('.', $part); if ($tableAlias != $mainTableAlias) { return true; } } return false; } /** * DQL PARSER * parses a DQL query * first splits the query in parts and then uses individual * parsers for each part * * @param string $query DQL query * @param boolean $clear whether or not to clear the aliases * @throws Doctrine_Query_Exception if some generic parsing error occurs * @return Doctrine_Query */ public function parseDqlQuery($query, $clear = true) { if ($clear) { $this->clear(); } $query = trim($query); $query = str_replace("\r", "\n", str_replace("\r\n", "\n", $query)); $query = str_replace("\n", ' ', $query); $parts = $this->_tokenizer->tokenizeQuery($query); foreach ($parts as $partName => $subParts) { $subParts = trim($subParts); $partName = strtolower($partName); switch ($partName) { case 'create': $this->_type = self::CREATE; break; case 'insert': $this->_type = self::INSERT; break; case 'delete': $this->_type = self::DELETE; break; case 'select': $this->_type = self::SELECT; $this->_addDqlQueryPart($partName, $subParts); break; case 'update': $this->_type = self::UPDATE; $partName = 'from'; case 'from': $this->_addDqlQueryPart($partName, $subParts); break; case 'set': $this->_addDqlQueryPart($partName, $subParts, true); break; case 'group': case 'order': $partName .= 'by'; case 'where': case 'having': case 'limit': case 'offset': $this->_addDqlQueryPart($partName, $subParts); break; } } return $this; } /** * @todo Describe & refactor... too long and nested. * @param string $path component alias * @param boolean $loadFields */ public function load($path, $loadFields = true) { if (isset($this->_queryComponents[$path])) { return $this->_queryComponents[$path]; } $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY '); $mapWith = null; if (count($e) > 1) { $mapWith = trim($e[1]); $path = $e[0]; } // parse custom join conditions $e = explode(' ON ', str_ireplace(' on ', ' ON ', $path)); $joinCondition = ''; if (count($e) > 1) { $joinCondition = substr($path, strlen($e[0]) + 4, strlen($e[1])); $path = substr($path, 0, strlen($e[0])); $overrideJoin = true; } else { $e = explode(' WITH ', str_ireplace(' with ', ' WITH ', $path)); if (count($e) > 1) { $joinCondition = substr($path, strlen($e[0]) + 6, strlen($e[1])); $path = substr($path, 0, strlen($e[0])); } $overrideJoin = false; } $tmp = explode(' ', $path); $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null; $e = preg_split("/[.:]/", $tmp[0], -1); $fullPath = $tmp[0]; $prevPath = ''; $fullLength = strlen($fullPath); if (isset($this->_queryComponents[$e[0]])) { $table = $this->_queryComponents[$e[0]]['table']; $componentAlias = $e[0]; $prevPath = $parent = array_shift($e); } foreach ($e as $key => $name) { // get length of the previous path $length = strlen($prevPath); // build the current component path $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; $delimeter = substr($fullPath, $length, 1); // if an alias is not given use the current path as an alias identifier if (strlen($prevPath) === $fullLength && isset($originalAlias)) { $componentAlias = $originalAlias; } else { $componentAlias = $prevPath; } // if the current alias already exists, skip it if (isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception("Duplicate alias '$componentAlias' in query."); } if ( ! isset($table)) { // process the root of the path $table = $this->loadRoot($name, $componentAlias); } else { $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; $relation = $table->getRelation($name); $localTable = $table; $table = $relation->getTable(); $this->_queryComponents[$componentAlias] = array('table' => $table, 'parent' => $parent, 'relation' => $relation, 'map' => null); if ( ! $relation->isOneToOne()) { $this->_needsSubquery = true; } $localAlias = $this->getSqlTableAlias($parent, $localTable->getTableName()); $foreignAlias = $this->getSqlTableAlias($componentAlias, $relation->getTable()->getTableName()); $foreignSql = $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $this->_conn->quoteIdentifier($foreignAlias); $map = $relation->getTable()->inheritanceMap; if ( ! $loadFields || ! empty($map) || $joinCondition) { $this->_subqueryAliases[] = $foreignAlias; } if ($relation instanceof Doctrine_Relation_Association) { $asf = $relation->getAssociationTable(); $assocTableName = $asf->getTableName(); if ( ! $loadFields || ! empty($map) || $joinCondition) { $this->_subqueryAliases[] = $assocTableName; } $assocPath = $prevPath . '.' . $asf->getComponentName() . ' ' . $componentAlias; $this->_queryComponents[$assocPath] = array( 'parent' => $prevPath, 'relation' => $relation, 'table' => $asf, 'ref' => true); $assocAlias = $this->getSqlTableAlias($assocPath, $asf->getTableName()); $queryPart = $join . $this->_conn->quoteIdentifier($assocTableName) . ' ' . $this->_conn->quoteIdentifier($assocAlias); $queryPart .= ' ON (' . $this->_conn->quoteIdentifier($localAlias . '.' . $localTable->getColumnName($localTable->getIdentifier())) // what about composite keys? . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocalRefColumnName()); if ($relation->isEqual()) { // equal nest relation needs additional condition $queryPart .= ' OR ' . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getColumnName($table->getIdentifier())) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeignRefColumnName()); } $queryPart .= ')'; $this->_sqlParts['from'][] = $queryPart; $queryPart = $join . $foreignSql; if ( ! $overrideJoin) { $queryPart .= $this->buildAssociativeRelationSql($relation, $assocAlias, $foreignAlias, $localAlias); } } else { $queryPart = $this->buildSimpleRelationSql($relation, $foreignAlias, $localAlias, $overrideJoin, $join); } $queryPart .= $this->buildInheritanceJoinSql($table->getComponentName(), $componentAlias); $this->_sqlParts['from'][$componentAlias] = $queryPart; if ( ! empty($joinCondition)) { $this->addPendingJoinCondition($componentAlias, $joinCondition); } } if ($loadFields) { $restoreState = false; // load fields if necessary if ($loadFields && empty($this->_dqlParts['select'])) { $this->_pendingFields[$componentAlias] = array('*'); } } $parent = $prevPath; } $table = $this->_queryComponents[$componentAlias]['table']; return $this->buildIndexBy($componentAlias, $mapWith); } protected function buildSimpleRelationSql(Doctrine_Relation $relation, $foreignAlias, $localAlias, $overrideJoin, $join) { $queryPart = $join . $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $this->_conn->quoteIdentifier($foreignAlias); if ( ! $overrideJoin) { $queryPart .= ' ON ' . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocalColumnName()) . ' = ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeignColumnName()); } return $queryPart; } protected function buildIndexBy($componentAlias, $mapWith = null) { $table = $this->_queryComponents[$componentAlias]['table']; $indexBy = null; $column = false; if (isset($mapWith)) { $terms = explode('.', $mapWith); if (count($terms) == 1) { $indexBy = $terms[0]; } else if (count($terms) == 2) { $column = true; $indexBy = $terms[1]; } } else if ($table->getBoundQueryPart('indexBy') !== null) { $indexBy = $table->getBoundQueryPart('indexBy'); } if ($indexBy !== null) { if ( $column && ! $table->hasColumn($table->getColumnName($indexBy))) { throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist."); } $this->_queryComponents[$componentAlias]['map'] = $indexBy; } return $this->_queryComponents[$componentAlias]; } protected function buildAssociativeRelationSql(Doctrine_Relation $relation, $assocAlias, $foreignAlias, $localAlias) { $table = $relation->getTable(); $queryPart = ' ON '; if ($relation->isEqual()) { $queryPart .= '('; } $localIdentifier = $table->getColumnName($table->getIdentifier()); $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeignRefColumnName()); if ($relation->isEqual()) { $queryPart .= ' OR ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocalRefColumnName()) . ') AND ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' != ' . $this->_conn->quoteIdentifier($localAlias . '.' . $localIdentifier); } return $queryPart; } /** * loadRoot * * @param string $name * @param string $componentAlias * @return Doctrine_Table * @todo DESCRIBE ME! * @todo this method is called only in Doctrine_Query class. Shouldn't be private or protected? */ public function loadRoot($name, $componentAlias) { // get the connection for the component $manager = Doctrine_Manager::getInstance(); if ( ! $this->_passedConn && $manager->hasConnectionForComponent($name)) { $this->_conn = $manager->getConnectionForComponent($name); } $table = $this->_conn->getTable($name); $tableName = $table->getTableName(); // get the short alias for this table $tableAlias = $this->getSqlTableAlias($componentAlias, $tableName); // quote table name $queryPart = $this->_conn->quoteIdentifier($tableName); if ($this->_type === self::SELECT) { $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias); } $this->_tableAliasMap[$tableAlias] = $componentAlias; $queryPart .= $this->buildInheritanceJoinSql($name, $componentAlias); $this->_sqlParts['from'][] = $queryPart; $this->_queryComponents[$componentAlias] = array('table' => $table, 'map' => null); return $table; } /** * @todo DESCRIBE ME! * @param string $name component class name * @param string $componentAlias alias of the component in the dql * @return string query part */ public function buildInheritanceJoinSql($name, $componentAlias) { // get the connection for the component $manager = Doctrine_Manager::getInstance(); if ( ! $this->_passedConn && $manager->hasConnectionForComponent($name)) { $this->_conn = $manager->getConnectionForComponent($name); } $table = $this->_conn->getTable($name); $tableName = $table->getTableName(); // get the short alias for this table $tableAlias = $this->getSqlTableAlias($componentAlias, $tableName); $queryPart = ''; foreach ($table->getOption('joinedParents') as $parent) { $parentTable = $this->_conn->getTable($parent); $parentAlias = $componentAlias . '.' . $parent; // get the short alias for the parent table $parentTableAlias = $this->getSqlTableAlias($parentAlias, $parentTable->getTableName()); $queryPart .= ' LEFT JOIN ' . $this->_conn->quoteIdentifier($parentTable->getTableName()) . ' ' . $this->_conn->quoteIdentifier($parentTableAlias) . ' ON '; //Doctrine_Core::dump($table->getIdentifier()); foreach ((array) $table->getIdentifier() as $identifier) { $column = $table->getColumnName($identifier); $queryPart .= $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($column) . ' = ' . $this->_conn->quoteIdentifier($parentTableAlias) . '.' . $this->_conn->quoteIdentifier($column); } } return $queryPart; } /** * Get count sql query for this Doctrine_Query instance. * * This method is used in Doctrine_Query::count() for returning an integer * for the number of records which will be returned when executed. * * @return string $q */ public function getCountSqlQuery() { // triggers dql parsing/processing $this->getSqlQuery(array(), false); // this is ugly // initialize temporary variables $where = $this->_sqlParts['where']; $having = $this->_sqlParts['having']; $groupby = $this->_sqlParts['groupby']; $rootAlias = $this->getRootAlias(); $tableAlias = $this->getSqlTableAlias($rootAlias); // Build the query base $q = 'SELECT COUNT(*) AS ' . $this->_conn->quoteIdentifier('num_results') . ' FROM '; // Build the from clause $from = $this->_buildSqlFromPart(true); // Build the where clause $where = ( ! empty($where)) ? ' WHERE ' . implode(' ', $where) : ''; // Build the group by clause $groupby = ( ! empty($groupby)) ? ' GROUP BY ' . implode(', ', $groupby) : ''; // Build the having clause $having = ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having) : ''; // Building the from clause and finishing query if (count($this->_queryComponents) == 1 && empty($having)) { $q .= $from . $where . $groupby . $having; } else { // Subselect fields will contain only the pk of root entity $ta = $this->_conn->quoteIdentifier($tableAlias); $map = $this->getRootDeclaration(); $idColumnNames = $map['table']->getIdentifierColumnNames(); $pkFields = $ta . '.' . implode(', ' . $ta . '.', $this->_conn->quoteMultipleIdentifier($idColumnNames)); // We need to do some magic in select fields if the query contain anything in having clause $selectFields = $pkFields; if ( ! empty($having)) { // For each field defined in select clause foreach ($this->_sqlParts['select'] as $field) { // We only include aggregate expressions to count query // This is needed because HAVING clause will use field aliases if (strpos($field, '(') !== false) { $selectFields .= ', ' . $field; } } // Add having fields that got stripped out of select preg_match_all('/`[a-z0-9_]+`\.`[a-z0-9_]+`/i', $having, $matches, PREG_PATTERN_ORDER); if (count($matches[0]) > 0) { $selectFields .= ', ' . implode(', ', array_unique($matches[0])); } } // If we do not have a custom group by, apply the default one if (empty($groupby)) { $groupby = ' GROUP BY ' . $pkFields; } $q .= '(SELECT ' . $selectFields . ' FROM ' . $from . $where . $groupby . $having . ') ' . $this->_conn->quoteIdentifier('dctrn_count_query'); } return $q; } /** * Fetches the count of the query. * * This method executes the main query without all the * selected fields, ORDER BY part, LIMIT part and OFFSET part. * * Example: * Main query: * SELECT u.*, p.phonenumber FROM User u * LEFT JOIN u.Phonenumber p * WHERE p.phonenumber = '123 123' LIMIT 10 * * The modified DQL query: * SELECT COUNT(DISTINCT u.id) FROM User u * LEFT JOIN u.Phonenumber p * WHERE p.phonenumber = '123 123' * * @param array $params an array of prepared statement parameters * @return integer the count of this query */ public function count($params = array()) { $q = $this->getCountSqlQuery(); $params = $this->getCountQueryParams($params); $params = $this->_conn->convertBooleans($params); if ($this->_resultCache) { $conn = $this->getConnection(); $cacheDriver = $this->getResultCacheDriver(); $hash = $this->getResultCacheHash($params).'_count'; $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); if ($cached === false) { // cache miss $results = $this->getConnection()->fetchAll($q, $params); $cacheDriver->save($hash, serialize($results), $this->getResultCacheLifeSpan()); } else { $results = unserialize($cached); } } else { $results = $this->getConnection()->fetchAll($q, $params); } if (count($results) > 1) { $count = count($results); } else { if (isset($results[0])) { $results[0] = array_change_key_case($results[0], CASE_LOWER); $count = $results[0]['num_results']; } else { $count = 0; } } return (int) $count; } /** * Queries the database with DQL (Doctrine Query Language). * * This methods parses a Dql query and builds the query parts. * * @param string $query Dql query * @param array $params prepared statement parameters * @param int $hydrationMode Doctrine_Core::HYDRATE_ARRAY or Doctrine_Core::HYDRATE_RECORD * @see Doctrine_Core::FETCH_* constants * @return mixed */ public function query($query, $params = array(), $hydrationMode = null) { $this->parseDqlQuery($query); return $this->execute($params, $hydrationMode); } /** * Copies a Doctrine_Query object. * * @return Doctrine_Query Copy of the Doctrine_Query instance. */ public function copy(Doctrine_Query $query = null) { if ( ! $query) { $query = $this; } $new = clone $query; return $new; } /** * Magic method called after cloning process. * * @return void */ public function __clone() { $this->_parsers = array(); $this->_hydrator = clone $this->_hydrator; // Subqueries share some information from the parent so it can intermingle // with the dql of the main query. So when a subquery is cloned we need to // kill those references or it causes problems if ($this->isSubquery()) { $this->_killReference('_params'); $this->_killReference('_tableAliasMap'); $this->_killReference('_queryComponents'); } } /** * Kill the reference for the passed class property. * This method simply copies the value to a temporary variable and then unsets * the reference and re-assigns the old value but not by reference * * @param string $key */ protected function _killReference($key) { $tmp = $this->$key; unset($this->$key); $this->$key = $tmp; } /** * Frees the resources used by the query object. It especially breaks a * cyclic reference between the query object and it's parsers. This enables * PHP's current GC to reclaim the memory. * This method can therefore be used to reduce memory usage when creating * a lot of query objects during a request. * * @return Doctrine_Query this object */ public function free() { $this->reset(); $this->_parsers = array(); $this->_dqlParts = array(); } } |
| Comment by Jonathan H. Wage [ 27/May/10 ] |
|
Do you test your changes against our test suite? We will still be releasing bug fix releases, we're just not monitoring and fixing bugs on a day to day basis. As I don't have any help on Doctrine 1 I have to spend a week every 1-2 months just going through issues and closing as many as possible. |
| Comment by will ferrer [ 27/May/10 ] |
|
Hi Jonathan Thanks for the write back. I hadn't been using the test cases but I just tried and this latest patch was causing many of them to fail (I have resolved the issues however – see below). The current problem is that with out my patch the following dql: SELECT Charity.id, Charity.id as charity_id, (SQL:SELECT p.id AS p__0 FROM product_customers p LIMIT 1) as custom_subQuery FROM Charity Charity LIMIT 20 Produces the following exception: {"type":"exception","tid":3,"exception":{},"message":"Couldn't get short alias for p","where":"#0 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Query.php(641): Doctrine_Query_Abstract->getSqlTableAlias('p')\n#1 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Query\\Select.php(37): Doctrine_Query->parseSelect('(SQL:SELECT p.i...')\n#2 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Query\\Abstract.php(2078): Doctrine_Query_Select->parse('(SQL:SELECT p.i...')\n#3 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Query.php(1168): Doctrine_Query_Abstract->_processDqlQueryPart('select', Array)\n#4 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Query.php(1134): Doctrine_Query->buildSqlQuery(true)\n#5 C:\\htdocs\\Root\\modules\\default\\util\\AnalyticsGrid.php(166): Doctrine_Query->getSqlQuery()\n#6 C:\\htdocs\\Root\\modules\\default\\models\\FileInFolders.php(166): Util_AnalyticsGrid->readAnalyticsGrid('{\"select\":{\"chi...', 0, 20, Array, Array, Array, Array, Array)\n#7 [internal function]: Models_FileInFolders->readAnalyticsTableProxy(37, 0, 20, Array, Array, Array, Array, Array)\n#8 C:\\htdocs\\php_library\\Doctrine-1.2.2\\lib\\Doctrine\\Table.php(2808): call_user_func_array(Array, Array)\n#9 [internal function]: Doctrine_Table->__call('readAnalytics', Array)\n#10 C:\\htdocs\\Root\\modules\\event\\controllers\\AnalyticsController.php(96): Doctrine_Table->readAnalytics(37, 0, 20, Array, Array, Array, Array, Array)\n#11 [internal function]: Event_AnalyticsController->readAnalyticsAction(Object(stdClass))\n#12 C:\\htdocs\\Root\\modules\\default\\util\\helper\\ZendDirectRouter.php(194): call_user_func_array(Array, Array)\n#13 C:\\htdocs\\Root\\modules\\default\\util\\helper\\ZendDirectRouter.php(68): Util_Helper_ZendDirectRouter->rpc(Object(stdClass))\n#14 C:\\htdocs\\Root\\modules\\default\\base\\ActionController.php(80): Util_Helper_ZendDirectRouter->dispatch()\n#15 C:\\htdocs\\php_library\\zendframework\\library\\Zend\\Controller\\Action.php(513): Base_ActionController->directAction()\n#16 C:\\htdocs\\php_library\\zendframework\\library\\Zend\\Controller\\Dispatcher\\Standard.php(289): Zend_Controller_Action->dispatch('directAction')\n#17 C:\\htdocs\\php_library\\zendframework\\library\\Zend\\Controller\\Front.php(955): Zend_Controller_Dispatcher_Standard->dispatch(Object(Zend_Controller_Request_Http), Object(Zend_Controller_Response_Http))\n#18 C:\\htdocs\\Root\\public\\index.php(78): Zend_Controller_Front->dispatch()\n#19 {main}"}
I looked at my code again and saw some very glaring errors in it (I was rushing through bugs and as soon as I thought I had fixed this one I didn't take the time to read it over – in the future will be sure to use the tests in order to avoid any such oversights). At any rate I have changed my patch to the code and this is both working for my needs and passing all the tests again. Here is the newest version of my patched code: <?php /* * $Id: Query.php 7490 2010-03-29 19:53:27Z jwage $ * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * and is licensed under the LGPL. For more information, see * <http://www.doctrine-project.org>. */ /** * Doctrine_Query * A Doctrine_Query object represents a DQL query. It is used to query databases for * data in an object-oriented fashion. A DQL query understands relations and inheritance * and is dbms independant. * * @package Doctrine * @subpackage Query * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.org * @since 1.0 * @version $Revision: 7490 $ * @author Konsta Vesterinen <kvesteri@cc.hut.fi> * @todo Proposal: This class does far too much. It should have only 1 task: Collecting * the DQL query parts and the query parameters (the query state and caching options/methods * can remain here, too). * The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?) * whose task it is to convert DQL into SQL. * Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?), * (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need * to know the tokenizer/parsers. There could be extending * implementations of SqlBuilder? that cover the specific SQL dialects. * This would release Doctrine_Connection and the Doctrine_Connection_xxx classes * from this tedious task. * This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract) * and better hide all these transformation internals from the public Query API. * * @internal The lifecycle of a Query object is the following: * After construction the query object is empty. Through using the fluent * query interface the user fills the query object with DQL parts and query parameters. * These get collected in {@link $_dqlParts} and {@link $_params}, respectively. * When the query is executed the first time, or when {@link getSqlQuery()} * is called the first time, the collected DQL parts get parsed and the resulting * connection-driver specific SQL is generated. The generated SQL parts are * stored in {@link $_sqlParts} and the final resulting SQL query is stored in * {@link $_sql}. */ class Doctrine_Query extends Doctrine_Query_Abstract implements Countable { /** * @var array The DQL keywords. */ protected static $_keywords = array('ALL', 'AND', 'ANY', 'AS', 'ASC', 'AVG', 'BETWEEN', 'BIT_LENGTH', 'BY', 'CHARACTER_LENGTH', 'CHAR_LENGTH', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DELETE', 'DESC', 'DISTINCT', 'EMPTY', 'EXISTS', 'FALSE', 'FETCH', 'FROM', 'GROUP', 'HAVING', 'IN', 'INDEXBY', 'INNER', 'IS', 'JOIN', 'LEFT', 'LIKE', 'LOWER', 'MEMBER', 'MOD', 'NEW', 'NOT', 'NULL', 'OBJECT', 'OF', 'OR', 'ORDER', 'OUTER', 'POSITION', 'SELECT', 'SOME', 'TRIM', 'TRUE', 'UNKNOWN', 'UPDATE', 'WHERE'); /** * @var array */ protected $_subqueryAliases = array(); /** * @var array $_aggregateAliasMap an array containing all aggregate aliases, keys as dql aliases * and values as sql aliases */ protected $_aggregateAliasMap = array(); /** * @var array */ protected $_pendingAggregates = array(); /** * @param boolean $needsSubquery */ protected $_needsSubquery = false; /** * @param boolean $isSubquery whether or not this query object is a subquery of another * query object */ protected $_isSubquery; /** * @var array $_neededTables an array containing the needed table aliases */ protected $_neededTables = array(); /** * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since * they cannot be parsed directly (some queries might be correlated) */ protected $_pendingSubqueries = array(); /** * @var array $_pendingFields an array of pending fields (fields waiting to be parsed) */ protected $_pendingFields = array(); /** * @var array $_parsers an array of parser objects, each DQL query part has its own parser */ protected $_parsers = array(); /** * @var array $_pendingJoinConditions an array containing pending joins */ protected $_pendingJoinConditions = array(); /** * @var array */ protected $_expressionMap = array(); /** * @var string $_sql cached SQL query */ protected $_sql; /** * create * returns a new Doctrine_Query object * * @param Doctrine_Connection $conn optional connection parameter * @param string $class Query class to instantiate * @return Doctrine_Query */ public static function create($conn = null, $class = null) { if ( ! $class) { $class = Doctrine_Manager::getInstance() ->getAttribute(Doctrine_Core::ATTR_QUERY_CLASS); } return new $class($conn); } /** * Clears all the sql parts. */ protected function clear() { $this->_preQueried = false; $this->_pendingJoinConditions = array(); $this->_state = self::STATE_DIRTY; } /** * Resets the query to the state just after it has been instantiated. */ public function reset() { $this->_subqueryAliases = array(); $this->_aggregateAliasMap = array(); $this->_pendingAggregates = array(); $this->_pendingSubqueries = array(); $this->_pendingFields = array(); $this->_neededTables = array(); $this->_expressionMap = array(); $this->_subqueryAliases = array(); $this->_needsSubquery = false; $this->_isLimitSubqueryUsed = false; } /** * createSubquery * creates a subquery * * @return Doctrine_Hydrate */ public function createSubquery() { $class = get_class($this); $obj = new $class(); // copy the aliases to the subquery $obj->copySubqueryInfo($this); // this prevents the 'id' being selected, re ticket #307 $obj->isSubquery(true); return $obj; } /** * addPendingJoinCondition * * @param string $componentAlias component alias * @param string $joinCondition dql join condition * @return Doctrine_Query this object */ public function addPendingJoinCondition($componentAlias, $joinCondition) { if ( ! isset($this->_pendingJoinConditions[$componentAlias])) { $this->_pendingJoinConditions[$componentAlias] = array(); } $this->_pendingJoinConditions[$componentAlias][] = $joinCondition; } /** * fetchArray * Convenience method to execute using array fetching as hydration mode. * * @param string $params * @return array */ public function fetchArray($params = array()) { return $this->execute($params, Doctrine_Core::HYDRATE_ARRAY); } /** * fetchOne * Convenience method to execute the query and return the first item * of the collection. * * @param string $params Query parameters * @param int $hydrationMode Hydration mode: see Doctrine_Core::HYDRATE_* constants * @return mixed Array or Doctrine_Collection, depending on hydration mode. False if no result. */ public function fetchOne($params = array(), $hydrationMode = null) { $collection = $this->execute($params, $hydrationMode); if (is_scalar($collection)) { return $collection; } if (count($collection) === 0) { return false; } if ($collection instanceof Doctrine_Collection) { return $collection->getFirst(); } else if (is_array($collection)) { return array_shift($collection); } return false; } /** * isSubquery * if $bool parameter is set this method sets the value of * Doctrine_Query::$isSubquery. If this value is set to true * the query object will not load the primary key fields of the selected * components. * * If null is given as the first parameter this method retrieves the current * value of Doctrine_Query::$isSubquery. * * @param boolean $bool whether or not this query acts as a subquery * @return Doctrine_Query|bool */ public function isSubquery($bool = null) { if ($bool === null) { return $this->_isSubquery; } $this->_isSubquery = (bool) $bool; return $this; } /** * getSqlAggregateAlias * * @param string $dqlAlias the dql alias of an aggregate value * @return string */ public function getSqlAggregateAlias($dqlAlias) { if (isset($this->_aggregateAliasMap[$dqlAlias])) { // mark the expression as used $this->_expressionMap[$dqlAlias][1] = true; return $this->_aggregateAliasMap[$dqlAlias]; } else if ( ! empty($this->_pendingAggregates)) { $this->processPendingAggregates(); return $this->getSqlAggregateAlias($dqlAlias); } else if( ! ($this->_conn->getAttribute(Doctrine_Core::ATTR_PORTABILITY) & Doctrine_Core::PORTABILITY_EXPR)){ return $dqlAlias; } else { throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias); } } /** * Check if a dql alias has a sql aggregate alias * * @param string $dqlAlias * @return boolean */ public function hasSqlAggregateAlias($dqlAlias) { try { $this->getSqlAggregateAlias($dqlAlias); return true; } catch (Exception $e) { return false; } } /** * Adjust the processed param index for "foo.bar IN ?" support * */ public function adjustProcessedParam($index) { // Retrieve all params $params = $this->getInternalParams(); // Retrieve already processed values $first = array_slice($params, 0, $index); $last = array_slice($params, $index, count($params) - $index); // Include array as values splicing the params array array_splice($last, 0, 1, $last[0]); // Put all param values into a single index $this->_execParams = array_merge($first, $last); } /** * Retrieves a specific DQL query part. * * @see Doctrine_Query_Abstract::$_dqlParts * <code> * var_dump($q->getDqlPart('where')); * // array(2) { [0] => string(8) 'name = ?' [1] => string(8) 'date > ?' } * </code> * @param string $queryPart the name of the query part; can be: * array from, containing strings; * array select, containg string; * boolean forUpdate; * array set; * array join; * array where; * array groupby; * array having; * array orderby, containing strings such as 'id ASC'; * array limit, containing numerics; * array offset, containing numerics; * @return array */ public function getDqlPart($queryPart) { if ( ! isset($this->_dqlParts[$queryPart])) { throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart); } return $this->_dqlParts[$queryPart]; } /** * contains * * Method to check if a arbitrary piece of dql exists * * @param string $dql Arbitrary piece of dql to check for * @return boolean */ public function contains($dql) { return stripos($this->getDql(), $dql) === false ? false : true; } /** * processPendingFields * the fields in SELECT clause cannot be parsed until the components * in FROM clause are parsed, hence this method is called everytime a * specific component is being parsed. For instance, the wildcard '*' * is expanded in the list of columns. * * @throws Doctrine_Query_Exception if unknown component alias has been given * @param string $componentAlias the alias of the component * @return string SQL code * @todo Description: What is a 'pending field' (and are there non-pending fields, too)? * What is 'processed'? (Meaning: What information is gathered & stored away) */ public function processPendingFields($componentAlias) { $tableAlias = $this->getSqlTableAlias($componentAlias); $table = $this->_queryComponents[$componentAlias]['table']; if ( ! isset($this->_pendingFields[$componentAlias])) { if ($this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_NONE) { if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) { throw new Doctrine_Query_Exception("The root class of the query (alias $componentAlias) " . " must have at least one field selected."); } } return; } // At this point we know the component is FETCHED (either it's the base class of // the query (FROM xyz) or its a "fetch join"). // Check that the parent join (if there is one), is a "fetch join", too. if ( ! $this->isSubquery() && isset($this->_queryComponents[$componentAlias]['parent'])) { $parentAlias = $this->_queryComponents[$componentAlias]['parent']; if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias]) && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_NONE && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_SCALAR && $this->_hydrator->getHydrationMode() != Doctrine_Core::HYDRATE_SINGLE_SCALAR) { throw new Doctrine_Query_Exception("The left side of the join between " . "the aliases '$parentAlias' and '$componentAlias' must have at least" . " the primary key field(s) selected."); } } $fields = $this->_pendingFields[$componentAlias]; // check for wildcards if (in_array('*', $fields)) { $fields = $table->getFieldNames(); } else { $driverClassName = $this->_hydrator->getHydratorDriverClassName(); // only auto-add the primary key fields if this query object is not // a subquery of another query object or we're using a child of the Object Graph // hydrator if ( ! $this->_isSubquery && is_subclass_of($driverClassName, 'Doctrine_Hydrator_Graph')) { $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); } } $sql = array(); foreach ($fields as $fieldName) { $columnName = $table->getColumnName($fieldName); if (($owner = $table->getColumnOwner($columnName)) !== null && $owner !== $table->getComponentName()) { $parent = $this->_conn->getTable($owner); $columnName = $parent->getColumnName($fieldName); $parentAlias = $this->getSqlTableAlias($componentAlias . '.' . $parent->getComponentName()); $sql[] = $this->_conn->quoteIdentifier($parentAlias) . '.' . $this->_conn->quoteIdentifier($columnName) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); } else { $columnName = $table->getColumnName($fieldName); $sql[] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($columnName) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); } } $this->_neededTables[] = $tableAlias; return implode(', ', $sql); } /** * Parses a nested field * <code> * $q->parseSelectField('u.Phonenumber.value'); * </code> * * @param string $field * @throws Doctrine_Query_Exception if unknown component alias has been given * @return string SQL fragment * @todo Description: Explain what this method does. Is there a relation to parseSelect()? * This method is not used from any class or testcase in the Doctrine package. * */ public function parseSelectField($field) { $terms = explode('.', $field); if (isset($terms[1])) { $componentAlias = $terms[0]; $field = $terms[1]; } else { reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $fields = $terms[0]; } $tableAlias = $this->getSqlTableAlias($componentAlias); $table = $this->_queryComponents[$componentAlias]['table']; // check for wildcards if ($field === '*') { $sql = array(); foreach ($table->getColumnNames() as $field) { $sql[] = $this->parseSelectField($componentAlias . '.' . $field); } return implode(', ', $sql); } else { $name = $table->getColumnName($field); $this->_neededTables[] = $tableAlias; return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) . ' AS ' . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); } } /** * getExpressionOwner * returns the component alias for owner of given expression * * @param string $expr expression from which to get to owner from * @return string the component alias * @todo Description: What does it mean if a component is an 'owner' of an expression? * What kind of 'expression' are we talking about here? */ public function getExpressionOwner($expr) { if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches); $match = current($matches); if (isset($match[0])) { $terms = explode('.', $match[0]); return $terms[0]; } } return $this->getRootAlias(); } /** * parseSelect * parses the query select part and * adds selected fields to pendingFields array * * @param string $dql * @todo Description: What information is extracted (and then stored)? */ public function parseSelect($dql) { $refs = $this->_tokenizer->sqlExplode($dql, ','); $pos = strpos(trim($refs[0]), ' '); $first = substr($refs[0], 0, $pos); // check for DISTINCT keyword if ($first === 'DISTINCT') { $this->_sqlParts['distinct'] = true; $refs[0] = substr($refs[0], ++$pos); } $parsedComponents = array(); foreach ($refs as $reference) { $reference = trim($reference); if (empty($reference)) { continue; } $terms = $this->_tokenizer->sqlExplode($reference, ' '); $pos = strpos($terms[0], '('); if (count($terms) > 1 || $pos !== false) { $expression = array_shift($terms); $alias = array_pop($terms); if ( ! $alias) { $alias = substr($expression, 0, $pos); } //Patched By Will Ferrer to detect when a subquery was encountered and use an existing componentAlias if the expression is a subquery. (not well tested) Fixes Bug -- DC-706 if ($pos !== false && substr($expression, 0, 1) !== "'" && substr($expression, 0, $pos) == '') { $_queryComponents = $this->_queryComponents; reset($_queryComponents); $componentAlias = key($_queryComponents); } else { $componentAlias = $this->getExpressionOwner($expression); } //End Patch $expression = $this->parseClause($expression); $tableAlias = $this->getSqlTableAlias($componentAlias); $index = count($this->_aggregateAliasMap); $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_expressionMap[$alias][0] = $expression; $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; $this->_neededTables[] = $tableAlias; } else { $e = explode('.', $terms[0]); if (isset($e[1])) { $componentAlias = $e[0]; $field = $e[1]; } else { reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $field = $e[0]; } $this->_pendingFields[$componentAlias][] = $field; } } } /** * parseClause * parses given DQL clause * * this method handles five tasks: * * 1. Converts all DQL functions to their native SQL equivalents * 2. Converts all component references to their table alias equivalents * 3. Converts all field names to actual column names * 4. Quotes all identifiers * 5. Parses nested clauses and subqueries recursively * * @return string SQL string * @todo Description: What is a 'dql clause' (and what not)? * Refactor: Too long & nesting level */ public function parseClause($clause) { $clause = $this->_conn->dataDict->parseBoolean(trim($clause)); if (is_numeric($clause)) { return $clause; } $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<=', '&', '|')); $str = ''; foreach ($terms as $term) { $pos = strpos($term[0], '('); if ($pos !== false && substr($term[0], 0, 1) !== "'") { $name = substr($term[0], 0, $pos); $term[0] = $this->parseFunctionExpression($term[0]); } else { if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") { if (strpos($term[0], '.') !== false) { if ( ! is_numeric($term[0])) { $e = explode('.', $term[0]); $field = array_pop($e); if ($this->getType() === Doctrine_Query::SELECT) { $componentAlias = implode('.', $e); if (empty($componentAlias)) { $componentAlias = $this->getRootAlias(); } $this->load($componentAlias); // check the existence of the component alias if ( ! isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); } $table = $this->_queryComponents[$componentAlias]['table']; $def = $table->getDefinitionOf($field); // get the actual field name from alias $field = $table->getColumnName($field); // check column existence if ( ! $def) { throw new Doctrine_Query_Exception('Unknown column ' . $field); } if (isset($def['owner'])) { $componentAlias = $componentAlias . '.' . $def['owner']; } $tableAlias = $this->getSqlTableAlias($componentAlias); // build sql expression $term[0] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($field); } else { // build sql expression $field = $this->getRoot()->getColumnName($field); $term[0] = $this->_conn->quoteIdentifier($field); } } } else { if ( ! empty($term[0]) && ! in_array(strtoupper($term[0]), self::$_keywords) && ! is_numeric($term[0]) && $term[0] !== '?' && substr($term[0], 0, 1) !== ':') { $componentAlias = $this->getRootAlias(); $found = false; if ($componentAlias !== false && $componentAlias !== null) { $table = $this->_queryComponents[$componentAlias]['table']; // check column existence if ($table->hasField($term[0])) { $found = true; $def = $table->getDefinitionOf($term[0]); // get the actual column name from field name $term[0] = $table->getColumnName($term[0]); if (isset($def['owner'])) { $componentAlias = $componentAlias . '.' . $def['owner']; } $tableAlias = $this->getSqlTableAlias($componentAlias); if ($this->getType() === Doctrine_Query::SELECT) { // build sql expression $term[0] = $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($term[0]); } else { // build sql expression $term[0] = $this->_conn->quoteIdentifier($term[0]); } } else { $found = false; } } if ( ! $found) { $term[0] = $this->getSqlAggregateAlias($term[0]); } } } } } $str .= $term[0] . $term[1]; } return $str; } public function parseIdentifierReference($expr) { } public function parseFunctionExpression($expr) { $pos = strpos($expr, '('); $name = substr($expr, 0, $pos); if ($name === '') { return $this->parseSubquery($expr); } $argStr = substr($expr, ($pos + 1), -1); $args = array(); // parse args foreach ($this->_tokenizer->sqlExplode($argStr, ',') as $arg) { $args[] = $this->parseClause($arg); } // convert DQL function to its RDBMS specific equivalent try { $expr = call_user_func_array(array($this->_conn->expression, $name), $args); } catch (Doctrine_Expression_Exception $e) { throw new Doctrine_Query_Exception('Unknown function ' . $name . '.'); } return $expr; } public function parseSubquery($subquery) { $trimmed = trim($this->_tokenizer->bracketTrim($subquery)); // check for possible subqueries if (substr($trimmed, 0, 4) == 'FROM' || substr($trimmed, 0, 6) == 'SELECT') { // parse subquery $q = $this->createSubquery()->parseDqlQuery($trimmed); $trimmed = $q->getSqlQuery(); $q->free(); } else if (substr($trimmed, 0, 4) == 'SQL:') { $trimmed = substr($trimmed, 4); } else { $e = $this->_tokenizer->sqlExplode($trimmed, ','); $value = array(); $index = false; foreach ($e as $part) { $value[] = $this->parseClause($part); } $trimmed = implode(', ', $value); } return '(' . $trimmed . ')'; } /** * processPendingSubqueries * processes pending subqueries * * subqueries can only be processed when the query is fully constructed * since some subqueries may be correlated * * @return void * @todo Better description. i.e. What is a 'pending subquery'? What does 'processed' mean? * (parsed? sql is constructed? some information is gathered?) */ public function processPendingSubqueries() { foreach ($this->_pendingSubqueries as $value) { list($dql, $alias) = $value; $subquery = $this->createSubquery(); $sql = $subquery->parseDqlQuery($dql, false)->getQuery(); $subquery->free(); reset($this->_queryComponents); $componentAlias = key($this->_queryComponents); $tableAlias = $this->getSqlTableAlias($componentAlias); $sqlAlias = $tableAlias . '__' . count($this->_aggregateAliasMap); $this->_sqlParts['select'][] = '(' . $sql . ') AS ' . $this->_conn->quoteIdentifier($sqlAlias); $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_queryComponents[$componentAlias]['agg'][] = $alias; } $this->_pendingSubqueries = array(); } /** * processPendingAggregates * processes pending aggregate values for given component alias * * @return void * @todo Better description. i.e. What is a 'pending aggregate'? What does 'processed' mean? */ public function processPendingAggregates() { // iterate trhough all aggregates foreach ($this->_pendingAggregates as $aggregate) { list ($expression, $components, $alias) = $aggregate; $tableAliases = array(); // iterate through the component references within the aggregate function if ( ! empty ($components)) { foreach ($components as $component) { if (is_numeric($component)) { continue; } $e = explode('.', $component); $field = array_pop($e); $componentAlias = implode('.', $e); // check the existence of the component alias if ( ! isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception('Unknown component alias ' . $componentAlias); } $table = $this->_queryComponents[$componentAlias]['table']; $field = $table->getColumnName($field); // check column existence if ( ! $table->hasColumn($field)) { throw new Doctrine_Query_Exception('Unknown column ' . $field); } $sqlTableAlias = $this->getSqlTableAlias($componentAlias); $tableAliases[$sqlTableAlias] = true; // build sql expression $identifier = $this->_conn->quoteIdentifier($sqlTableAlias . '.' . $field); $expression = str_replace($component, $identifier, $expression); } } if (count($tableAliases) !== 1) { $componentAlias = reset($this->_tableAliasMap); $tableAlias = key($this->_tableAliasMap); } $index = count($this->_aggregateAliasMap); $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; $this->_aggregateAliasMap[$alias] = $sqlAlias; $this->_expressionMap[$alias][0] = $expression; $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; $this->_neededTables[] = $tableAlias; } // reset the state $this->_pendingAggregates = array(); } /** * _buildSqlQueryBase * returns the base of the generated sql query * On mysql driver special strategy has to be used for DELETE statements * (where is this special strategy??) * * @return string the base of the generated sql query */ protected function _buildSqlQueryBase() { switch ($this->_type) { case self::DELETE: $q = 'DELETE FROM '; break; case self::UPDATE: $q = 'UPDATE '; break; case self::SELECT: $distinct = ($this->_sqlParts['distinct']) ? 'DISTINCT ' : ''; $q = 'SELECT ' . $distinct . implode(', ', $this->_sqlParts['select']) . ' FROM '; break; } return $q; } /** * _buildSqlFromPart * builds the from part of the query and returns it * * @return string the query sql from part */ protected function _buildSqlFromPart($ignorePending = false) { $q = ''; foreach ($this->_sqlParts['from'] as $k => $part) { $e = explode(' ', $part); if ($k === 0) { if ( ! $ignorePending && $this->_type == self::SELECT) { // We may still have pending conditions $alias = count($e) > 1 ? $this->getComponentAlias($e[1]) : null; $where = $this->_processPendingJoinConditions($alias); // apply inheritance to WHERE part if ( ! empty($where)) { if (count($this->_sqlParts['where']) > 0) { $this->_sqlParts['where'][] = 'AND'; } if (substr($where, 0, 1) === '(' && substr($where, -1) === ')') { $this->_sqlParts['where'][] = $where; } else { $this->_sqlParts['where'][] = '(' . $where . ')'; } } } $q .= $part; continue; } // preserve LEFT JOINs only if needed // Check if it's JOIN, if not add a comma separator instead of space if ( ! preg_match('/\bJOIN\b/i', $part) && ! isset($this->_pendingJoinConditions[$k])) { $q .= ', ' . $part; } else { if (substr($part, 0, 9) === 'LEFT JOIN') { $aliases = array_merge($this->_subqueryAliases, array_keys($this->_neededTables)); if ( ! in_array($e[3], $aliases) && ! in_array($e[2], $aliases) && ! empty($this->_pendingFields)) { continue; } } if ( ! $ignorePending && isset($this->_pendingJoinConditions[$k])) { if (strpos($part, ' ON ') !== false) { $part .= ' AND '; } else { $part .= ' ON '; } $part .= $this->_processPendingJoinConditions($k); } $componentAlias = $this->getComponentAlias($e[3]); $string = $this->getInheritanceCondition($componentAlias); if ($string) { $part = $part . ' AND ' . $string; } $q .= ' ' . $part; } $this->_sqlParts['from'][$k] = $part; } return $q; } /** * Processes the pending join conditions, used for dynamically add conditions * to root component/joined components without interfering in the main dql * handling. * * @param string $alias Component Alias * @return Processed pending conditions */ protected function _processPendingJoinConditions($alias) { $parts = array(); if ($alias !== null && isset($this->_pendingJoinConditions[$alias])) { $parser = new Doctrine_Query_JoinCondition($this, $this->_tokenizer); foreach ($this->_pendingJoinConditions[$alias] as $joinCondition) { $parts[] = $parser->parse($joinCondition); } // FIX #1860 and #1876: Cannot unset them, otherwise query cannot be reused later //unset($this->_pendingJoinConditions[$alias]); } return (count($parts) > 0 ? '(' . implode(') AND (', $parts) . ')' : ''); } /** * builds the sql query from the given parameters and applies things such as * column aggregation inheritance and limit subqueries if needed * * @param array $params an array of prepared statement params (needed only in mysql driver * when limit subquery algorithm is used) * @param bool $limitSubquery Whether or not to try and apply the limit subquery algorithm * @return string the built sql query */ public function getSqlQuery($params = array(), $limitSubquery = true) { // Assign building/execution specific params $this->_params['exec'] = $params; // Initialize prepared parameters array $this->_execParams = $this->getFlattenedParams(); if ($this->_state !== self::STATE_DIRTY) { $this->fixArrayParameterValues($this->getInternalParams()); // Return compiled SQL return $this->_sql; } return $this->buildSqlQuery($limitSubquery); } /** * Build the SQL query from the DQL * * @param bool $limitSubquery Whether or not to try and apply the limit subquery algorithm * @return string $sql The generated SQL string */ public function buildSqlQuery($limitSubquery = true) { // reset the state if ( ! $this->isSubquery()) { $this->_queryComponents = array(); $this->_pendingAggregates = array(); $this->_aggregateAliasMap = array(); } $this->reset(); // invoke the preQuery hook $this->_preQuery(); // process the DQL parts => generate the SQL parts. // this will also populate the $_queryComponents. foreach ($this->_dqlParts as $queryPartName => $queryParts) { // If we are parsing FROM clause, we'll need to diff the queryComponents later if ($queryPartName == 'from') { // Pick queryComponents before processing $queryComponentsBefore = $this->getQueryComponents(); } // FIX #1667: _sqlParts are cleaned inside _processDqlQueryPart. if ($queryPartName != 'forUpdate') { $this->_processDqlQueryPart($queryPartName, $queryParts); } // We need to define the root alias if ($queryPartName == 'from') { // Pick queryComponents aftr processing $queryComponentsAfter = $this->getQueryComponents(); // Root alias is the key of difference of query components $diffQueryComponents = array_diff_key($queryComponentsAfter, $queryComponentsBefore); $this->_rootAlias = key($diffQueryComponents); } } $this->_state = self::STATE_CLEAN; // Proceed with the generated SQL if (empty($this->_sqlParts['from'])) { return false; } $needsSubQuery = false; $subquery = ''; $map = $this->getRootDeclaration(); $table = $map['table']; $rootAlias = $this->getRootAlias(); if ( ! empty($this->_sqlParts['limit']) && $this->_needsSubquery && $table->getAttribute(Doctrine_Core::ATTR_QUERY_LIMIT) == Doctrine_Core::LIMIT_RECORDS) { // We do not need a limit-subquery if DISTINCT is used // and the selected fields are either from the root component or from a localKey relation (hasOne) // (i.e. DQL: SELECT DISTINCT u.id FROM User u LEFT JOIN u.phonenumbers LIMIT 5). if(!$this->_sqlParts['distinct']) { $this->_isLimitSubqueryUsed = true; $needsSubQuery = true; } else { foreach( array_keys($this->_pendingFields) as $alias){ //no subquery for root fields if($alias == $this->getRootAlias()){ continue; } //no subquery for ONE relations if(isset($this->_queryComponents[$alias]['relation']) && $this->_queryComponents[$alias]['relation']->getType() == Doctrine_Relation::ONE){ continue; } $this->_isLimitSubqueryUsed = true; $needsSubQuery = true; } } } $sql = array(); if ( ! empty($this->_pendingFields)) { foreach ($this->_queryComponents as $alias => $map) { $fieldSql = $this->processPendingFields($alias); if ( ! empty($fieldSql)) { $sql[] = $fieldSql; } } } if ( ! empty($sql)) { array_unshift($this->_sqlParts['select'], implode(', ', $sql)); } $this->_pendingFields = array(); // build the basic query $q = $this->_buildSqlQueryBase(); $q .= $this->_buildSqlFromPart(); if ( ! empty($this->_sqlParts['set'])) { $q .= ' SET ' . implode(', ', $this->_sqlParts['set']); } $string = $this->getInheritanceCondition($this->getRootAlias()); // apply inheritance to WHERE part if ( ! empty($string)) { if (count($this->_sqlParts['where']) > 0) { $this->_sqlParts['where'][] = 'AND'; } if (substr($string, 0, 1) === '(' && substr($string, -1) === ')') { $this->_sqlParts['where'][] = $string; } else { $this->_sqlParts['where'][] = '(' . $string . ')'; } } $modifyLimit = true; $limitSubquerySql = ''; if ( ( ! empty($this->_sqlParts['limit']) || ! empty($this->_sqlParts['offset'])) && $needsSubQuery && $limitSubquery) { $subquery = $this->getLimitSubquery(); // what about composite keys? $idColumnName = $table->getColumnName($table->getIdentifier()); switch (strtolower($this->_conn->getDriverName())) { case 'mysql': $this->useQueryCache(false); // mysql doesn't support LIMIT in subqueries $list = $this->_conn->execute($subquery, $this->_execParams)->fetchAll(Doctrine_Core::FETCH_COLUMN); $subquery = implode(', ', array_map(array($this->_conn, 'quote'), $list)); break; case 'pgsql': $subqueryAlias = $this->_conn->quoteIdentifier('doctrine_subquery_alias'); // pgsql needs special nested LIMIT subquery $subquery = 'SELECT ' . $subqueryAlias . '.' . $this->_conn->quoteIdentifier($idColumnName) . ' FROM (' . $subquery . ') AS ' . $subqueryAlias; break; } $field = $this->getSqlTableAlias($rootAlias) . '.' . $idColumnName; // FIX #1868: If not ID under MySQL is found to be restricted, restrict pk column for null // (which will lead to a return of 0 items) $limitSubquerySql = $this->_conn->quoteIdentifier($field) . (( ! empty($subquery)) ? ' IN (' . $subquery . ')' : ' IS NULL') . ((count($this->_sqlParts['where']) > 0) ? ' AND ' : ''); $modifyLimit = false; } // FIX #DC-26: Include limitSubquerySql as major relevance in conditions $emptyWhere = empty($this->_sqlParts['where']); if ( ! ($emptyWhere && $limitSubquerySql == '')) { $where = implode(' ', $this->_sqlParts['where']); $where = ($where == '' || (substr($where, 0, 1) === '(' && substr($where, -1) === ')')) ? $where : '(' . $where . ')'; $q .= ' WHERE ' . $limitSubquerySql . $where; // . (($limitSubquerySql == '' && count($this->_sqlParts['where']) == 1) ? substr($where, 1, -1) : $where); } // Fix the orderbys so we only have one orderby per value foreach ($this->_sqlParts['orderby'] as $k => $orderBy) { $e = explode(', ', $orderBy); unset($this->_sqlParts['orderby'][$k]); foreach ($e as $v) { $this->_sqlParts['orderby'][] = $v; } } // Add the default orderBy statements defined in the relationships and table classes // Only do this for SELECT queries if ($this->_type === self::SELECT) { foreach ($this->_queryComponents as $alias => $map) { $sqlAlias = $this->getSqlTableAlias($alias); if (isset($map['relation'])) { $orderBy = $map['relation']->getOrderByStatement($sqlAlias, true); if ($orderBy == $map['relation']['orderBy']) { if (isset($map['ref'])) { $orderBy = $map['relation']['refTable']->processOrderBy($sqlAlias, $map['relation']['orderBy'], true); } else { $orderBy = null; } } } else { $orderBy = $map['table']->getOrderByStatement($sqlAlias, true); } if ($orderBy) { $e = explode(',', $orderBy); $e = array_map('trim', $e); foreach ($e as $v) { if ( ! in_array($v, $this->_sqlParts['orderby'])) { $this->_sqlParts['orderby'][] = $v; } } } } } $q .= ( ! empty($this->_sqlParts['groupby'])) ? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; $q .= ( ! empty($this->_sqlParts['having'])) ? ' HAVING ' . implode(' AND ', $this->_sqlParts['having']): ''; $q .= ( ! empty($this->_sqlParts['orderby'])) ? ' ORDER BY ' . implode(', ', $this->_sqlParts['orderby']) : ''; if ($modifyLimit) { $q = $this->_conn->modifyLimitQuery($q, $this->_sqlParts['limit'], $this->_sqlParts['offset']); } $q .= $this->_sqlParts['forUpdate'] === true ? ' FOR UPDATE ' : ''; $this->_sql = $q; $this->clear(); return $q; } /** * getLimitSubquery * this is method is used by the record limit algorithm * * when fetching one-to-many, many-to-many associated data with LIMIT clause * an additional subquery is needed for limiting the number of returned records instead * of limiting the number of sql result set rows * * @return string the limit subquery * @todo A little refactor to make the method easier to understand & maybe shorter? */ public function getLimitSubquery() { $map = reset($this->_queryComponents); $table = $map['table']; $componentAlias = key($this->_queryComponents); // get short alias $alias = $this->getSqlTableAlias($componentAlias); // what about composite keys? $primaryKey = $alias . '.' . $table->getColumnName($table->getIdentifier()); $driverName = $this->_conn->getAttribute(Doctrine_Core::ATTR_DRIVER_NAME); // initialize the base of the subquery if (($driverName == 'oracle' || $driverName == 'oci') && $this->_isOrderedByJoinedColumn()) { $subquery = 'SELECT '; } else { $subquery = 'SELECT DISTINCT '; } $subquery .= $this->_conn->quoteIdentifier($primaryKey); // pgsql & oracle need the order by fields to be preserved in select clause if ($driverName == 'pgsql' || $driverName == 'oracle' || $driverName == 'oci' || $driverName == 'mssql' || $driverName == 'odbc') { foreach ($this->_sqlParts['orderby'] as $part) { // Remove identifier quoting if it exists $e = $this->_tokenizer->bracketExplode($part, ' '); foreach ($e as $f) { if ($f == 0 || $f % 2 == 0) { $partOriginal = str_replace(',', '', trim($f)); $callback = create_function('$e', 'return trim($e, \'[]`"\');'); $part = trim(implode('.', array_map($callback, explode('.', $partOriginal)))); if (strpos($part, '.') === false) { continue; } // don't add functions if (strpos($part, '(') !== false) { continue; } // don't add primarykey column (its already in the select clause) if ($part !== $primaryKey) { $subquery .= ', ' . $partOriginal; } } } } } $orderby = $this->_sqlParts['orderby']; $having = $this->_sqlParts['having']; if ($driverName == 'mysql' || $driverName == 'pgsql') { foreach ($this->_expressionMap as $dqlAlias => $expr) { if (isset($expr[1])) { $subquery .= ', ' . $expr[0] . ' AS ' . $this->_aggregateAliasMap[$dqlAlias]; } } } else { foreach ($this->_expressionMap as $dqlAlias => $expr) { if (isset($expr[1])) { foreach ($having as $k => $v) { $having[$k] = str_replace($this->_aggregateAliasMap[$dqlAlias], $expr[0], $v); } foreach ($orderby as $k => $v) { $e = explode(' ', $v); if ($e[0] == $this->_aggregateAliasMap[$dqlAlias]) { $orderby[$k] = $expr[0]; } } } } } // Add having fields that got stripped out of select preg_match_all('/`[a-z0-9_]+`\.`[a-z0-9_]+`/i', implode(' ', $having), $matches, PREG_PATTERN_ORDER); if (count($matches[0]) > 0) { $subquery .= ', ' . implode(', ', array_unique($matches[0])); } $subquery .= ' FROM'; foreach ($this->_sqlParts['from'] as $part) { // preserve LEFT JOINs only if needed if (substr($part, 0, 9) === 'LEFT JOIN') { $e = explode(' ', $part); //Patched by Will Ferrer to also check for groupBys. Fixes Bug -- DC-594 if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where']) && empty($this->_sqlParts['having']) && empty($this->_sqlParts['groupby'])) { //End Patch continue; } } $subquery .= ' ' . $part; } // all conditions must be preserved in subquery $subquery .= ( ! empty($this->_sqlParts['where']))? ' WHERE ' . implode(' ', $this->_sqlParts['where']) : ''; $subquery .= ( ! empty($this->_sqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_sqlParts['groupby']) : ''; $subquery .= ( ! empty($having))? ' HAVING ' . implode(' AND ', $having) : ''; $subquery .= ( ! empty($orderby))? ' ORDER BY ' . implode(', ', $orderby) : ''; if (($driverName == 'oracle' || $driverName == 'oci') && $this->_isOrderedByJoinedColumn()) { // When using "ORDER BY x.foo" where x.foo is a column of a joined table, // we may get duplicate primary keys because all columns in ORDER BY must appear // in the SELECT list when using DISTINCT. Hence we need to filter out the // primary keys with an additional DISTINCT subquery. // #1038 $quotedIdentifierColumnName = $this->_conn->quoteIdentifier($table->getColumnName($table->getIdentifier())); $subquery = 'SELECT doctrine_subquery_alias.' . $quotedIdentifierColumnName . ' FROM (' . $subquery . ') doctrine_subquery_alias' . ' GROUP BY doctrine_subquery_alias.' . $quotedIdentifierColumnName . ' ORDER BY MIN(ROWNUM)'; } // add driver specific limit clause $subquery = $this->_conn->modifyLimitSubquery($table, $subquery, $this->_sqlParts['limit'], $this->_sqlParts['offset']); $parts = $this->_tokenizer->quoteExplode($subquery, ' ', "'", "'"); foreach ($parts as $k => $part) { if (strpos($part, ' ') !== false) { continue; } $part = str_replace(array('"', "'", '`'), "", $part); if ($this->hasSqlTableAlias($part)) { $parts[$k] = $this->_conn->quoteIdentifier($this->generateNewSqlTableAlias($part)); continue; } if (strpos($part, '.') === false) { continue; } preg_match_all("/[a-zA-Z0-9_]+\.[a-z0-9_]+/i", $part, $m); foreach ($m[0] as $match) { $e = explode('.', $match); // Rebuild the original part without the newly generate alias and with quoting reapplied $e2 = array(); foreach ($e as $k2 => $v2) { $e2[$k2] = $this->_conn->quoteIdentifier($v2); } $match = implode('.', $e2); // Generate new table alias $e[0] = $this->generateNewSqlTableAlias($e[0]); // Requote the part with the newly generated alias foreach ($e as $k2 => $v2) { $e[$k2] = $this->_conn->quoteIdentifier($v2); } $replace = implode('.' , $e); // Replace the original part with the new part with new sql table alias $parts[$k] = str_replace($match, $replace, $parts[$k]); } } if ($driverName == 'mysql' || $driverName == 'pgsql') { foreach ($parts as $k => $part) { if (strpos($part, "'") !== false) { continue; } if (strpos($part, '__') == false) { continue; } preg_match_all("/[a-zA-Z0-9_]+\_\_[a-z0-9_]+/i", $part, $m); foreach ($m[0] as $match) { $e = explode('__', $match); $e[0] = $this->generateNewSqlTableAlias($e[0]); $parts[$k] = str_replace($match, implode('__', $e), $parts[$k]); } } } $subquery = implode(' ', $parts); return $subquery; } /** * Checks whether the query has an ORDER BY on a column of a joined table. * This information is needed in special scenarios like the limit-offset when its * used with an Oracle database. * * @return boolean TRUE if the query is ordered by a joined column, FALSE otherwise. */ private function _isOrderedByJoinedColumn() { if ( ! $this->_queryComponents) { throw new Doctrine_Query_Exception("The query is in an invalid state for this " . "operation. It must have been fully parsed first."); } $componentAlias = key($this->_queryComponents); $mainTableAlias = $this->getSqlTableAlias($componentAlias); foreach ($this->_sqlParts['orderby'] as $part) { $part = trim($part); $e = $this->_tokenizer->bracketExplode($part, ' '); $part = trim($e[0]); if (strpos($part, '.') === false) { continue; } list($tableAlias, $columnName) = explode('.', $part); if ($tableAlias != $mainTableAlias) { return true; } } return false; } /** * DQL PARSER * parses a DQL query * first splits the query in parts and then uses individual * parsers for each part * * @param string $query DQL query * @param boolean $clear whether or not to clear the aliases * @throws Doctrine_Query_Exception if some generic parsing error occurs * @return Doctrine_Query */ public function parseDqlQuery($query, $clear = true) { if ($clear) { $this->clear(); } $query = trim($query); $query = str_replace("\r", "\n", str_replace("\r\n", "\n", $query)); $query = str_replace("\n", ' ', $query); $parts = $this->_tokenizer->tokenizeQuery($query); foreach ($parts as $partName => $subParts) { $subParts = trim($subParts); $partName = strtolower($partName); switch ($partName) { case 'create': $this->_type = self::CREATE; break; case 'insert': $this->_type = self::INSERT; break; case 'delete': $this->_type = self::DELETE; break; case 'select': $this->_type = self::SELECT; $this->_addDqlQueryPart($partName, $subParts); break; case 'update': $this->_type = self::UPDATE; $partName = 'from'; case 'from': $this->_addDqlQueryPart($partName, $subParts); break; case 'set': $this->_addDqlQueryPart($partName, $subParts, true); break; case 'group': case 'order': $partName .= 'by'; case 'where': case 'having': case 'limit': case 'offset': $this->_addDqlQueryPart($partName, $subParts); break; } } return $this; } /** * @todo Describe & refactor... too long and nested. * @param string $path component alias * @param boolean $loadFields */ public function load($path, $loadFields = true) { if (isset($this->_queryComponents[$path])) { return $this->_queryComponents[$path]; } $e = $this->_tokenizer->quoteExplode($path, ' INDEXBY '); $mapWith = null; if (count($e) > 1) { $mapWith = trim($e[1]); $path = $e[0]; } // parse custom join conditions $e = explode(' ON ', str_ireplace(' on ', ' ON ', $path)); $joinCondition = ''; if (count($e) > 1) { $joinCondition = substr($path, strlen($e[0]) + 4, strlen($e[1])); $path = substr($path, 0, strlen($e[0])); $overrideJoin = true; } else { $e = explode(' WITH ', str_ireplace(' with ', ' WITH ', $path)); if (count($e) > 1) { $joinCondition = substr($path, strlen($e[0]) + 6, strlen($e[1])); $path = substr($path, 0, strlen($e[0])); } $overrideJoin = false; } $tmp = explode(' ', $path); $componentAlias = $originalAlias = (count($tmp) > 1) ? end($tmp) : null; $e = preg_split("/[.:]/", $tmp[0], -1); $fullPath = $tmp[0]; $prevPath = ''; $fullLength = strlen($fullPath); if (isset($this->_queryComponents[$e[0]])) { $table = $this->_queryComponents[$e[0]]['table']; $componentAlias = $e[0]; $prevPath = $parent = array_shift($e); } foreach ($e as $key => $name) { // get length of the previous path $length = strlen($prevPath); // build the current component path $prevPath = ($prevPath) ? $prevPath . '.' . $name : $name; $delimeter = substr($fullPath, $length, 1); // if an alias is not given use the current path as an alias identifier if (strlen($prevPath) === $fullLength && isset($originalAlias)) { $componentAlias = $originalAlias; } else { $componentAlias = $prevPath; } // if the current alias already exists, skip it if (isset($this->_queryComponents[$componentAlias])) { throw new Doctrine_Query_Exception("Duplicate alias '$componentAlias' in query."); } if ( ! isset($table)) { // process the root of the path $table = $this->loadRoot($name, $componentAlias); } else { $join = ($delimeter == ':') ? 'INNER JOIN ' : 'LEFT JOIN '; $relation = $table->getRelation($name); $localTable = $table; $table = $relation->getTable(); $this->_queryComponents[$componentAlias] = array('table' => $table, 'parent' => $parent, 'relation' => $relation, 'map' => null); if ( ! $relation->isOneToOne()) { $this->_needsSubquery = true; } $localAlias = $this->getSqlTableAlias($parent, $localTable->getTableName()); $foreignAlias = $this->getSqlTableAlias($componentAlias, $relation->getTable()->getTableName()); $foreignSql = $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $this->_conn->quoteIdentifier($foreignAlias); $map = $relation->getTable()->inheritanceMap; if ( ! $loadFields || ! empty($map) || $joinCondition) { $this->_subqueryAliases[] = $foreignAlias; } if ($relation instanceof Doctrine_Relation_Association) { $asf = $relation->getAssociationTable(); $assocTableName = $asf->getTableName(); if ( ! $loadFields || ! empty($map) || $joinCondition) { $this->_subqueryAliases[] = $assocTableName; } $assocPath = $prevPath . '.' . $asf->getComponentName() . ' ' . $componentAlias; $this->_queryComponents[$assocPath] = array( 'parent' => $prevPath, 'relation' => $relation, 'table' => $asf, 'ref' => true); $assocAlias = $this->getSqlTableAlias($assocPath, $asf->getTableName()); $queryPart = $join . $this->_conn->quoteIdentifier($assocTableName) . ' ' . $this->_conn->quoteIdentifier($assocAlias); $queryPart .= ' ON (' . $this->_conn->quoteIdentifier($localAlias . '.' . $localTable->getColumnName($localTable->getIdentifier())) // what about composite keys? . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocalRefColumnName()); if ($relation->isEqual()) { // equal nest relation needs additional condition $queryPart .= ' OR ' . $this->_conn->quoteIdentifier($localAlias . '.' . $table->getColumnName($table->getIdentifier())) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeignRefColumnName()); } $queryPart .= ')'; $this->_sqlParts['from'][] = $queryPart; $queryPart = $join . $foreignSql; if ( ! $overrideJoin) { $queryPart .= $this->buildAssociativeRelationSql($relation, $assocAlias, $foreignAlias, $localAlias); } } else { $queryPart = $this->buildSimpleRelationSql($relation, $foreignAlias, $localAlias, $overrideJoin, $join); } $queryPart .= $this->buildInheritanceJoinSql($table->getComponentName(), $componentAlias); $this->_sqlParts['from'][$componentAlias] = $queryPart; if ( ! empty($joinCondition)) { $this->addPendingJoinCondition($componentAlias, $joinCondition); } } if ($loadFields) { $restoreState = false; // load fields if necessary if ($loadFields && empty($this->_dqlParts['select'])) { $this->_pendingFields[$componentAlias] = array('*'); } } $parent = $prevPath; } $table = $this->_queryComponents[$componentAlias]['table']; return $this->buildIndexBy($componentAlias, $mapWith); } protected function buildSimpleRelationSql(Doctrine_Relation $relation, $foreignAlias, $localAlias, $overrideJoin, $join) { $queryPart = $join . $this->_conn->quoteIdentifier($relation->getTable()->getTableName()) . ' ' . $this->_conn->quoteIdentifier($foreignAlias); if ( ! $overrideJoin) { $queryPart .= ' ON ' . $this->_conn->quoteIdentifier($localAlias . '.' . $relation->getLocalColumnName()) . ' = ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $relation->getForeignColumnName()); } return $queryPart; } protected function buildIndexBy($componentAlias, $mapWith = null) { $table = $this->_queryComponents[$componentAlias]['table']; $indexBy = null; $column = false; if (isset($mapWith)) { $terms = explode('.', $mapWith); if (count($terms) == 1) { $indexBy = $terms[0]; } else if (count($terms) == 2) { $column = true; $indexBy = $terms[1]; } } else if ($table->getBoundQueryPart('indexBy') !== null) { $indexBy = $table->getBoundQueryPart('indexBy'); } if ($indexBy !== null) { if ( $column && ! $table->hasColumn($table->getColumnName($indexBy))) { throw new Doctrine_Query_Exception("Couldn't use key mapping. Column " . $indexBy . " does not exist."); } $this->_queryComponents[$componentAlias]['map'] = $indexBy; } return $this->_queryComponents[$componentAlias]; } protected function buildAssociativeRelationSql(Doctrine_Relation $relation, $assocAlias, $foreignAlias, $localAlias) { $table = $relation->getTable(); $queryPart = ' ON '; if ($relation->isEqual()) { $queryPart .= '('; } $localIdentifier = $table->getColumnName($table->getIdentifier()); $queryPart .= $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getForeignRefColumnName()); if ($relation->isEqual()) { $queryPart .= ' OR ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' = ' . $this->_conn->quoteIdentifier($assocAlias . '.' . $relation->getLocalRefColumnName()) . ') AND ' . $this->_conn->quoteIdentifier($foreignAlias . '.' . $localIdentifier) . ' != ' . $this->_conn->quoteIdentifier($localAlias . '.' . $localIdentifier); } return $queryPart; } /** * loadRoot * * @param string $name * @param string $componentAlias * @return Doctrine_Table * @todo DESCRIBE ME! * @todo this method is called only in Doctrine_Query class. Shouldn't be private or protected? */ public function loadRoot($name, $componentAlias) { // get the connection for the component $manager = Doctrine_Manager::getInstance(); if ( ! $this->_passedConn && $manager->hasConnectionForComponent($name)) { $this->_conn = $manager->getConnectionForComponent($name); } $table = $this->_conn->getTable($name); $tableName = $table->getTableName(); // get the short alias for this table $tableAlias = $this->getSqlTableAlias($componentAlias, $tableName); // quote table name $queryPart = $this->_conn->quoteIdentifier($tableName); if ($this->_type === self::SELECT) { $queryPart .= ' ' . $this->_conn->quoteIdentifier($tableAlias); } $this->_tableAliasMap[$tableAlias] = $componentAlias; $queryPart .= $this->buildInheritanceJoinSql($name, $componentAlias); $this->_sqlParts['from'][] = $queryPart; $this->_queryComponents[$componentAlias] = array('table' => $table, 'map' => null); return $table; } /** * @todo DESCRIBE ME! * @param string $name component class name * @param string $componentAlias alias of the component in the dql * @return string query part */ public function buildInheritanceJoinSql($name, $componentAlias) { // get the connection for the component $manager = Doctrine_Manager::getInstance(); if ( ! $this->_passedConn && $manager->hasConnectionForComponent($name)) { $this->_conn = $manager->getConnectionForComponent($name); } $table = $this->_conn->getTable($name); $tableName = $table->getTableName(); // get the short alias for this table $tableAlias = $this->getSqlTableAlias($componentAlias, $tableName); $queryPart = ''; foreach ($table->getOption('joinedParents') as $parent) { $parentTable = $this->_conn->getTable($parent); $parentAlias = $componentAlias . '.' . $parent; // get the short alias for the parent table $parentTableAlias = $this->getSqlTableAlias($parentAlias, $parentTable->getTableName()); $queryPart .= ' LEFT JOIN ' . $this->_conn->quoteIdentifier($parentTable->getTableName()) . ' ' . $this->_conn->quoteIdentifier($parentTableAlias) . ' ON '; //Doctrine_Core::dump($table->getIdentifier()); foreach ((array) $table->getIdentifier() as $identifier) { $column = $table->getColumnName($identifier); $queryPart .= $this->_conn->quoteIdentifier($tableAlias) . '.' . $this->_conn->quoteIdentifier($column) . ' = ' . $this->_conn->quoteIdentifier($parentTableAlias) . '.' . $this->_conn->quoteIdentifier($column); } } return $queryPart; } /** * Get count sql query for this Doctrine_Query instance. * * This method is used in Doctrine_Query::count() for returning an integer * for the number of records which will be returned when executed. * * @return string $q */ public function getCountSqlQuery() { // triggers dql parsing/processing $this->getSqlQuery(array(), false); // this is ugly // initialize temporary variables $where = $this->_sqlParts['where']; $having = $this->_sqlParts['having']; $groupby = $this->_sqlParts['groupby']; $rootAlias = $this->getRootAlias(); $tableAlias = $this->getSqlTableAlias($rootAlias); // Build the query base $q = 'SELECT COUNT(*) AS ' . $this->_conn->quoteIdentifier('num_results') . ' FROM '; // Build the from clause $from = $this->_buildSqlFromPart(true); // Build the where clause $where = ( ! empty($where)) ? ' WHERE ' . implode(' ', $where) : ''; // Build the group by clause $groupby = ( ! empty($groupby)) ? ' GROUP BY ' . implode(', ', $groupby) : ''; // Build the having clause $having = ( ! empty($having)) ? ' HAVING ' . implode(' AND ', $having) : ''; // Building the from clause and finishing query if (count($this->_queryComponents) == 1 && empty($having)) { $q .= $from . $where . $groupby . $having; } else { // Subselect fields will contain only the pk of root entity $ta = $this->_conn->quoteIdentifier($tableAlias); $map = $this->getRootDeclaration(); $idColumnNames = $map['table']->getIdentifierColumnNames(); $pkFields = $ta . '.' . implode(', ' . $ta . '.', $this->_conn->quoteMultipleIdentifier($idColumnNames)); // We need to do some magic in select fields if the query contain anything in having clause $selectFields = $pkFields; if ( ! empty($having)) { // For each field defined in select clause foreach ($this->_sqlParts['select'] as $field) { // We only include aggregate expressions to count query // This is needed because HAVING clause will use field aliases if (strpos($field, '(') !== false) { $selectFields .= ', ' . $field; } } // Add having fields that got stripped out of select preg_match_all('/`[a-z0-9_]+`\.`[a-z0-9_]+`/i', $having, $matches, PREG_PATTERN_ORDER); if (count($matches[0]) > 0) { $selectFields .= ', ' . implode(', ', array_unique($matches[0])); } } // If we do not have a custom group by, apply the default one if (empty($groupby)) { $groupby = ' GROUP BY ' . $pkFields; } $q .= '(SELECT ' . $selectFields . ' FROM ' . $from . $where . $groupby . $having . ') ' . $this->_conn->quoteIdentifier('dctrn_count_query'); } return $q; } /** * Fetches the count of the query. * * This method executes the main query without all the * selected fields, ORDER BY part, LIMIT part and OFFSET part. * * Example: * Main query: * SELECT u.*, p.phonenumber FROM User u * LEFT JOIN u.Phonenumber p * WHERE p.phonenumber = '123 123' LIMIT 10 * * The modified DQL query: * SELECT COUNT(DISTINCT u.id) FROM User u * LEFT JOIN u.Phonenumber p * WHERE p.phonenumber = '123 123' * * @param array $params an array of prepared statement parameters * @return integer the count of this query */ public function count($params = array()) { $q = $this->getCountSqlQuery(); $params = $this->getCountQueryParams($params); $params = $this->_conn->convertBooleans($params); if ($this->_resultCache) { $conn = $this->getConnection(); $cacheDriver = $this->getResultCacheDriver(); $hash = $this->getResultCacheHash($params).'_count'; $cached = ($this->_expireResultCache) ? false : $cacheDriver->fetch($hash); if ($cached === false) { // cache miss $results = $this->getConnection()->fetchAll($q, $params); $cacheDriver->save($hash, serialize($results), $this->getResultCacheLifeSpan()); } else { $results = unserialize($cached); } } else { $results = $this->getConnection()->fetchAll($q, $params); } if (count($results) > 1) { $count = count($results); } else { if (isset($results[0])) { $results[0] = array_change_key_case($results[0], CASE_LOWER); $count = $results[0]['num_results']; } else { $count = 0; } } return (int) $count; } /** * Queries the database with DQL (Doctrine Query Language). * * This methods parses a Dql query and builds the query parts. * * @param string $query Dql query * @param array $params prepared statement parameters * @param int $hydrationMode Doctrine_Core::HYDRATE_ARRAY or Doctrine_Core::HYDRATE_RECORD * @see Doctrine_Core::FETCH_* constants * @return mixed */ public function query($query, $params = array(), $hydrationMode = null) { $this->parseDqlQuery($query); return $this->execute($params, $hydrationMode); } /** * Copies a Doctrine_Query object. * * @return Doctrine_Query Copy of the Doctrine_Query instance. */ public function copy(Doctrine_Query $query = null) { if ( ! $query) { $query = $this; } $new = clone $query; return $new; } /** * Magic method called after cloning process. * * @return void */ public function __clone() { $this->_parsers = array(); $this->_hydrator = clone $this->_hydrator; // Subqueries share some information from the parent so it can intermingle // with the dql of the main query. So when a subquery is cloned we need to // kill those references or it causes problems if ($this->isSubquery()) { $this->_killReference('_params'); $this->_killReference('_tableAliasMap'); $this->_killReference('_queryComponents'); } } /** * Kill the reference for the passed class property. * This method simply copies the value to a temporary variable and then unsets * the reference and re-assigns the old value but not by reference * * @param string $key */ protected function _killReference($key) { $tmp = $this->$key; unset($this->$key); $this->$key = $tmp; } /** * Frees the resources used by the query object. It especially breaks a * cyclic reference between the query object and it's parsers. This enables * PHP's current GC to reclaim the memory. * This method can therefore be used to reduce memory usage when creating * a lot of query objects during a request. * * @return Doctrine_Query this object */ public function free() { $this->reset(); $this->_parsers = array(); $this->_dqlParts = array(); } } I have loved working with Doctrine and would be happy to contribute back to the code base any patches I resolve for my project in the future. Best regards Will Ferrer |
| Comment by Jonathan H. Wage [ 27/May/10 ] |
|
Hi, in order to apply your changes we need a patch and not the whole file. Thanks, Jon |
| Comment by will ferrer [ 02/Jun/10 ] |
|
Hi Jonathan I tried using winmerge to make a patch file but it seems contain both the entire before and after files in it. I am not sure if I am doing something wrong with the software or if this is just what a patch file looks like. At any rate I have attached the patch file to this issue. Please let me know if this patch file is correct. Best Regards Will Ferrer |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Hi, the patch is not correct. Just go into your svn checkout where the changes are and run the command svn diff. Output that to a file and attach it here. Thanks, Jon |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
I was able to generate a patch. It had some errors in our test suite but I fixed them. Since we don't have a test case for it I am not sure if the changes I made affected anything for you. Can you test the patch or provide a test case? |
| Comment by will ferrer [ 08/Jun/10 ] |
|
Hi Jon I checked out the svn branch 1.2.2 and noticed that you had my original/broken patch in the code (that one failed some tests for me so I fixed it and tried to upload it in the last patch file I attached to this thread – the patch file that didn't work). Using the technique you described I made a working patch to put the correct version of my code into the 1.2.2 branch. I also added a test case for this fix in my patch. Please see the new patch I have attached to this thread. Thanks for all your help. Will Ferrer |
| Comment by will ferrer [ 08/Jun/10 ] |
|
Here is the correct patch for the bug fix, along with the test case for it. |
| Comment by will ferrer [ 08/Jun/10 ] |
|
I reopened the issue to call attention to the fixed patch/test case I added to the thread. |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
That one works and looks better. Just had a few tabs vs spaces problems that I fixed. Thanks for your work on this. It is much appreciated!!!!! |
| Comment by will ferrer [ 08/Jun/10 ] |
|
Hi Jon No problem – I am very glad to contribute back to the project Hope you are well. Will Ferrer |
[DC-705] synchronizeWithArray does not properly set foreign key validation Created: 25/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Relations, Validators |
| Affects Version/s: | 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Jeff Chu | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
I've discovered a variation of the problem tested in tests/Validator/ForeignKeysTestCase.php. This happens when you synchronizeWithArray and a foreign relation is set - isValid will trigger the isnull validator. This is probably better explained through two new test cases. I've included them below. The first test case passes. However, the second test case (testSynchronizedForeignKeyIsValidIfForeignRelationIsSet) fails. tests/Validator/ForeignKeysTestCase.php - also attached as patch // Place in tests/Validator/ForeignKeysTestCase.php public function testSynchronizedForeignKeyIsValidIfLocalRelationIsSet() { $person = new TestPerson(); $person->synchronizeWithArray(array('Addresses' => array(array()))); $address = $person->Addresses[0]; $table = $address->getTable(); $errors = $table->validateField('person_id', $address->person_id, $address); $this->assertEqual(0, $errors->count()); } public function testSynchronizedForeignKeyIsValidIfForeignRelationIsSet() { $address = new TestAddress(); $address->synchronizeWithArray(array('Person' => array())); $table = $address->getTable(); $errors = $table->validateField('person_id', $address->person_id, $address); $this->assertEqual(0, $errors->count()); } I've discovered a workaround, if you reassign the value it will work. $address->synchronizeWithArray(array('Person' => array()));
$address->Person = $address->Person;
A quick and likely terrible (or wrong?) fix is to have the synchronizeWithArray function do it for you. I've attached a patch does just that. |
| Comments |
| Comment by Jeff Chu [ 25/May/10 ] |
|
Just as a note - I was looking at this further and noticed that doing this also fails: public function testGetForeignKeyIsValidIfForeignRelationIsSet() { $address = new TestAddress(); $address->Person; $table = $address->getTable(); $errors = $table->validateField('person_id', $address->person_id, $address); $this->assertEqual(0, $errors->count()); } But oddly enough, the following works: $address->Person; $address->Person = $address->Person; I think this has to do with the inconsistencies in whether get should create a real relation or fake it until it's actually set with a setter. From what I can tell, this all stems from the support for the following behavior: $address = new Address(); $address->Person->first_name = "Bob"; This behavior is taken advantage of from within synchronizeWithArray: $this->$key->synchronizeWithArray($value);
However, because it doesn't create a real relation this way - the original issue comes up. Updating record's get to create a real relation requires us to update Doctrine_Record's _get to use coreSetRelated (instead of directly modifying $this->_references). However, doing this will conflict directly with test Ticket 1072. What is the intended behavior of all of this? |
[DC-703] [PATCH] Doctrine_Connection_Oracle unquoted query aliases in _createLimitSubquery Created: 25/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection |
| Affects Version/s: | 1.2.0-RC1, 1.2.0, 1.2.1, 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Miloslav "adrive" Kmet | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
When using identifier quoting with oracle driver, the _createLimitSubquery is giving into the select statement unquoted subquery aliases. That leads to Oracle errors. Simple patch: } |
| Comments |
| Comment by Miloslav "adrive" Kmet [ 25/May/10 ] |
[DC-699] DC292 test doesn't pass due to missing 'migrations' directory Created: 24/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Federico Gimenez Nieto | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Debian Lenny |
||
| Description |
|
After downloading Doctrine 1.2.2 from [1] and unpacking it, when trying to run the tests by:
i get: Unexpected UnexpectedValueException thrown in [Doctrine_Ticket_DC292_TestCase] with message [RecursiveDirectoryIterator::__construct(/home/fgimenez/alioth/doctrine_test/Doctrine-1.2.2/tests/Ticket/DC292/migrations): failed to open dir: No such file or directory] in /home/fgimenez/alioth/doctrine_test/Doctrine-1.2.2/lib/Doctrine/Migration.php on line 137 If i create a 'migrations' directory under Doctrine-1.2.2/tests/Ticket/DC292 all tests pass. Cheers, [1] http://www.doctrine-project.org/downloads/Doctrine-1.2.2.tgz |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
SVN contains an empty directory named migrations already. I will add some code to ensure the directory is created though. |
[DC-698] Record::link method does not work with setting null one-to-one Created: 22/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Martin Cohen | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
I have an Issue model with one owner User (relation Owner). I have this code: $issue->link( 'Owner', array( $owner_id ) ); At the time of calling this code, the $issue->Owner is null. When this is called, the link fails to assign the new owner to the relation. I've tried to hotfix it changing line around 2516 in Record.php: $this->get($alias)->add($record); to this: if( $c = $this->get($alias) ) { $c->add($record); }else { $this->set( $alias, $record ); }In this case the $this->get($alias) returns NULL, so the subsequent add($record) fails. |
[DC-697] Doctrine Support of custom enum types in Pgsql Created: 20/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Minor |
| Reporter: | Leif Jackson | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
In postgres with existing schema you can have custom enum types. To support this with our current code base (symfony w/ doctrine 1.2.2) I had to make changes to doctrine import and datadict for pgsql driver. The patches attached are from the git tree. 0001-Postgresql-custome-enum-types-as-native-enum.patch The patches are formatted from the git tree. Thanks for an excellent ORM for php! |
[DC-696] Sluggable behavior doesn't work properly with french accents in utf8 Created: 20/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Behaviors |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Nicolas Deguine | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Windows XP, Symfony 1.4.4, PHP 5.2.8, MySQL 5.0.x |
||
| Description |
|
In UTF8, "Législation 2008" was "slugged" as "legislation-ta-2-8" which is not correct. It is caused as a bug in the Doctrine_Inflector::unaccent function. If $string seems to be UTF8, we use the $chars array to replace accents. But the last line of the $chars array (Norwegian characters replacements) is incorrect : 'Å'=>'Aa','Æ'=>'Ae','Ø'=>'O','æ'=>'a','ø'=>'o','å','aa' should be replaced by 'Å'=>'Aa','Æ'=>'Ae','Ø'=>'O','æ'=>'a','ø'=>'o','å' => 'aa' |
[DC-695] Fixtures created badly when primary key is not autonumber Created: 20/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Luís Eduardo Jason Santos | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Tested on Linux/Mysql |
||
| Attachments: |
|
||||||||
| Issue Links: |
|
||||||||
| Description |
|
When exporting data where the primary key is not an autonumber the export function will hide the field anyway, merging the keys to the text identifier of the resulting record. This creates two problems:
A fix is attached that prevents the export function of hiding the said field from the fixture. |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks for the issue and patch! |
[DC-694] Doctrine_Record::replace() discards Doctrine_Expression values, uses empty value instead Created: 20/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.1, 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Exception e | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
php 5.3.0 @ Windows XP SP3 |
||
| Description |
|
I experienced a bug when working with a datetime field that only rears its head when I use $record->replace() // BUG!! $record->date_created = new Doctrine_Expression('NOW()'); $record->replace(); // date_created will be 0000-00-00 00:00:00, i.e. empty // WORKS FINE! $record->date_created = '2022-12-23 14:14:12'; $record->replace(); // WORKS FINE! $record->date_created = new Doctrine_Expression('NOW()'); $record->insert(); // WORKS FINE! $record->date_created = '2022-12-23 14:14:12'; $record->insert(); |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks for the issue and description. I committed a fix that addresses the problem. |
[DC-691] No sequence_id increment after fixtures loaded, while setting their ids Created: 18/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Data Fixtures |
| Affects Version/s: | None |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | A. Simonneau | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Ubuntu lucid up to date - Symfony 1.4.4 - Postgresql 8.4 |
||
| Description |
|
If I force id in fixtures (they are autoincremented in model), the sequence is not set correctly in postgresql. Model : dn: { type: string(255) }name: { type: string(127), notnull: true} relations: ParentCompany: { local: company_id, foreign: parent_company_id, foreignAlias: ChildCompany, refClass: J_Company_Company, class: T_Company }ParentLinkType: { local: company_id, foreign: parent_company_id, refClass: J_Company_Company, class: S_CompanyLinkType }Fixtures : Postgresql : sl3dev=# select nextval('t__company_company_id_seq'::regclass); 1 instead of 4 naturally. |
[DC-688] data-load don't work when the primary key is a string Created: 18/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Gilles | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Windows 7 |
||
| Issue Links: |
|
||||||||
| Description |
|
The export/import system don't work if a table have a primary key string. I have this schema :
TypeChampPerso:
columns:
id: { type: string(20), primary: true }
description: { type: string(100), notnull: true }
Here is the result when I extract data with data-dump :
TypeChampPerso:
TypeChampPerso_date:
description: Date
TypeChampPerso_numeric:
description: Numérique
TypeChampPerso_text:
description: Texte
Then, I've got an error when I used data-load : The id column is empty is the DB. It works if I add manually the id value in the yml file :
TypeChampPerso:
TypeChampPerso_date:
id: date
description: Date
TypeChampPerso_numeric:
id: numeric
description: Numérique
TypeChampPerso_text:
id: text
description: Texte
|
[DC-687] Doctrine_Inflector::unaccent replaces UTF8-Chars - 0 with å, 1 with aa Created: 18/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Sluggable |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Steffkes | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
Changeset 7359 which fixes # |
[DC-685] Invalid conditions in Import/Mysql.php Created: 15/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Karma Dordrak (Drak) | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Lines 68 and 190 have the following conditional Should read: |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Thanks for the issue and required change! |
[DC-683] orderBy many field defined in yml files doesnt work Created: 14/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.0, 1.2.1, 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Trivial |
| Reporter: | wojtek kolodziejczyk | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
symfony - sfDoctrinePlugin, MySQL |
||
| Description |
|
Hi, orderBy[ field1 ASC, field2 DESC ] and models build by doctrine:build --model Doctrine_Table class reports warnings, when executing select query i.e.: that's causes no order at all in sql query quick solution for this problem is change file \doctrine\Doctrine\Table.php, function processOrderBy() - around line 1134 from: $e1 = explode(',', $orderBy); to: if( !is_array($orderBy)){ hope it helps to someone best, |
[DC-681] Command-line option rebuild-db is touching the models Created: 13/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Cli |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Jerry Verhoef | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
The command-line option "rebuild-db" is doing the following tasks: ( from the constructor ) $this->dropDb = new Doctrine_Task_DropDb($this->dispatcher); But the BuildAll tasks is executing three tasks including one which is regenerating the models. ( from the constructor ) My suggestion is to replace the build-all in the rebuild-db task with just the following. This would make more sense. $this->createDb = new Doctrine_Task_CreateDb($this->dispatcher); |
[DC-678] Need case-insensitive checks for dupes in the $options['indexes'] array Created: 13/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Bryan Zarzuela | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP 5.3, OSX, APC |
||
| Description |
|
I ran into a bug where there were two duplicate indexes that Doctrine was trying to create. Not too sure what the cause is yet but this fix took care of the problem for me. Line 161 of Doctrine/Export/MySQL.php // Case Insensitive checking for duplicate indexes... else { $dupes[] = strtolower($key); } } |
| Comments |
| Comment by Jonathan H. Wage [ 13/May/10 ] |
|
I don't think this is a good change. You should instead fix it by identifying why you have 2 indexes with the same name, instead of this hack. |
| Comment by Bryan Zarzuela [ 13/May/10 ] |
|
I'll try to debug why there were two indexes named employeecode and employeeCode. I think there's a bug somewhere in the MySQL drivers where the case-conversions failed. Had no time yesterday as I was focused more on getting my work done than actually fixing the problem |
| Comment by Bryan Zarzuela [ 13/May/10 ] |
|
If you do this in the base model, note the capital C in the employeeCode index: $this->hasColumn('employeeCode', 'string', 11); It will result in this SQL statement for the table creation: CREATE TABLE employees (id BIGINT AUTO_INCREMENT, employeecode VARCHAR(11), first VARCHAR(50), last VARCHAR(50), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX employeeCode_idx (employeecode), INDEX employeecode_idx (employeecode), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB Note that it tries to create 2 indexes named employeeCode_idx and employeecode_idx. If I change the declaration to $this->index('employeecode', array('fields' => array('employeeCode')));, then it works: CREATE TABLE employees (id BIGINT AUTO_INCREMENT, first VARCHAR(50), last VARCHAR(50), employeecode VARCHAR(11), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, INDEX employeecode_idx (employeecode), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = INNODB Still not sure what the cause is. I'm not too familiar with the flow of the Export Drivers yet. |
| Comment by Jonathan H. Wage [ 14/May/10 ] |
|
I see, I think we have some problems when you have columns which are camel case. I would recommend using underscores to avoid any issues until this can be resolved. |
[DC-677] Incorrect property uset before Collection serialize Created: 11/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Daniel Bojdo | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
On line 155 (Doctrine/Collection.php) should unset "referenceFiled" property not "reference_field". It causes problems after Collection unserialize. |
[DC-675] Doctrine_Connection_Mssql charset problem Created: 10/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | None |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Steve Müller | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Recently I got problems with converting UTF-8 data to iso ISO-8859-1 while trying to insert/update a Microsoft SQL Server database. SQLSTATE[HY000]: General error: trying to execute an empty query As i tracked the error down I realized, that the exception only occurs if I try to insert/update data with special characters like "ü", "ö", "ä", "ß" etc. Example: Task snippet: $dest->setZip($src->getZip()); RESULTING DQL: UPDATE table SET zip = ?, city = ?, street = ? WHERE id = ?; Params: After replaceBoundParamsWithInlineValuesInQuery() replaces param 'München', the query string is replaced by an empty string in the following iteration. The root of the Problem seems to lie in the regex modifier 'u' which treats the pattern as UTF-8 in the param replacements. Removing this modifier solves the problem for me. What purpose has this modifier? |
[DC-673] Doctrine_Export::dropForeignKey() doesn't format foreign key names Created: 10/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Kousuke Ebihara | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP 5.3.3-dev, mysql 5.1.41 |
||
| Attachments: |
|
| Description |
|
Doctrine_Export::createForeignKey() formats foreign key names, but Doctrine_Export::dropForeignKey() doesn't. It is a problem. I tested following:
I have a patch to fix this problem. |
| Comments |
| Comment by Kousuke Ebihara [ 13/May/10 ] |
|
http://github.com/ebihara/doctrine1/commit/6ae3d28a44796dad13d8387b390515ad45fd26aa I commited a patch to fix this issue to my fork. |
[DC-671] Record could be saved, even tough a NotNull column is null Created: 09/May/10 Updated: 10/May/10 Resolved: 09/May/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Validators |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Fabian Spillner | Assignee: | Jonathan H. Wage |
| Resolution: | Invalid | Votes: | 0 |
| Labels: | None | ||
| Environment: |
OS X 10.6.3 - php 5.3.1 |
||
| Attachments: |
|
| Description |
|
Strange behavior: The record could be saved without getting validation exception even tough a NotNull column is empty. If you use Sqlite as mock driver you get validation exception. Test case is added into ticket! |
| Comments |
| Comment by Fabian Spillner [ 09/May/10 ] |
|
there the test case (About filename: I wonder why there is same file (empty test) on the doctrine ticket tests - please rename this test case if I call it wrong) |
| Comment by Jonathan H. Wage [ 09/May/10 ] |
|
Do you have validation enabled? It is off by default. |
| Comment by Fabian Spillner [ 09/May/10 ] |
|
Ah! It works now! Thank you! It's new default behavior since 1.2? |
| Comment by Fabian Spillner [ 09/May/10 ] |
|
I forget to enable the attribute Doctrine_Core::ATTR_VALIDATE. |
| Comment by Jonathan H. Wage [ 09/May/10 ] |
|
No it has always been off by default. |
| Comment by Fabian Spillner [ 10/May/10 ] |
|
The reason of my confusion: On Symfony 1.2 the validation is enabled by default: http://trac.symfony-project.org/browser/branches/1.2/lib/plugins/sfDoctrinePlugin/config/config.php And nothing is talked about it: |
[DC-670] Doctrine/Collection: toHierarchy() accepts not only NestedSet Created: 08/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Nested Set |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Minor |
| Reporter: | Maxim Olenik | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
I've got it after trying to implement NestedSet+AjacencyList behaviour. |
[DC-669] listTableIndexes() return an empty array with doctrine-1.2.2 Created: 07/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Christophe Beaujean | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
On linux fedora with MySQL 5.1.45 |
||
| Description |
|
I've a table A with 3 indexes. I would like to retrieve this list of indexes by using listTableIndexes() but I just get an empty array. I found the problem into listTableIndexes(): But maybe that's a config problem from my part and the case of $keyName and $nonUnique should not be modified. |
| Comments |
| Comment by Christophe Beaujean [ 17/May/10 ] |
[DC-664] PATCH: Docrine_Record _isValueModified should not type check when comparing integers Created: 04/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Dennis Verspuij | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 1 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
The _isValueModified function in Record.php does the following for checking whether or not an integral value was modified:
This does not make sense, it implies strict type checking is always done for integers. |
| Comments |
| Comment by Dennis Verspuij [ 04/May/10 ] |
|
Attaching the simple patch to correct this. |
| Comment by David Jeanmonod [ 13/May/10 ] |
|
This ticket seems to be a duplicate of this one: http://www.doctrine-project.org/jira/browse/DC-550 |
| Comment by David Jeanmonod [ 13/May/10 ] |
|
Oh no, I misunderstood the |
| Comment by David Jeanmonod [ 13/May/10 ] |
|
This regression can be a real problem. With this, if we set an integer value to a record, then the record is set as modified even if the value was the same as before. TestCase for <?php
require_once('doctrine/lib/Doctrine.php');
//require_once(dirname(__FILE__).'/../../lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine.php');
spl_autoload_register(array('Doctrine', 'autoload'));
$manager = Doctrine_Manager::getInstance();
$manager->setAttribute(Doctrine::ATTR_EXPORT, Doctrine::EXPORT_ALL);
$conn = Doctrine_Manager::connection('mysql://root:root@localhost/test_doctrine');
echo "Connection is set up\n";
class Record extends Doctrine_Record {
public function setTableDefinition(){
$this->setTableName('record');
$this->hasColumn('number', 'integer');
}
}
try {Doctrine::dropDatabases();}catch(Exception $e){} // Drop if exist :-)
Doctrine::createDatabases();
Doctrine::createTablesFromArray(array('Record'));
echo "Database table is create\n";
$record = new Record();
$record->number = 5;
$record->save();
$record = Doctrine::getTable('Record')->createQuery()->fetchOne();
$record->number = 5;
echo "BEFORE:";
var_dump($record->getModified(true));
echo "AFTER:";
var_dump($record->getModified());
Output Connection is set up
Database table is create
BEFORE:array(1) {
["number"]=>
string(1) "5"
}
AFTHER:array(1) {
["number"]=>
int(5)
}
|
[DC-663] Doctrine_Table::createQuery creates a query with the default connection instead of the current connection Created: 04/May/10 Updated: 28/Sep/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Query |
| Affects Version/s: | 1.2.1, 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Costache Catalin | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Ubuntu 9.10 |
||
| Description |
|
If we manually set a connection on the Doctrine_Table instance Doctrine silently ignores it when creating a Doctrine_Query instance through createQuery method $manager->setConnection($defaultConnection); $table = Doctrine::getTable('Users')->setConnection($newConnection); The code: public function createQuery($alias = '') $class = $this->getAttribute(Doctrine_Core::ATTR_QUERY_CLASS); return Doctrine_Query::create(null, $class) <-- fix too quick for a patch |
| Comments |
| Comment by Christoph Berg [ 28/Sep/10 ] |
|
When using Table classes generated with the latest Symfony 1.4 version (1.4.8) the table gets bound to a specific connection, but Doctrine still uses the default connection. Using the above mentioned quick fix - replacing $this->_conn with null - everything works as it should. |
[DC-662] Obsolete Doctrine::getTable() call in Template_Listener_Sluggable Created: 04/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Sluggable |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Aleksey V. Zapprov | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Subj. Found in Doctrine/Template/Listener/Sluggable.php on line 165th: $table = Doctrine::getTable($parentTable[$i]); Should be replaced with: $table = Doctrine_Core::getTable($parentTable[$i]); |
[DC-658] _setPage in Doctrine_Pager is set to Private method Created: 01/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Pager |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Trivial |
| Reporter: | Jerry Verhoef | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
The method _setPage is set as private. This will make extending the Doctrine_Pager unnecessary complex. |
[DC-657] EXISTS not correctly parsed if there's no space between keyword and the open bracket Created: 01/May/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Query |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Andrea Baron | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Mac OS X 10.6.3, MySQL 5.1.34, PHP 5.2.10 |
||
| Description |
|
Executing a query that has an EXISTS in the WHERE clause without a space before its bracket make Doctrine ignore the EXISTS part and output it as is, this cause problems because, as an example, table aliases are not correctly resolved. For example this works SELECT * FROM TableA a WHERE EXISTS (SELECT id FROM TableB b WHERE b.a = a.id) while this doesn't SELECT * FROM TableA a WHERE EXISTS(SELECT id FROM TableB b WHERE b.a = a.id) I found the error could be in Doctrine/Query/Where.php line 49: if (count($terms) > 1) { if (substr($where, 0, 6) == 'EXISTS') { return $this->parseExists($where, true); } elseif (substr($where, 0, 10) == 'NOT EXISTS') { return $this->parseExists($where, false); } } could be modified with: if (substr($where, 0, 6) == 'EXISTS') { return $this->parseExists($where, true); } elseif (substr($where, 0, 10) == 'NOT EXISTS') { return $this->parseExists($where, false); } to solve the problem. As a plus, since one could write "NOT EXISTS" (for some reason), I think it's better to write it this way: if (substr($where, 0, 6) == 'EXISTS') { return $this->parseExists($where, true); } elseif (preg_match('/^NOT\s+EXISTS\b/i', $where) !== 0) { return $this->parseExists($where, false); } Hope this helps. PS. Please add php code formatter |
[DC-656] Endless loop when using HYDRATE_SINGLE_SCALAR and Oracle OCI adapter Doctrine Created: 29/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection |
| Affects Version/s: | 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | vadik56 | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Following loop inside Doctrine_Hydrator_SingleScalarDriver never terminates since fetchColumn inside Doctrine_Adapter_Statement_Oracle never returns FALSE. while (($val = $stmt->fetchColumn()) !== false) { $result[] = $val; } Below is the patch for Doctrine_Adapter_Statement_Oracle ---------------------------------------------------------------------------------------------------------------------------------- Index: Oracle.php =================================================================== --- Oracle.php (revision 7546) +++ Oracle.php (working copy) @@ -398,7 +398,7 @@ return false; }$row = $this->fetch(Doctrine_Core::FETCH_NUM);
/** |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
This appears to be already fixed by http://trac.doctrine-project.org/changeset/7578 |
[DC-651] [PATCH] Doctrine_Record::option('orderBy', ...) of join's right side being applied to refTable in m2m relationship Created: 26/Apr/10 Updated: 31/Aug/11 |
|
| Status: | Open |
| Project: | Doctrine 1 |
| Component/s: | Query, Relations |
| Affects Version/s: | 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.2, 1.2.3, 1.2.4 |
| Type: | Bug | Priority: | Major |
| Reporter: | suhock | Assignee: | Guilherme Blanco |
| Resolution: | Unresolved | Votes: | 2 |
| Labels: | None | ||
| Environment: |
CentOS 5.4 |
||
| Attachments: |
|
| Description |
|
When using the Doctrine_Record::option('orderBy', ...) feature on a table definition, where that table is the target of a many-to-many join, the specified orderBy columns are applied to the relation table's alias. So for example, given the following definitions: class User extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn('uid', 'integer', null, array('primary' => true)); $this->option('orderBy', 'uid'); } public function setUp() { $this->hasMany('Group as groups', array('refClass' => 'UserGroup', 'local' => 'user_uid', 'foreign' => 'group_id')); } } class Group extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn('gid', 'integer', null, array('primary' => true)); } public function setUp() { $this->hasMany('User as users', array('refClass' => 'UserGroup', 'local' => 'group_gid', 'foreign' => 'user_id')); } } class UserGroup extends Doctrine_Record { public function setTableDefinition() { $this->hasColumn('user_uid', 'integer', null, array('primary' => true)); $this->hasColumn('group_gid', 'integer', null, array('primary' => true)); } public function setUp() { $this->hasOne('User as user', array('local' => 'user_uid', 'foreign' => 'uid')); $this->hasOne('Group as group', array('local' => 'group_gid', 'foreign' => 'gid')); } } the following queries: $query = Doctrine_Query::create()
->select('u.*')
->from('User u')
->leftJoin('u.groups g WITH g.gid=?', 1);
echo $query->getSqlQuery() . "\n";
$query = Doctrine_Query::create()
->select('g.*')
->from('Group g')
->leftJoin('g.users u WITH u.uid=?', 1);
echo $query->getSqlQuery() . "\n";
will output the following:
The orderBy option() call is applied to the User definition. The SQL for the first query is correct (where User is on the left side of the join). The SQL for the second query (where User is on the right-most side of the join), however, is obviously incorrect (UserGroup doesn't even have a uid column). Basically, User's orderBy option is being applied to both the User table and its respective reference table, UserGroup, when it is the target of a join. After digging through the source for a while, I believe I've come up with a patch for this issue (which should be checked by someone more knowledgeable of Doctrine's internals). Basically, in the Doctrine_Query::buildSqlQuery() function, a call is made to Doctrine_Relation::getOrderByStatement() with the reference table (UserGroup)'s alias (u2), which in turn makes a call to Doctrine_Table::getOrderByStatement() on the referenced table (User), filling in the ORDER BY clause with User columns using UserGroup's alias. My solution was to reorder the logic so that the test for a reference class is made before the initial call to getOrderByStatement() is made. It seems to work against my test case and the test cases in the repository. I'll post my patch momentarily. This bug was first mentioned in the comments in DC-313, but the original ticket comes across as more of a feature request for the hasMany() orderBy feature. |
| Comments |
| Comment by suhock [ 26/Apr/10 ] |
|
attached a test case for this bug |
| Comment by suhock [ 26/Apr/10 ] |
|
patch against /branches/1.2 HEAD (should also work apply to 1.2.2 tag) |
| Comment by Dan Ordille [ 30/Aug/10 ] |
|
I can confirm this as an issue. However I don't think the above patch adequately fixes the problem it seems like with it an order by is still added for the ref column however the relation alias is lost. My query with the patch became I made an another patch that prevents this extra order by clause from being added and have attached it. |
| Comment by suhock [ 21/Sep/10 ] |
|
I tried out the new patch (Query_orderby_relation.diff), but it provides a reversed diff (patching goes from a patched version to the original). After applying it manually, it fails the provided test case and several additional test cases from the repository. The original patch DOES pass the provided test case, when applied against 1.2.2, 1.2.3, or the 1.2 branch from the repository. It does not pass, however, Doctrine_Query_Orderby_TestCase. As the previous poster mentioned, it fails to resolve aliases in instances where the 'orderBy' option is specified in a relation definition. I deleted the original patch and am providing a revised patch (Ticket_DC651.patch) against branch 1.2 HEAD (also works with 1.2.3), which fixes this issue. It passes all working test cases, including Doctrine_Query_Orderby_TestCase and DC651TestCase. |
| Comment by José De Araujo [ 31/Aug/11 ] |
|
I had this issue recently on a application I'm working on as described the oderBy option was applied on the joined table on a column that even doesn't exist in it. I used the DC651 patch provided and it solved the issue, so far I haven't seen any side effect to it. |
[DC-650] SoftDelete sets "default" => null for deleted_at field, causing MSSQL to silently create a Default Constraint which in turns causes Migrations to fail when reverting Created: 26/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Behaviors |
| Affects Version/s: | 1.2.0-ALPHA1, 1.2.0-ALPHA2, 1.2.0-ALPHA3, 1.2.0-BETA1, 1.2.0-BETA2, 1.2.0-BETA3, 1.2.0-RC1, 1.2.0, 1.2.1, 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Daniel Cousineau | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP 5.2.11, Windows 7, Sql Server 2005, php_mssql extension |
||
| Attachments: |
|
| Description |
|
When MSSQL receives even just a default value of null it still creates a default constraint. Said constraint prevents doctrine from removing the deleted_at field in migrations (due to the dependency on the constraint). Removing "default" => null prevents the silent creation of the constraint. I have no machine to test the effects of this on MySQL. I would imagine that it would not materially affect MySQL as the default values are never used anyways (hence the default value of null). Attached is a patch to fix the behavior (for rev 7544 in /branches/1.2) — If one is running into problems with migrations being unable to move backwards due to default constraints on SoftDelete columns, one can run the following T-SQL script to remove all default value constraints from a database (EVEN THOSE YOU SET MANUALLY, USE WITH CAUTION): {{ USE YOURDATABASENAMEHERE; Declare @name nvarchar(155) – find constraint names OPEN default_constraints FETCH NEXT FROM default_constraints INTO @name, @table WHILE @@FETCH_STATUS = 0 FETCH NEXT FROM default_constraints INTO @name, @table |
| Comments |
| Comment by Daniel Cousineau [ 26/Apr/10 ] |
|
I should also note this affects not only rollbacks, but any sort of ability to drop a deleted_at column (say, removing the SoftDelete behavior) |
| Comment by Craig Marvelley [ 27/Apr/10 ] |
|
This is related to an issue I reported a month or so ago - http://www.doctrine-project.org/jira/browse/DC-584. A solution I proposed there was to allow Doctrine to name constraints so they can be referenced and dropped later. If that were in place, the SoftDelete behaviour could manage the constraint itself? |
| Comment by Daniel Cousineau [ 27/Apr/10 ] |
|
Craig, It maybe would if Doctrine were creating the default constraints itself. If Doctrine doesn't handle the default constraints then naming has no effect as MSSQL will silently create said constraint. And now that I think of it, this issue is going to crop up any and every time you use default values in MSSQL. Perhaps it would be best to consider this ticket more of a cleanup (the behavior isn't using default values so there's no point in creating the constraint anyways) and side effect of the problems listed in |
[DC-647] Error in Doctrine_Manager#_buildDsnPartsArray() Created: 24/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Karma Dordrak (Drak) | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
The method Doctrine_Manager#_buildDsnPartsArray() ultimately relies on PHP's native parse_url(). This method, according to the PHP documentation does not parse unix_sockets in the DSN, so the following code will not find the unix_socket because parse_url only finds at most (http://us2.php.net/parse_url) scheme // silence any warnings $names = array('dsn', 'scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment', 'unix_socket'); foreach ($names as $name) { } It would appear that in order to support unix sockets we have to use parsePdoDsn() instead but this is not clear because of parseDsn() which also claims to be able to find the unix_socket. I would suggest removing 'unix_socket' from the $names array so it is clear a unix_socket cannot be represented in the normal Dsn, but only a PdoDsn. |
[DC-641] undefined method Doctrine_Manager::getExtensionsClasses() Created: 21/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Witold Wasiczko | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
On line 589 in Doctrine_Core: In manager method getExtensionsClasses() doesn't exist. |
[DC-626] Subqueries in HAVING Created: 11/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | New Feature | Priority: | Minor |
| Reporter: | Ian Banfield | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
Adds support for subqueries in HAVING. |
[DC-621] Doctrine_Inflector :: urlize changed behaviour Created: 07/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Sluggable |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Gunther Groenewege | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
before when using the sluggable behavior the Doctrine_Inflector :: urlize function changed "catégorie 1" to "categorie-1" |
| Comments |
| Comment by Pavel Campr [ 08/Jun/10 ] |
|
I see a bug here: probably, there is => missing and should be: {{ 'Å'=>'Aa','Æ'=>'Ae','Ø'=>'O','æ'=>'a','ø'=>'o','å'=>'aa'}} now, character '0' is replaced by 'å' and '1' is replaced by 'aa' |
[DC-620] Unserialize does not add entity to the table entitymap Created: 06/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Maurice Makaay | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
Hello, Considering this test script: if (!file_exists("/tmp/serialize.doctrine")) { $a = Address::getById(1); $b = Address::getById(2); $c = Address::getById(3); $d = Address::getById(4); print "Serialize using oid=" . $d->getOid() . "\n"; file_put_contents("/tmp/serialize.doctrine", serialize($d)); } else { $d = unserialize(file_get_contents("/tmp/serialize.doctrine")); print "unserialized oid = " . $d->getOid() . "\n"; $d2 = Address::getById(4); print "new object oid = " . $d2->getOid() . "\n"; $d->housenumber ++; print "{$d->housenumber} versus {$d2->housenumber}\n"; } Things are going wrong in the unserialization. In this test script, I manage to create two objects holding the same oid, but pointing at different object instances (but for the same database id). The output of this script after running it twice shows me: $ php test.php
Serialize using oid=5
$ php test.php
unserialized oid = 5
new object oid = 5
24 versus 23
Looking at the unserialize() code in Doctrine_Record, the problem seems to be coming from two issues in there:
See the attached patch for a fix that I did on our code tree to make the test work. Note that I added some extra code in the unserialize method to cleanup any existing entitymap and table repository entry for the unserialized object. This is of course not the best route, but it helps with some unit testing code where a lot of serialize/unserialize handling is going on and objects got mixed up when not doing this cleanup. When running above script with this patch applied and with removing the $a, $b and $c assignments (just for making the oid's different between the two script runs), we get the following output: $ php test.php
Serialize using oid=2
$ php test.php
unserialized oid = 5
new object oid = 5
24 versus 24
So here, unserializing an object and then reloading the object through the table object gives use two times the same object, representing db object with id = 4; When doing things in a different order, we still can force an issue, but this is due to the things mentioned above: to cleanly handle this, unserialization should be handled from a factory method on Table. This script shows the behavior: if (!file_exists("/tmp/serialize.doctrine")) { $d = Address::getById(4); print "Serialize using oid=" . $d->getOid() . "\n"; file_put_contents("/tmp/serialize.doctrine", serialize($d)); } else { $d2 = Address::getById(4); print "new object oid = " . $d2->getOid() . "\n"; $d = unserialize(file_get_contents("/tmp/serialize.doctrine")); print "unserialized oid = " . $d->getOid() . "\n"; $d3 = Address::getById(4); print "new object oid = " . $d3->getOid() . "\n"; $d->housenumber ++; print "{$d->housenumber} versus {$d2->housenumber} versus {$d3->housenumber}\n"; } The output of the second run being: $php test.php new object oid = 2 unserialized oid = 5 new object oid = 5 24 versus 23 versus 24 The first object that was created is still different from the unserialized object, but at least the third object that is created, is the object that was unserialized. I hope that this makes the issue clear and that my input helps in fixing things. This issue provided us with some really unexpected behavior and we're glad that we were able to track it down to here. |
| Comments |
| Comment by Maurice Makaay [ 07/Apr/10 ] |
|
FYI: we removed the "Remove existing record from the repository and table entity map" code from our patch. We had some issues with unit testing without this bit, but we updated the unit tests to work without this bit of code. The "Add the unserialized record to repository and entity map' part stays of course. I think you can ignore the cleanup code in the patch. As long as scripts unserialize data before working with it, things should be fine without it. |
| Comment by Maurice Makaay [ 07/Apr/10 ] |
|
New version of the patch, without the cleanup code in it. |
| Comment by Maurice Makaay [ 07/Apr/10 ] |
|
Trying to always use the same object for the same entity in the database, we came up with a factory method that we put on our record class as a static public. Maybe this is an idea that you want to include in Doctrine as well? static public function fromSerialized($serialized) { $entity = unserialize($serialized); if (!($entity instanceof Doctrine_Record)) throw new Exception( __METHOD__ . ': serialized object is not a Doctrine_Record object' ); // If the unserialized object is a persisted entity, then we must // check if there is already an object for that entity available in // Doctrine's table repository. if ($entity->exists()) { // Retrieve the entity through the table repository. $table = $entity->getTable(); $repository_entity = $table->find($entity->id); // If a different object was returned than our unserialized // object, then there was an object loaded before unserialization. // We will merge the data from the unserialized object with // the existing object and return the existing object to the caller. if ($entity->getOid() !== $repository_entity->getOid()) { $repository_entity->merge($entity); $entity = $repository_entity; } } return $entity; } For us, it's working wonders in combination with above Doctrine_Record::unserialize() patch. When letting this code handle our unserialization, the same object (checked by its oid) is used for referencing the same entity throughout the code. We call the code like this: $object = Doctrine_Record::fromSerialized($serialized_data); |
[DC-613] Doctrine_Record::copy() ignores IDENTIFIER_SEQUENCE for NULLifying the id field on copy Created: 01/Apr/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.0-ALPHA1, 1.2.0-ALPHA2, 1.2.0-ALPHA3, 1.2.0-BETA1, 1.2.0-BETA2, 1.2.0-BETA3, 1.2.0-RC1, 1.2.0, 1.2.1, 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Maurice Makaay | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Doctrine_Record::copy() ignores IDENTIFIER_SEQUENCE for NULLifying the id field on copy. In our environment, it was fixed by adding Doctrine_Core::IDENTIFIER_SEQUENCE to the check at the start of the copy() method: public function copy($deep = false) { $data = $this->_data; $idtype = $this->_table->getIdentifierType(); ! if ($idtype === Doctrine_Core::IDENTIFIER_AUTOINC || ! $idtype === Doctrine_Core::IDENTIFIER_SEQUENCE) { $id = $this->_table->getIdentifier(); unset($data[$id]); } |
[DC-594] When using a combination of: a group by field referencing a table in a relation, a join to a different table via a many type relation and a limit clause, doctrine creates a broken query then throws an exception Created: 22/Mar/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Query, Relations |
| Affects Version/s: | 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Blocker |
| Reporter: | will ferrer | Assignee: | Guilherme Blanco |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
XP Xamp Current |
||
| Description |
|
Hi All I have run into a very problematic Doctrine 1.2.2 bug which puts me in quite a bit of danger of having to rewrite my whole app in Doctrine 2 (hopefully if this is a bug it isn't present in Doctrine 2). The problem I am running into seems like something that probably would have been found and rectified however so hopefully there is something wrong in my execution. The problem I am running into is caused when I have a combination of the following 3 things in my query: This combination causes Doctrine to create a broken SQL query which it then throws an exception about when I try to execute the query or call getSqlQuery() on it. Using some sample data I put together some very simple examples to illustrate the problem: $q = Doctrine_Query::create();
$q->from('Customer Customer');
$q->leftJoin('Customer.Order Order');
$q->leftJoin('Customer.Zip Zip');
$q->addGroupBy('Zip.city');
$q->addSelect('Zip.city as city');
$q->addSelect('Customer.customer_id');
$q->addSelect('Order.order_id');
$q->offset(0);
$q->limit(5);
This creates the following dql: SELECT Zip.city as city, Customer.customer_id, Order.order_id FROM Customer Customer LEFT JOIN Customer.Order Order LEFT JOIN Customer.Zip Zip GROUP BY Zip.city LIMIT 5 OFFSET 0 However when I attempt to run $q->getSqlQuery() an exception is thrown: As you can see from the SQL in the exception Doctrine is trying to create a query that groups by a field from the Zip table with out first joining to it (SELECT DISTINCT c2.customer_id FROM customers c2 GROUP BY z2.city LIMIT 5). I know Doctrine works some magic to get limit statements to work with joins and I suspect that something with in that magic may be broken, but hopefully its something I am doing wrong. Take out any one of the 3 things I mentioned I above and everything works fine – the following all work: Remove the limit: $q = Doctrine_Query::create();
$q->from('Customer Customer');
$q->leftJoin('Customer.Order Order');
$q->leftJoin('Customer.Zip Zip');
$q->addGroupBy('Zip.city');
$q->addSelect('Zip.city as city');
$q->addSelect('Order.order_id');
$q->addSelect('Customer.customer_id');
Remove the additional join: $q = Doctrine_Query::create();
$q->from('Customer Customer');
$q->leftJoin('Customer.Zip Zip');
$q->addGroupBy('Zip.city');
$q->addSelect('Zip.city as city');
$q->addSelect('Customer.customer_id');
$q->offset(0);
$q->limit(5);
Remove the group by: $q = Doctrine_Query::create();
$q->from('Customer Customer');
$q->leftJoin('Customer.Order Order');
$q->leftJoin('Customer.Zip Zip');
$q->addSelect('Zip.city as city');
$q->addSelect('Customer.customer_id');
$q->addSelect('Order.order_id');
$q->offset(0);
$q->limit(5);
Its also worth noting that the following changes also stop the problem from happening: Here are the relevant parts of my sample data schema: detect_relations: false package: Example options: type: INNODB charset: utf8 Order: tableName: orders columns: order_id: type: integer(4) primary: true notnull: true customer_id: type: integer(4) order_date: timestamp relations: OrderItem: type: many local: order_id foreign: order_id Customer: type: one local: customer_id foreign: customer_id options: type: InnoDB Customer: tableName: customers columns: customer_id: type: integer(4) primary: true notnull: true autoincrement: true firstname: type: string(45) lastname: type: string(45) streetaddress: type: string(45) city: type: string(45) state: type: string(45) postalcode: type: string(45) relations: Order: type: many local: customer_id foreign: customer_id Zip: type: one local: postalcode foreign: postalcode options: type: InnoDB Zip: connection: default_schema tableName: zips columns: postalcode: type: varchar(30) primary: true latitude: 'float(10,6)' longitude: 'float(10,6)' city: string(50) state: string(50) country: string(50) type: string(50) relations: Customer: type: many local: postalcode foreign: postalcode Thank for any advice or information you can give me on this. Best regards Will Ferrer |
| Comments |
| Comment by will ferrer [ 24/Mar/10 ] |
|
I realized another crucial aspect of the problem. I am using a left join to my Zip table instead of using an inner join. When I change my code to do an inner join it works again: $q = Doctrine_Query::create();
$q->from('Customer Customer');
$q->leftJoin('Customer.Order Order');
$q->innerJoin('Customer.Zip Zip');
$q->addGroupBy('Zip.city');
$q->addSelect('Zip.city as city');
$q->addSelect('Customer.customer_id');
$q->addSelect('Order.order_id');
$q->offset(0);
$q->limit(5);
Thanks much in advance. Will Ferrer |
| Comment by will ferrer [ 03/Apr/10 ] |
|
I took a look at the doctrine 1.2.2 code to try to track down what was causing this bug and I think I have found and fixed it in my copy of the code base. The problem is on line 1459 of Doctrine_Query and looks like it was just an oversight. The code was checking if it should preserve left joins while generating the subquery based on whether or not there were any orderBys, wheres, or havings added to the query. I changed the code to also watch for groupBys and it seems to have resolved this issue. The code was: if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where']) && empty($this->_sqlParts['having'])) { I changed it to: if (empty($this->_sqlParts['orderby']) && empty($this->_sqlParts['where']) && empty($this->_sqlParts['having']) && empty($this->_sqlParts['groupby'])) { Please let me know if I am over looking anything. If I am not then this change should probably be added to the next revision of doctrine. Sincerely Will Ferrer |
[DC-584] MSSQL Server: Cannot alter / drop columns which have indexes or constraints applied to them Created: 18/Mar/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Migrations |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Craig Marvelley | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP 5.3 |
||
| Attachments: |
|
| Description |
|
SQL Server will not allow attempts to alter or drop columns with indexes or constraints applied to them, because they are dependent on the column. In addition to this, SQL Server's syntax for altering columns is different for that of adding them in that it does not allow default value constraints to be present in the alter statement. I've attached a patch which attempts to circumvent this issue by allowing the author of the changeset to provide a name for the default constraint, which SQL Server supports. Giving the constraint a name of our choosing allows us to reference and drop it before running a command against a constrained column. In the case of an ALTER query the default constraint portion is spliced and subsequently run. When dropping a column the author need only make sure they name the constraint, or know its name, then drop it before the query is run. |
| Comments |
| Comment by Craig Marvelley [ 22/Mar/10 ] |
|
Added patch and test cases for this issue. |
[DC-583] MSSQL Server: No accounting for max varchar size when creating columns. Created: 18/Mar/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Migrations |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Craig Marvelley | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
PHP 5.3 |
||
| Attachments: |
|
| Description |
|
The Doctrine_Connection_Mysql and Doctrine_Datadict_Mysql classes utilise a connection property max_varchar_size so if the field length of a string column is specified and exceeds 255 chars a 'text' column type is used instead of a 'varchar' one. This logic is missing for MSSQL; Doctrine will attempt to create varchar columns even if greater than the max size (8000 chars). I've attached a patch for MSSQL which mirrors the MySQL functionality, would you consider including it please? Thanks! |
[DC-558] CLONE -generate-migrations-diff is producing bogus migrations (drops the whole database) Created: 09/Mar/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Migrations |
| Affects Version/s: | 1.2.0, 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Kyle Spraggs | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 1 |
| Labels: | None | ||
| Environment: |
Vanilla sandbox on version 1.2.1 and 1.2.0 |
||
| Description |
|
Replicating the bug: Expected behaviour: Real behaviour: Similar issue: |
| Comments |
| Comment by Kyle Spraggs [ 09/Mar/10 ] |
|
This issue is NOT fixed for certain configurations of Doctrine (namely PEAR). |
| Comment by Martin Shopland [ 22/Apr/10 ] |
|
I agree, this is an issue still in 1.2.2. The problem seems to be to do with the model class name prefix, it manifests itself in Doctrine_Migration_Diff on line 190. The keys of two arrays are compared to see what needs to be changed: Unable to find source-code formatter for language: php. Available languages are: actionscript, html, java, javascript, none, sql, xhtml, xml $from = array (
'Model_CaseStudy' => array(...),
'Model_Event' => array(...),
'Model_Group' => array(...)
);
$to = array(
'CaseStudy' => array(...),
'Event' => array(...),
'Group' => array(...)
);
Because none of the array keys match it assumes that the $to array are all new models and so generates the migration to drop all of $from and create all of $to; as the models have the same table names, if you run the down you end up with no tables at all. So I think we need to inject the prefix into the $to models creation process somehow, as yet I haven't figured out an effective place to do this. |
| Comment by Martin Shopland [ 22/Apr/10 ] |
|
I've got it working. Not sure how robust or appropriate this fix is: Unable to find source-code formatter for language: diff. Available languages are: actionscript, html, java, javascript, none, sql, xhtml, xml
diff --git a/lib/Doctrine/Core.php b/lib/Doctrine/Core.php
index ef10b6e..2fa7a73 100644
--- a/lib/Doctrine/Core.php
+++ b/lib/Doctrine/Core.php
@@ -664,7 +664,7 @@ class Doctrine_Core
$className = $e[0];
}
- if ($classPrefix) {
+ if ($classPrefix && $classPrefix != substr($className, 0, strlen($classPrefix))) {
$className = $classPrefix . $className;
}
diff --git a/lib/Doctrine/Migration/Diff.php b/lib/Doctrine/Migration/Diff.php
index 7f00b38..4260f27 100644
--- a/lib/Doctrine/Migration/Diff.php
+++ b/lib/Doctrine/Migration/Diff.php
@@ -112,7 +112,13 @@ class Doctrine_Migration_Diff
$this->_cleanup();
$from = $this->_generateModels(self::$_fromPrefix, $this->_from);
- $to = $this->_generateModels(self::$_toPrefix, $this->_to);
+ $to = $this->_generateModels(
+
+ Doctrine_Manager::getInstance()->getAttribute(Doctrine_Core::ATTR_MODEL_CLASS_PREFIX) .
+ self::$_toPrefix,
+
+ $this->_to
+ );
return $this->_diff($from, $to);
}
|
[DC-538] Doctrine_Table::enumIndex() and Doctrine_Table::enumValue() should return easily distinguishable value (e.g. false) when index/value is not found Created: 03/Mar/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Major |
| Reporter: | Michal Olszewski | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Any |
||
| Attachments: |
|
| Description |
|
At the moment:
This actually makes writing code which deals with enums a bit difficult as one would like to detect incorrect parameter as soon as possible (e.g. not when Doctrine validation kicks in and checks that enum value is incorrect). Current algorithm that returns index or value is not sufficient to determine error in situation when index=value, e.g. enum values are [0, 1, 2, 3]. Calling e.g. enumValue('sample', 4) would yield 4 - so it makes difficult to determine whether it's correct value. I hope this makes sense, let me know if anything is unclear. The workaround for this is to check whether mapped index/value exists in array returned by Doctrine_Table::getEnumValues(), but this is an extra unnecessary check. So instead of writing: $index = 5;
$someTable = Doctrine_Core::getTable('Sample');
$value = $someTable->enumValue('sample', $index);
$values = $someTable->getEnumValues('sample');
if (in_array($value, $values) == false)
{
throw new UnexpectedValueException('illegal index specified!');
}
I'd like to write: $index = 5;
$value = Doctrine_Core::getTable('Sample')->enumValue('sample', $index);
if ($value === false)
{
throw new UnexpectedValueException('illegal index specified!');
}
Simpler, eh? |
| Comments |
| Comment by Michal Olszewski [ 03/Mar/10 ] |
|
Please find patches for Doctrine/Table.php and Doctrine/Column.php against their versions from rev. 7298 Patches includes proposed change (returning false instead of original parameter). Interestingly, Doctrine_Column::enumIndex() returns FALSE when index is not found for specified value as it uses array_search(). |
| Comment by Jonathan H. Wage [ 15/Mar/10 ] |
|
Hi, did you run your patch against the test suite? It breaks several tests so we cannot apply it. Please re-open if you have more information and a new patch. Thanks, Jon |
| Comment by Michal Olszewski [ 25/Mar/10 ] |
|
Hi, I think I've nailed this one down. First of all, Doctrine_Table::enumValue() is used incorrectly in two places:
Please note I assumed two things:
Under these assumptions I propose two changes:
I've prepared patch for these changes and will attach it soon. Thanks. |
| Comment by Michal Olszewski [ 25/Mar/10 ] |
|
Attaching patch (
Please apply it if you agree with my previous comment. |
[DC-470] [PATCH] Search : wrong connection used for some queries when multiples connections are defined Created: 01/Feb/10 Updated: 06/Jul/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Behaviors |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Colin Darie | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Debian Sid + php 5.3, symfony 1.4.1 |
||
| Attachments: |
|
| Description |
|
When multiples connections are defined, sometimes the Search behavior use a wrong one. This happens because the Query::loadRoot() method calls the Manager::getConnectionForComponent(), which returns the bound connection for the root component (like ModelIndex in this case), but the connection for this component is never bound. Search / connection patch Index: lib/Doctrine/Search.php
===================================================================
--- lib/Doctrine/Search.php (révision 7080)
+++ lib/Doctrine/Search.php (copie de travail)
@@ -76,7 +76,9 @@
$result = parent::buildTable();
if ( ! isset($this->_options['connection'])) {
- $this->_options['connection'] = $this->_options['table']->getConnection();
+ $manager = Doctrine_Manager::getInstance();
+ $this->_options['connection'] = $manager->getConnectionForComponent($this->_options['table']->getComponentName());
+ $manager->bindComponent($this->_options['className'], $this->_options['connection']->getName());
}
return $result;
Note : we can't use directly |
| Comments |
| Comment by Jonathan H. Wage [ 01/Mar/10 ] |
|
Can you test this against the latest 1.2 SVN? I applied a fix to Generator.php that might help this issue. |
| Comment by Colin Darie [ 10/Mar/10 ] |
|
No, it doesn't work (with the very last revision, 7323). I basically did a $object = Doctrine::getTable('Foo')->search('bar'); and got the following error : SQLSTATE[HY000]: General error: 1 no such table: foo_index. Failing Query: "SELECT COUNT(keyword) AS relevance, id FROM foo_index WHERE keyword = ? GROUP BY id ORDER BY relevance DESC" (the first defined connection is used). Plus if it can help, I just discovered a weird thing with the (odd) code : $table = Doctrine::getTable('Foo');
$object = Doctrine::getTable('Foo')->search('bar');
returns the error : Invalid argument type. Expected instance of Doctrine_Table. |
| Comment by Jonathan H. Wage [ 11/Mar/10 ] |
|
Hi, Please re-open if you can provide some more information. The error you mentioned, please also include the stacktrace and other relevant information. The error message itself is useless Thanks, Jon |
| Comment by Colin Darie [ 11/Mar/10 ] |
|
Hi, I uploaded 2 stacktraces here (with revision 7326, in a symfony 1.4.4-dev environment) and for the simple code
The script was called by a "php /path/to/my/test" to have a more readable stacktrace, but of course in a "normal" context, the same error happens. The relevant databases.yml : test:
dbsport:
param:
dsn: 'mysql:host=localhost;dbname=db_sport'
dbclient:
param:
dsn: 'sqlite:%SF_DATA_DIR%/sqlite/db_client_test.db'
all:
dbsport:
param:
dsn: 'mysql:host=localhost;dbname=db_sport_test'
username: username
password: 'password'
dbclient:
param:
dsn: 'sqlite:%SF_DATA_DIR%/sqlite/db_client.db'
A simple switch of the connections order solves the issue, but then it's no possible to search on the dbclient because the last connection defined is used instead (unlike I told in my previous comment, it's not the first one). Thanks for your time. |
| Comment by Colin Darie [ 11/Mar/10 ] |
|
Reopen with my previous comment http://www.doctrine-project.org/jira/browse/DC-470?focusedCommentId=12091&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_12091 |
| Comment by Jonathan H. Wage [ 15/Mar/10 ] |
|
Hmm, I am not sure of the problem here. Any way to reproduce this in a Doctrine test case so that i can more clearly see the problem? |
| Comment by Colin Darie [ 16/Mar/10 ] |
|
I'm unable to write a test case for this, because the problem seems to come only when the base model class has not been loaded before to perform the search. Moreover, I found this base class is never loaded by the Doctrine::getTable('foo')->search() method, so the bindComponent() in top of the class is never executed. Here is a scenario that fails, if it can help you : Doctrine_Core::getTable('Foo')->search('bar');
-> Doctrine_Manager::getInstance()
-> getConnectionForComponent('Foo')
-> Doctrine_Core::modelsAutoload('Foo')
At this point, self::$_modelsDirectory is not setted and self::$_loadedModelFiles is empty
because no model has been loaded yet, so the BaseFoo class is not loaded and the component is not bound
-> getCurrentConnection() returns the last defined connection, not the connection for the Foo component
-> getTable('Foo') .... the following operates on the bad connection,
so the query tries to select from the foo_index table in the wrong database.
(sorry for my poor english ^^) |
| Comment by Marcel Berteler [ 06/Jul/10 ] |
|
Seems like this could be a duplicate of DC-740 and not an actual Doctrine bug but an sfDoctrinePlugin bug. |
[DC-439] Import of table with (silly) name "index" Created: 20/Jan/10 Updated: 24/Mar/11 Resolved: 09/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Import/Export |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Jochen Bayer | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
MS-SQL-Server 2005 |
||
| Description |
|
In Doctrine/Import/Mssql.php are this procedure calls: 92: $sql = 'EXEC sp_primary_keys_rowset @table_name = ' . $this->conn->quoteIdentifier($table, true); which fail with sql "keyword" table names. Using ...['. $table . ' ]' around the table name worked for me. Many thanx for all! |
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
It is hard to understand what you changed exactly. Can you provide a diff? |
| Comment by Jochen Bayer [ 09/Jun/10 ] |
|
Index: Mssql.php
@@ -216,10 +216,10 @@
$result = array(); |
| Comment by Jonathan H. Wage [ 09/Jun/10 ] |
|
You need to turn on identifier quoting. The quoteIdentifier() method takes care of this for you. http://www.doctrine-project.org/documentation/manual/1_2/en/configuration:identifier-quoting |
| Comment by Jonathan H. Wage [ 09/Jun/10 ] |
|
Fixed missed calls to quoteIdentifier(). Turn on identifier quoting for identifiers to be wrapped with [] http://www.doctrine-project.org/documentation/manual/1_2/en/configuration:identifier-quoting |
| Comment by T. B. [ 24/Mar/11 ] |
|
I have a similar problem after executing the symfony (1.4.10) build-schema task: SQLSTATE[42000]: Syntax error or access violation: 2812 [Microsoft][SQL Server Native Client 10.0][SQL Server]Could not find stored procedure 'sp_primary_keys_rowset'. (SQLExecute[2812] at ext\pdo_odbc\odbc_stmt.c:254). Failing Query: "EXEC sp_primary_keys_rowset @table_name = Appointment" I hoped ATTR_QUOTE_IDENTIFIER will also solve my problem but I get the sama error message: SQLSTATE[42000]: Syntax error or access violation: 2812 [Microsoft][SQL Server Native Client 10.0][SQL Server]Could not find stored procedure 'sp_primary_keys_rowset'. (SQLExecute[2812] at ext\pdo_odbc\odbc_stmt.c:254). Failing Query: "EXEC sp_primary_keys_rowset @table_name = [Appointment]" |
[DC-405] isset() via array interface does not "see" virtual property defined inside the model definition Created: 07/Jan/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | None |
| Affects Version/s: | 1.2.1, 1.2.2 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Lukas Kahwe | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
see: http://groups.google.com/group/doctrine-user/browse_thread/thread/0e41373cd46872a9?hl=en# The issue is caused by isset() returning false if the column is not setup via hasColumn(). The accessor method is however obviously callable on the OO interface. By preventing the isset() call in contains() we got things to work, but its obviously a hack: class entity extends base_entity { public function setUp() { parent::setUp(); $this->hasAccessor('urlizedname', 'getUrlizedname'); } public function getUrlizedid() { $type = $this->_get('type'); return $this->_get('id') - $type::$idOffset; } /** * FIXME:hackish override of the parent contains method to support array access of urlizedid, remove if possible */ public function contains($fieldName) { if ($fieldName === 'urlizedid') { return true; } return parent::contains($fieldName); } } |
| Comments |
| Comment by Jordi Boggiano [ 08/Jan/10 ] |
|
To specify a bit more, the problem is caused only if you call isset() to check if the var exists, which obviously is not really required in this case since we know it's there, but the template engine automatically adds it to prevent notices so it means we can't read the value unless we call $entity->getUrlizedid() manually, which is not so great. Other than that the arrayaccess interface still works if you don't check isset() first. |
| Comment by Jonathan H. Wage [ 01/Mar/10 ] |
|
I've been looking at this, and I don't think we have any possible fix. This is just how it is, unfortunately. Doctrine 1 FTW |
| Comment by Reza Esmaili [ 23/Mar/10 ] |
|
Why not extend contains() with this simple check? if ($this->hasAccessor($fieldName) {
return true;
}
|
| Comment by Lukas Kahwe [ 24/Mar/10 ] |
|
hmm .. that looks indeed like it would fix the issue. do not know the code in question well enough, but this should also not break anyones code .. would it? |
| Comment by Jonathan H. Wage [ 24/Mar/10 ] |
|
I think that might work. Did anyone try it out against the test suite? |
| Comment by Reza Esmaili [ 24/Mar/10 ] |
|
Tests against 1.2.1 Fails in: Tests agains the current 1.2 branch from SVN: Fails in: Those tests fail with and without the patch. |
| Comment by Jonathan H. Wage [ 29/Mar/10 ] |
|
Thanks for the issue and the idea for the patch. It seems to work fine. Hopefully it has no adverse side effects. |
| Comment by Maciej Hołyszko [ 29/Mar/10 ] |
|
Unfortunately, this patch caused problems in my case. Sample code: if($invoker->contains('resource')) { // ok, now we have value set, so do sth with it... } else { // no value set yet, do it for the first time $invoker->mapValue('resource', $someResourceValue); } Now the first condition is always met. |
| Comment by Jonathan H. Wage [ 29/Mar/10 ] |
|
I have reverted the change for now. I don't quite understand your use case. Are you setting a hasAccessor() somewhere? |
| Comment by Maciej Hołyszko [ 29/Mar/10 ] |
|
Yes, sorry I forgot to mention it, I must've thought it is obvious I have something like: Thank you for reverting that change. An author of this ticket could become angry, but I'm sorry I think we have to find another solution. |
| Comment by Reza Esmaili [ 29/Mar/10 ] |
|
Isn't contains() just for checking if the field exist - like isset()? Alternatively, we could modify the patch with something like this (untested): if ($this->hasAccessor($fieldName) && $this->$fieldName) {
return true;
}
|
| Comment by Reza Esmaili [ 29/Mar/10 ] |
|
Just checked with the code-comment of contains() in Doctrine_Record "test whether a field (column, mapped value, related component, accessor) is accessible by @see get()" So, I guess the solution to the problem is still valid because the field is accessible. I think Maciej Hołyszko's code should be updated to check for an actual value and not just for the existence of the field itself. If not - please reopen the issue. As of now it's included in the list of fixed issues of the 1.2.2 release which is no longer correct |
| Comment by Maciej Hołyszko [ 30/Mar/10 ] |
|
Ok now I see that I did not notice hasMappedValue was introduced in rev [6730], which I should use now instead of contains() in my case. |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
The new method should take care of this problem now. |
[DC-399] Doctrine_Record::unlink() causes records to be deleted Created: 05/Jan/10 Updated: 09/Jun/10 Resolved: 09/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record, Relations |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Minor |
| Reporter: | Svetoslav Shterev | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 2 |
| Labels: | None | ||
| Environment: |
Doctrine 1.2.1 svn tag checkout (http://svn.doctrine-project.org/tags/1.2.1) |
||
| Attachments: |
|
| Description |
|
Using Doctrine_Record::unlink() with a One-To-Many relationship, and then saving the record will cause the related records to be deleted. Example: You have a Discount linked to many Customers. When revoking a customer's discount, you unlink the two records. $discount->unlink("Customers", $customer->id); if you $discount->save(); afterwards, the Customer will be deleted. This seems to be caused due to unlink() not calling the collection's takeSnapshot() after removing the records, which causes the save to delete them. There didn't seem to be a clear indication in the documentation it would do this, and in version 1.0.14 it doesn't seem to do this deletion. |
| Comments |
| Comment by Svetoslav Shterev [ 05/Jan/10 ] |
|
Testcase attached |
| Comment by Jonathan H. Wage [ 01/Mar/10 ] |
|
Is this not the expected behavior? |
| Comment by Svetoslav Shterev [ 02/Mar/10 ] |
|
Re-uploaded the test case to show the actually reported bug(oops :/) unlink() even with $now=true only sets the foreign key in Phonenumber to null. But if you do unlink(), and then save(), the phonenumber is outright deleted. I'm not sure if it is expected behavior, since in the example (Discounts and Customers), it deletes the customer record outright |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
Can you provide a patch? All i see is the full UnitOfWork file and I believe it has changed since you attached your version. |
| Comment by Steffen Zeidler [ 09/Jun/10 ] |
|
patch for UnitOfWork.php |
| Comment by Jonathan H. Wage [ 09/Jun/10 ] |
|
Thanks for the issue, patch and test case! All looks good and I committed it! |
[DC-391] Connection rollback exception hides real exception Created: 01/Jan/10 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection |
| Affects Version/s: | 1.1.6 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | Alessandro Vermeulen | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Description |
|
Same issue as http://www.doctrine-project.org/jira/browse/DC-207 but also for the 1.1 branch. This is not only an issue when trying to talk to non-existing databases but for any exception thrown by the connection. UnitOfWork.php:133 } catch (Exception $e) {
// Make sure we roll back our internal transaction
//$record->state($state);
$conn->rollback();
throw $e;
}
|
| Comments |
| Comment by Jonathan H. Wage [ 08/Jun/10 ] |
|
This is already fixed in Doctrine 1.2 |
[DC-364] Adding the fieldName to mutator/accessor methods Created: 16/Dec/09 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Record |
| Affects Version/s: | 1.2.1 |
| Fix Version/s: | 1.2.3 |
| Type: | Improvement | Priority: | Minor |
| Reporter: | Carl Alexander | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 1 |
| Labels: | None | ||
| Attachments: |
|
| Description |
|
Would it be possible to add the fieldName to the function variables when a mutator or accessor function is called. This would allow for more generalized accessor / mutator functions that can be reused easily. example: public function mutatorFunction($value, $load, $fieldName) Thank you |
| Comments |
| Comment by Jonathan H. Wage [ 01/Mar/10 ] |
|
I don't understand the issue. Can you please provide more valid information, detailed descriptions, sample code, test cases, etc. |
| Comment by Andrew Coulton [ 05/Apr/10 ] |
|
I believe the issue is that there may be logic common to mutators across a whole record (or indeed a set of records). A classic example would be date fields. Doctrine uses the ISO 8601 format for dates entirely, but often we want a date field to be accessible directly as a DateTime object or a UNIX timestamp to allow use in date formatting functions without additional strtotime overhead. Or we might want the application to automatically convert a user-provided date from a locale based format to ISO 8601. The current pattern requires a custom accessor/mutator to each field - therefore a record with three date fields would have three accessors and mutators. I believe the OP's suggestion is that the fieldName be added as a parameter to the call to the accessor/mutator so that a single accessor/mutator pair could serve multiple fields. Indeed, we could then write a behaviour that includes the accessor/mutator and registers it on all date fields, or whatever other recurring field type/name/etc we have in the application. As PHP will allow a function to be called with additional arguments, I believe all that's needed is to add the parameter to the calls in Doctrine_Record::set and Doctrine_Record::get as per the attached patch. Mutators/Accessors that care would then be able to access the field name as a second parameter, but without breaking any existing code. |
| Comment by Andrew Coulton [ 05/Apr/10 ] |
|
Suggested patch to Doctrine_Record to implement this improvement |
| Comment by Carl Alexander [ 06/Apr/10 ] |
|
Sorry meant to get back to you about this sooner. What Andrew has described is exactly the issue I am having and his suggested patch is what I would hope to see implemented. |
[DC-363] Multiple connections and i18n Created: 16/Dec/09 Updated: 28/Mar/12 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection, I18n |
| Affects Version/s: | 1.0.14, 1.2.2, 1.2.3 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Blocker |
| Reporter: | Xav. | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 1 |
| Labels: | None | ||
| Environment: |
MySQL 5.1.37 symfony 1.4.13 |
||
| Description |
|
I used to work with a single database named "doctrine". The query was working properly. I then decided to use 2 databases so I got my schema like this:
I did setup my connections in config/databases.yml this way:
build-model, build-forms, build-filters and cc got ran. But now, I got an exception saying the "Translation" relation doesn't exist. The Base Models include correctly the bindComponent line:
For now, I managed to kind of fixing it with simply swapping the databases order in my config/databases.yml and it's now working again perfectly. I forgot to mention that in the CategoryTable when i call $this->getConnection()->getName(), it outputs "second" |
| Comments |
| Comment by Colin Darie [ 04/Feb/10 ] |
|
I'm experiencing the same issue with 4 connections. The I18n behavior is almost unusable for models whose connection is not the last one defined. I searched for an acceptable solution but I haven't found one (IMHO the setting to the right connection before each query is not acceptable in large projects). I tried to do like in Search behavior, but it didn't work, I doesn't know enough doctrine internals to understand why. |
| Comment by Jonathan H. Wage [ 01/Mar/10 ] |
|
Can you test this in Doctrine 1.2? I believe it is fixed. |
| Comment by Georg [ 17/Feb/11 ] |
|
I am using the latest symfony 1.4 which is doctrine 1.2.3 afaik, and this bug still exists. actAs: and the resulting model base class is not bound to the correct connection. |
| Comment by Joe Siponen [ 27/May/11 ] |
|
I've just now battled with the very same problem in Doctrine 1.2 (the version bundled with symfony 1.4) and the problem seems to be caused by the fact that Doctrine_Record_Generator simply isn't written such that it is able to reinitialize generators for unloaded table instances after a connection is closed. This problem also manifests itself after a table has been loaded in a connection and one tries retrieve a table again after Doctrine_Connection->evictTables() has been called. This makes it impossible to to open more than one connection at a time in a request/script when using behaviors that dynamically modify table instances (such as the i18n behavior). The issue states that this has been fixed but I looked at the latest code and the problem still seems to be very much the same. Doctrine_Record_Generator determines if it needs to run its initialization methods simply by checking if the to-be generated class, as defined by the className option, exists using a class_exists call. This means that the first time this method is called the initialization happens but for every subsequent call no initialization is made. Now, in the i18m behavior, the important initialization happens in its setTableDefinition method in which it removes any of the translated fields from the table instance that is been setup and redefines them as relations on the to-be-created Translation class. It then finishes off by dynamically declaring the new class for the translation record using its generateClassFromTable method. Thus, the first time everything goes smoothly and the i18n generator's setTableDefinition is called and the table instance is properly initialized. Everything will now work as expected while the current connection is open since the connection instance keeps the i18n modified table instances alive and well for callers. But, when the current connection is closed the i18n modified table instances it holds are also removed (goes out of scope). Then, when a new connection is opened, this new connection will start without having any table instances. This means that the next time one asks the new connection for a table instance of the same class with the i18n behavior the i18n behaviors will fail to initialize because the generator at this time believes its class has actually been initialized which, in turn, means that the table using the i18n behavior isn't properly initialized. No initialization means that this table will now include the non-existant i18n fields in the select part of its queries (those are in the translation table) causing those queries to fail miserably. I believe this could be fixed by adding a static attribute to Doctrine_Record_Generator that tracks the spl_object_hash of the underlying dbh instance variable of the doctrine connection of the table parameter. If the hash is the same the next time that the initialize method is called the generator can decide not to reinitialize itself but if it detects that the hash of the current connection is different then that is definitely a clue to the generator that it needs to reinitialize itself (i.e. run all of the initialization methods but generateClassFromTable which should't be called more than once). Maybe do it like this perhaps:
abstract class Doctrine_Record_Generator extends Doctrine_Record_Abstract
{
public function initialize(Doctrine_Table $table)
{
/* ... */
$currentConnectionHash = spl_object_hash($table->getConnection()->getDbh());
//Next part is called if this is the first connection made or if this is a new open connection with new table instances
if ($currentConnectionHash != self::$lastConnectionHash)
{
self::$lastConnectionHash = $currentConnectionHash;
$this->buildTable();
$fk = $this->buildForeignKeys($this->_options['table']);
$this->_table->setColumns($fk);
$this->buildRelation();
$this->setTableDefinition();
$this->setUp();
if ($this->_options['generateFiles'] === false && class_exists($this->_options['className'])) {
$this->generateClassFromTable($this->_table); //Don't generate the class more than once ever
}
$this->buildChildDefinitions();
$this->_table->initIdentifier();
}
}
}
|
| Comment by James Bell [ 23/Aug/11 ] |
|
I'm also experiencing this issue, using the stable version of Symfony 1.4.13. If I define multiple database connections, the i18n Translation relations fail with this call: Doctrine_Relation_Parser->getRelation('Translation', ) 'Unknown relation alias Translation' dev: mysql2: postgresql: In this case, the primary connection is the postgresql one, and that is where the i18n behaviour is defined: Category: I tried to implement the suggest above (ie adding a static hash of the database handle to the Doctrine_Record_Generator class file, which does clear out the connections. However, I then have difficulty with Doctrine recognizing CategoryTranslation as a class: "( ! ) Fatal error: Class 'CategoryTranslation' not found in /sitename/lib/vendor/symfony/lib/plugins/sfDoctrinePlugin/lib/vendor/doctrine/Doctrine/Table.php on line 545" Is there anything else I can to do test/fix this? |
| Comment by Joe Siponen [ 23/Aug/11 ] |
|
This is a duplicate of http://www.doctrine-project.org/jira/browse/DC-373. I've also added a failing test case to that issue that should reproduce the issue as described here. |
| Comment by Andy.L [ 28/Mar/12 ] |
|
still exists in 1.2.4 |
[DC-349] Issue with quoting of booleans in Oracle Created: 10/Dec/09 Updated: 08/Jun/10 Resolved: 08/Jun/10 |
|
| Status: | Resolved |
| Project: | Doctrine 1 |
| Component/s: | Connection |
| Affects Version/s: | 1.0.14 |
| Fix Version/s: | 1.2.3 |
| Type: | Bug | Priority: | Major |
| Reporter: | David Brewer | Assignee: | Jonathan H. Wage |
| Resolution: | Fixed | Votes: | 0 |
| Labels: | None | ||
| Environment: |
Oracle Driver |
||
| Description |
|
I've run into a problem where I'm unable to set a default value for a boolean field when creating Oracle tables using Doctrine. The summary of this issue is that when Doctrine quotes a '0' or false value for a boolean field, it comes back blank... which doesn't work as an option for the DEFAULT parameter of field definitions in table creation. Here are the hairy details. I have a schema which looks something like this (simplified for the example): User:
tableName: users
columns:
id: {type: integer, primary: true, autoincrement: true}
username: {type: string, length: 255, unique: true, notnull: true}
disabled: {type: boolean, default: false}
When I export the table, it's generating invalid DDL like this: CREATE TABLE users (id NUMBER(8), username VARCHAR2(255) NOT NULL UNIQUE, disabled NUMBER(1) DEFAULT , PRIMARY KEY(id)) The important part of this is the DEFAULT parameter on the 'disabled' field definition – it's missing the value. I tracked all this down to an issue in Doctrine_Formatter->quote(), which gets called when the DDL is being created. When passed '0' as its input with a type of boolean, it apparently returns the empty string. This may ultimately be a bug in the PDO_OCI driver, because the quote method is relying on the quote method of the database handle. I've hacked around this by overriding the behaviour of the quote method in Doctrine_Connection_Oracle. I did this by adding the following method: /**
* Override quote behaviour for boolean to fix issues with quoting of
* boolean values.
*/
public function quote($input, $type = null)
{
if ($type === 'boolean') {
if ($input === null) {
return null;
} else {
return (($input) ? 1 : 0);
}
} else {
return parent::quote($input, $type);
}
}
It seems to work for me, but I'd appreciate it if wiser heads than me would take a look and see if this is a sign of some larger issue, and if the way I've fixed it seems appropriate. I also am unsure of how/if this needs to propogate to newer versions. |