From c65f997731d3036ddf1ea35af6657d656e79ace9 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Mon, 14 Jan 2013 17:46:55 +0100 Subject: [PATCH 01/11] Add Doctrine DBAL and dependencies --- doctrine/common/.gitignore | 4 + doctrine/common/.gitmodules | 3 + doctrine/common/.travis.yml | 10 + doctrine/common/LICENSE | 19 + doctrine/common/README.md | 12 + doctrine/common/UPGRADE_TO_2_1 | 38 + doctrine/common/UPGRADE_TO_2_2 | 61 + doctrine/common/bin/travis-setup.php | 141 + doctrine/common/build.properties | 6 + doctrine/common/build.xml | 59 + doctrine/common/composer.json | 26 + .../Common/Annotations/Annotation.php | 79 + .../Annotations/Annotation/Attribute.php | 47 + .../Annotations/Annotation/Attributes.php | 37 + .../Annotation/IgnoreAnnotation.php | 54 + .../Annotations/Annotation/Required.php | 33 + .../Common/Annotations/Annotation/Target.php | 107 + .../Annotations/AnnotationException.php | 127 + .../Common/Annotations/AnnotationReader.php | 310 ++ .../Common/Annotations/AnnotationRegistry.php | 139 + .../Common/Annotations/CachedReader.php | 250 ++ .../Doctrine/Common/Annotations/DocLexer.php | 132 + .../Doctrine/Common/Annotations/DocParser.php | 988 ++++++ .../Common/Annotations/FileCacheReader.php | 258 ++ .../Common/Annotations/IndexedReader.php | 141 + .../Doctrine/Common/Annotations/PhpParser.php | 80 + .../Doctrine/Common/Annotations/Reader.php | 67 + .../Annotations/SimpleAnnotationReader.php | 157 + .../Common/Annotations/TokenParser.php | 175 + .../lib/Doctrine/Common/Cache/ApcCache.php | 93 + .../lib/Doctrine/Common/Cache/ArrayCache.php | 96 + .../lib/Doctrine/Common/Cache/Cache.php | 102 + .../Doctrine/Common/Cache/CacheProvider.php | 231 ++ .../lib/Doctrine/Common/Cache/FileCache.php | 132 + .../Doctrine/Common/Cache/FilesystemCache.php | 114 + .../Doctrine/Common/Cache/MemcacheCache.php | 121 + .../Doctrine/Common/Cache/MemcachedCache.php | 124 + .../Doctrine/Common/Cache/PhpFileCache.php | 108 + .../lib/Doctrine/Common/Cache/RedisCache.php | 119 + .../Doctrine/Common/Cache/WinCacheCache.php | 93 + .../lib/Doctrine/Common/Cache/XcacheCache.php | 110 + .../Doctrine/Common/Cache/ZendDataCache.php | 84 + .../lib/Doctrine/Common/ClassLoader.php | 263 ++ .../Common/Collections/ArrayCollection.php | 499 +++ .../Common/Collections/Collection.php | 243 ++ .../Doctrine/Common/Collections/Criteria.php | 239 ++ .../Expr/ClosureExpressionVisitor.php | 194 ++ .../Common/Collections/Expr/Comparison.php | 74 + .../Collections/Expr/CompositeExpression.php | 71 + .../Common/Collections/Expr/Expression.php | 30 + .../Collections/Expr/ExpressionVisitor.php | 80 + .../Common/Collections/Expr/Value.php | 40 + .../Common/Collections/ExpressionBuilder.php | 148 + .../Common/Collections/Selectable.php | 47 + .../lib/Doctrine/Common/CommonException.php | 28 + .../common/lib/Doctrine/Common/Comparable.php | 48 + .../common/lib/Doctrine/Common/EventArgs.php | 67 + .../lib/Doctrine/Common/EventManager.php | 147 + .../lib/Doctrine/Common/EventSubscriber.php | 45 + doctrine/common/lib/Doctrine/Common/Lexer.php | 266 ++ .../Doctrine/Common/NotifyPropertyChanged.php | 44 + .../Persistence/AbstractManagerRegistry.php | 259 ++ .../Common/Persistence/ConnectionRegistry.php | 63 + .../Persistence/Event/LifecycleEventArgs.php | 77 + .../Event/LoadClassMetadataEventArgs.php | 75 + .../Persistence/Event/ManagerEventArgs.php | 59 + .../Persistence/Event/OnClearEventArgs.php | 84 + .../Persistence/Event/PreUpdateEventArgs.php | 132 + .../Common/Persistence/ManagerRegistry.php | 112 + .../Mapping/AbstractClassMetadataFactory.php | 383 +++ .../Persistence/Mapping/ClassMetadata.php | 165 + .../Mapping/ClassMetadataFactory.php | 74 + .../Mapping/Driver/AnnotationDriver.php | 214 ++ .../Mapping/Driver/DefaultFileLocator.php | 170 + .../Persistence/Mapping/Driver/FileDriver.php | 214 ++ .../Mapping/Driver/FileLocator.php | 71 + .../Mapping/Driver/MappingDriver.php | 56 + .../Mapping/Driver/MappingDriverChain.php | 168 + .../Persistence/Mapping/Driver/PHPDriver.php | 72 + .../Mapping/Driver/StaticPHPDriver.php | 141 + .../Mapping/Driver/SymfonyFileLocator.php | 214 ++ .../Persistence/Mapping/MappingException.php | 86 + .../Persistence/Mapping/ReflectionService.php | 79 + .../Mapping/RuntimeReflectionService.php | 101 + .../Mapping/StaticReflectionService.php | 106 + .../Common/Persistence/ObjectManager.php | 152 + .../Common/Persistence/ObjectManagerAware.php | 49 + .../Common/Persistence/ObjectRepository.php | 78 + .../Common/Persistence/PersistentObject.php | 244 ++ .../lib/Doctrine/Common/Persistence/Proxy.php | 60 + .../Common/PropertyChangedListener.php | 47 + .../Reflection/ClassFinderInterface.php | 38 + .../Common/Reflection/Psr0FindFile.php | 83 + .../ReflectionProviderInterface.php | 45 + .../Reflection/StaticReflectionClass.php | 112 + .../Reflection/StaticReflectionMethod.php | 103 + .../Reflection/StaticReflectionParser.php | 282 ++ .../Reflection/StaticReflectionProperty.php | 77 + .../lib/Doctrine/Common/Util/ClassUtils.php | 103 + .../common/lib/Doctrine/Common/Util/Debug.php | 135 + .../lib/Doctrine/Common/Util/Inflector.php | 72 + .../common/lib/Doctrine/Common/Version.php | 55 + doctrine/common/phpunit.xml.dist | 31 + doctrine/common/tests/.gitignore | 3 + .../Common/Annotations/AbstractReaderTest.php | 517 +++ .../Annotations/AnnotationReaderTest.php | 13 + .../Common/Annotations/CachedReaderTest.php | 56 + .../Tests/Common/Annotations/DocLexerTest.php | 137 + .../Common/Annotations/DocParserTest.php | 1208 +++++++ .../Tests/Common/Annotations/DummyClass.php | 48 + .../Annotations/FileCacheReaderTest.php | 40 + .../Annotation/AnnotWithDefaultValue.php | 10 + .../Fixtures/Annotation/Autoload.php | 10 + .../Annotations/Fixtures/Annotation/Route.php | 11 + .../Fixtures/Annotation/Secure.php | 18 + .../Fixtures/Annotation/Template.php | 14 + .../Fixtures/Annotation/Version.php | 11 + .../Fixtures/AnnotationTargetAll.php | 14 + .../Fixtures/AnnotationTargetAnnotation.php | 14 + .../Fixtures/AnnotationTargetClass.php | 15 + .../Fixtures/AnnotationTargetMethod.php | 15 + .../AnnotationTargetPropertyMethod.php | 14 + .../Fixtures/AnnotationWithAttributes.php | 119 + .../Fixtures/AnnotationWithConstants.php | 20 + .../AnnotationWithRequiredAttributes.php | 50 + ...ithRequiredAttributesWithoutContructor.php | 24 + .../AnnotationWithTargetSyntaxError.php | 11 + .../Fixtures/AnnotationWithVarType.php | 62 + .../Annotations/Fixtures/ClassDDC1660.php | 30 + ...assWithAnnotationWithTargetSyntaxError.php | 21 + .../ClassWithAnnotationWithVarType.php | 31 + .../Annotations/Fixtures/ClassWithClosure.php | 52 + .../Fixtures/ClassWithConstants.php | 10 + .../ClassWithFullyQualifiedUseStatements.php | 11 + ...lassWithInvalidAnnotationTargetAtClass.php | 17 + ...assWithInvalidAnnotationTargetAtMethod.php | 20 + ...sWithInvalidAnnotationTargetAtProperty.php | 24 + .../ClassWithValidAnnotationTarget.php | 41 + .../Annotations/Fixtures/Controller.php | 300 ++ ...erentNamespacesPerFileWithClassAsFirst.php | 15 + ...ferentNamespacesPerFileWithClassAsLast.php | 15 + ...EqualNamespacesPerFileWithClassAsFirst.php | 13 + .../EqualNamespacesPerFileWithClassAsLast.php | 12 + ...lobalNamespacesPerFileWithClassAsFirst.php | 12 + ...GlobalNamespacesPerFileWithClassAsLast.php | 12 + .../Fixtures/IntefaceWithConstants.php | 10 + .../InvalidAnnotationUsageButIgnoredClass.php | 14 + .../Fixtures/InvalidAnnotationUsageClass.php | 10 + .../Fixtures/MultipleClassesInFile.php | 9 + .../MultipleImportsInUseStatement.php | 10 + .../NamespaceAndClassCommentedOut.php | 20 + .../NamespaceWithClosureDeclaration.php | 15 + .../Fixtures/NamespacedSingleClassLOC1000.php | 1009 ++++++ .../Annotations/Fixtures/NoAnnotation.php | 5 + .../Fixtures/NonNamespacedClass.php | 10 + .../Fixtures/SingleClassLOC1000.php | 1006 ++++++ .../Annotations/Fixtures/TestInterface.php | 13 + .../Common/Annotations/PerformanceTest.php | 194 ++ .../Common/Annotations/PhpParserTest.php | 194 ++ .../SimpleAnnotationReaderTest.php | 97 + .../Common/Annotations/Ticket/DCOM55Test.php | 65 + .../Annotations/Ticket/DCOM58Entity.php | 8 + .../Common/Annotations/Ticket/DCOM58Test.php | 112 + .../Common/Annotations/TopLevelAnnotation.php | 8 + .../Tests/Common/Cache/ApcCacheTest.php | 20 + .../Tests/Common/Cache/ArrayCacheTest.php | 21 + .../Doctrine/Tests/Common/Cache/CacheTest.php | 91 + .../Common/Cache/FilesystemCacheTest.php | 97 + .../Tests/Common/Cache/MemcacheCacheTest.php | 45 + .../Tests/Common/Cache/MemcachedCacheTest.php | 48 + .../Tests/Common/Cache/PhpFileCacheTest.php | 149 + .../Tests/Common/Cache/RedisCacheTest.php | 30 + .../Tests/Common/Cache/WinCacheCacheTest.php | 20 + .../Tests/Common/Cache/XcacheCacheTest.php | 20 + .../Tests/Common/Cache/ZendDataCacheTest.php | 28 + .../Doctrine/Tests/Common/ClassLoaderTest.php | 45 + .../Common/ClassLoaderTest/ClassA.class.php | 6 + .../Common/ClassLoaderTest/ClassB.class.php | 6 + .../Common/ClassLoaderTest/ClassC.class.php | 6 + .../Tests/Common/ClassLoaderTest/ClassD.php | 5 + .../ClosureExpressionVisitorTest.php | 197 ++ .../Common/Collections/CollectionTest.php | 251 ++ .../Tests/Common/Collections/CriteriaTest.php | 82 + .../Collections/ExpressionBuilderTest.php | 113 + .../Tests/Common/DoctrineExceptionTest.php | 0 .../Tests/Common/EventManagerTest.php | 88 + .../Persistence/Mapping/ChainDriverTest.php | 130 + .../Mapping/ClassMetadataFactoryTest.php | 139 + .../Mapping/DefaultFileLocatorTest.php | 90 + .../Persistence/Mapping/FileDriverTest.php | 142 + .../Persistence/Mapping/PHPDriverTest.php | 18 + .../Mapping/RuntimeReflectionServiceTest.php | 69 + .../Mapping/StaticPHPDriverTest.php | 35 + .../Mapping/StaticReflectionServiceTest.php | 69 + .../Mapping/SymfonyFileLocatorTest.php | 88 + .../Persistence/Mapping/_files/TestEntity.php | 3 + .../Persistence/Mapping/_files/global.yml | 1 + .../Persistence/Mapping/_files/stdClass.yml | 1 + .../Persistence/PersistentObjectTest.php | 247 ++ .../Reflection/DeeperNamespaceParent.php | 7 + .../Common/Reflection/Dummies/NoParent.php | 8 + .../Reflection/FullyClassifiedParent.php | 7 + .../Tests/Common/Reflection/NoParent.php | 8 + .../Common/Reflection/SameNamespaceParent.php | 7 + .../Reflection/StaticReflectionParserTest.php | 45 + .../Tests/Common/Reflection/UseParent.php | 9 + .../Tests/Common/Util/ClassUtilsTest.php | 100 + .../Doctrine/Tests/Common/Util/DebugTest.php | 27 + .../tests/Doctrine/Tests/DoctrineTestCase.php | 10 + .../common/tests/Doctrine/Tests/TestInit.php | 31 + doctrine/common/tests/NativePhpunitTask.php | 246 ++ doctrine/common/tests/README.markdown | 27 + doctrine/dbal/.gitignore | 6 + doctrine/dbal/.gitmodules | 9 + doctrine/dbal/.travis.yml | 21 + doctrine/dbal/LICENSE | 19 + doctrine/dbal/README.md | 14 + doctrine/dbal/UPGRADE | 148 + doctrine/dbal/bin/doctrine-dbal | 4 + doctrine/dbal/bin/doctrine-dbal.php | 43 + doctrine/dbal/bin/doctrine.php | 42 + doctrine/dbal/build.properties | 10 + doctrine/dbal/build.xml | 91 + doctrine/dbal/composer.json | 26 + .../dbal/docs/design/AZURE_FEDERATIONS.md | 94 + doctrine/dbal/docs/design/SHARDING.md | 74 + .../dbal/docs/examples/sharding/README.md | 25 + .../dbal/docs/examples/sharding/bootstrap.php | 25 + .../dbal/docs/examples/sharding/composer.json | 6 + .../docs/examples/sharding/create_schema.php | 50 + .../docs/examples/sharding/insert_data.php | 132 + .../sharding/insert_data_aftersplit.php | 27 + .../examples/sharding/query_filtering_off.php | 8 + .../examples/sharding/query_filtering_on.php | 9 + .../examples/sharding/split_federation.php | 5 + .../sharding/view_federation_members.php | 8 + .../Doctrine/DBAL/Cache/ArrayStatement.php | 103 + .../Doctrine/DBAL/Cache/CacheException.php | 37 + .../Doctrine/DBAL/Cache/QueryCacheProfile.php | 131 + .../DBAL/Cache/ResultCacheStatement.php | 239 ++ .../dbal/lib/Doctrine/DBAL/Configuration.php | 113 + .../dbal/lib/Doctrine/DBAL/Connection.php | 1308 ++++++++ .../lib/Doctrine/DBAL/ConnectionException.php | 54 + .../Connections/MasterSlaveConnection.php | 353 ++ .../dbal/lib/Doctrine/DBAL/DBALException.php | 106 + doctrine/dbal/lib/Doctrine/DBAL/Driver.php | 72 + .../lib/Doctrine/DBAL/Driver/Connection.php | 42 + .../Driver/DrizzlePDOMySql/Connection.php | 41 + .../DBAL/Driver/DrizzlePDOMySql/Driver.php | 99 + .../DBAL/Driver/IBMDB2/DB2Connection.php | 115 + .../Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php | 111 + .../DBAL/Driver/IBMDB2/DB2Exception.php | 27 + .../DBAL/Driver/IBMDB2/DB2Statement.php | 214 ++ .../Doctrine/DBAL/Driver/Mysqli/Driver.php | 69 + .../DBAL/Driver/Mysqli/MysqliConnection.php | 146 + .../DBAL/Driver/Mysqli/MysqliException.php | 26 + .../DBAL/Driver/Mysqli/MysqliStatement.php | 342 ++ .../lib/Doctrine/DBAL/Driver/OCI8/Driver.php | 99 + .../DBAL/Driver/OCI8/OCI8Connection.php | 200 ++ .../DBAL/Driver/OCI8/OCI8Exception.php | 30 + .../DBAL/Driver/OCI8/OCI8Statement.php | 268 ++ .../Doctrine/DBAL/Driver/PDOConnection.php | 40 + .../Doctrine/DBAL/Driver/PDOIbm/Driver.php | 126 + .../Doctrine/DBAL/Driver/PDOMySql/Driver.php | 102 + .../Doctrine/DBAL/Driver/PDOOracle/Driver.php | 98 + .../Doctrine/DBAL/Driver/PDOPgSql/Driver.php | 70 + .../Doctrine/DBAL/Driver/PDOSqlite/Driver.php | 116 + .../DBAL/Driver/PDOSqlsrv/Connection.php | 45 + .../Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php | 87 + .../lib/Doctrine/DBAL/Driver/PDOStatement.php | 50 + .../Doctrine/DBAL/Driver/ResultStatement.php | 92 + .../Doctrine/DBAL/Driver/SQLSrv/Driver.php | 71 + .../DBAL/Driver/SQLSrv/LastInsertId.php | 41 + .../DBAL/Driver/SQLSrv/SQLSrvConnection.php | 160 + .../DBAL/Driver/SQLSrv/SQLSrvException.php | 42 + .../DBAL/Driver/SQLSrv/SQLSrvStatement.php | 250 ++ .../lib/Doctrine/DBAL/Driver/Statement.php | 125 + .../dbal/lib/Doctrine/DBAL/DriverManager.php | 176 + .../DBAL/Event/ConnectionEventArgs.php | 79 + .../DBAL/Event/Listeners/MysqlSessionInit.php | 74 + .../Event/Listeners/OracleSessionInit.php | 80 + .../DBAL/Event/Listeners/SQLSessionInit.php | 63 + .../SchemaAlterTableAddColumnEventArgs.php | 114 + .../SchemaAlterTableChangeColumnEventArgs.php | 114 + .../DBAL/Event/SchemaAlterTableEventArgs.php | 99 + .../SchemaAlterTableRemoveColumnEventArgs.php | 114 + .../SchemaAlterTableRenameColumnEventArgs.php | 129 + .../Event/SchemaColumnDefinitionEventArgs.php | 137 + .../SchemaCreateTableColumnEventArgs.php | 114 + .../DBAL/Event/SchemaCreateTableEventArgs.php | 128 + .../DBAL/Event/SchemaDropTableEventArgs.php | 98 + .../Doctrine/DBAL/Event/SchemaEventArgs.php | 56 + .../Event/SchemaIndexDefinitionEventArgs.php | 122 + doctrine/dbal/lib/Doctrine/DBAL/Events.php | 48 + .../lib/Doctrine/DBAL/Id/TableGenerator.php | 159 + .../DBAL/Id/TableGeneratorSchemaVisitor.php | 89 + doctrine/dbal/lib/Doctrine/DBAL/LockMode.php | 42 + .../lib/Doctrine/DBAL/Logging/DebugStack.php | 68 + .../Doctrine/DBAL/Logging/EchoSQLLogger.php | 61 + .../lib/Doctrine/DBAL/Logging/LoggerChain.php | 63 + .../lib/Doctrine/DBAL/Logging/SQLLogger.php | 54 + .../DBAL/Platforms/AbstractPlatform.php | 2857 +++++++++++++++++ .../Doctrine/DBAL/Platforms/DB2Platform.php | 545 ++++ .../DBAL/Platforms/DrizzlePlatform.php | 495 +++ .../DBAL/Platforms/Keywords/DB2Keywords.php | 437 +++ .../Platforms/Keywords/DrizzleKeywords.php | 340 ++ .../DBAL/Platforms/Keywords/KeywordList.php | 63 + .../DBAL/Platforms/Keywords/MsSQLKeywords.php | 243 ++ .../DBAL/Platforms/Keywords/MySQLKeywords.php | 268 ++ .../Platforms/Keywords/OracleKeywords.php | 156 + .../Platforms/Keywords/PostgreSQLKeywords.php | 131 + .../Keywords/ReservedKeywordsValidator.php | 116 + .../Platforms/Keywords/SQLiteKeywords.php | 164 + .../Doctrine/DBAL/Platforms/MySqlPlatform.php | 722 +++++ .../DBAL/Platforms/OraclePlatform.php | 822 +++++ .../DBAL/Platforms/PostgreSqlPlatform.php | 767 +++++ .../DBAL/Platforms/SQLAzurePlatform.php | 50 + .../DBAL/Platforms/SQLServer2005Platform.php | 53 + .../DBAL/Platforms/SQLServer2008Platform.php | 100 + .../DBAL/Platforms/SQLServerPlatform.php | 927 ++++++ .../DBAL/Platforms/SqlitePlatform.php | 529 +++ .../Doctrine/DBAL/Portability/Connection.php | 119 + .../Doctrine/DBAL/Portability/Statement.php | 195 ++ .../Query/Expression/CompositeExpression.php | 130 + .../Query/Expression/ExpressionBuilder.php | 264 ++ .../lib/Doctrine/DBAL/Query/QueryBuilder.php | 1087 +++++++ .../Doctrine/DBAL/Query/QueryException.php | 40 + .../dbal/lib/Doctrine/DBAL/README.markdown | 0 .../dbal/lib/Doctrine/DBAL/SQLParserUtils.php | 183 ++ .../Doctrine/DBAL/Schema/AbstractAsset.php | 214 ++ .../DBAL/Schema/AbstractSchemaManager.php | 896 ++++++ .../dbal/lib/Doctrine/DBAL/Schema/Column.php | 423 +++ .../lib/Doctrine/DBAL/Schema/ColumnDiff.php | 58 + .../lib/Doctrine/DBAL/Schema/Comparator.php | 427 +++ .../lib/Doctrine/DBAL/Schema/Constraint.php | 42 + .../Doctrine/DBAL/Schema/DB2SchemaManager.php | 214 ++ .../DBAL/Schema/DrizzleSchemaManager.php | 95 + .../DBAL/Schema/ForeignKeyConstraint.php | 193 ++ .../dbal/lib/Doctrine/DBAL/Schema/Index.php | 241 ++ .../DBAL/Schema/MySqlSchemaManager.php | 208 ++ .../DBAL/Schema/OracleSchemaManager.php | 286 ++ .../DBAL/Schema/PostgreSqlSchemaManager.php | 359 +++ .../DBAL/Schema/SQLServerSchemaManager.php | 263 ++ .../dbal/lib/Doctrine/DBAL/Schema/Schema.php | 373 +++ .../lib/Doctrine/DBAL/Schema/SchemaConfig.php | 119 + .../lib/Doctrine/DBAL/Schema/SchemaDiff.php | 176 + .../Doctrine/DBAL/Schema/SchemaException.php | 126 + .../lib/Doctrine/DBAL/Schema/Sequence.php | 120 + .../DBAL/Schema/SqliteSchemaManager.php | 190 ++ .../AbstractSchemaSynchronizer.php | 57 + .../Synchronizer/SchemaSynchronizer.php | 95 + .../SingleDatabaseSynchronizer.php | 196 ++ .../dbal/lib/Doctrine/DBAL/Schema/Table.php | 678 ++++ .../lib/Doctrine/DBAL/Schema/TableDiff.php | 136 + .../dbal/lib/Doctrine/DBAL/Schema/View.php | 53 + .../Visitor/CreateSchemaSqlCollector.php | 178 + .../Schema/Visitor/DropSchemaSqlCollector.php | 160 + .../Doctrine/DBAL/Schema/Visitor/Graphviz.php | 151 + .../Schema/Visitor/RemoveNamespacedAssets.php | 113 + .../Doctrine/DBAL/Schema/Visitor/Visitor.php | 75 + .../DBAL/Sharding/PoolingShardConnection.php | 200 ++ .../DBAL/Sharding/PoolingShardManager.php | 97 + .../SQLAzureFederationsSynchronizer.php | 295 ++ .../SQLAzure/SQLAzureShardManager.php | 237 ++ .../SQLAzure/Schema/MultiTenantVisitor.php | 160 + .../ShardChoser/MultiTenantShardChoser.php | 36 + .../DBAL/Sharding/ShardChoser/ShardChoser.php | 40 + .../Doctrine/DBAL/Sharding/ShardManager.php | 94 + .../DBAL/Sharding/ShardingException.php | 60 + doctrine/dbal/lib/Doctrine/DBAL/Statement.php | 264 ++ .../Tools/Console/Command/ImportCommand.php | 124 + .../Console/Command/ReservedWordsCommand.php | 133 + .../Tools/Console/Command/RunSqlCommand.php | 87 + .../Tools/Console/Helper/ConnectionHelper.php | 74 + .../lib/Doctrine/DBAL/Types/ArrayType.php | 64 + .../lib/Doctrine/DBAL/Types/BigIntType.php | 56 + .../dbal/lib/Doctrine/DBAL/Types/BlobType.php | 67 + .../lib/Doctrine/DBAL/Types/BooleanType.php | 57 + .../DBAL/Types/ConversionException.php | 65 + .../lib/Doctrine/DBAL/Types/DateTimeType.php | 59 + .../Doctrine/DBAL/Types/DateTimeTzType.php | 79 + .../dbal/lib/Doctrine/DBAL/Types/DateType.php | 59 + .../lib/Doctrine/DBAL/Types/DecimalType.php | 45 + .../lib/Doctrine/DBAL/Types/FloatType.php | 54 + .../dbal/lib/Doctrine/DBAL/Types/GuidType.php | 41 + .../lib/Doctrine/DBAL/Types/IntegerType.php | 53 + .../lib/Doctrine/DBAL/Types/JsonArrayType.php | 66 + .../lib/Doctrine/DBAL/Types/ObjectType.php | 64 + .../Doctrine/DBAL/Types/SimpleArrayType.php | 69 + .../lib/Doctrine/DBAL/Types/SmallIntType.php | 52 + .../lib/Doctrine/DBAL/Types/StringType.php | 50 + .../dbal/lib/Doctrine/DBAL/Types/TextType.php | 56 + .../dbal/lib/Doctrine/DBAL/Types/TimeType.php | 68 + .../dbal/lib/Doctrine/DBAL/Types/Type.php | 305 ++ .../Doctrine/DBAL/Types/VarDateTimeType.php | 60 + doctrine/dbal/lib/Doctrine/DBAL/Version.php | 55 + doctrine/dbal/phpunit.xml.dist | 51 + doctrine/dbal/run-all.sh | 21 + doctrine/dbal/tests/.gitignore | 3 + .../Doctrine/Tests/DBAL/ConnectionTest.php | 177 + .../DBAL/Driver/OCI8/OCI8StatementTest.php | 82 + .../Doctrine/Tests/DBAL/DriverManagerTest.php | 117 + .../DBAL/Events/MysqlSessionInitTest.php | 33 + .../DBAL/Events/OracleSessionInitTest.php | 33 + .../Tests/DBAL/Events/SQLSessionInitTest.php | 35 + .../Tests/DBAL/Functional/BlobTest.php | 83 + .../Tests/DBAL/Functional/ConnectionTest.php | 219 ++ .../Tests/DBAL/Functional/DataAccessTest.php | 543 ++++ .../Tests/DBAL/Functional/LoggingTest.php | 52 + .../Functional/MasterSlaveConnectionTest.php | 105 + .../DBAL/Functional/ModifyLimitQueryTest.php | 114 + .../DBAL/Functional/NamedParametersTest.php | 166 + .../Tests/DBAL/Functional/PortabilityTest.php | 97 + .../Tests/DBAL/Functional/ResultCacheTest.php | 208 ++ .../Schema/Db2SchemaManagerTest.php | 12 + .../Schema/DrizzleSchemaManagerTest.php | 12 + .../Schema/MySqlSchemaManagerTest.php | 50 + .../Schema/OracleSchemaManagerTest.php | 39 + .../Schema/PostgreSqlSchemaManagerTest.php | 262 ++ .../Schema/SQLServerSchemaManagerTest.php | 37 + .../SchemaManagerFunctionalTestCase.php | 647 ++++ .../Schema/SqliteSchemaManagerTest.php | 46 + .../DBAL/Functional/TableGeneratorTest.php | 58 + .../DBAL/Functional/TemporaryTableTest.php | 102 + .../DBAL/Functional/Ticket/DBAL168Test.php | 27 + .../DBAL/Functional/Ticket/DBAL202Test.php | 48 + .../DBAL/Functional/TypeConversionTest.php | 101 + .../Tests/DBAL/Functional/WriteTest.php | 185 ++ .../Tests/DBAL/Logging/DebugStackTest.php | 47 + .../Tests/DBAL/Mocks/MockPlatform.php | 49 + .../Platforms/AbstractPlatformTestCase.php | 421 +++ .../DBAL/Platforms/MySqlPlatformTest.php | 243 ++ .../DBAL/Platforms/OraclePlatformTest.php | 285 ++ .../DBAL/Platforms/PostgreSqlPlatformTest.php | 243 ++ .../ReservedKeywordsValidatorTest.php | 47 + .../DBAL/Platforms/SQLAzurePlatformTest.php | 28 + .../DBAL/Platforms/SQLServerPlatformTest.php | 245 ++ .../DBAL/Platforms/SqlitePlatformTest.php | 170 + .../Expression/CompositeExpressionTest.php | 82 + .../Expression/ExpressionBuilderTest.php | 201 ++ .../Tests/DBAL/Query/QueryBuilderTest.php | 572 ++++ .../Tests/DBAL/SQLParserUtilsTest.php | 250 ++ .../Doctrine/Tests/DBAL/Schema/ColumnTest.php | 114 + .../Tests/DBAL/Schema/ComparatorTest.php | 839 +++++ .../Doctrine/Tests/DBAL/Schema/IndexTest.php | 115 + .../DBAL/Schema/MySqlSchemaManagerTest.php | 74 + .../DBAL/Schema/Platforms/MySQLSchemaTest.php | 88 + .../Tests/DBAL/Schema/SchemaDiffTest.php | 109 + .../Doctrine/Tests/DBAL/Schema/SchemaTest.php | 224 ++ .../Tests/DBAL/Schema/SequenceTest.php | 27 + .../SingleDatabaseSynchronizerTest.php | 87 + .../Doctrine/Tests/DBAL/Schema/TableTest.php | 528 +++ .../Visitor/RemoveNamespacedAssetsTest.php | 76 + .../Schema/Visitor/SchemaSqlCollectorTest.php | 80 + .../Sharding/PoolingShardConnectionTest.php | 181 ++ .../DBAL/Sharding/PoolingShardManagerTest.php | 107 + .../Sharding/SQLAzure/AbstractTestCase.php | 82 + .../DBAL/Sharding/SQLAzure/FunctionalTest.php | 43 + .../SQLAzure/MultiTenantVisitorTest.php | 64 + .../SQLAzureFederationsSynchronizerTest.php | 49 + .../SQLAzure/SQLAzureShardManagerTest.php | 92 + .../MultiTenantShardChoserTest.php | 39 + .../Doctrine/Tests/DBAL/Types/ArrayTest.php | 61 + .../Doctrine/Tests/DBAL/Types/BlobTest.php | 26 + .../Doctrine/Tests/DBAL/Types/BooleanTest.php | 36 + .../Doctrine/Tests/DBAL/Types/DateTest.php | 81 + .../Tests/DBAL/Types/DateTimeTest.php | 56 + .../Tests/DBAL/Types/DateTimeTzTest.php | 56 + .../Doctrine/Tests/DBAL/Types/DecimalTest.php | 31 + .../Doctrine/Tests/DBAL/Types/FloatTest.php | 39 + .../Tests/DBAL/Types/GuidTypeTest.php | 30 + .../Doctrine/Tests/DBAL/Types/IntegerTest.php | 32 + .../Doctrine/Tests/DBAL/Types/ObjectTest.php | 56 + .../Tests/DBAL/Types/SmallIntTest.php | 32 + .../Doctrine/Tests/DBAL/Types/StringTest.php | 49 + .../Doctrine/Tests/DBAL/Types/TimeTest.php | 53 + .../Tests/DBAL/Types/VarDateTimeTest.php | 68 + .../tests/Doctrine/Tests/DBAL/UtilTest.php | 78 + .../Doctrine/Tests/DbalFunctionalTestCase.php | 77 + .../tests/Doctrine/Tests/DbalTestCase.php | 10 + .../tests/Doctrine/Tests/DoctrineTestCase.php | 10 + .../Doctrine/Tests/Mocks/ConnectionMock.php | 92 + .../Tests/Mocks/DatabasePlatformMock.php | 98 + .../Tests/Mocks/DriverConnectionMock.php | 17 + .../tests/Doctrine/Tests/Mocks/DriverMock.php | 72 + .../Tests/Mocks/HydratorMockStatement.php | 101 + .../Tests/Mocks/SchemaManagerMock.php | 13 + .../tests/Doctrine/Tests/Mocks/TaskMock.php | 81 + .../dbal/tests/Doctrine/Tests/TestInit.php | 21 + .../dbal/tests/Doctrine/Tests/TestUtil.php | 130 + doctrine/dbal/tests/README.markdown | 25 + doctrine/dbal/tests/travis/mysql.travis.xml | 31 + doctrine/dbal/tests/travis/mysqli.travis.xml | 31 + doctrine/dbal/tests/travis/pgsql.travis.xml | 29 + doctrine/dbal/tests/travis/sqlite.travis.xml | 14 + 495 files changed, 63934 insertions(+) create mode 100644 doctrine/common/.gitignore create mode 100644 doctrine/common/.gitmodules create mode 100644 doctrine/common/.travis.yml create mode 100644 doctrine/common/LICENSE create mode 100644 doctrine/common/README.md create mode 100644 doctrine/common/UPGRADE_TO_2_1 create mode 100644 doctrine/common/UPGRADE_TO_2_2 create mode 100644 doctrine/common/bin/travis-setup.php create mode 100644 doctrine/common/build.properties create mode 100644 doctrine/common/build.xml create mode 100644 doctrine/common/composer.json create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attribute.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attributes.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Required.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Target.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/AnnotationException.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/AnnotationReader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/AnnotationRegistry.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/CachedReader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/DocLexer.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/DocParser.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/FileCacheReader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/IndexedReader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/PhpParser.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/Reader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Annotations/TokenParser.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/ApcCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/ArrayCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/Cache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/CacheProvider.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/FileCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/FilesystemCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/MemcacheCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/MemcachedCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/PhpFileCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/RedisCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/WinCacheCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/XcacheCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/Cache/ZendDataCache.php create mode 100644 doctrine/common/lib/Doctrine/Common/ClassLoader.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/ArrayCollection.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Collection.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Criteria.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/ClosureExpressionVisitor.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/Comparison.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/CompositeExpression.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/Expression.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/ExpressionVisitor.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Expr/Value.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/ExpressionBuilder.php create mode 100644 doctrine/common/lib/Doctrine/Common/Collections/Selectable.php create mode 100644 doctrine/common/lib/Doctrine/Common/CommonException.php create mode 100644 doctrine/common/lib/Doctrine/Common/Comparable.php create mode 100644 doctrine/common/lib/Doctrine/Common/EventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/EventManager.php create mode 100644 doctrine/common/lib/Doctrine/Common/EventSubscriber.php create mode 100644 doctrine/common/lib/Doctrine/Common/Lexer.php create mode 100644 doctrine/common/lib/Doctrine/Common/NotifyPropertyChanged.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/AbstractManagerRegistry.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/ConnectionRegistry.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Event/LifecycleEventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Event/LoadClassMetadataEventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Event/ManagerEventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Event/OnClearEventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Event/PreUpdateEventArgs.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/ManagerRegistry.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadata.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadataFactory.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/DefaultFileLocator.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileDriver.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileLocator.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriver.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/PHPDriver.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/StaticPHPDriver.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/SymfonyFileLocator.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/MappingException.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ReflectionService.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/RuntimeReflectionService.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Mapping/StaticReflectionService.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/ObjectManager.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/ObjectManagerAware.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/ObjectRepository.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/PersistentObject.php create mode 100644 doctrine/common/lib/Doctrine/Common/Persistence/Proxy.php create mode 100644 doctrine/common/lib/Doctrine/Common/PropertyChangedListener.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/ClassFinderInterface.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/Psr0FindFile.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/ReflectionProviderInterface.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionClass.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionMethod.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php create mode 100644 doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionProperty.php create mode 100644 doctrine/common/lib/Doctrine/Common/Util/ClassUtils.php create mode 100644 doctrine/common/lib/Doctrine/Common/Util/Debug.php create mode 100644 doctrine/common/lib/Doctrine/Common/Util/Inflector.php create mode 100644 doctrine/common/lib/Doctrine/Common/Version.php create mode 100644 doctrine/common/phpunit.xml.dist create mode 100644 doctrine/common/tests/.gitignore create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/AbstractReaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/CachedReaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/DummyClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/FileCacheReaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/AnnotWithDefaultValue.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Autoload.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Route.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Secure.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Template.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Version.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationTargetAll.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationTargetAnnotation.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationTargetClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationTargetMethod.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationTargetPropertyMethod.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithAttributes.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithConstants.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithRequiredAttributes.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithRequiredAttributesWithoutContructor.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithTargetSyntaxError.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithVarType.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassDDC1660.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithAnnotationWithTargetSyntaxError.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithAnnotationWithVarType.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithClosure.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithConstants.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithFullyQualifiedUseStatements.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithInvalidAnnotationTargetAtClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithInvalidAnnotationTargetAtMethod.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithInvalidAnnotationTargetAtProperty.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithValidAnnotationTarget.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Controller.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/DifferentNamespacesPerFileWithClassAsFirst.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/DifferentNamespacesPerFileWithClassAsLast.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/EqualNamespacesPerFileWithClassAsFirst.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/EqualNamespacesPerFileWithClassAsLast.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/GlobalNamespacesPerFileWithClassAsFirst.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/GlobalNamespacesPerFileWithClassAsLast.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/IntefaceWithConstants.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/InvalidAnnotationUsageButIgnoredClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/InvalidAnnotationUsageClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/MultipleClassesInFile.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/MultipleImportsInUseStatement.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NamespaceAndClassCommentedOut.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NamespaceWithClosureDeclaration.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NamespacedSingleClassLOC1000.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NoAnnotation.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NonNamespacedClass.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/SingleClassLOC1000.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/TestInterface.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/PerformanceTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/PhpParserTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM55Test.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Test.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Annotations/TopLevelAnnotation.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/ApcCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/ArrayCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/CacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/FilesystemCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcacheCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcachedCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/PhpFileCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/RedisCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/WinCacheCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/XcacheCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Cache/ZendDataCacheTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassA.class.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassB.class.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassC.class.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassD.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Collections/ClosureExpressionVisitorTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Collections/CollectionTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Collections/CriteriaTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Collections/ExpressionBuilderTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/DoctrineExceptionTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/EventManagerTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ChainDriverTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ClassMetadataFactoryTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/DefaultFileLocatorTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/FileDriverTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/PHPDriverTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/RuntimeReflectionServiceTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticPHPDriverTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticReflectionServiceTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/SymfonyFileLocatorTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/TestEntity.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/global.yml create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/stdClass.yml create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Persistence/PersistentObjectTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/DeeperNamespaceParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/Dummies/NoParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/FullyClassifiedParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/NoParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/SameNamespaceParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/StaticReflectionParserTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Reflection/UseParent.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Util/ClassUtilsTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/Common/Util/DebugTest.php create mode 100644 doctrine/common/tests/Doctrine/Tests/DoctrineTestCase.php create mode 100644 doctrine/common/tests/Doctrine/Tests/TestInit.php create mode 100644 doctrine/common/tests/NativePhpunitTask.php create mode 100644 doctrine/common/tests/README.markdown create mode 100644 doctrine/dbal/.gitignore create mode 100644 doctrine/dbal/.gitmodules create mode 100644 doctrine/dbal/.travis.yml create mode 100644 doctrine/dbal/LICENSE create mode 100644 doctrine/dbal/README.md create mode 100644 doctrine/dbal/UPGRADE create mode 100644 doctrine/dbal/bin/doctrine-dbal create mode 100644 doctrine/dbal/bin/doctrine-dbal.php create mode 100644 doctrine/dbal/bin/doctrine.php create mode 100644 doctrine/dbal/build.properties create mode 100644 doctrine/dbal/build.xml create mode 100644 doctrine/dbal/composer.json create mode 100644 doctrine/dbal/docs/design/AZURE_FEDERATIONS.md create mode 100644 doctrine/dbal/docs/design/SHARDING.md create mode 100644 doctrine/dbal/docs/examples/sharding/README.md create mode 100644 doctrine/dbal/docs/examples/sharding/bootstrap.php create mode 100644 doctrine/dbal/docs/examples/sharding/composer.json create mode 100644 doctrine/dbal/docs/examples/sharding/create_schema.php create mode 100644 doctrine/dbal/docs/examples/sharding/insert_data.php create mode 100644 doctrine/dbal/docs/examples/sharding/insert_data_aftersplit.php create mode 100644 doctrine/dbal/docs/examples/sharding/query_filtering_off.php create mode 100644 doctrine/dbal/docs/examples/sharding/query_filtering_on.php create mode 100644 doctrine/dbal/docs/examples/sharding/split_federation.php create mode 100644 doctrine/dbal/docs/examples/sharding/view_federation_members.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Cache/ArrayStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Cache/CacheException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Cache/QueryCacheProfile.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Configuration.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/ConnectionException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Connections/MasterSlaveConnection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/DBALException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOIbm/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOOracle/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlite/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/DriverManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/ConnectionEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/SQLSessionInit.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableAddColumnEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableChangeColumnEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRemoveColumnEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRenameColumnEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaColumnDefinitionEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableColumnEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaDropTableEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaIndexDefinitionEventArgs.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Events.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/LockMode.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/DB2Platform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/DrizzlePlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MsSQLKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQLKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/ReservedKeywordsValidator.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLiteKeywords.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Portability/Connection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Portability/Statement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/CompositeExpression.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/README.markdown create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/DB2SchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/OracleSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Schema.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/View.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Graphviz.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/RemoveNamespacedAssets.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Statement.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ReservedWordsCommand.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.php create mode 100755 doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php create mode 100755 doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeType.php create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/Version.php create mode 100644 doctrine/dbal/phpunit.xml.dist create mode 100755 doctrine/dbal/run-all.sh create mode 100644 doctrine/dbal/tests/.gitignore create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/ConnectionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Driver/OCI8/OCI8StatementTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/DriverManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/MysqlSessionInitTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/OracleSessionInitTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/SQLSessionInitTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/BlobTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/DataAccessTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/LoggingTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ModifyLimitQueryTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/NamedParametersTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/PortabilityTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/Db2SchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/DrizzleSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/MySqlSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/OracleSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SQLServerSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TemporaryTableTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL168Test.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL202Test.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TypeConversionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/WriteTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Logging/DebugStackTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Mocks/MockPlatform.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/AbstractPlatformTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/ReservedKeywordsValidatorTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SqlitePlatformTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/CompositeExpressionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/ExpressionBuilderTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/IndexTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/MySqlSchemaManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Platforms/MySQLSchemaTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaDiffTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/TableTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/SchemaSqlCollectorTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/AbstractTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ArrayTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BlobTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BooleanTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTzTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DecimalTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/FloatTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/IntegerTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ObjectTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/SmallIntTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/StringTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/TimeTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/VarDateTimeTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/UtilTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DbalFunctionalTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DbalTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DoctrineTestCase.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/ConnectionMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/DriverConnectionMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/DriverMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/SchemaManagerMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/Mocks/TaskMock.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/TestInit.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/TestUtil.php create mode 100644 doctrine/dbal/tests/README.markdown create mode 100644 doctrine/dbal/tests/travis/mysql.travis.xml create mode 100644 doctrine/dbal/tests/travis/mysqli.travis.xml create mode 100644 doctrine/dbal/tests/travis/pgsql.travis.xml create mode 100644 doctrine/dbal/tests/travis/sqlite.travis.xml diff --git a/doctrine/common/.gitignore b/doctrine/common/.gitignore new file mode 100644 index 00000000..fb5e79d6 --- /dev/null +++ b/doctrine/common/.gitignore @@ -0,0 +1,4 @@ +build/ +logs/ +reports/ +dist/ diff --git a/doctrine/common/.gitmodules b/doctrine/common/.gitmodules new file mode 100644 index 00000000..51f08435 --- /dev/null +++ b/doctrine/common/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/vendor/doctrine-build-common"] + path = lib/vendor/doctrine-build-common + url = git://github.com/doctrine/doctrine-build-common.git diff --git a/doctrine/common/.travis.yml b/doctrine/common/.travis.yml new file mode 100644 index 00000000..fc050563 --- /dev/null +++ b/doctrine/common/.travis.yml @@ -0,0 +1,10 @@ +language: php + +env: + - OPCODE_CACHE=apc + +php: + - 5.3 + - 5.4 + +before_script: php ./bin/travis-setup.php $OPCODE_CACHE \ No newline at end of file diff --git a/doctrine/common/LICENSE b/doctrine/common/LICENSE new file mode 100644 index 00000000..4a91f0bf --- /dev/null +++ b/doctrine/common/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006-2012 Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doctrine/common/README.md b/doctrine/common/README.md new file mode 100644 index 00000000..c63f7627 --- /dev/null +++ b/doctrine/common/README.md @@ -0,0 +1,12 @@ +# Doctrine Common + +[![Build Status](https://secure.travis-ci.org/doctrine/common.png)](http://travis-ci.org/doctrine/common) + +The Doctrine Common project is a library that provides extensions to core PHP functionality. + +## More resources: + +* [Website](http://www.doctrine-project.org) +* [Documentation](http://www.doctrine-project.org/projects/common/current/docs/en) +* [Issue Tracker](http://www.doctrine-project.org/jira/browse/DCOM) +* [Downloads](http://github.com/doctrine/common/downloads) diff --git a/doctrine/common/UPGRADE_TO_2_1 b/doctrine/common/UPGRADE_TO_2_1 new file mode 100644 index 00000000..c0755f86 --- /dev/null +++ b/doctrine/common/UPGRADE_TO_2_1 @@ -0,0 +1,38 @@ +This document details all the possible changes that you should investigate when updating +your project from Doctrine Common 2.0.x to 2.1 + +## AnnotationReader changes + +The annotation reader was heavily refactored between 2.0 and 2.1-RC1. In theory the operation of the new reader should be backwards compatible, but it has to be setup differently to work that way: + + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); + // new code necessary starting here + $reader->setIgnoreNotImportedAnnotations(true); + $reader->setEnableParsePhpImports(false); + $reader = new \Doctrine\Common\Annotations\CachedReader( + new \Doctrine\Common\Annotations\IndexedReader($reader), new ArrayCache() + ); + +## Annotation Base class or @Annotation + +Beginning after 2.1-RC2 you have to either extend ``Doctrine\Common\Annotations\Annotation`` or add @Annotation to your annotations class-level docblock, otherwise the class will simply be ignored. + +## Removed methods on AnnotationReader + +* AnnotationReader::setAutoloadAnnotations() +* AnnotationReader::getAutoloadAnnotations() +* AnnotationReader::isAutoloadAnnotations() + +## AnnotationRegistry + +Autoloading through the PHP autoloader is removed from the 2.1 AnnotationReader. Instead you have to use the global AnnotationRegistry for loading purposes: + + \Doctrine\Common\Annotations\AnnotationRegistry::registerFile($fileWithAnnotations); + \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace($namespace, $dirs = null); + \Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespaces($namespaces); + \Doctrine\Common\Annotations\AnnotationRegistry::registerLoader($callable); + +The $callable for registering a loader accepts a class as first and only parameter and must try to silently autoload it. On success true has to be returned. +The registerAutoloadNamespace function registers a PSR-0 compatible silent autoloader for all classes with the given namespace in the given directories. +If null is passed as directory the include path will be used. diff --git a/doctrine/common/UPGRADE_TO_2_2 b/doctrine/common/UPGRADE_TO_2_2 new file mode 100644 index 00000000..3b675109 --- /dev/null +++ b/doctrine/common/UPGRADE_TO_2_2 @@ -0,0 +1,61 @@ +This document details all the possible changes that you should investigate when +updating your project from Doctrine Common 2.1 to 2.2: + +## Annotation Changes + +- AnnotationReader::setIgnoreNotImportedAnnotations has been removed, you need to + add ignore annotation names which are supposed to be ignored via + AnnotationReader::addGlobalIgnoredName + +- AnnotationReader::setAutoloadAnnotations was deprecated by the AnnotationRegistry + in 2.1 and has been removed in 2.2 + +- AnnotationReader::setEnableParsePhpImports was added to ease transition to the new + annotation mechanism in 2.1 and is removed in 2.2 + +- AnnotationReader::isParsePhpImportsEnabled is removed (see above) + +- AnnotationReader::setDefaultAnnotationNamespace was deprecated in favor of explicit + configuration in 2.1 and will be removed in 2.2 (for isolated projects where you + have full-control over _all_ available annotations, we offer a dedicated reader + class ``SimpleAnnotationReader``) + +- AnnotationReader::setAnnotationCreationFunction was deprecated in 2.1 and will be + removed in 2.2. We only offer two creation mechanisms which cannot be changed + anymore to allow the same reader instance to work with all annotations regardless + of which library they are coming from. + +- AnnotationReader::setAnnotationNamespaceAlias was deprecated in 2.1 and will be + removed in 2.2 (see setDefaultAnnotationNamespace) + +- If you use a class as annotation which has not the @Annotation marker in it's + class block, we will now throw an exception instead of silently ignoring it. You + can however still achieve the previous behavior using the @IgnoreAnnotation, or + AnnotationReader::addGlobalIgnoredName (the exception message will contain detailed + instructions when you run into this problem). + +## Cache Changes + +- Renamed old AbstractCache to CacheProvider + +- Dropped the support to the following functions of all cache providers: + + - CacheProvider::deleteByWildcard + + - CacheProvider::deleteByRegEx + + - CacheProvider::deleteByPrefix + + - CacheProvider::deleteBySuffix + +- CacheProvider::deleteAll will not remove ALL entries, it will only mark them as invalid + +- CacheProvider::flushAll will remove ALL entries, namespaced or not + +- Added support to MemcachedCache + +- Added support to WincacheCache + +## ClassLoader Changes + +- ClassLoader::fileExistsInIncludePath() no longer exists. Use the native stream_resolve_include_path() PHP function \ No newline at end of file diff --git a/doctrine/common/bin/travis-setup.php b/doctrine/common/bin/travis-setup.php new file mode 100644 index 00000000..e1c1efd5 --- /dev/null +++ b/doctrine/common/bin/travis-setup.php @@ -0,0 +1,141 @@ +. + */ + +/** + * Install PHP extensions required for testing by Travis CI. + * + * @author Victor Berchet + * @since 2.2 + */ +$installer = new PhpExtensions(); + +if (isset($argv[1]) && 'APC' === strtoupper($argv[1])) { + $installer->install('apc'); +} else { + $installer->install('xcache'); +} + +$installer->install('memcache'); +$installer->install('memcached'); + +class PhpExtensions +{ + protected $extensions; + protected $phpVersion; + protected $iniPath; + + public function __construct() + { + $this->phpVersion = phpversion(); + $this->iniPath = php_ini_loaded_file(); + $this->extensions = array( + 'memcache' => array( + 'url' => 'http://pecl.php.net/get/memcache-2.2.6.tgz', + 'php_version' => array(), + 'cfg' => array('--enable-memcache'), + 'ini' => array('extension=memcache.so'), + ), + 'memcached' => array( + 'url' => 'http://pecl.php.net/get/memcached-1.0.2.tgz', + 'php_version' => array( + // memcached 1.0.2 does not build on PHP 5.4 + array('<', '5.4'), + ), + 'cfg' => array(), + 'ini' => array('extension=memcached.so'), + ), + 'apc' => array( + 'url' => 'http://pecl.php.net/get/APC-3.1.9.tgz', + 'php_version' => array( + // apc 3.1.9 causes a segfault on PHP 5.4 + array('<', '5.4'), + ), + 'cfg' => array(), + 'ini' => array( + 'extension=apc.so', + 'apc.enabled=1', + 'apc.enable_cli=1' + ), + ), + 'xcache' => array( + 'url' => 'http://xcache.lighttpd.net/pub/Releases/1.2.2/xcache-1.2.2.tar.gz', + 'php_version' => array( + // xcache does not build with Travis CI (as of 2012-01-09) + array('<', '5'), + ), + 'cfg' => array('--enable-xcache'), + 'ini' => array( + 'extension=xcache.so', + 'xcache.cacher=false', + 'xcache.admin.enable_auth=0', + 'xcache.var_size=1M', + ), + ), + ); + } + + public function install($name) + { + if (array_key_exists($name, $this->extensions)) { + $extension = $this->extensions[$name]; + + + echo "== extension: $name ==\n"; + + foreach ($extension['php_version'] as $version) { + if (!version_compare($this->phpVersion, $version[1], $version[0])) { + printf( + "=> not installed, requires a PHP version %s %s (%s installed)\n", + $version[0], + $version[1], + $this->phpVersion + ); + + return; + } + } + + $this->system(sprintf("wget %s > /dev/null 2>&1", $extension['url'])); + $file = basename($extension['url']); + $this->system(sprintf("tar -xzf %s > /dev/null 2>&1", $file)); + $folder = basename($file, ".tgz"); + $folder = basename($folder, ".tar.gz"); + $this->system(sprintf( + 'sh -c "cd %s && phpize && ./configure %s && make && sudo make install" > /dev/null 2>&1', + $folder, + implode(' ', $extension['cfg']) + )); + foreach ($extension['ini'] as $ini) { + $this->system(sprintf("echo %s >> %s", $ini, $this->iniPath)); + } + printf("=> installed (%s)\n", $folder); + } + } + + private function system($cmd) + { + $ret = 0; + system($cmd, $ret); + if (0 !== $ret) { + printf("=> Command '%s' failed !", $cmd); + + exit($ret); + } + } +} diff --git a/doctrine/common/build.properties b/doctrine/common/build.properties new file mode 100644 index 00000000..ff311a45 --- /dev/null +++ b/doctrine/common/build.properties @@ -0,0 +1,6 @@ +# Project Name +project.name=DoctrineCommon + +# Version class and file +project.version_class = Doctrine\Common\Version +project.version_file = lib/Doctrine/Common/Version.php diff --git a/doctrine/common/build.xml b/doctrine/common/build.xml new file mode 100644 index 00000000..9c937dff --- /dev/null +++ b/doctrine/common/build.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DoctrineCommon + Doctrine Common PHP Extensions + pear.doctrine-project.org + The Doctrine Common package contains shared code between the other packages. + + + + + LGPL + + + - + + + + + + + diff --git a/doctrine/common/composer.json b/doctrine/common/composer.json new file mode 100644 index 00000000..c87258de --- /dev/null +++ b/doctrine/common/composer.json @@ -0,0 +1,26 @@ +{ + "name": "doctrine/common", + "type": "library", + "description": "Common Library for Doctrine projects", + "keywords": ["collections", "spl", "eventmanager", "annotations", "persistence"], + "homepage": "http://www.doctrine-project.org", + "license": "MIT", + "authors": [ + {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, + {"name": "Roman Borschel", "email": "roman@code-factory.org"}, + {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, + {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, + {"name": "Johannes Schmitt", "email": "schmittjoh@gmail.com"} + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Doctrine\\Common": "lib/" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation.php new file mode 100644 index 00000000..6a1390af --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation.php @@ -0,0 +1,79 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Annotations class + * + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Annotation +{ + /** + * Value property. Common among all derived classes. + * + * @var string + */ + public $value; + + /** + * Constructor + * + * @param array $data Key-value for properties to be defined in this class + */ + public final function __construct(array $data) + { + foreach ($data as $key => $value) { + $this->$key = $value; + } + } + + /** + * Error handler for unknown property accessor in Annotation class. + * + * @param string $name Unknown property name + * + * @throws \BadMethodCallException + */ + public function __get($name) + { + throw new \BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + ); + } + + /** + * Error handler for unknown property mutator in Annotation class. + * + * @param string $name Unkown property name + * @param mixed $value Property value + * + * @throws \BadMethodCallException + */ + public function __set($name, $value) + { + throw new \BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attribute.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attribute.php new file mode 100644 index 00000000..dbef6df0 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attribute.php @@ -0,0 +1,47 @@ +. + */ + +namespace Doctrine\Common\Annotations\Annotation; + +/** + * Annotation that can be used to signal to the parser + * to check the attribute type during the parsing process. + * + * @author Fabio B. Silva + * + * @Annotation + */ +final class Attribute +{ + /** + * @var string + */ + public $name; + + /** + * @var string + */ + public $type; + + /** + * @var boolean + */ + public $required = false; +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attributes.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attributes.php new file mode 100644 index 00000000..53134e30 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Attributes.php @@ -0,0 +1,37 @@ +. + */ + +namespace Doctrine\Common\Annotations\Annotation; + +/** + * Annotation that can be used to signal to the parser + * to check the types of all declared attributes during the parsing process. + * + * @author Fabio B. Silva + * + * @Annotation + */ +final class Attributes +{ + /** + * @var array + */ + public $value; +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php new file mode 100644 index 00000000..a84a4f51 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/IgnoreAnnotation.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\Common\Annotations\Annotation; + +/** + * Annotation that can be used to signal to the parser to ignore specific + * annotations during the parsing process. + * + * @Annotation + * @author Johannes M. Schmitt + */ +final class IgnoreAnnotation +{ + /** + * @var array + */ + public $names; + + /** + * Constructor + * + * @param array $values + * + * @throws \RuntimeException + */ + public function __construct(array $values) + { + if (is_string($values['value'])) { + $values['value'] = array($values['value']); + } + if (!is_array($values['value'])) { + throw new \RuntimeException(sprintf('@IgnoreAnnotation expects either a string name, or an array of strings, but got %s.', json_encode($values['value']))); + } + + $this->names = $values['value']; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Required.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Required.php new file mode 100644 index 00000000..d67f9606 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Required.php @@ -0,0 +1,33 @@ +. + */ + +namespace Doctrine\Common\Annotations\Annotation; + +/** + * Annotation that can be used to signal to the parser + * to check if that attribute is required during the parsing process. + * + * @author Fabio B. Silva + * + * @Annotation + */ +final class Required +{ +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Target.php b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Target.php new file mode 100644 index 00000000..64655ef6 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Annotation/Target.php @@ -0,0 +1,107 @@ +. + */ + +namespace Doctrine\Common\Annotations\Annotation; + +/** + * Annotation that can be used to signal to the parser + * to check the annotation target during the parsing process. + * + * @author Fabio B. Silva + * + * @Annotation + */ +final class Target +{ + const TARGET_CLASS = 1; + const TARGET_METHOD = 2; + const TARGET_PROPERTY = 4; + const TARGET_ANNOTATION = 8; + const TARGET_ALL = 15; + + /** + * @var array + */ + private static $map = array( + 'ALL' => self::TARGET_ALL, + 'CLASS' => self::TARGET_CLASS, + 'METHOD' => self::TARGET_METHOD, + 'PROPERTY' => self::TARGET_PROPERTY, + 'ANNOTATION' => self::TARGET_ANNOTATION, + ); + + /** + * @var array + */ + public $value; + + /** + * Targets as bitmask. + * + * @var integer + */ + public $targets; + + /** + * Literal target declaration. + * + * @var integer + */ + public $literal; + + /** + * Annotation construct + * + * @param array $values + * + * @throws \InvalidArgumentException + */ + public function __construct(array $values) + { + if (!isset($values['value'])){ + $values['value'] = null; + } + if (is_string($values['value'])){ + $values['value'] = array($values['value']); + } + if (!is_array($values['value'])){ + throw new \InvalidArgumentException( + sprintf('@Target expects either a string value, or an array of strings, "%s" given.', + is_object($values['value']) ? get_class($values['value']) : gettype($values['value']) + ) + ); + } + + $bitmask = 0; + foreach ($values['value'] as $literal) { + if(!isset(self::$map[$literal])){ + throw new \InvalidArgumentException( + sprintf('Invalid Target "%s". Available targets: [%s]', + $literal, implode(', ', array_keys(self::$map))) + ); + } + $bitmask += self::$map[$literal]; + } + + $this->targets = $bitmask; + $this->value = $values['value']; + $this->literal = implode(', ', $this->value); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationException.php b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationException.php new file mode 100644 index 00000000..109beeb9 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationException.php @@ -0,0 +1,127 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Description of AnnotationException + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AnnotationException extends \Exception +{ + /** + * Creates a new AnnotationException describing a Syntax error. + * + * @param string $message Exception message + * @return AnnotationException + */ + public static function syntaxError($message) + { + return new self('[Syntax Error] ' . $message); + } + + /** + * Creates a new AnnotationException describing a Semantical error. + * + * @param string $message Exception message + * @return AnnotationException + */ + public static function semanticalError($message) + { + return new self('[Semantical Error] ' . $message); + } + + /** + * Creates a new AnnotationException describing a constant semantical error. + * + * @since 2.3 + * @param string $identifier + * @param string $context + * @return AnnotationException + */ + public static function semanticalErrorConstants($identifier, $context = null) + { + return self::semanticalError(sprintf( + "Couldn't find constant %s%s", $identifier, + $context ? ", $context." : "." + )); + } + + /** + * Creates a new AnnotationException describing an error which occurred during + * the creation of the annotation. + * + * @since 2.2 + * @param string $message + * @return AnnotationException + */ + public static function creationError($message) + { + return new self('[Creation Error] ' . $message); + } + + /** + * Creates a new AnnotationException describing an type error of an attribute. + * + * @since 2.2 + * @param string $attributeName + * @param string $annotationName + * @param string $context + * @param string $expected + * @param mixed $actual + * @return AnnotationException + */ + public static function typeError($attributeName, $annotationName, $context, $expected, $actual) + { + return new self(sprintf( + '[Type Error] Attribute "%s" of @%s declared on %s expects %s, but got %s.', + $attributeName, + $annotationName, + $context, + $expected, + is_object($actual) ? 'an instance of '.get_class($actual) : gettype($actual) + )); + } + + /** + * Creates a new AnnotationException describing an required error of an attribute. + * + * @since 2.2 + * @param string $attributeName + * @param string $annotationName + * @param string $context + * @param string $expected + * @return AnnotationException + */ + public static function requiredError($attributeName, $annotationName, $context, $expected) + { + return new self(sprintf( + '[Type Error] Attribute "%s" of @%s declared on %s expects %s. This value should not be null.', + $attributeName, + $annotationName, + $context, + $expected + )); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationReader.php b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationReader.php new file mode 100644 index 00000000..286e7d09 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationReader.php @@ -0,0 +1,310 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation; +use Doctrine\Common\Annotations\Annotation\Target; +use Closure; +use ReflectionClass; +use ReflectionMethod; +use ReflectionProperty; + +/** + * A reader for docblock annotations. + * + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Johannes M. Schmitt + */ +class AnnotationReader implements Reader +{ + /** + * Global map for imports. + * + * @var array + */ + private static $globalImports = array( + 'ignoreannotation' => 'Doctrine\Common\Annotations\Annotation\IgnoreAnnotation', + ); + + /** + * A list with annotations that are not causing exceptions when not resolved to an annotation class. + * + * The names are case sensitive. + * + * @var array + */ + private static $globalIgnoredNames = array( + 'access'=> true, 'author'=> true, 'copyright'=> true, 'deprecated'=> true, + 'example'=> true, 'ignore'=> true, 'internal'=> true, 'link'=> true, 'see'=> true, + 'since'=> true, 'tutorial'=> true, 'version'=> true, 'package'=> true, + 'subpackage'=> true, 'name'=> true, 'global'=> true, 'param'=> true, + 'return'=> true, 'staticvar'=> true, 'category'=> true, 'staticVar'=> true, + 'static'=> true, 'var'=> true, 'throws'=> true, 'inheritdoc'=> true, + 'inheritDoc'=> true, 'license'=> true, 'todo'=> true, + 'deprec'=> true, 'property' => true, 'method' => true, + 'abstract'=> true, 'exception'=> true, 'magic' => true, 'api' => true, + 'final'=> true, 'filesource'=> true, 'throw' => true, 'uses' => true, + 'usedby'=> true, 'private' => true, 'Annotation' => true, 'override' => true, + 'codeCoverageIgnore' => true, 'codeCoverageIgnoreStart' => true, 'codeCoverageIgnoreEnd' => true, + 'Required' => true, 'Attribute' => true, 'Attributes' => true, + 'Target' => true, 'SuppressWarnings' => true, + 'ingroup' => true, 'code' => true, 'endcode' => true, + 'package_version' => true, + ); + + /** + * Add a new annotation to the globally ignored annotation names with regard to exception handling. + * + * @param string $name + */ + static public function addGlobalIgnoredName($name) + { + self::$globalIgnoredNames[$name] = true; + } + + /** + * Annotations Parser + * + * @var \Doctrine\Common\Annotations\DocParser + */ + private $parser; + + /** + * Annotations Parser used to collect parsing metadata + * + * @var \Doctrine\Common\Annotations\DocParser + */ + private $preParser; + + /** + * PHP Parser used to collect imports. + * + * @var \Doctrine\Common\Annotations\PhpParser + */ + private $phpParser; + + /** + * In-memory cache mechanism to store imported annotations per class. + * + * @var array + */ + private $imports = array(); + + /** + * In-memory cache mechanism to store ignored annotations per class. + * + * @var array + */ + private $ignoredAnnotationNames = array(); + + /** + * Constructor. + * + * Initializes a new AnnotationReader. + */ + public function __construct() + { + AnnotationRegistry::registerFile(__DIR__ . '/Annotation/IgnoreAnnotation.php'); + + $this->parser = new DocParser; + + $this->preParser = new DocParser; + $this->preParser->setImports(self::$globalImports); + $this->preParser->setIgnoreNotImportedAnnotations(true); + + $this->phpParser = new PhpParser; + } + + /** + * Gets the annotations applied to a class. + * + * @param ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. + * @return array An array of Annotations. + */ + public function getClassAnnotations(ReflectionClass $class) + { + $this->parser->setTarget(Target::TARGET_CLASS); + $this->parser->setImports($this->getImports($class)); + $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + + return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); + } + + /** + * Gets a class annotation. + * + * @param ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getClassAnnotation(ReflectionClass $class, $annotationName) + { + $annotations = $this->getClassAnnotations($class); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Gets the annotations applied to a property. + * + * @param ReflectionProperty $property The ReflectionProperty of the property + * from which the annotations should be read. + * @return array An array of Annotations. + */ + public function getPropertyAnnotations(ReflectionProperty $property) + { + $class = $property->getDeclaringClass(); + $context = 'property ' . $class->getName() . "::\$" . $property->getName(); + $this->parser->setTarget(Target::TARGET_PROPERTY); + $this->parser->setImports($this->getImports($class)); + $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + + return $this->parser->parse($property->getDocComment(), $context); + } + + /** + * Gets a property annotation. + * + * @param ReflectionProperty $property + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) + { + $annotations = $this->getPropertyAnnotations($property); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Gets the annotations applied to a method. + * + * @param \ReflectionMethod $method The ReflectionMethod of the method from which + * the annotations should be read. + * + * @return array An array of Annotations. + */ + public function getMethodAnnotations(ReflectionMethod $method) + { + $class = $method->getDeclaringClass(); + $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; + $this->parser->setTarget(Target::TARGET_METHOD); + $this->parser->setImports($this->getImports($class)); + $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); + + return $this->parser->parse($method->getDocComment(), $context); + } + + /** + * Gets a method annotation. + * + * @param ReflectionMethod $method + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) + { + $annotations = $this->getMethodAnnotations($method); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Returns the ignored annotations for the given class. + * + * @param ReflectionClass $class + * @return array + */ + private function getIgnoredAnnotationNames(ReflectionClass $class) + { + if (isset($this->ignoredAnnotationNames[$name = $class->getName()])) { + return $this->ignoredAnnotationNames[$name]; + } + $this->collectParsingMetadata($class); + + return $this->ignoredAnnotationNames[$name]; + } + + /** + * Retrieve imports + * + * @param \ReflectionClass $class + * @return array + */ + private function getImports(ReflectionClass $class) + { + if (isset($this->imports[$name = $class->getName()])) { + return $this->imports[$name]; + } + $this->collectParsingMetadata($class); + + return $this->imports[$name]; + } + + /** + * Collects parsing metadata for a given class + * + * @param ReflectionClass $class + */ + private function collectParsingMetadata(ReflectionClass $class) + { + $ignoredAnnotationNames = self::$globalIgnoredNames; + + $annotations = $this->preParser->parse($class->getDocComment(), 'class '.$class->name); + foreach ($annotations as $annotation) { + if ($annotation instanceof IgnoreAnnotation) { + foreach ($annotation->names AS $annot) { + $ignoredAnnotationNames[$annot] = true; + } + } + } + + $name = $class->getName(); + $this->imports[$name] = array_merge( + self::$globalImports, + $this->phpParser->parseClass($class), + array('__NAMESPACE__' => $class->getNamespaceName()) + ); + $this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationRegistry.php b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationRegistry.php new file mode 100644 index 00000000..dfa846a0 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/AnnotationRegistry.php @@ -0,0 +1,139 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * AnnotationRegistry + */ +final class AnnotationRegistry +{ + /** + * A map of namespaces to use for autoloading purposes based on a PSR-0 convention. + * + * Contains the namespace as key and an array of directories as value. If the value is NULL + * the include path is used for checking for the corresponding file. + * + * This autoloading mechanism does not utilize the PHP autoloading but implements autoloading on its own. + * + * @var array + */ + static private $autoloadNamespaces = array(); + + /** + * A map of autoloader callables. + * + * @var array + */ + static private $loaders = array(); + + static public function reset() + { + self::$autoloadNamespaces = array(); + self::$loaders = array(); + } + + /** + * Register file + * + * @param string $file + */ + static public function registerFile($file) + { + require_once $file; + } + + /** + * Add a namespace with one or many directories to look for files or null for the include path. + * + * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. + * + * @param string $namespace + * @param string|array|null $dirs + */ + static public function registerAutoloadNamespace($namespace, $dirs = null) + { + self::$autoloadNamespaces[$namespace] = $dirs; + } + + /** + * Register multiple namespaces + * + * Loading of this namespaces will be done with a PSR-0 namespace loading algorithm. + * + * @param array $namespaces + */ + static public function registerAutoloadNamespaces(array $namespaces) + { + self::$autoloadNamespaces = array_merge(self::$autoloadNamespaces, $namespaces); + } + + /** + * Register an autoloading callable for annotations, much like spl_autoload_register(). + * + * NOTE: These class loaders HAVE to be silent when a class was not found! + * IMPORTANT: Loaders have to return true if they loaded a class that could contain the searched annotation class. + * + * @param callable $callable + * + * @throws \InvalidArgumentException + */ + static public function registerLoader($callable) + { + if (!is_callable($callable)) { + throw new \InvalidArgumentException("A callable is expected in AnnotationRegistry::registerLoader()."); + } + self::$loaders[] = $callable; + } + + /** + * Autoload an annotation class silently. + * + * @param string $class + * @return boolean + */ + static public function loadAnnotationClass($class) + { + foreach (self::$autoloadNamespaces AS $namespace => $dirs) { + if (strpos($class, $namespace) === 0) { + $file = str_replace("\\", DIRECTORY_SEPARATOR, $class) . ".php"; + if ($dirs === null) { + if ($path = stream_resolve_include_path($file)) { + require $path; + return true; + } + } else { + foreach((array)$dirs AS $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $file)) { + require $dir . DIRECTORY_SEPARATOR . $file; + return true; + } + } + } + } + } + + foreach (self::$loaders AS $loader) { + if (call_user_func($loader, $class) === true) { + return true; + } + } + return false; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/CachedReader.php b/doctrine/common/lib/Doctrine/Common/Annotations/CachedReader.php new file mode 100644 index 00000000..e377e3b3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/CachedReader.php @@ -0,0 +1,250 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Doctrine\Common\Cache\Cache; + +/** + * A cache aware annotation reader. + * + * @author Johannes M. Schmitt + * @author Benjamin Eberlei + */ +final class CachedReader implements Reader +{ + /** + * @var string + */ + private static $CACHE_SALT = '@[Annot]'; + + /** + * @var Reader + */ + private $delegate; + + /** + * @var Cache + */ + private $cache; + + /** + * @var boolean + */ + private $debug; + + /** + * @var array + */ + private $loadedAnnotations; + + /** + * Constructor + * + * @param Reader $reader + * @param Cache $cache + * @param bool $debug + */ + public function __construct(Reader $reader, Cache $cache, $debug = false) + { + $this->delegate = $reader; + $this->cache = $cache; + $this->debug = (Boolean) $debug; + } + + /** + * Get annotations for class + * + * @param \ReflectionClass $class + * @return array + */ + public function getClassAnnotations(\ReflectionClass $class) + { + $cacheKey = $class->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->delegate->getClassAnnotations($class); + $this->saveToCache($cacheKey, $annots); + } + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * Get selected annotation for class + * + * @param \ReflectionClass $class + * @param string $annotationName + * @return null + */ + public function getClassAnnotation(\ReflectionClass $class, $annotationName) + { + foreach ($this->getClassAnnotations($class) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * Get annotations for property + * + * @param \ReflectionProperty $property + * @return array + */ + public function getPropertyAnnotations(\ReflectionProperty $property) + { + $class = $property->getDeclaringClass(); + $cacheKey = $class->getName().'$'.$property->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->delegate->getPropertyAnnotations($property); + $this->saveToCache($cacheKey, $annots); + } + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * Get selected annotation for property + * + * @param \ReflectionProperty $property + * @param string $annotationName + * @return null + */ + public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + { + foreach ($this->getPropertyAnnotations($property) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * Get method annotations + * + * @param \ReflectionMethod $method + * @return array + */ + public function getMethodAnnotations(\ReflectionMethod $method) + { + $class = $method->getDeclaringClass(); + $cacheKey = $class->getName().'#'.$method->getName(); + + if (isset($this->loadedAnnotations[$cacheKey])) { + return $this->loadedAnnotations[$cacheKey]; + } + + if (false === ($annots = $this->fetchFromCache($cacheKey, $class))) { + $annots = $this->delegate->getMethodAnnotations($method); + $this->saveToCache($cacheKey, $annots); + } + + return $this->loadedAnnotations[$cacheKey] = $annots; + } + + /** + * Get selected method annotation + * + * @param \ReflectionMethod $method + * @param string $annotationName + * @return null + */ + public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + { + foreach ($this->getMethodAnnotations($method) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * Clear loaded annotations + */ + public function clearLoadedAnnotations() + { + $this->loadedAnnotations = array(); + } + + /** + * Fetches a value from the cache. + * + * @param string $rawCacheKey The cache key. + * @param \ReflectionClass $class The related class. + * @return mixed|boolean The cached value or false when the value is not in cache. + */ + private function fetchFromCache($rawCacheKey, \ReflectionClass $class) + { + $cacheKey = $rawCacheKey . self::$CACHE_SALT; + if (($data = $this->cache->fetch($cacheKey)) !== false) { + if (!$this->debug || $this->isCacheFresh($cacheKey, $class)) { + return $data; + } + } + + return false; + } + + /** + * Saves a value to the cache + * + * @param string $rawCacheKey The cache key. + * @param mixed $value The value. + */ + private function saveToCache($rawCacheKey, $value) + { + $cacheKey = $rawCacheKey . self::$CACHE_SALT; + $this->cache->save($cacheKey, $value); + if ($this->debug) { + $this->cache->save('[C]'.$cacheKey, time()); + } + } + + /** + * Check if cache is fresh + * + * @param string $cacheKey + * @param \ReflectionClass $class + * @return bool + */ + private function isCacheFresh($cacheKey, \ReflectionClass $class) + { + if (false === $filename = $class->getFilename()) { + return true; + } + + return $this->cache->fetch('[C]'.$cacheKey) >= filemtime($filename); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/DocLexer.php b/doctrine/common/lib/Doctrine/Common/Annotations/DocLexer.php new file mode 100644 index 00000000..c9a6f7a2 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/DocLexer.php @@ -0,0 +1,132 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Doctrine\Common\Lexer; + +/** + * Simple lexer for docblock annotations. + * + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Johannes M. Schmitt + */ +final class DocLexer extends Lexer +{ + const T_NONE = 1; + const T_INTEGER = 2; + const T_STRING = 3; + const T_FLOAT = 4; + + // All tokens that are also identifiers should be >= 100 + const T_IDENTIFIER = 100; + const T_AT = 101; + const T_CLOSE_CURLY_BRACES = 102; + const T_CLOSE_PARENTHESIS = 103; + const T_COMMA = 104; + const T_EQUALS = 105; + const T_FALSE = 106; + const T_NAMESPACE_SEPARATOR = 107; + const T_OPEN_CURLY_BRACES = 108; + const T_OPEN_PARENTHESIS = 109; + const T_TRUE = 110; + const T_NULL = 111; + const T_COLON = 112; + + protected $noCase = array( + '@' => self::T_AT, + ',' => self::T_COMMA, + '(' => self::T_OPEN_PARENTHESIS, + ')' => self::T_CLOSE_PARENTHESIS, + '{' => self::T_OPEN_CURLY_BRACES, + '}' => self::T_CLOSE_CURLY_BRACES, + '=' => self::T_EQUALS, + ':' => self::T_COLON, + '\\' => self::T_NAMESPACE_SEPARATOR + ); + + protected $withCase = array( + 'true' => self::T_TRUE, + 'false' => self::T_FALSE, + 'null' => self::T_NULL + ); + + /** + * {@inheritdoc} + */ + protected function getCatchablePatterns() + { + return array( + '[a-z_\\\][a-z0-9_\:\\\]*[a-z]{1}', + '(?:[+-]?[0-9]+(?:[\.][0-9]+)*)(?:[eE][+-]?[0-9]+)?', + '"(?:[^"]|"")*"', + ); + } + + /** + * {@inheritdoc} + */ + protected function getNonCatchablePatterns() + { + return array('\s+', '\*+', '(.)'); + } + + /** + * {@inheritdoc} + * + * @param string $value + * + * @return int + */ + protected function getType(&$value) + { + $type = self::T_NONE; + + if ($value[0] === '"') { + $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2)); + + return self::T_STRING; + } + + if (isset($this->noCase[$value])) { + return $this->noCase[$value]; + } + + if ($value[0] === '_' || $value[0] === '\\' || ctype_alpha($value[0])) { + return self::T_IDENTIFIER; + } + + $lowerValue = strtolower($value); + + if (isset($this->withCase[$lowerValue])) { + return $this->withCase[$lowerValue]; + } + + // Checking numeric value + if (is_numeric($value)) { + return (strpos($value, '.') !== false || stripos($value, 'e') !== false) + ? self::T_FLOAT : self::T_INTEGER; + } + + return $type; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/DocParser.php b/doctrine/common/lib/Doctrine/Common/Annotations/DocParser.php new file mode 100644 index 00000000..de31e0b4 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/DocParser.php @@ -0,0 +1,988 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Closure; +use ReflectionClass; +use Doctrine\Common\Annotations\Annotation\Target; +use Doctrine\Common\Annotations\Annotation\Attribute; +use Doctrine\Common\Annotations\Annotation\Attributes; + +/** + * A parser for docblock annotations. + * + * It is strongly discouraged to change the default annotation parsing process. + * + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Johannes M. Schmitt + * @author Fabio B. Silva + */ +final class DocParser +{ + /** + * An array of all valid tokens for a class name. + * + * @var array + */ + private static $classIdentifiers = array(DocLexer::T_IDENTIFIER, DocLexer::T_TRUE, DocLexer::T_FALSE, DocLexer::T_NULL); + + /** + * The lexer. + * + * @var \Doctrine\Common\Annotations\DocLexer + */ + private $lexer; + + /** + * Current target context + * + * @var string + */ + private $target; + + /** + * Doc Parser used to collect annotation target + * + * @var \Doctrine\Common\Annotations\DocParser + */ + private static $metadataParser; + + /** + * Flag to control if the current annotation is nested or not. + * + * @var boolean + */ + private $isNestedAnnotation = false; + + /** + * Hashmap containing all use-statements that are to be used when parsing + * the given doc block. + * + * @var array + */ + private $imports = array(); + + /** + * This hashmap is used internally to cache results of class_exists() + * look-ups. + * + * @var array + */ + private $classExists = array(); + + /** + * Whether annotations that have not been imported should be ignored. + * + * @var boolean + */ + private $ignoreNotImportedAnnotations = false; + + /** + * An array of default namespaces if operating in simple mode. + * + * @var array + */ + private $namespaces = array(); + + /** + * A list with annotations that are not causing exceptions when not resolved to an annotation class. + * + * The names must be the raw names as used in the class, not the fully qualified + * class names. + * + * @var array + */ + private $ignoredAnnotationNames = array(); + + /** + * @var string + */ + private $context = ''; + + /** + * Hash-map for caching annotation metadata + * @var array + */ + private static $annotationMetadata = array( + 'Doctrine\Common\Annotations\Annotation\Target' => array( + 'is_annotation' => true, + 'has_constructor' => true, + 'properties' => array(), + 'targets_literal' => 'ANNOTATION_CLASS', + 'targets' => Target::TARGET_CLASS, + 'default_property' => 'value', + 'attribute_types' => array( + 'value' => array( + 'required' => false, + 'type' =>'array', + 'array_type'=>'string', + 'value' =>'array' + ) + ), + ), + 'Doctrine\Common\Annotations\Annotation\Attribute' => array( + 'is_annotation' => true, + 'has_constructor' => false, + 'targets_literal' => 'ANNOTATION_ANNOTATION', + 'targets' => Target::TARGET_ANNOTATION, + 'default_property' => 'name', + 'properties' => array( + 'name' => 'name', + 'type' => 'type', + 'required' => 'required' + ), + 'attribute_types' => array( + 'value' => array( + 'required' => true, + 'type' =>'string', + 'value' =>'string' + ), + 'type' => array( + 'required' =>true, + 'type' =>'string', + 'value' =>'string' + ), + 'required' => array( + 'required' =>false, + 'type' =>'boolean', + 'value' =>'boolean' + ) + ), + ), + 'Doctrine\Common\Annotations\Annotation\Attributes' => array( + 'is_annotation' => true, + 'has_constructor' => false, + 'targets_literal' => 'ANNOTATION_CLASS', + 'targets' => Target::TARGET_CLASS, + 'default_property' => 'value', + 'properties' => array( + 'value' => 'value' + ), + 'attribute_types' => array( + 'value' => array( + 'type' =>'array', + 'required' =>true, + 'array_type'=>'Doctrine\Common\Annotations\Annotation\Attribute', + 'value' =>'array' + ) + ), + ), + ); + + /** + * Hash-map for handle types declaration + * + * @var array + */ + private static $typeMap = array( + 'float' => 'double', + 'bool' => 'boolean', + // allow uppercase Boolean in honor of George Boole + 'Boolean' => 'boolean', + 'int' => 'integer', + ); + + /** + * Constructs a new DocParser. + */ + public function __construct() + { + $this->lexer = new DocLexer; + } + + /** + * Sets the annotation names that are ignored during the parsing process. + * + * The names are supposed to be the raw names as used in the class, not the + * fully qualified class names. + * + * @param array $names + */ + public function setIgnoredAnnotationNames(array $names) + { + $this->ignoredAnnotationNames = $names; + } + + /** + * Sets ignore on not-imported annotations + * + * @param $bool + */ + public function setIgnoreNotImportedAnnotations($bool) + { + $this->ignoreNotImportedAnnotations = (Boolean) $bool; + } + + /** + * Sets the default namespaces. + * + * @param array $namespace + * + * @throws \RuntimeException + */ + public function addNamespace($namespace) + { + if ($this->imports) { + throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); + } + $this->namespaces[] = $namespace; + } + + /** + * Sets the imports + * + * @param array $imports + * @throws \RuntimeException + */ + public function setImports(array $imports) + { + if ($this->namespaces) { + throw new \RuntimeException('You must either use addNamespace(), or setImports(), but not both.'); + } + $this->imports = $imports; + } + + /** + * Sets current target context as bitmask. + * + * @param integer $target + */ + public function setTarget($target) + { + $this->target = $target; + } + + /** + * Parses the given docblock string for annotations. + * + * @param string $input The docblock string to parse. + * @param string $context The parsing context. + * @return array Array of annotations. If no annotations are found, an empty array is returned. + */ + public function parse($input, $context = '') + { + if (false === $pos = strpos($input, '@')) { + return array(); + } + + // also parse whatever character is before the @ + if ($pos > 0) { + $pos -= 1; + } + + $this->context = $context; + $this->lexer->setInput(trim(substr($input, $pos), '* /')); + $this->lexer->moveNext(); + + return $this->Annotations(); + } + + /** + * Attempts to match the given token with the current lookahead token. + * If they match, updates the lookahead token; otherwise raises a syntax error. + * + * @param int $token type of Token. + * @return bool True if tokens match; false otherwise. + */ + private function match($token) + { + if ( ! $this->lexer->isNextToken($token) ) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + + return $this->lexer->moveNext(); + } + + /** + * Attempts to match the current lookahead token with any of the given tokens. + * + * If any of them matches, this method updates the lookahead token; otherwise + * a syntax error is raised. + * + * @param array $tokens + * @return bool + */ + private function matchAny(array $tokens) + { + if ( ! $this->lexer->isNextTokenAny($tokens)) { + $this->syntaxError(implode(' or ', array_map(array($this->lexer, 'getLiteral'), $tokens))); + } + + return $this->lexer->moveNext(); + } + + /** + * Generates a new syntax error. + * + * @param string $expected Expected string. + * @param array $token Optional token. + * + * @throws AnnotationException + */ + private function syntaxError($expected, $token = null) + { + if ($token === null) { + $token = $this->lexer->lookahead; + } + + $message = "Expected {$expected}, got "; + + if ($this->lexer->lookahead === null) { + $message .= 'end of string'; + } else { + $message .= "'{$token['value']}' at position {$token['position']}"; + } + + if (strlen($this->context)) { + $message .= ' in ' . $this->context; + } + + $message .= '.'; + + throw AnnotationException::syntaxError($message); + } + + /** + * Attempt to check if a class exists or not. This never goes through the PHP autoloading mechanism + * but uses the {@link AnnotationRegistry} to load classes. + * + * @param string $fqcn + * @return boolean + */ + private function classExists($fqcn) + { + if (isset($this->classExists[$fqcn])) { + return $this->classExists[$fqcn]; + } + + // first check if the class already exists, maybe loaded through another AnnotationReader + if (class_exists($fqcn, false)) { + return $this->classExists[$fqcn] = true; + } + + // final check, does this class exist? + return $this->classExists[$fqcn] = AnnotationRegistry::loadAnnotationClass($fqcn); + } + + /** + * Collects parsing metadata for a given annotation class + * + * @param string $name The annotation name + */ + private function collectAnnotationMetadata($name) + { + if (self::$metadataParser == null){ + self::$metadataParser = new self(); + self::$metadataParser->setTarget(Target::TARGET_CLASS); + self::$metadataParser->setIgnoreNotImportedAnnotations(true); + self::$metadataParser->setImports(array( + 'target' => 'Doctrine\Common\Annotations\Annotation\Target', + 'attribute' => 'Doctrine\Common\Annotations\Annotation\Attribute', + 'attributes' => 'Doctrine\Common\Annotations\Annotation\Attributes' + )); + AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Target.php'); + AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attribute.php'); + AnnotationRegistry::registerFile(__DIR__ . '/Annotation/Attributes.php'); + } + + $class = new \ReflectionClass($name); + $docComment = $class->getDocComment(); + + // Sets default values for annotation metadata + $metadata = array( + 'default_property' => null, + 'has_constructor' => (null !== $constructor = $class->getConstructor()) && $constructor->getNumberOfParameters() > 0, + 'properties' => array(), + 'property_types' => array(), + 'attribute_types' => array(), + 'targets_literal' => null, + 'targets' => Target::TARGET_ALL, + 'is_annotation' => false !== strpos($docComment, '@Annotation'), + ); + + // verify that the class is really meant to be an annotation + if ($metadata['is_annotation']) { + foreach (self::$metadataParser->parse($docComment, 'class @' . $name) as $annotation) { + if ($annotation instanceof Target) { + $metadata['targets'] = $annotation->targets; + $metadata['targets_literal'] = $annotation->literal; + + } elseif ($annotation instanceof Attributes) { + foreach ($annotation->value as $attrib) { + // handle internal type declaration + $type = isset(self::$typeMap[$attrib->type]) ? self::$typeMap[$attrib->type] : $attrib->type; + + // handle the case if the property type is mixed + if ('mixed' !== $type) { + // Checks if the property has array + if (false !== $pos = strpos($type, '<')) { + $arrayType = substr($type, $pos+1, -1); + $type = 'array'; + + if (isset(self::$typeMap[$arrayType])) { + $arrayType = self::$typeMap[$arrayType]; + } + + $metadata['attribute_types'][$attrib->name]['array_type'] = $arrayType; + } + + $metadata['attribute_types'][$attrib->name]['type'] = $type; + $metadata['attribute_types'][$attrib->name]['value'] = $attrib->type; + $metadata['attribute_types'][$attrib->name]['required'] = $attrib->required; + } + } + } + } + + // if not has a constructor will inject values into public properties + if (false === $metadata['has_constructor']) { + // collect all public properties + foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $metadata['properties'][$property->name] = $property->name; + + // checks if the property has @var annotation + if ((false !== $propertyComment = $property->getDocComment()) + && false !== strpos($propertyComment, '@var') + && preg_match('/@var\s+([^\s]+)/',$propertyComment, $matches)) { + // literal type declaration + $value = $matches[1]; + + // handle internal type declaration + $type = isset(self::$typeMap[$value]) ? self::$typeMap[$value] : $value; + + // handle the case if the property type is mixed + if ('mixed' !== $type) { + // Checks if the property has @var array annotation + if (false !== $pos = strpos($type, '<')) { + $arrayType = substr($type, $pos+1, -1); + $type = 'array'; + + if (isset(self::$typeMap[$arrayType])) { + $arrayType = self::$typeMap[$arrayType]; + } + + $metadata['attribute_types'][$property->name]['array_type'] = $arrayType; + } + + $metadata['attribute_types'][$property->name]['type'] = $type; + $metadata['attribute_types'][$property->name]['value'] = $value; + $metadata['attribute_types'][$property->name]['required'] = false !== strpos($propertyComment, '@Required'); + } + } + } + + // choose the first property as default property + $metadata['default_property'] = reset($metadata['properties']); + } + } + + self::$annotationMetadata[$name] = $metadata; + } + + /** + * Annotations ::= Annotation {[ "*" ]* [Annotation]}* + * + * @return array + */ + private function Annotations() + { + $annotations = array(); + + while (null !== $this->lexer->lookahead) { + if (DocLexer::T_AT !== $this->lexer->lookahead['type']) { + $this->lexer->moveNext(); + continue; + } + + // make sure the @ is preceded by non-catchable pattern + if (null !== $this->lexer->token && $this->lexer->lookahead['position'] === $this->lexer->token['position'] + strlen($this->lexer->token['value'])) { + $this->lexer->moveNext(); + continue; + } + + // make sure the @ is followed by either a namespace separator, or + // an identifier token + if ((null === $peek = $this->lexer->glimpse()) + || (DocLexer::T_NAMESPACE_SEPARATOR !== $peek['type'] && !in_array($peek['type'], self::$classIdentifiers, true)) + || $peek['position'] !== $this->lexer->lookahead['position'] + 1) { + $this->lexer->moveNext(); + continue; + } + + $this->isNestedAnnotation = false; + if (false !== $annot = $this->Annotation()) { + $annotations[] = $annot; + } + } + + return $annotations; + } + + /** + * Annotation ::= "@" AnnotationName ["(" [Values] ")"] + * AnnotationName ::= QualifiedName | SimpleName + * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName + * NameSpacePart ::= identifier | null | false | true + * SimpleName ::= identifier | null | false | true + * + * @throws AnnotationException + * @return mixed False if it is not a valid annotation. + */ + private function Annotation() + { + $this->match(DocLexer::T_AT); + + // check if we have an annotation + $name = $this->Identifier(); + + // only process names which are not fully qualified, yet + // fully qualified names must start with a \ + $originalName = $name; + if ('\\' !== $name[0]) { + $alias = (false === $pos = strpos($name, '\\'))? $name : substr($name, 0, $pos); + + $found = false; + if ($this->namespaces) { + foreach ($this->namespaces as $namespace) { + if ($this->classExists($namespace.'\\'.$name)) { + $name = $namespace.'\\'.$name; + $found = true; + break; + } + } + } elseif (isset($this->imports[$loweredAlias = strtolower($alias)])) { + if (false !== $pos) { + $name = $this->imports[$loweredAlias].substr($name, $pos); + } else { + $name = $this->imports[$loweredAlias]; + } + $found = true; + } elseif (isset($this->imports['__NAMESPACE__']) && $this->classExists($this->imports['__NAMESPACE__'].'\\'.$name)) { + $name = $this->imports['__NAMESPACE__'].'\\'.$name; + $found = true; + } elseif ($this->classExists($name)) { + $found = true; + } + + if (!$found) { + if ($this->ignoreNotImportedAnnotations || isset($this->ignoredAnnotationNames[$name])) { + return false; + } + + throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s was never imported. Did you maybe forget to add a "use" statement for this annotation?', $name, $this->context)); + } + } + + if (!$this->classExists($name)) { + throw AnnotationException::semanticalError(sprintf('The annotation "@%s" in %s does not exist, or could not be auto-loaded.', $name, $this->context)); + } + + // at this point, $name contains the fully qualified class name of the + // annotation, and it is also guaranteed that this class exists, and + // that it is loaded + + + // collects the metadata annotation only if there is not yet + if (!isset(self::$annotationMetadata[$name])) { + $this->collectAnnotationMetadata($name); + } + + // verify that the class is really meant to be an annotation and not just any ordinary class + if (self::$annotationMetadata[$name]['is_annotation'] === false) { + if (isset($this->ignoredAnnotationNames[$originalName])) { + return false; + } + + throw AnnotationException::semanticalError(sprintf('The class "%s" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "%s". If it is indeed no annotation, then you need to add @IgnoreAnnotation("%s") to the _class_ doc comment of %s.', $name, $name, $originalName, $this->context)); + } + + //if target is nested annotation + $target = $this->isNestedAnnotation ? Target::TARGET_ANNOTATION : $this->target; + + // Next will be nested + $this->isNestedAnnotation = true; + + //if annotation does not support current target + if (0 === (self::$annotationMetadata[$name]['targets'] & $target) && $target) { + throw AnnotationException::semanticalError( + sprintf('Annotation @%s is not allowed to be declared on %s. You may only use this annotation on these code elements: %s.', + $originalName, $this->context, self::$annotationMetadata[$name]['targets_literal']) + ); + } + + $values = array(); + if ($this->lexer->isNextToken(DocLexer::T_OPEN_PARENTHESIS)) { + $this->match(DocLexer::T_OPEN_PARENTHESIS); + + if ( ! $this->lexer->isNextToken(DocLexer::T_CLOSE_PARENTHESIS)) { + $values = $this->Values(); + } + + $this->match(DocLexer::T_CLOSE_PARENTHESIS); + } + + // checks all declared attributes + foreach (self::$annotationMetadata[$name]['attribute_types'] as $property => $type) { + if ($property === self::$annotationMetadata[$name]['default_property'] + && !isset($values[$property]) && isset($values['value'])) { + $property = 'value'; + } + + // handle a not given attribute or null value + if (!isset($values[$property])) { + if ($type['required']) { + throw AnnotationException::requiredError($property, $originalName, $this->context, 'a(n) '.$type['value']); + } + + continue; + } + + if ($type['type'] === 'array') { + // handle the case of a single value + if (!is_array($values[$property])) { + $values[$property] = array($values[$property]); + } + + // checks if the attribute has array type declaration, such as "array" + if (isset($type['array_type'])) { + foreach ($values[$property] as $item) { + if (gettype($item) !== $type['array_type'] && !$item instanceof $type['array_type']) { + throw AnnotationException::typeError($property, $originalName, $this->context, 'either a(n) '.$type['array_type'].', or an array of '.$type['array_type'].'s', $item); + } + } + } + } elseif (gettype($values[$property]) !== $type['type'] && !$values[$property] instanceof $type['type']) { + throw AnnotationException::typeError($property, $originalName, $this->context, 'a(n) '.$type['value'], $values[$property]); + } + } + + // check if the annotation expects values via the constructor, + // or directly injected into public properties + if (self::$annotationMetadata[$name]['has_constructor'] === true) { + return new $name($values); + } + + $instance = new $name(); + foreach ($values as $property => $value) { + if (!isset(self::$annotationMetadata[$name]['properties'][$property])) { + if ('value' !== $property) { + throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not have a property named "%s". Available properties: %s', $originalName, $this->context, $property, implode(', ', self::$annotationMetadata[$name]['properties']))); + } + + // handle the case if the property has no annotations + if (!$property = self::$annotationMetadata[$name]['default_property']) { + throw AnnotationException::creationError(sprintf('The annotation @%s declared on %s does not accept any values, but got %s.', $originalName, $this->context, json_encode($values))); + } + } + + $instance->{$property} = $value; + } + + return $instance; + } + + /** + * Values ::= Array | Value {"," Value}* + * + * @return array + */ + private function Values() + { + $values = array(); + + // Handle the case of a single array as value, i.e. @Foo({....}) + if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { + $values['value'] = $this->Value(); + return $values; + } + + $values[] = $this->Value(); + + while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { + $this->match(DocLexer::T_COMMA); + $token = $this->lexer->lookahead; + $value = $this->Value(); + + if ( ! is_object($value) && ! is_array($value)) { + $this->syntaxError('Value', $token); + } + + $values[] = $value; + } + + foreach ($values as $k => $value) { + if (is_object($value) && $value instanceof \stdClass) { + $values[$value->name] = $value->value; + } else if ( ! isset($values['value'])){ + $values['value'] = $value; + } else { + if ( ! is_array($values['value'])) { + $values['value'] = array($values['value']); + } + + $values['value'][] = $value; + } + + unset($values[$k]); + } + + return $values; + } + + /** + * Constant ::= integer | string | float | boolean + * + * @throws AnnotationException + * @return mixed + */ + private function Constant() + { + $identifier = $this->Identifier(); + + if (!defined($identifier) && false !== strpos($identifier, '::') && '\\' !== $identifier[0]) { + + list($className, $const) = explode('::', $identifier); + $alias = (false === $pos = strpos($className, '\\'))? $className : substr($className, 0, $pos); + + $found = false; + switch (true) { + case !empty ($this->namespaces): + foreach ($this->namespaces as $ns) { + if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { + $className = $ns.'\\'.$className; + $found = true; + break; + } + } + break; + + case isset($this->imports[$loweredAlias = strtolower($alias)]): + $found = true; + if (false !== $pos) { + $className = $this->imports[$loweredAlias].substr($className, $pos); + } else { + $className = $this->imports[$loweredAlias]; + } + break; + + default: + if(isset($this->imports['__NAMESPACE__'])) { + $ns = $this->imports['__NAMESPACE__']; + if (class_exists($ns.'\\'.$className) || interface_exists($ns.'\\'.$className)) { + $className = $ns.'\\'.$className; + $found = true; + } + } + break; + } + + if ($found) { + $identifier = $className . '::' . $const; + } + } + + if (!defined($identifier)) { + throw AnnotationException::semanticalErrorConstants($identifier, $this->context); + } + + return constant($identifier); + } + + /** + * Identifier ::= string + * + * @return string + */ + private function Identifier() + { + // check if we have an annotation + if ($this->lexer->isNextTokenAny(self::$classIdentifiers)) { + $this->lexer->moveNext(); + $className = $this->lexer->token['value']; + } else { + $this->syntaxError('namespace separator or identifier'); + } + + while ($this->lexer->lookahead['position'] === ($this->lexer->token['position'] + strlen($this->lexer->token['value'])) + && $this->lexer->isNextToken(DocLexer::T_NAMESPACE_SEPARATOR)) { + + $this->match(DocLexer::T_NAMESPACE_SEPARATOR); + $this->matchAny(self::$classIdentifiers); + $className .= '\\' . $this->lexer->token['value']; + } + + return $className; + } + + /** + * Value ::= PlainValue | FieldAssignment + * + * @return mixed + */ + private function Value() + { + $peek = $this->lexer->glimpse(); + + if (DocLexer::T_EQUALS === $peek['type']) { + return $this->FieldAssignment(); + } + + return $this->PlainValue(); + } + + /** + * PlainValue ::= integer | string | float | boolean | Array | Annotation + * + * @return mixed + */ + private function PlainValue() + { + if ($this->lexer->isNextToken(DocLexer::T_OPEN_CURLY_BRACES)) { + return $this->Arrayx(); + } + + if ($this->lexer->isNextToken(DocLexer::T_AT)) { + return $this->Annotation(); + } + + if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { + return $this->Constant(); + } + + switch ($this->lexer->lookahead['type']) { + case DocLexer::T_STRING: + $this->match(DocLexer::T_STRING); + return $this->lexer->token['value']; + + case DocLexer::T_INTEGER: + $this->match(DocLexer::T_INTEGER); + return (int)$this->lexer->token['value']; + + case DocLexer::T_FLOAT: + $this->match(DocLexer::T_FLOAT); + return (float)$this->lexer->token['value']; + + case DocLexer::T_TRUE: + $this->match(DocLexer::T_TRUE); + return true; + + case DocLexer::T_FALSE: + $this->match(DocLexer::T_FALSE); + return false; + + case DocLexer::T_NULL: + $this->match(DocLexer::T_NULL); + return null; + + default: + $this->syntaxError('PlainValue'); + } + } + + /** + * FieldAssignment ::= FieldName "=" PlainValue + * FieldName ::= identifier + * + * @return array + */ + private function FieldAssignment() + { + $this->match(DocLexer::T_IDENTIFIER); + $fieldName = $this->lexer->token['value']; + + $this->match(DocLexer::T_EQUALS); + + $item = new \stdClass(); + $item->name = $fieldName; + $item->value = $this->PlainValue(); + + return $item; + } + + /** + * Array ::= "{" ArrayEntry {"," ArrayEntry}* [","] "}" + * + * @return array + */ + private function Arrayx() + { + $array = $values = array(); + + $this->match(DocLexer::T_OPEN_CURLY_BRACES); + $values[] = $this->ArrayEntry(); + + while ($this->lexer->isNextToken(DocLexer::T_COMMA)) { + $this->match(DocLexer::T_COMMA); + + // optional trailing comma + if ($this->lexer->isNextToken(DocLexer::T_CLOSE_CURLY_BRACES)) { + break; + } + + $values[] = $this->ArrayEntry(); + } + + $this->match(DocLexer::T_CLOSE_CURLY_BRACES); + + foreach ($values as $value) { + list ($key, $val) = $value; + + if ($key !== null) { + $array[$key] = $val; + } else { + $array[] = $val; + } + } + + return $array; + } + + /** + * ArrayEntry ::= Value | KeyValuePair + * KeyValuePair ::= Key ("=" | ":") PlainValue | Constant + * Key ::= string | integer | Constant + * + * @return array + */ + private function ArrayEntry() + { + $peek = $this->lexer->glimpse(); + + if (DocLexer::T_EQUALS === $peek['type'] + || DocLexer::T_COLON === $peek['type']) { + + if ($this->lexer->isNextToken(DocLexer::T_IDENTIFIER)) { + $key = $this->Constant(); + } else { + $this->matchAny(array(DocLexer::T_INTEGER, DocLexer::T_STRING)); + $key = $this->lexer->token['value']; + } + + $this->matchAny(array(DocLexer::T_EQUALS, DocLexer::T_COLON)); + + return array($key, $this->PlainValue()); + } + + return array(null, $this->Value()); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/FileCacheReader.php b/doctrine/common/lib/Doctrine/Common/Annotations/FileCacheReader.php new file mode 100644 index 00000000..3934861b --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/FileCacheReader.php @@ -0,0 +1,258 @@ +. + */ + +namespace Doctrine\Common\Annotations; + + +/** + * File cache reader for annotations. + * + * @author Johannes M. Schmitt + * @author Benjamin Eberlei + */ +class FileCacheReader implements Reader +{ + /** + * @var Reader + */ + private $reader; + + /** + * @var string + */ + private $dir; + + /** + * @var bool + */ + private $debug; + + /** + * @var array + */ + private $loadedAnnotations = array(); + + /** + * Constructor + * + * @param Reader $reader + * @param string $cacheDir + * @param bool $debug + * + * @throws \InvalidArgumentException + */ + public function __construct(Reader $reader, $cacheDir, $debug = false) + { + $this->reader = $reader; + if (!is_dir($cacheDir) && !@mkdir($cacheDir, 0777, true)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $cacheDir)); + } + if (!is_writable($cacheDir)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable. Both, the webserver and the console user need access. You can manage access rights for multiple users with "chmod +a". If your system does not support this, check out the acl package.', $cacheDir)); + } + + $this->dir = rtrim($cacheDir, '\\/'); + $this->debug = $debug; + } + + /** + * Retrieve annotations for class + * + * @param \ReflectionClass $class + * @return array + */ + public function getClassAnnotations(\ReflectionClass $class) + { + $key = $class->getName(); + + if (isset($this->loadedAnnotations[$key])) { + return $this->loadedAnnotations[$key]; + } + + $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; + if (!file_exists($path)) { + $annot = $this->reader->getClassAnnotations($class); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + if ($this->debug + && (false !== $filename = $class->getFilename()) + && filemtime($path) < filemtime($filename)) { + @unlink($path); + + $annot = $this->reader->getClassAnnotations($class); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + return $this->loadedAnnotations[$key] = include $path; + } + + /** + * Get annotations for property + * + * @param \ReflectionProperty $property + * @return array + */ + public function getPropertyAnnotations(\ReflectionProperty $property) + { + $class = $property->getDeclaringClass(); + $key = $class->getName().'$'.$property->getName(); + + if (isset($this->loadedAnnotations[$key])) { + return $this->loadedAnnotations[$key]; + } + + $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; + if (!file_exists($path)) { + $annot = $this->reader->getPropertyAnnotations($property); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + if ($this->debug + && (false !== $filename = $class->getFilename()) + && filemtime($path) < filemtime($filename)) { + unlink($path); + + $annot = $this->reader->getPropertyAnnotations($property); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + return $this->loadedAnnotations[$key] = include $path; + } + + /** + * Retrieve annotations for method + * + * @param \ReflectionMethod $method + * @return array + */ + public function getMethodAnnotations(\ReflectionMethod $method) + { + $class = $method->getDeclaringClass(); + $key = $class->getName().'#'.$method->getName(); + + if (isset($this->loadedAnnotations[$key])) { + return $this->loadedAnnotations[$key]; + } + + $path = $this->dir.'/'.strtr($key, '\\', '-').'.cache.php'; + if (!file_exists($path)) { + $annot = $this->reader->getMethodAnnotations($method); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + if ($this->debug + && (false !== $filename = $class->getFilename()) + && filemtime($path) < filemtime($filename)) { + unlink($path); + + $annot = $this->reader->getMethodAnnotations($method); + $this->saveCacheFile($path, $annot); + return $this->loadedAnnotations[$key] = $annot; + } + + return $this->loadedAnnotations[$key] = include $path; + } + + /** + * Save cache file + * + * @param string $path + * @param mixed $data + */ + private function saveCacheFile($path, $data) + { + file_put_contents($path, 'getClassAnnotations($class); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Gets a method annotation. + * + * @param \ReflectionMethod $method + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + { + $annotations = $this->getMethodAnnotations($method); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Gets a property annotation. + * + * @param \ReflectionProperty $property + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + { + $annotations = $this->getPropertyAnnotations($property); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + return $annotation; + } + } + + return null; + } + + /** + * Clear stores annotations + */ + public function clearLoadedAnnotations() + { + $this->loadedAnnotations = array(); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/IndexedReader.php b/doctrine/common/lib/Doctrine/Common/Annotations/IndexedReader.php new file mode 100644 index 00000000..2dfdd4da --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/IndexedReader.php @@ -0,0 +1,141 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Doctrine\Common\Annotations\Reader; + +/** + * Allows the reader to be used in-place of Doctrine's reader. + * + * @author Johannes M. Schmitt + */ +class IndexedReader implements Reader +{ + /** + * @var Reader + */ + private $delegate; + + /** + * Constructor + * + * @param Reader $reader + */ + public function __construct(Reader $reader) + { + $this->delegate = $reader; + } + + /** + * Get Annotations for class + * + * @param \ReflectionClass $class + * @return array + */ + public function getClassAnnotations(\ReflectionClass $class) + { + $annotations = array(); + foreach ($this->delegate->getClassAnnotations($class) as $annot) { + $annotations[get_class($annot)] = $annot; + } + + return $annotations; + } + + /** + * Get selected annotation for class + * + * @param \ReflectionClass $class + * @param string $annotation + * @return mixed + */ + public function getClassAnnotation(\ReflectionClass $class, $annotation) + { + return $this->delegate->getClassAnnotation($class, $annotation); + } + + /** + * Get Annotations for method + * + * @param \ReflectionMethod $method + * @return array + */ + public function getMethodAnnotations(\ReflectionMethod $method) + { + $annotations = array(); + foreach ($this->delegate->getMethodAnnotations($method) as $annot) { + $annotations[get_class($annot)] = $annot; + } + + return $annotations; + } + + /** + * Get selected annotation for method + * + * @param \ReflectionMethod $method + * @param string $annotation + * @return mixed + */ + public function getMethodAnnotation(\ReflectionMethod $method, $annotation) + { + return $this->delegate->getMethodAnnotation($method, $annotation); + } + + /** + * Get annotations for property + * + * @param \ReflectionProperty $property + * @return array + */ + public function getPropertyAnnotations(\ReflectionProperty $property) + { + $annotations = array(); + foreach ($this->delegate->getPropertyAnnotations($property) as $annot) { + $annotations[get_class($annot)] = $annot; + } + + return $annotations; + } + + /** + * Get selected annotation for property + * + * @param \ReflectionProperty $property + * @param string $annotation + * @return mixed + */ + public function getPropertyAnnotation(\ReflectionProperty $property, $annotation) + { + return $this->delegate->getPropertyAnnotation($property, $annotation); + } + + /** + * Proxy all methods to the delegate. + * + * @param string $method + * @param array $args + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array(array($this->delegate, $method), $args); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/PhpParser.php b/doctrine/common/lib/Doctrine/Common/Annotations/PhpParser.php new file mode 100644 index 00000000..c09dd51d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/PhpParser.php @@ -0,0 +1,80 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use SplFileObject; + +/** + * Parses a file for namespaces/use/class declarations. + * + * @author Fabien Potencier + * @author Christian Kaps + */ +final class PhpParser +{ + /** + * Parses a class. + * + * @param \ReflectionClass $class A ReflectionClass object. + * @return array A list with use statements in the form (Alias => FQN). + */ + public function parseClass(\ReflectionClass $class) + { + if (method_exists($class, 'getUseStatements')) { + return $class->getUseStatements(); + } + + if (false === $filename = $class->getFilename()) { + return array(); + } + + $content = $this->getFileContent($filename, $class->getStartLine()); + $namespace = str_replace('\\', '\\\\', $class->getNamespaceName()); + $content = preg_replace('/^.*?(\bnamespace\s+' . $namespace . '\s*[;{].*)$/s', '\\1', $content); + $tokenizer = new TokenParser('parseUseStatements($class->getNamespaceName()); + + return $statements; + } + + /** + * Get the content of the file right up to the given line number. + * + * @param string $filename The name of the file to load. + * @param int $lineNumber The number of lines to read from file. + * @return string The content of the file. + */ + private function getFileContent($filename, $lineNumber) + { + $content = ''; + $lineCnt = 0; + $file = new SplFileObject($filename); + while (!$file->eof()) { + if ($lineCnt++ == $lineNumber) { + break; + } + + $content .= $file->fgets(); + } + + return $content; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/Reader.php b/doctrine/common/lib/Doctrine/Common/Annotations/Reader.php new file mode 100644 index 00000000..6a01cb4a --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/Reader.php @@ -0,0 +1,67 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Interface for annotation readers. + * + * @author Johannes M. Schmitt + */ +interface Reader +{ + /** + * @param \ReflectionClass $class + * @return mixed + */ + function getClassAnnotations(\ReflectionClass $class); + + /** + * @param \ReflectionClass $class + * @param string $annotationName + * @return mixed + */ + function getClassAnnotation(\ReflectionClass $class, $annotationName); + + /** + * @param \ReflectionMethod $method + * @return mixed + */ + function getMethodAnnotations(\ReflectionMethod $method); + + /** + * @param \ReflectionMethod $method + * @param string $annotationName + * @return mixed + */ + function getMethodAnnotation(\ReflectionMethod $method, $annotationName); + + /** + * @param \ReflectionProperty $property + * @return mixed + */ + function getPropertyAnnotations(\ReflectionProperty $property); + + /** + * @param \ReflectionProperty $property + * @param string $annotationName + * @return mixed + */ + function getPropertyAnnotation(\ReflectionProperty $property, $annotationName); +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php b/doctrine/common/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php new file mode 100644 index 00000000..4210d901 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/SimpleAnnotationReader.php @@ -0,0 +1,157 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Doctrine\Common\Annotations\Annotation\Target; + +/** + * Simple Annotation Reader. + * + * This annotation reader is intended to be used in projects where you have + * full-control over all annotations that are available. + * + * @since 2.2 + * @author Johannes M. Schmitt + * @author Fabio B. Silva + */ +class SimpleAnnotationReader implements Reader +{ + /** + * @var DocParser + */ + private $parser; + + /** + * Constructor. + * + * Initializes a new SimpleAnnotationReader. + */ + public function __construct() + { + $this->parser = new DocParser(); + $this->parser->setIgnoreNotImportedAnnotations(true); + } + + /** + * Adds a namespace in which we will look for annotations. + * + * @param string $namespace + */ + public function addNamespace($namespace) + { + $this->parser->addNamespace($namespace); + } + + /** + * Gets the annotations applied to a class. + * + * @param \ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. + * + * @return array An array of Annotations. + */ + public function getClassAnnotations(\ReflectionClass $class) + { + return $this->parser->parse($class->getDocComment(), 'class '.$class->getName()); + } + + /** + * Gets the annotations applied to a method. + * + * @param \ReflectionMethod $method The ReflectionMethod of the method from which + * the annotations should be read. + * + * @return array An array of Annotations. + */ + public function getMethodAnnotations(\ReflectionMethod $method) + { + return $this->parser->parse($method->getDocComment(), 'method '.$method->getDeclaringClass()->name.'::'.$method->getName().'()'); + } + + /** + * Gets the annotations applied to a property. + * + * @param \ReflectionProperty $property The ReflectionProperty of the property + * from which the annotations should be read. + * + * @return array An array of Annotations. + */ + public function getPropertyAnnotations(\ReflectionProperty $property) + { + return $this->parser->parse($property->getDocComment(), 'property '.$property->getDeclaringClass()->name.'::$'.$property->getName()); + } + + /** + * Gets a class annotation. + * + * @param \ReflectionClass $class The ReflectionClass of the class from which + * the class annotations should be read. + * @param string $annotationName The name of the annotation. + * + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getClassAnnotation(\ReflectionClass $class, $annotationName) + { + foreach ($this->getClassAnnotations($class) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * Gets a method annotation. + * + * @param \ReflectionMethod $method + * @param string $annotationName The name of the annotation. + * + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getMethodAnnotation(\ReflectionMethod $method, $annotationName) + { + foreach ($this->getMethodAnnotations($method) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } + + /** + * Gets a property annotation. + * + * @param \ReflectionProperty $property + * @param string $annotationName The name of the annotation. + * @return mixed The Annotation or NULL, if the requested annotation does not exist. + */ + public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) + { + foreach ($this->getPropertyAnnotations($property) as $annot) { + if ($annot instanceof $annotationName) { + return $annot; + } + } + + return null; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Annotations/TokenParser.php b/doctrine/common/lib/Doctrine/Common/Annotations/TokenParser.php new file mode 100644 index 00000000..a1ef1154 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Annotations/TokenParser.php @@ -0,0 +1,175 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Parses a file for namespaces/use/class declarations. + * + * @author Fabien Potencier + * @author Christian Kaps + */ +class TokenParser +{ + /** + * The token list. + * + * @var array + */ + private $tokens; + + /** + * The number of tokens. + * + * @var int + */ + private $numTokens = 0; + + /** + * The current array pointer. + * + * @var int + */ + private $pointer = 0; + + public function __construct($contents) + { + $this->tokens = token_get_all($contents); + $this->numTokens = count($this->tokens); + $this->pointer = 0; + } + + /** + * Gets the next non whitespace and non comment token. + * + * @param $docCommentIsComment + * If TRUE then a doc comment is considered a comment and skipped. + * If FALSE then only whitespace and normal comments are skipped. + * + * @return array The token if exists, null otherwise. + */ + public function next($docCommentIsComment = TRUE) + { + for ($i = $this->pointer; $i < $this->numTokens; $i++) { + $this->pointer++; + if ($this->tokens[$i][0] === T_WHITESPACE || + $this->tokens[$i][0] === T_COMMENT || + ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)) { + + continue; + } + + return $this->tokens[$i]; + } + + return null; + } + + /** + * Parse a single use statement. + * + * @return array A list with all found class names for a use statement. + */ + public function parseUseStatement() + { + $class = ''; + $alias = ''; + $statements = array(); + $explicitAlias = false; + while (($token = $this->next())) { + $isNameToken = $token[0] === T_STRING || $token[0] === T_NS_SEPARATOR; + if (!$explicitAlias && $isNameToken) { + $class .= $token[1]; + $alias = $token[1]; + } else if ($explicitAlias && $isNameToken) { + $alias .= $token[1]; + } else if ($token[0] === T_AS) { + $explicitAlias = true; + $alias = ''; + } else if ($token === ',') { + $statements[strtolower($alias)] = $class; + $class = ''; + $alias = ''; + $explicitAlias = false; + } else if ($token === ';') { + $statements[strtolower($alias)] = $class; + break; + } else { + break; + } + } + + return $statements; + } + + /** + * Get all use statements. + * + * @param string $namespaceName The namespace name of the reflected class. + * @return array A list with all found use statements. + */ + public function parseUseStatements($namespaceName) + { + $statements = array(); + while (($token = $this->next())) { + if ($token[0] === T_USE) { + $statements = array_merge($statements, $this->parseUseStatement()); + continue; + } + if ($token[0] !== T_NAMESPACE || $this->parseNamespace() != $namespaceName) { + continue; + } + + // Get fresh array for new namespace. This is to prevent the parser to collect the use statements + // for a previous namespace with the same name. This is the case if a namespace is defined twice + // or if a namespace with the same name is commented out. + $statements = array(); + } + + return $statements; + } + + /** + * Get the namespace. + * + * @return string The found namespace. + */ + public function parseNamespace() + { + $name = ''; + while (($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR)) { + $name .= $token[1]; + } + + return $name; + } + + /** + * Get the class name. + * + * @return string The foundclass name. + */ + public function parseClass() + { + // Namespaces and class names are tokenized the same: T_STRINGs + // separated by T_NS_SEPARATOR so we can use one function to provide + // both. + return $this->parseNamespace(); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/ApcCache.php b/doctrine/common/lib/Doctrine/Common/Cache/ApcCache.php new file mode 100644 index 00000000..2d0cd23a --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/ApcCache.php @@ -0,0 +1,93 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * APC cache provider. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class ApcCache extends CacheProvider +{ + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return apc_fetch($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return apc_exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + return (bool) apc_store($id, $data, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return apc_delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + return apc_clear_cache() && apc_clear_cache('user'); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $info = apc_cache_info(); + $sma = apc_sma_info(); + + return array( + Cache::STATS_HITS => $info['num_hits'], + Cache::STATS_MISSES => $info['num_misses'], + Cache::STATS_UPTIME => $info['start_time'], + Cache::STATS_MEMORY_USAGE => $info['mem_size'], + Cache::STATS_MEMORY_AVAILIABLE => $sma['avail_mem'], + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/ArrayCache.php b/doctrine/common/lib/Doctrine/Common/Cache/ArrayCache.php new file mode 100644 index 00000000..a7a70aad --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/ArrayCache.php @@ -0,0 +1,96 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Array cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class ArrayCache extends CacheProvider +{ + /** + * @var array $data + */ + private $data = array(); + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return (isset($this->data[$id])) ? $this->data[$id] : false; + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return isset($this->data[$id]); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + $this->data[$id] = $data; + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + unset($this->data[$id]); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + $this->data = array(); + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + return null; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/Cache.php b/doctrine/common/lib/Doctrine/Common/Cache/Cache.php new file mode 100644 index 00000000..5493562d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/Cache.php @@ -0,0 +1,102 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Interface for cache drivers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Fabio B. Silva + */ +interface Cache +{ + const STATS_HITS = 'hits'; + const STATS_MISSES = 'misses'; + const STATS_UPTIME = 'uptime'; + const STATS_MEMORY_USAGE = 'memory_usage'; + const STATS_MEMORY_AVAILIABLE = 'memory_available'; + + /** + * Fetches an entry from the cache. + * + * @param string $id cache id The id of the cache entry to fetch. + * @return mixed The cached data or FALSE, if no cache entry exists for the given id. + */ + function fetch($id); + + /** + * Test if an entry exists in the cache. + * + * @param string $id cache id The cache id of the entry to check for. + * @return boolean TRUE if a cache entry exists for the given cache id, FALSE otherwise. + */ + function contains($id); + + /** + * Puts data into the cache. + * + * @param string $id The cache id. + * @param mixed $data The cache entry/data. + * @param int $lifeTime The lifetime. If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime). + * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. + */ + function save($id, $data, $lifeTime = 0); + + /** + * Deletes a cache entry. + * + * @param string $id cache id + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + function delete($id); + + /** + * Retrieves cached information from data store + * + * The server's statistics array has the following values: + * + * - hits + * Number of keys that have been requested and found present. + * + * - misses + * Number of items that have been requested and not found. + * + * - uptime + * Time that the server is running. + * + * - memory_usage + * Memory used by this server to store items. + * + * - memory_available + * Memory allowed to use for storage. + * + * @since 2.2 + * @var array Associative array with server's statistics if available, NULL otherwise. + */ + function getStats(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/CacheProvider.php b/doctrine/common/lib/Doctrine/Common/Cache/CacheProvider.php new file mode 100644 index 00000000..4221a62e --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/CacheProvider.php @@ -0,0 +1,231 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Base class for cache provider implementations. + * + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Fabio B. Silva + */ +abstract class CacheProvider implements Cache +{ + const DOCTRINE_NAMESPACE_CACHEKEY = 'DoctrineNamespaceCacheKey[%s]'; + + /** + * @var string The namespace to prefix all cache ids with + */ + private $namespace = ''; + + /** + * @var string The namespace version + */ + private $namespaceVersion; + + /** + * Set the namespace to prefix all cache ids with. + * + * @param string $namespace + * @return void + */ + public function setNamespace($namespace) + { + $this->namespace = (string) $namespace; + } + + /** + * Retrieve the namespace that prefixes all cache ids. + * + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * {@inheritdoc} + */ + public function fetch($id) + { + return $this->doFetch($this->getNamespacedId($id)); + } + + /** + * {@inheritdoc} + */ + public function contains($id) + { + return $this->doContains($this->getNamespacedId($id)); + } + + /** + * {@inheritdoc} + */ + public function save($id, $data, $lifeTime = 0) + { + return $this->doSave($this->getNamespacedId($id), $data, $lifeTime); + } + + /** + * {@inheritdoc} + */ + public function delete($id) + { + return $this->doDelete($this->getNamespacedId($id)); + } + + /** + * {@inheritdoc} + */ + public function getStats() + { + return $this->doGetStats(); + } + + /** + * Deletes all cache entries. + * + * @return boolean TRUE if the cache entries were successfully flushed, FALSE otherwise. + */ + public function flushAll() + { + return $this->doFlush(); + } + + /** + * Delete all cache entries. + * + * @return boolean TRUE if the cache entries were successfully deleted, FALSE otherwise. + */ + public function deleteAll() + { + $namespaceCacheKey = $this->getNamespaceCacheKey(); + $namespaceVersion = $this->getNamespaceVersion() + 1; + + $this->namespaceVersion = $namespaceVersion; + + return $this->doSave($namespaceCacheKey, $namespaceVersion); + } + + /** + * Prefix the passed id with the configured namespace value + * + * @param string $id The id to namespace + * @return string $id The namespaced id + */ + private function getNamespacedId($id) + { + $namespaceVersion = $this->getNamespaceVersion(); + + return sprintf('%s[%s][%s]', $this->namespace, $id, $namespaceVersion); + } + + /** + * Namespace cache key + * + * @return string $namespaceCacheKey + */ + private function getNamespaceCacheKey() + { + return sprintf(self::DOCTRINE_NAMESPACE_CACHEKEY, $this->namespace); + } + + /** + * Namespace version + * + * @return string $namespaceVersion + */ + private function getNamespaceVersion() + { + if (null !== $this->namespaceVersion) { + return $this->namespaceVersion; + } + + $namespaceCacheKey = $this->getNamespaceCacheKey(); + $namespaceVersion = $this->doFetch($namespaceCacheKey); + + if (false === $namespaceVersion) { + $namespaceVersion = 1; + + $this->doSave($namespaceCacheKey, $namespaceVersion); + } + + $this->namespaceVersion = $namespaceVersion; + + return $this->namespaceVersion; + } + + /** + * Fetches an entry from the cache. + * + * @param string $id cache id The id of the cache entry to fetch. + * @return string The cached data or FALSE, if no cache entry exists for the given id. + */ + abstract protected function doFetch($id); + + /** + * Test if an entry exists in the cache. + * + * @param string $id cache id The cache id of the entry to check for. + * @return boolean TRUE if a cache entry exists for the given cache id, FALSE otherwise. + */ + abstract protected function doContains($id); + + /** + * Puts data into the cache. + * + * @param string $id The cache id. + * @param string $data The cache entry/data. + * @param bool|int $lifeTime The lifetime. If != false, sets a specific lifetime for this + * cache entry (null => infinite lifeTime). + * + * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. + */ + abstract protected function doSave($id, $data, $lifeTime = false); + + /** + * Deletes a cache entry. + * + * @param string $id cache id + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + abstract protected function doDelete($id); + + /** + * Deletes all cache entries. + * + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + abstract protected function doFlush(); + + /** + * Retrieves cached information from data store + * + * @since 2.2 + * @return array An associative array with server's statistics if available, NULL otherwise. + */ + abstract protected function doGetStats(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/FileCache.php b/doctrine/common/lib/Doctrine/Common/Cache/FileCache.php new file mode 100644 index 00000000..e808a1e3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/FileCache.php @@ -0,0 +1,132 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Base file cache driver. + * + * @since 2.3 + * @author Fabio B. Silva + */ +abstract class FileCache extends CacheProvider +{ + /** + * @var string Cache directory. + */ + protected $directory; + + /** + * @var string Cache file extension. + */ + protected $extension; + + /** + * Constructor + * + * @param string $directory Cache directory. + * @param string $directory Cache file extension. + * + * @throws \InvalidArgumentException + */ + public function __construct($directory, $extension = null) + { + if ( ! is_dir($directory) && ! @mkdir($directory, 0777, true)) { + throw new \InvalidArgumentException(sprintf( + 'The directory "%s" does not exist and could not be created.', + $directory + )); + } + + if ( ! is_writable($directory)) { + throw new \InvalidArgumentException(sprintf( + 'The directory "%s" is not writable.', + $directory + )); + } + + $this->directory = realpath($directory); + $this->extension = $extension ?: $this->extension; + } + + /** + * Gets the cache directory. + * + * @return string + */ + public function getDirectory() + { + return $this->directory; + } + + /** + * Gets the cache file extension. + * + * @return string + */ + public function getExtension() + { + return $this->extension; + } + + /** + * @return string + */ + protected function getFilename($id) + { + $path = implode(str_split(md5($id), 12), DIRECTORY_SEPARATOR); + $path = $this->directory . DIRECTORY_SEPARATOR . $path; + + return $path . DIRECTORY_SEPARATOR . $id . $this->extension; + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return @unlink($this->getFilename($id)); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + $pattern = '/^.+\\' . $this->extension . '$/i'; + $iterator = new \RecursiveDirectoryIterator($this->directory); + $iterator = new \RecursiveIteratorIterator($iterator); + $iterator = new \RegexIterator($iterator, $pattern); + + foreach ($iterator as $name => $file) { + @unlink($name); + } + + return true; + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + return null; + } +} \ No newline at end of file diff --git a/doctrine/common/lib/Doctrine/Common/Cache/FilesystemCache.php b/doctrine/common/lib/Doctrine/Common/Cache/FilesystemCache.php new file mode 100644 index 00000000..a27a7176 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/FilesystemCache.php @@ -0,0 +1,114 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Filesystem cache driver. + * + * @since 2.3 + * @author Fabio B. Silva + */ +class FilesystemCache extends FileCache +{ + const EXTENSION = '.doctrinecache.data'; + + /** + * {@inheritdoc} + */ + protected $extension = self::EXTENSION; + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + $data = ''; + $lifetime = -1; + $filename = $this->getFilename($id); + + if ( ! file_exists($filename)) { + return false; + } + + $resource = fopen($filename, "r"); + + if (false !== ($line = fgets($resource))) { + $lifetime = (integer) $line; + } + + if ($lifetime !== 0 && $lifetime < time()) { + fclose($resource); + + return false; + } + + while (false !== ($line = fgets($resource))) { + $data .= $line; + } + + fclose($resource); + + return unserialize($data); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + $lifetime = -1; + $filename = $this->getFilename($id); + + if ( ! file_exists($filename)) { + return false; + } + + $resource = fopen($filename, "r"); + + if (false !== ($line = fgets($resource))) { + $lifetime = (integer) $line; + } + + fclose($resource); + + return $lifetime === 0 || $lifetime > time(); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + if ($lifeTime > 0) { + $lifeTime = time() + $lifeTime; + } + + $data = serialize($data); + $filename = $this->getFilename($id); + $filepath = pathinfo($filename, PATHINFO_DIRNAME); + + if ( ! is_dir($filepath)) { + mkdir($filepath, 0777, true); + } + + return file_put_contents($filename, $lifeTime . PHP_EOL . $data); + } +} \ No newline at end of file diff --git a/doctrine/common/lib/Doctrine/Common/Cache/MemcacheCache.php b/doctrine/common/lib/Doctrine/Common/Cache/MemcacheCache.php new file mode 100644 index 00000000..5687b965 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/MemcacheCache.php @@ -0,0 +1,121 @@ +. + */ + +namespace Doctrine\Common\Cache; + +use \Memcache; + +/** + * Memcache cache provider. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class MemcacheCache extends CacheProvider +{ + /** + * @var Memcache + */ + private $memcache; + + /** + * Sets the memcache instance to use. + * + * @param Memcache $memcache + */ + public function setMemcache(Memcache $memcache) + { + $this->memcache = $memcache; + } + + /** + * Gets the memcache instance used by the cache. + * + * @return Memcache + */ + public function getMemcache() + { + return $this->memcache; + } + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return $this->memcache->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return (bool) $this->memcache->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + if ($lifeTime > 30 * 24 * 3600) { + $lifeTime = time() + $lifeTime; + } + return $this->memcache->set($id, $data, 0, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return $this->memcache->delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + return $this->memcache->flush(); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $stats = $this->memcache->getStats(); + return array( + Cache::STATS_HITS => $stats['get_hits'], + Cache::STATS_MISSES => $stats['get_misses'], + Cache::STATS_UPTIME => $stats['uptime'], + Cache::STATS_MEMORY_USAGE => $stats['bytes'], + Cache::STATS_MEMORY_AVAILIABLE => $stats['limit_maxbytes'], + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/MemcachedCache.php b/doctrine/common/lib/Doctrine/Common/Cache/MemcachedCache.php new file mode 100644 index 00000000..75f13455 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/MemcachedCache.php @@ -0,0 +1,124 @@ +. + */ + +namespace Doctrine\Common\Cache; + +use \Memcached; + +/** + * Memcached cache provider. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class MemcachedCache extends CacheProvider +{ + /** + * @var Memcached + */ + private $memcached; + + /** + * Sets the memcache instance to use. + * + * @param Memcached $memcached + */ + public function setMemcached(Memcached $memcached) + { + $this->memcached = $memcached; + } + + /** + * Gets the memcached instance used by the cache. + * + * @return Memcached + */ + public function getMemcached() + { + return $this->memcached; + } + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return $this->memcached->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return (false !== $this->memcached->get($id)); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + if ($lifeTime > 30 * 24 * 3600) { + $lifeTime = time() + $lifeTime; + } + return $this->memcached->set($id, $data, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return $this->memcached->delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + return $this->memcached->flush(); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $stats = $this->memcached->getStats(); + $servers = $this->memcached->getServerList(); + $key = $servers[0]['host'] . ':' . $servers[0]['port']; + $stats = $stats[$key]; + return array( + Cache::STATS_HITS => $stats['get_hits'], + Cache::STATS_MISSES => $stats['get_misses'], + Cache::STATS_UPTIME => $stats['uptime'], + Cache::STATS_MEMORY_USAGE => $stats['bytes'], + Cache::STATS_MEMORY_AVAILIABLE => $stats['limit_maxbytes'], + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/PhpFileCache.php b/doctrine/common/lib/Doctrine/Common/Cache/PhpFileCache.php new file mode 100644 index 00000000..0971cd96 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/PhpFileCache.php @@ -0,0 +1,108 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Php file cache driver. + * + * @since 2.3 + * @author Fabio B. Silva + */ +class PhpFileCache extends FileCache +{ + const EXTENSION = '.doctrinecache.php'; + + /** + * {@inheritdoc} + */ + protected $extension = self::EXTENSION; + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + $filename = $this->getFilename($id); + + if ( ! file_exists($filename)) { + return false; + } + + $value = include $filename; + + if ($value['lifetime'] !== 0 && $value['lifetime'] < time()) { + return false; + } + + return $value['data']; + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + $filename = $this->getFilename($id); + + if ( ! file_exists($filename)) { + return false; + } + + $value = include $filename; + + return $value['lifetime'] === 0 || $value['lifetime'] > time(); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + if ($lifeTime > 0) { + $lifeTime = time() + $lifeTime; + } + + if (is_object($data) && ! method_exists($data, '__set_state')) { + throw new \InvalidArgumentException( + "Invalid argument given, PhpFileCache only allows objects that implement __set_state() " . + "and fully support var_export(). You can use the FilesystemCache to save arbitrary object " . + "graphs using serialize()/deserialize()." + ); + } + + $filename = $this->getFilename($id); + $filepath = pathinfo($filename, PATHINFO_DIRNAME); + + if ( ! is_dir($filepath)) { + mkdir($filepath, 0777, true); + } + + $value = array( + 'lifetime' => $lifeTime, + 'data' => $data + ); + + $value = var_export($value, true); + $code = sprintf('. + */ + +namespace Doctrine\Common\Cache; + +use Redis; + +/** + * Redis cache provider. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Osman Ungur + */ +class RedisCache extends CacheProvider +{ + /** + * @var Redis + */ + private $redis; + + /** + * Sets the redis instance to use. + * + * @param Redis $redis + */ + public function setRedis(Redis $redis) + { + $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); + $this->redis = $redis; + } + + /** + * Gets the redis instance used by the cache. + * + * @return Redis + */ + public function getRedis() + { + return $this->redis; + } + + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return $this->redis->get($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return $this->redis->exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + $result = $this->redis->set($id, $data); + if ($lifeTime > 0) { + $this->redis->expire($id, $lifeTime); + } + return $result; + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return $this->redis->delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + return $this->redis->flushDB(); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $info = $this->redis->info(); + return array( + Cache::STATS_HITS => false, + Cache::STATS_MISSES => false, + Cache::STATS_UPTIME => $info['uptime_in_seconds'], + Cache::STATS_MEMORY_USAGE => $info['used_memory'], + Cache::STATS_MEMORY_AVAILIABLE => false + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/WinCacheCache.php b/doctrine/common/lib/Doctrine/Common/Cache/WinCacheCache.php new file mode 100644 index 00000000..777d0fd5 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/WinCacheCache.php @@ -0,0 +1,93 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * WinCache cache provider. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class WinCacheCache extends CacheProvider +{ + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return wincache_ucache_get($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return wincache_ucache_exists($id); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + return (bool) wincache_ucache_set($id, $data, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return wincache_ucache_delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + return wincache_ucache_clear(); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $info = wincache_ucache_info(); + $meminfo = wincache_ucache_meminfo(); + + return array( + Cache::STATS_HITS => $info['total_hit_count'], + Cache::STATS_MISSES => $info['total_miss_count'], + Cache::STATS_UPTIME => $info['total_cache_uptime'], + Cache::STATS_MEMORY_USAGE => $meminfo['memory_total'], + Cache::STATS_MEMORY_AVAILIABLE => $meminfo['memory_free'], + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/XcacheCache.php b/doctrine/common/lib/Doctrine/Common/Cache/XcacheCache.php new file mode 100644 index 00000000..8733e266 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/XcacheCache.php @@ -0,0 +1,110 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Xcache cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class XcacheCache extends CacheProvider +{ + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return $this->doContains($id) ? unserialize(xcache_get($id)) : false; + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return xcache_isset($id); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + return xcache_set($id, serialize($data), (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return xcache_unset($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + $this->checkAuthorization(); + + xcache_clear_cache(XC_TYPE_VAR, 0); + + return true; + } + + /** + * Checks that xcache.admin.enable_auth is Off + * + * @throws \BadMethodCallException When xcache.admin.enable_auth is On + * @return void + */ + protected function checkAuthorization() + { + if (ini_get('xcache.admin.enable_auth')) { + throw new \BadMethodCallException('To use all features of \Doctrine\Common\Cache\XcacheCache, you must set "xcache.admin.enable_auth" to "Off" in your php.ini.'); + } + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + $this->checkAuthorization(); + + $info = xcache_info(XC_TYPE_VAR, 0); + return array( + Cache::STATS_HITS => $info['hits'], + Cache::STATS_MISSES => $info['misses'], + Cache::STATS_UPTIME => null, + Cache::STATS_MEMORY_USAGE => $info['size'], + Cache::STATS_MEMORY_AVAILIABLE => $info['avail'], + ); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Cache/ZendDataCache.php b/doctrine/common/lib/Doctrine/Common/Cache/ZendDataCache.php new file mode 100644 index 00000000..fc90bc69 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Cache/ZendDataCache.php @@ -0,0 +1,84 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Zend Data Cache cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Ralph Schindler + * @author Guilherme Blanco + */ +class ZendDataCache extends CacheProvider +{ + /** + * {@inheritdoc} + */ + protected function doFetch($id) + { + return zend_shm_cache_fetch($id); + } + + /** + * {@inheritdoc} + */ + protected function doContains($id) + { + return (false !== zend_shm_cache_fetch($id)); + } + + /** + * {@inheritdoc} + */ + protected function doSave($id, $data, $lifeTime = 0) + { + return zend_shm_cache_store($id, $data, $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function doDelete($id) + { + return zend_shm_cache_delete($id); + } + + /** + * {@inheritdoc} + */ + protected function doFlush() + { + $namespace = $this->getNamespace(); + if (empty($namespace)) { + return zend_shm_cache_clear(); + } + return zend_shm_cache_clear($namespace); + } + + /** + * {@inheritdoc} + */ + protected function doGetStats() + { + return null; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/ClassLoader.php b/doctrine/common/lib/Doctrine/Common/ClassLoader.php new file mode 100644 index 00000000..45024e16 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/ClassLoader.php @@ -0,0 +1,263 @@ +. + */ + +namespace Doctrine\Common; + +/** + * A ClassLoader is an autoloader for class files that can be + * installed on the SPL autoload stack. It is a class loader that either loads only classes + * of a specific namespace or all namespaces and it is suitable for working together + * with other autoloaders in the SPL autoload stack. + * + * If no include path is configured through the constructor or {@link setIncludePath}, a ClassLoader + * relies on the PHP include_path. + * + * @author Roman Borschel + * @since 2.0 + */ +class ClassLoader +{ + /** + * @var string PHP file extension + */ + protected $fileExtension = '.php'; + + /** + * @var string Current namespace + */ + protected $namespace; + + /** + * @var string Current include path + */ + protected $includePath; + + /** + * @var string PHP namespace separator + */ + protected $namespaceSeparator = '\\'; + + /** + * Creates a new ClassLoader that loads classes of the + * specified namespace from the specified include path. + * + * If no include path is given, the ClassLoader relies on the PHP include_path. + * If neither a namespace nor an include path is given, the ClassLoader will + * be responsible for loading all classes, thereby relying on the PHP include_path. + * + * @param string $ns The namespace of the classes to load. + * @param string $includePath The base include path to use. + */ + public function __construct($ns = null, $includePath = null) + { + $this->namespace = $ns; + $this->includePath = $includePath; + } + + /** + * Sets the namespace separator used by classes in the namespace of this ClassLoader. + * + * @param string $sep The separator to use. + */ + public function setNamespaceSeparator($sep) + { + $this->namespaceSeparator = $sep; + } + + /** + * Gets the namespace separator used by classes in the namespace of this ClassLoader. + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Sets the base include path for all class files in the namespace of this ClassLoader. + * + * @param string $includePath + */ + public function setIncludePath($includePath) + { + $this->includePath = $includePath; + } + + /** + * Gets the base include path for all class files in the namespace of this ClassLoader. + * + * @return string + */ + public function getIncludePath() + { + return $this->includePath; + } + + /** + * Sets the file extension of class files in the namespace of this ClassLoader. + * + * @param string $fileExtension + */ + public function setFileExtension($fileExtension) + { + $this->fileExtension = $fileExtension; + } + + /** + * Gets the file extension of class files in the namespace of this ClassLoader. + * + * @return string + */ + public function getFileExtension() + { + return $this->fileExtension; + } + + /** + * Registers this ClassLoader on the SPL autoload stack. + */ + public function register() + { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Removes this ClassLoader from the SPL autoload stack. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $className The name of the class to load. + + * @return boolean TRUE if the class has been successfully loaded, FALSE otherwise. + */ + public function loadClass($className) + { + if ($this->namespace !== null && strpos($className, $this->namespace.$this->namespaceSeparator) !== 0) { + return false; + } + + require ($this->includePath !== null ? $this->includePath . DIRECTORY_SEPARATOR : '') + . str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) + . $this->fileExtension; + + return true; + } + + /** + * Asks this ClassLoader whether it can potentially load the class (file) with + * the given name. + * + * @param string $className The fully-qualified name of the class. + * @return boolean TRUE if this ClassLoader can load the class, FALSE otherwise. + */ + public function canLoadClass($className) + { + if ($this->namespace !== null && strpos($className, $this->namespace.$this->namespaceSeparator) !== 0) { + return false; + } + + $file = str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) . $this->fileExtension; + + if ($this->includePath !== null) { + return file_exists($this->includePath . DIRECTORY_SEPARATOR . $file); + } + + return (false !== stream_resolve_include_path($file)); + } + + /** + * Checks whether a class with a given name exists. A class "exists" if it is either + * already defined in the current request or if there is an autoloader on the SPL + * autoload stack that is a) responsible for the class in question and b) is able to + * load a class file in which the class definition resides. + * + * If the class is not already defined, each autoloader in the SPL autoload stack + * is asked whether it is able to tell if the class exists. If the autoloader is + * a ClassLoader, {@link canLoadClass} is used, otherwise the autoload + * function of the autoloader is invoked and expected to return a value that + * evaluates to TRUE if the class (file) exists. As soon as one autoloader reports + * that the class exists, TRUE is returned. + * + * Note that, depending on what kinds of autoloaders are installed on the SPL + * autoload stack, the class (file) might already be loaded as a result of checking + * for its existence. This is not the case with a ClassLoader, who separates + * these responsibilities. + * + * @param string $className The fully-qualified name of the class. + * @return boolean TRUE if the class exists as per the definition given above, FALSE otherwise. + */ + public static function classExists($className) + { + if (class_exists($className, false) || interface_exists($className, false)) { + return true; + } + + foreach (spl_autoload_functions() as $loader) { + if (is_array($loader)) { // array(???, ???) + if (is_object($loader[0])) { + if ($loader[0] instanceof ClassLoader) { // array($obj, 'methodName') + if ($loader[0]->canLoadClass($className)) { + return true; + } + } else if ($loader[0]->{$loader[1]}($className)) { + return true; + } + } else if ($loader[0]::$loader[1]($className)) { // array('ClassName', 'methodName') + return true; + } + } else if ($loader instanceof \Closure) { // function($className) {..} + if ($loader($className)) { + return true; + } + } else if (is_string($loader) && $loader($className)) { // "MyClass::loadClass" + return true; + } + } + + return class_exists($className, false) || interface_exists($className, false); + } + + /** + * Gets the ClassLoader from the SPL autoload stack that is responsible + * for (and is able to load) the class with the given name. + * + * @param string $className The name of the class. + * @return ClassLoader The ClassLoader for the class or NULL if no such ClassLoader exists. + */ + public static function getClassLoader($className) + { + foreach (spl_autoload_functions() as $loader) { + if (is_array($loader) + && $loader[0] instanceof ClassLoader + && $loader[0]->canLoadClass($className) + ) { + return $loader[0]; + } + } + + return null; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/ArrayCollection.php b/doctrine/common/lib/Doctrine/Common/Collections/ArrayCollection.php new file mode 100644 index 00000000..3f62cdd3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/ArrayCollection.php @@ -0,0 +1,499 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Closure, ArrayIterator; +use Doctrine\Common\Collections\Expr\Expression; +use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor; + +/** + * An ArrayCollection is a Collection implementation that wraps a regular PHP array. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ArrayCollection implements Collection, Selectable +{ + /** + * An array containing the entries of this collection. + * + * @var array + */ + private $_elements; + + /** + * Initializes a new ArrayCollection. + * + * @param array $elements + */ + public function __construct(array $elements = array()) + { + $this->_elements = $elements; + } + + /** + * Gets the PHP array representation of this collection. + * + * @return array The PHP array representation of this collection. + */ + public function toArray() + { + return $this->_elements; + } + + /** + * Sets the internal iterator to the first element in the collection and + * returns this element. + * + * @return mixed + */ + public function first() + { + return reset($this->_elements); + } + + /** + * Sets the internal iterator to the last element in the collection and + * returns this element. + * + * @return mixed + */ + public function last() + { + return end($this->_elements); + } + + /** + * Gets the current key/index at the current internal iterator position. + * + * @return mixed + */ + public function key() + { + return key($this->_elements); + } + + /** + * Moves the internal iterator position to the next element. + * + * @return mixed + */ + public function next() + { + return next($this->_elements); + } + + /** + * Gets the element of the collection at the current internal iterator position. + * + * @return mixed + */ + public function current() + { + return current($this->_elements); + } + + /** + * Removes an element with a specific key/index from the collection. + * + * @param mixed $key + * @return mixed The removed element or NULL, if no element exists for the given key. + */ + public function remove($key) + { + if (isset($this->_elements[$key])) { + $removed = $this->_elements[$key]; + unset($this->_elements[$key]); + + return $removed; + } + + return null; + } + + /** + * Removes the specified element from the collection, if it is found. + * + * @param mixed $element The element to remove. + * @return boolean TRUE if this collection contained the specified element, FALSE otherwise. + */ + public function removeElement($element) + { + $key = array_search($element, $this->_elements, true); + + if ($key !== false) { + unset($this->_elements[$key]); + + return true; + } + + return false; + } + + /** + * ArrayAccess implementation of offsetExists() + * + * @see containsKey() + * + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return $this->containsKey($offset); + } + + /** + * ArrayAccess implementation of offsetGet() + * + * @see get() + * + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * ArrayAccess implementation of offsetSet() + * + * @see add() + * @see set() + * + * @param mixed $offset + * @param mixed $value + * @return bool + */ + public function offsetSet($offset, $value) + { + if ( ! isset($offset)) { + return $this->add($value); + } + return $this->set($offset, $value); + } + + /** + * ArrayAccess implementation of offsetUnset() + * + * @see remove() + * + * @param mixed $offset + * @return mixed + */ + public function offsetUnset($offset) + { + return $this->remove($offset); + } + + /** + * Checks whether the collection contains a specific key/index. + * + * @param mixed $key The key to check for. + * @return boolean TRUE if the given key/index exists, FALSE otherwise. + */ + public function containsKey($key) + { + return isset($this->_elements[$key]); + } + + /** + * Checks whether the given element is contained in the collection. + * Only element values are compared, not keys. The comparison of two elements + * is strict, that means not only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element + * @return boolean TRUE if the given element is contained in the collection, + * FALSE otherwise. + */ + public function contains($element) + { + foreach ($this->_elements as $collectionElement) { + if ($element === $collectionElement) { + return true; + } + } + + return false; + } + + /** + * Tests for the existence of an element that satisfies the given predicate. + * + * @param Closure $p The predicate. + * @return boolean TRUE if the predicate is TRUE for at least one element, FALSE otherwise. + */ + public function exists(Closure $p) + { + foreach ($this->_elements as $key => $element) { + if ($p($key, $element)) { + return true; + } + } + return false; + } + + /** + * Searches for a given element and, if found, returns the corresponding key/index + * of that element. The comparison of two elements is strict, that means not + * only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element The element to search for. + * @return mixed The key/index of the element or FALSE if the element was not found. + */ + public function indexOf($element) + { + return array_search($element, $this->_elements, true); + } + + /** + * Gets the element with the given key/index. + * + * @param mixed $key The key. + * @return mixed The element or NULL, if no element exists for the given key. + */ + public function get($key) + { + if (isset($this->_elements[$key])) { + return $this->_elements[$key]; + } + return null; + } + + /** + * Gets all keys/indexes of the collection elements. + * + * @return array + */ + public function getKeys() + { + return array_keys($this->_elements); + } + + /** + * Gets all elements. + * + * @return array + */ + public function getValues() + { + return array_values($this->_elements); + } + + /** + * Returns the number of elements in the collection. + * + * Implementation of the Countable interface. + * + * @return integer The number of elements in the collection. + */ + public function count() + { + return count($this->_elements); + } + + /** + * Adds/sets an element in the collection at the index / with the specified key. + * + * When the collection is a Map this is like put(key,value)/add(key,value). + * When the collection is a List this is like add(position,value). + * + * @param mixed $key + * @param mixed $value + */ + public function set($key, $value) + { + $this->_elements[$key] = $value; + } + + /** + * Adds an element to the collection. + * + * @param mixed $value + * @return boolean Always TRUE. + */ + public function add($value) + { + $this->_elements[] = $value; + return true; + } + + /** + * Checks whether the collection is empty. + * + * Note: This is preferable over count() == 0. + * + * @return boolean TRUE if the collection is empty, FALSE otherwise. + */ + public function isEmpty() + { + return ! $this->_elements; + } + + /** + * Gets an iterator for iterating over the elements in the collection. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_elements); + } + + /** + * Applies the given function to each element in the collection and returns + * a new collection with the elements returned by the function. + * + * @param Closure $func + * @return Collection + */ + public function map(Closure $func) + { + return new static(array_map($func, $this->_elements)); + } + + /** + * Returns all the elements of this collection that satisfy the predicate p. + * The order of the elements is preserved. + * + * @param Closure $p The predicate used for filtering. + * @return Collection A collection with the results of the filter operation. + */ + public function filter(Closure $p) + { + return new static(array_filter($this->_elements, $p)); + } + + /** + * Applies the given predicate p to all elements of this collection, + * returning true, if the predicate yields true for all elements. + * + * @param Closure $p The predicate. + * @return boolean TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p) + { + foreach ($this->_elements as $key => $element) { + if ( ! $p($key, $element)) { + return false; + } + } + + return true; + } + + /** + * Partitions this collection in two collections according to a predicate. + * Keys are preserved in the resulting collections. + * + * @param Closure $p The predicate on which to partition. + * @return array An array with two elements. The first element contains the collection + * of elements where the predicate returned TRUE, the second element + * contains the collection of elements where the predicate returned FALSE. + */ + public function partition(Closure $p) + { + $coll1 = $coll2 = array(); + foreach ($this->_elements as $key => $element) { + if ($p($key, $element)) { + $coll1[$key] = $element; + } else { + $coll2[$key] = $element; + } + } + return array(new static($coll1), new static($coll2)); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return __CLASS__ . '@' . spl_object_hash($this); + } + + /** + * Clears the collection. + */ + public function clear() + { + $this->_elements = array(); + } + + /** + * Extract a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset + * @param int $length + * @return array + */ + public function slice($offset, $length = null) + { + return array_slice($this->_elements, $offset, $length, true); + } + + /** + * Select all elements from a selectable that match the criteria and + * return a new collection containing these elements. + * + * @param Criteria $criteria + * @return Collection + */ + public function matching(Criteria $criteria) + { + $expr = $criteria->getWhereExpression(); + $filtered = $this->_elements; + + if ($expr) { + $visitor = new ClosureExpressionVisitor(); + $filter = $visitor->dispatch($expr); + $filtered = array_filter($filtered, $filter); + } + + if ($orderings = $criteria->getOrderings()) { + $next = null; + foreach (array_reverse($orderings) as $field => $ordering) { + $next = ClosureExpressionVisitor::sortByField($field, $ordering == 'DESC' ? -1 : 1, $next); + } + + usort($filtered, $next); + } + + $offset = $criteria->getFirstResult(); + $length = $criteria->getMaxResults(); + + if ($offset || $length) { + $filtered = array_slice($filtered, (int)$offset, $length); + } + + return new static($filtered); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Collection.php b/doctrine/common/lib/Doctrine/Common/Collections/Collection.php new file mode 100644 index 00000000..51eb9e7a --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Collection.php @@ -0,0 +1,243 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Closure, Countable, IteratorAggregate, ArrayAccess; + +/** + * The missing (SPL) Collection/Array/OrderedMap interface. + * + * A Collection resembles the nature of a regular PHP array. That is, + * it is essentially an ordered map that can also be used + * like a list. + * + * A Collection has an internal iterator just like a PHP array. In addition, + * a Collection can be iterated with external iterators, which is preferrable. + * To use an external iterator simply use the foreach language construct to + * iterate over the collection (which calls {@link getIterator()} internally) or + * explicitly retrieve an iterator though {@link getIterator()} which can then be + * used to iterate over the collection. + * You can not rely on the internal iterator of the collection being at a certain + * position unless you explicitly positioned it before. Prefer iteration with + * external iterators. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface Collection extends Countable, IteratorAggregate, ArrayAccess +{ + /** + * Adds an element at the end of the collection. + * + * @param mixed $element The element to add. + * @return boolean Always TRUE. + */ + function add($element); + + /** + * Clears the collection, removing all elements. + */ + function clear(); + + /** + * Checks whether an element is contained in the collection. + * This is an O(n) operation, where n is the size of the collection. + * + * @param mixed $element The element to search for. + * @return boolean TRUE if the collection contains the element, FALSE otherwise. + */ + function contains($element); + + /** + * Checks whether the collection is empty (contains no elements). + * + * @return boolean TRUE if the collection is empty, FALSE otherwise. + */ + function isEmpty(); + + /** + * Removes the element at the specified index from the collection. + * + * @param string|integer $key The kex/index of the element to remove. + * @return mixed The removed element or NULL, if the collection did not contain the element. + */ + function remove($key); + + /** + * Removes the specified element from the collection, if it is found. + * + * @param mixed $element The element to remove. + * @return boolean TRUE if this collection contained the specified element, FALSE otherwise. + */ + function removeElement($element); + + /** + * Checks whether the collection contains an element with the specified key/index. + * + * @param string|integer $key The key/index to check for. + * @return boolean TRUE if the collection contains an element with the specified key/index, + * FALSE otherwise. + */ + function containsKey($key); + + /** + * Gets the element at the specified key/index. + * + * @param string|integer $key The key/index of the element to retrieve. + * @return mixed + */ + function get($key); + + /** + * Gets all keys/indices of the collection. + * + * @return array The keys/indices of the collection, in the order of the corresponding + * elements in the collection. + */ + function getKeys(); + + /** + * Gets all values of the collection. + * + * @return array The values of all elements in the collection, in the order they + * appear in the collection. + */ + function getValues(); + + /** + * Sets an element in the collection at the specified key/index. + * + * @param string|integer $key The key/index of the element to set. + * @param mixed $value The element to set. + */ + function set($key, $value); + + /** + * Gets a native PHP array representation of the collection. + * + * @return array + */ + function toArray(); + + /** + * Sets the internal iterator to the first element in the collection and + * returns this element. + * + * @return mixed + */ + function first(); + + /** + * Sets the internal iterator to the last element in the collection and + * returns this element. + * + * @return mixed + */ + function last(); + + /** + * Gets the key/index of the element at the current iterator position. + * + */ + function key(); + + /** + * Gets the element of the collection at the current iterator position. + * + */ + function current(); + + /** + * Moves the internal iterator position to the next element. + * + */ + function next(); + + /** + * Tests for the existence of an element that satisfies the given predicate. + * + * @param Closure $p The predicate. + * @return boolean TRUE if the predicate is TRUE for at least one element, FALSE otherwise. + */ + function exists(Closure $p); + + /** + * Returns all the elements of this collection that satisfy the predicate p. + * The order of the elements is preserved. + * + * @param Closure $p The predicate used for filtering. + * @return Collection A collection with the results of the filter operation. + */ + function filter(Closure $p); + + /** + * Applies the given predicate p to all elements of this collection, + * returning true, if the predicate yields true for all elements. + * + * @param Closure $p The predicate. + * @return boolean TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + function forAll(Closure $p); + + /** + * Applies the given function to each element in the collection and returns + * a new collection with the elements returned by the function. + * + * @param Closure $func + * @return Collection + */ + function map(Closure $func); + + /** + * Partitions this collection in two collections according to a predicate. + * Keys are preserved in the resulting collections. + * + * @param Closure $p The predicate on which to partition. + * @return array An array with two elements. The first element contains the collection + * of elements where the predicate returned TRUE, the second element + * contains the collection of elements where the predicate returned FALSE. + */ + function partition(Closure $p); + + /** + * Gets the index/key of a given element. The comparison of two elements is strict, + * that means not only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element The element to search for. + * @return mixed The key/index of the element or FALSE if the element was not found. + */ + function indexOf($element); + + /** + * Extract a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset + * @param int $length + * @return array + */ + function slice($offset, $length = null); +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Criteria.php b/doctrine/common/lib/Doctrine/Common/Collections/Criteria.php new file mode 100644 index 00000000..a2c7daaa --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Criteria.php @@ -0,0 +1,239 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Doctrine\Common\Collections\Expr\Expression; +use Doctrine\Common\Collections\Expr\CompositeExpression; + +/** + * Criteria for filtering Selectable collections. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class Criteria +{ + /** + * @var string + */ + const ASC = 'ASC'; + + /** + * @var string + */ + const DESC = 'DESC'; + + /** + * @var \Doctrine\Common\Collections\ExpressionBuilder + */ + private static $expressionBuilder; + + /** + * @var \Doctrine\Common\Collections\Expr\Expression + */ + private $expression; + + /** + * @var array|null + */ + private $orderings; + + /** + * @var int + */ + private $firstResult; + + /** + * @var int + */ + private $maxResults; + + /** + * Creates an instance of the class. + * + * @return Criteria + */ + public static function create() + { + return new static(); + } + + /** + * Return the expression builder. + * + * @return \Doctrine\Common\Collections\ExpressionBuilder + */ + public static function expr() + { + if (self::$expressionBuilder === null) { + self::$expressionBuilder = new ExpressionBuilder(); + } + return self::$expressionBuilder; + } + + /** + * Construct new criteria + * + * @param Expression $expression + * @param array $orderings + * @param int $firstResult + * @param int $maxResults + */ + public function __construct(Expression $expression = null, array $orderings = null, $firstResult = null, $maxResults = null) + { + $this->expression = $expression; + $this->orderings = $orderings; + $this->firstResult = $firstResult; + $this->maxResults = $maxResults; + } + + /** + * Set the where expression to evaluate when this criteria is searched for. + * + * @param Expression + * @return Criteria + */ + public function where(Expression $expression) + { + $this->expression = $expression; + return $this; + } + + /** + * Append the where expression to evaluate when this criteria is searched for + * using an AND with previous expression. + * + * @param Expression + * @return Criteria + */ + public function andWhere(Expression $expression) + { + if ($this->expression === null) { + return $this->where($expression); + } + + $this->expression = new CompositeExpression(CompositeExpression::TYPE_AND, array( + $this->expression, $expression + )); + + return $this; + } + + /** + * Append the where expression to evaluate when this criteria is searched for + * using an OR with previous expression. + * + * @param Expression + * @return Criteria + */ + public function orWhere(Expression $expression) + { + if ($this->expression === null) { + return $this->where($expression); + } + + $this->expression = new CompositeExpression(CompositeExpression::TYPE_OR, array( + $this->expression, $expression + )); + + return $this; + } + + /** + * Get the expression attached to this criteria. + * + * @return Expression|null + */ + public function getWhereExpression() + { + return $this->expression; + } + + /** + * Get current orderings of this Criteria + * + * @return array + */ + public function getOrderings() + { + return $this->orderings; + } + + /** + * Set the ordering of the result of this criteria. + * + * Keys are field and values are the order, being either ASC or DESC. + * + * @see Criteria::ASC + * @see Criteria::DESC + * + * @param array + * @return Criteria + */ + public function orderBy(array $orderings) + { + $this->orderings = $orderings; + return $this; + } + + /** + * Get current first result option of the critera. + * + * @return firstResult. + */ + public function getFirstResult() + { + return $this->firstResult; + } + + /** + * Set number of first result that this criteria should return. + * + * @param firstResult the value to set. + * @return Criteria + */ + public function setFirstResult($firstResult) + { + $this->firstResult = $firstResult; + return $this; + } + + /** + * Get maxResults. + * + * @return maxResults. + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Set maxResults. + * + * @param maxResults the value to set. + * @return Criteria + */ + public function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + return $this; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/ClosureExpressionVisitor.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/ClosureExpressionVisitor.php new file mode 100644 index 00000000..bf9dfe5f --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/ClosureExpressionVisitor.php @@ -0,0 +1,194 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +/** + * Walks an expression graph and turns it into a PHP closure. + * + * This closure can be used with {@Collection#filter()} and is used internally + * by {@ArrayCollection#select()}. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class ClosureExpressionVisitor extends ExpressionVisitor +{ + /** + * Access the field of a given object. This field has to be public directly + * or indirectly (through an accessor get* or a magic method, __get, __call). + * + * is*() is not supported. + * + * @return mixed + */ + static public function getObjectFieldValue($object, $field) + { + $accessor = "get" . $field; + + if (method_exists($object, $accessor) || method_exists($object, '__call')) { + return $object->$accessor(); + } + + if ($object instanceof \ArrayAccess) { + return $object[$field]; + } + + return $object->$field; + } + + /** + * Helper for sorting arrays of objects based on multiple fields + + * orientations. + * + * @param string $name + * @param int $orientation + * @param Closure $next + * @return Closure + */ + static public function sortByField($name, $orientation = 1, \Closure $next = null) + { + if (!$next) { + $next = function() { + return 0; + }; + } + + return function ($a, $b) use ($name, $next, $orientation) { + $aValue = ClosureExpressionVisitor::getObjectFieldValue($a, $name); + $bValue = ClosureExpressionVisitor::getObjectFieldValue($b, $name); + + if ($aValue === $bValue) { + return $next($a, $b); + } + + return (($aValue > $bValue) ? 1 : -1) * $orientation; + }; + } + + /** + * {@inheritDoc} + */ + public function walkComparison(Comparison $comparison) + { + $field = $comparison->getField(); + $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + + switch ($comparison->getOperator()) { + case Comparison::EQ: + case Comparison::IS: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) === $value; + }; + + case Comparison::NEQ: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) !== $value; + }; + + case Comparison::LT: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) < $value; + }; + + case Comparison::LTE: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) <= $value; + }; + + case Comparison::GT: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) > $value; + }; + + case Comparison::GTE: + return function ($object) use ($field, $value) { + return ClosureExpressionVisitor::getObjectFieldValue($object, $field) >= $value; + }; + + case Comparison::IN: + return function ($object) use ($field, $value) { + return in_array(ClosureExpressionVisitor::getObjectFieldValue($object, $field), $value); + }; + + case Comparison::NIN: + return function ($object) use ($field, $value) { + return ! in_array(ClosureExpressionVisitor::getObjectFieldValue($object, $field), $value); + }; + + default: + throw new \RuntimeException("Unknown comparison operator: " . $comparison->getOperator()); + } + } + + /** + * {@inheritDoc} + */ + public function walkValue(Value $value) + { + return $value->getValue(); + } + + /** + * {@inheritDoc} + */ + public function walkCompositeExpression(CompositeExpression $expr) + { + $expressionList = array(); + + foreach ($expr->getExpressionList() as $child) { + $expressionList[] = $this->dispatch($child); + } + + switch($expr->getType()) { + case CompositeExpression::TYPE_AND: + return $this->andExpressions($expressionList); + + case CompositeExpression::TYPE_OR: + return $this->orExpressions($expressionList); + + default: + throw new \RuntimeException("Unknown composite " . $expr->getType()); + } + } + + private function andExpressions($expressions) + { + return function ($object) use ($expressions) { + foreach ($expressions as $expression) { + if ( ! $expression($object)) { + return false; + } + } + return true; + }; + } + + private function orExpressions($expressions) + { + return function ($object) use ($expressions) { + foreach ($expressions as $expression) { + if ($expression($object)) { + return true; + } + } + return false; + }; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/Comparison.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Comparison.php new file mode 100644 index 00000000..7df8e118 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Comparison.php @@ -0,0 +1,74 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +/** + * Comparison of a field with a value by the given operator. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class Comparison implements Expression +{ + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + const IS = 'IS'; + const IN = 'IN'; + const NIN = 'NIN'; + + private $field; + private $op; + private $value; + + public function __construct($field, $operator, $value) + { + if ( ! ($value instanceof Value)) { + $value = new Value($value); + } + + $this->field = $field; + $this->op = $operator; + $this->value = $value; + } + + public function getField() + { + return $this->field; + } + + public function getValue() + { + return $this->value; + } + + public function getOperator() + { + return $this->op; + } + + public function visit(ExpressionVisitor $visitor) + { + return $visitor->walkComparison($this); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/CompositeExpression.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/CompositeExpression.php new file mode 100644 index 00000000..d2fd96c7 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/CompositeExpression.php @@ -0,0 +1,71 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +/** + * Expression of Expressions combined by AND or OR operation. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class CompositeExpression implements Expression +{ + const TYPE_AND = 'AND'; + const TYPE_OR = 'OR'; + + private $type; + private $expressions = array(); + + public function __construct($type, array $expressions) + { + $this->type = $type; + + foreach ($expressions as $expr) { + if ($expr instanceof Value) { + throw new \RuntimeException("Values are not supported expressions as children of and/or expressions."); + } + if ( ! ($expr instanceof Expression)) { + throw new \RuntimeException("No expression given to CompositeExpression."); + } + + $this->expressions[] = $expr; + } + } + + /** + * Return the list of expressions nested in this composite. + * + * @return Expression[] + */ + public function getExpressionList() + { + return $this->expressions; + } + + public function getType() + { + return $this->type; + } + + public function visit(ExpressionVisitor $visitor) + { + return $visitor->walkCompositeExpression($this); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/Expression.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Expression.php new file mode 100644 index 00000000..d84aad50 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Expression.php @@ -0,0 +1,30 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +/** + * Expression for the {@link Selectable} interface. + * + * @author Benjamin Eberlei + */ +interface Expression +{ + public function visit(ExpressionVisitor $visitor); +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/ExpressionVisitor.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/ExpressionVisitor.php new file mode 100644 index 00000000..c1cf83ca --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/ExpressionVisitor.php @@ -0,0 +1,80 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +/** + * An Expression visitor walks a graph of expressions and turns them into a + * query for the underlying implementation. + * + * @author Benjamin Eberlei + */ +abstract class ExpressionVisitor +{ + /** + * Convert a comparison expression into the target query language output + * + * @param Comparison $comparison + * + * @return mixed + */ + abstract public function walkComparison(Comparison $comparison); + + /** + * Convert a value expression into the target query language part. + * + * @param Value $value + * + * @return mixed + */ + abstract public function walkValue(Value $value); + + /** + * Convert a composite expression into the target query language output + * + * @param CompositeExpression $expr + * + * @return mixed + */ + abstract public function walkCompositeExpression(CompositeExpression $expr); + + /** + * Dispatch walking an expression to the appropriate handler. + * + * @param Expression + * + * @return mixed + */ + public function dispatch(Expression $expr) + { + switch (true) { + case ($expr instanceof Comparison): + return $this->walkComparison($expr); + + case ($expr instanceof Value): + return $this->walkValue($expr); + + case ($expr instanceof CompositeExpression): + return $this->walkCompositeExpression($expr); + + default: + throw new \RuntimeException("Unknown Expression " . get_class($expr)); + } + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Expr/Value.php b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Value.php new file mode 100644 index 00000000..5c775228 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Expr/Value.php @@ -0,0 +1,40 @@ +. + */ + +namespace Doctrine\Common\Collections\Expr; + +class Value implements Expression +{ + private $value; + + public function __construct($value) + { + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } + + public function visit(ExpressionVisitor $visitor) + { + return $visitor->walkValue($this); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/ExpressionBuilder.php b/doctrine/common/lib/Doctrine/Common/Collections/ExpressionBuilder.php new file mode 100644 index 00000000..7e0d6492 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/ExpressionBuilder.php @@ -0,0 +1,148 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Doctrine\Common\Collections\Expr\Comparison; +use Doctrine\Common\Collections\Expr\CompositeExpression; +use Doctrine\Common\Collections\Expr\Value; + +/** + * Builder for Expressions in the {@link Selectable} interface. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class ExpressionBuilder +{ + /** + * @return CompositeExpression + */ + public function andX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + /** + * @return CompositeExpression + */ + public function orX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function eq($field, $value) + { + return new Comparison($field, Comparison::EQ, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function gt($field, $value) + { + return new Comparison($field, Comparison::GT, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function lt($field, $value) + { + return new Comparison($field, Comparison::LT, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function gte($field, $value) + { + return new Comparison($field, Comparison::GTE, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function lte($field, $value) + { + return new Comparison($field, Comparison::LTE, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function neq($field, $value) + { + return new Comparison($field, Comparison::NEQ, new Value($value)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function isNull($field) + { + return new Comparison($field, Comparison::IS, new Value(null)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function in($field, array $values) + { + return new Comparison($field, Comparison::IN, new Value($values)); + } + + /** + * @param string $field + * @param mixed $value + * + * @return Comparison + */ + public function notIn($field, array $values) + { + return new Comparison($field, Comparison::NIN, new Value($values)); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Collections/Selectable.php b/doctrine/common/lib/Doctrine/Common/Collections/Selectable.php new file mode 100644 index 00000000..8b7631e3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Collections/Selectable.php @@ -0,0 +1,47 @@ +. + */ + +namespace Doctrine\Common\Collections; + +/** + * Interface for collections that allow efficient filtering with an expression API. + * + * Goal of this interface is a backend independent method to fetch elements + * from a collections. {@link Expression} is crafted in a way that you can + * implement queries from both in-memory and database-backed collections. + * + * For database backed collections this allows very efficient access by + * utilizing the query APIs, for example SQL in the ORM. Applications using + * this API can implement efficient database access without having to ask the + * EntityManager or Repositories. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +interface Selectable +{ + /** + * Select all elements from a selectable that match the expression and + * return a new collection containing these elements. + * + * @param Criteria $criteria + * @return Collection + */ + function matching(Criteria $criteria); +} diff --git a/doctrine/common/lib/Doctrine/Common/CommonException.php b/doctrine/common/lib/Doctrine/Common/CommonException.php new file mode 100644 index 00000000..6db76759 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/CommonException.php @@ -0,0 +1,28 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Base exception class for package Doctrine\Common + * @author heinrich + * + */ +class CommonException extends \Exception { +} diff --git a/doctrine/common/lib/Doctrine/Common/Comparable.php b/doctrine/common/lib/Doctrine/Common/Comparable.php new file mode 100644 index 00000000..bcc2cabf --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Comparable.php @@ -0,0 +1,48 @@ +. + */ + + +namespace Doctrine\Common; + +/** + * Comparable interface that allows to compare two value objects to each other for similarity. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + */ +interface Comparable +{ + /** + * Compare the current object to the passed $other. + * + * Returns 0 if they are semantically equal, 1 if the other object + * is less than the current one, or -1 if its more than the current one. + * + * This method should not check for identity using ===, only for semantical equality for example + * when two different DateTime instances point to the exact same Date + TZ. + * + * @param mixed $other + * + * @return int + */ + public function compareTo($other); +} diff --git a/doctrine/common/lib/Doctrine/Common/EventArgs.php b/doctrine/common/lib/Doctrine/Common/EventArgs.php new file mode 100644 index 00000000..a87eee89 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/EventArgs.php @@ -0,0 +1,67 @@ +. + */ + +namespace Doctrine\Common; + +/** + * EventArgs is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass state + * information to an event handler when an event is raised. The single empty EventArgs + * instance can be obtained through {@link getEmptyInstance}. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EventArgs +{ + /** + * @var EventArgs Single instance of EventArgs + */ + private static $_emptyEventArgsInstance; + + /** + * Gets the single, empty and immutable EventArgs instance. + * + * This instance will be used when events are dispatched without any parameter, + * like this: EventManager::dispatchEvent('eventname'); + * + * The benefit from this is that only one empty instance is instantiated and shared + * (otherwise there would be instances for every dispatched in the abovementioned form) + * + * @see EventManager::dispatchEvent + * @link http://msdn.microsoft.com/en-us/library/system.eventargs.aspx + * @return EventArgs + */ + public static function getEmptyInstance() + { + if ( ! self::$_emptyEventArgsInstance) { + self::$_emptyEventArgsInstance = new EventArgs; + } + + return self::$_emptyEventArgsInstance; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/EventManager.php b/doctrine/common/lib/Doctrine/Common/EventManager.php new file mode 100644 index 00000000..1e57df23 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/EventManager.php @@ -0,0 +1,147 @@ +. + */ + +namespace Doctrine\Common; + +/** + * The EventManager is the central point of Doctrine's event listener system. + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EventManager +{ + /** + * Map of registered listeners. + * => + * + * @var array + */ + private $_listeners = array(); + + /** + * Dispatches an event to all registered listeners. + * + * @param string $eventName The name of the event to dispatch. The name of the event is + * the name of the method that is invoked on listeners. + * @param EventArgs $eventArgs The event arguments to pass to the event handlers/listeners. + * If not supplied, the single empty EventArgs instance is used. + * @return boolean + */ + public function dispatchEvent($eventName, EventArgs $eventArgs = null) + { + if (isset($this->_listeners[$eventName])) { + $eventArgs = $eventArgs === null ? EventArgs::getEmptyInstance() : $eventArgs; + + foreach ($this->_listeners[$eventName] as $listener) { + $listener->$eventName($eventArgs); + } + } + } + + /** + * Gets the listeners of a specific event or all listeners. + * + * @param string $event The name of the event. + * @return array The event listeners for the specified event, or all event listeners. + */ + public function getListeners($event = null) + { + return $event ? $this->_listeners[$event] : $this->_listeners; + } + + /** + * Checks whether an event has any registered listeners. + * + * @param string $event + * @return boolean TRUE if the specified event has any listeners, FALSE otherwise. + */ + public function hasListeners($event) + { + return isset($this->_listeners[$event]) && $this->_listeners[$event]; + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string|array $events The event(s) to listen on. + * @param object $listener The listener object. + */ + public function addEventListener($events, $listener) + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Overrides listener if a previous one was associated already + // Prevents duplicate listeners on same event (same instance only) + $this->_listeners[$event][$hash] = $listener; + } + } + + /** + * Removes an event listener from the specified events. + * + * @param string|array $events + * @param object $listener + */ + public function removeEventListener($events, $listener) + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Check if actually have this listener associated + if (isset($this->_listeners[$event][$hash])) { + unset($this->_listeners[$event][$hash]); + } + } + } + + /** + * Adds an EventSubscriber. The subscriber is asked for all the events he is + * interested in and added as a listener for these events. + * + * @param \Doctrine\Common\EventSubscriber $subscriber The subscriber. + */ + public function addEventSubscriber(EventSubscriber $subscriber) + { + $this->addEventListener($subscriber->getSubscribedEvents(), $subscriber); + } + + /** + * Removes an EventSubscriber. The subscriber is asked for all the events it is + * interested in and removed as a listener for these events. + * + * @param \Doctrine\Common\EventSubscriber $subscriber The subscriber. + */ + public function removeEventSubscriber(EventSubscriber $subscriber) + { + $this->removeEventListener($subscriber->getSubscribedEvents(), $subscriber); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/EventSubscriber.php b/doctrine/common/lib/Doctrine/Common/EventSubscriber.php new file mode 100644 index 00000000..14587913 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/EventSubscriber.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\Common; + +/** + * An EventSubscriber knows himself what events he is interested in. + * If an EventSubscriber is added to an EventManager, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface EventSubscriber +{ + /** + * Returns an array of events this subscriber wants to listen to. + * + * @return array + */ + function getSubscribedEvents(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Lexer.php b/doctrine/common/lib/Doctrine/Common/Lexer.php new file mode 100644 index 00000000..8e2554c7 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Lexer.php @@ -0,0 +1,266 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Base class for writing simple lexers, i.e. for creating small DSLs. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @todo Rename: AbstractLexer + */ +abstract class Lexer +{ + /** + * @var array Array of scanned tokens + */ + private $tokens = array(); + + /** + * @var integer Current lexer position in input string + */ + private $position = 0; + + /** + * @var integer Current peek of current lexer position + */ + private $peek = 0; + + /** + * @var array The next token in the input. + */ + public $lookahead; + + /** + * @var array The last matched/seen token. + */ + public $token; + + /** + * Sets the input data to be tokenized. + * + * The Lexer is immediately reset and the new input tokenized. + * Any unprocessed tokens from any previous input are lost. + * + * @param string $input The input to be tokenized. + */ + public function setInput($input) + { + $this->tokens = array(); + $this->reset(); + $this->scan($input); + } + + /** + * Resets the lexer. + */ + public function reset() + { + $this->lookahead = null; + $this->token = null; + $this->peek = 0; + $this->position = 0; + } + + /** + * Resets the peek pointer to 0. + */ + public function resetPeek() + { + $this->peek = 0; + } + + /** + * Resets the lexer position on the input to the given position. + * + * @param integer $position Position to place the lexical scanner + */ + public function resetPosition($position = 0) + { + $this->position = $position; + } + + /** + * Checks whether a given token matches the current lookahead. + * + * @param integer|string $token + * @return boolean + */ + public function isNextToken($token) + { + return null !== $this->lookahead && $this->lookahead['type'] === $token; + } + + /** + * Checks whether any of the given tokens matches the current lookahead + * + * @param array $tokens + * @return boolean + */ + public function isNextTokenAny(array $tokens) + { + return null !== $this->lookahead && in_array($this->lookahead['type'], $tokens, true); + } + + /** + * Moves to the next token in the input string. + * + * A token is an associative array containing three items: + * - 'value' : the string value of the token in the input string + * - 'type' : the type of the token (identifier, numeric, string, input + * parameter, none) + * - 'position' : the position of the token in the input string + * + * @return array|null the next token; null if there is no more tokens left + */ + public function moveNext() + { + $this->peek = 0; + $this->token = $this->lookahead; + $this->lookahead = (isset($this->tokens[$this->position])) + ? $this->tokens[$this->position++] : null; + + return $this->lookahead !== null; + } + + /** + * Tells the lexer to skip input tokens until it sees a token with the given value. + * + * @param string $type The token type to skip until. + */ + public function skipUntil($type) + { + while ($this->lookahead !== null && $this->lookahead['type'] !== $type) { + $this->moveNext(); + } + } + + /** + * Checks if given value is identical to the given token + * + * @param mixed $value + * @param integer $token + * @return boolean + */ + public function isA($value, $token) + { + return $this->getType($value) === $token; + } + + /** + * Moves the lookahead token forward. + * + * @return array | null The next token or NULL if there are no more tokens ahead. + */ + public function peek() + { + if (isset($this->tokens[$this->position + $this->peek])) { + return $this->tokens[$this->position + $this->peek++]; + } else { + return null; + } + } + + /** + * Peeks at the next token, returns it and immediately resets the peek. + * + * @return array|null The next token or NULL if there are no more tokens ahead. + */ + public function glimpse() + { + $peek = $this->peek(); + $this->peek = 0; + return $peek; + } + + /** + * Scans the input string for tokens. + * + * @param string $input a query string + */ + protected function scan($input) + { + static $regex; + + if ( ! isset($regex)) { + $regex = '/(' . implode(')|(', $this->getCatchablePatterns()) . ')|' + . implode('|', $this->getNonCatchablePatterns()) . '/i'; + } + + $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; + $matches = preg_split($regex, $input, -1, $flags); + + foreach ($matches as $match) { + // Must remain before 'value' assignment since it can change content + $type = $this->getType($match[0]); + + $this->tokens[] = array( + 'value' => $match[0], + 'type' => $type, + 'position' => $match[1], + ); + } + } + + /** + * Gets the literal for a given token. + * + * @param integer $token + * @return string + */ + public function getLiteral($token) + { + $className = get_class($this); + $reflClass = new \ReflectionClass($className); + $constants = $reflClass->getConstants(); + + foreach ($constants as $name => $value) { + if ($value === $token) { + return $className . '::' . $name; + } + } + + return $token; + } + + /** + * Lexical catchable patterns. + * + * @return array + */ + abstract protected function getCatchablePatterns(); + + /** + * Lexical non-catchable patterns. + * + * @return array + */ + abstract protected function getNonCatchablePatterns(); + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * @return integer + */ + abstract protected function getType(&$value); +} diff --git a/doctrine/common/lib/Doctrine/Common/NotifyPropertyChanged.php b/doctrine/common/lib/Doctrine/Common/NotifyPropertyChanged.php new file mode 100644 index 00000000..bf1d4219 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/NotifyPropertyChanged.php @@ -0,0 +1,44 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Contract for classes that provide the service of notifying listeners of + * changes to their properties. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface NotifyPropertyChanged +{ + /** + * Adds a listener that wants to be notified about property changes. + * + * @param PropertyChangedListener $listener + */ + function addPropertyChangedListener(PropertyChangedListener $listener); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/AbstractManagerRegistry.php b/doctrine/common/lib/Doctrine/Common/Persistence/AbstractManagerRegistry.php new file mode 100644 index 00000000..94fcd052 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/AbstractManagerRegistry.php @@ -0,0 +1,259 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +use Doctrine\Common\Persistence\ManagerRegistry; + +/** + * Abstract implementation of the ManagerRegistry contract. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Fabien Potencier + * @author Benjamin Eberlei + * @author Lukas Kahwe Smith + */ +abstract class AbstractManagerRegistry implements ManagerRegistry +{ + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $connections; + + /** + * @var array + */ + private $managers; + + /** + * @var string + */ + private $defaultConnection; + + /** + * @var string + */ + private $defaultManager; + + /** + * @var string + */ + private $proxyInterfaceName; + + /** + * Constructor + * + * @param string $name + * @param array $connections + * @param array $managers + * @param string $defaultConnection + * @param string $defaultManager + * @param string $proxyInterfaceName + */ + public function __construct($name, array $connections, array $managers, $defaultConnection, $defaultManager, $proxyInterfaceName) + { + $this->name = $name; + $this->connections = $connections; + $this->managers = $managers; + $this->defaultConnection = $defaultConnection; + $this->defaultManager = $defaultManager; + $this->proxyInterfaceName = $proxyInterfaceName; + } + + /** + * Fetches/creates the given services + * + * A service in this context is connection or a manager instance + * + * @param string $name name of the service + * @return object instance of the given service + */ + abstract protected function getService($name); + + /** + * Resets the given services + * + * A service in this context is connection or a manager instance + * + * @param string $name name of the service + * @return void + */ + abstract protected function resetService($name); + + /** + * Get the name of the registry + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getConnection($name = null) + { + if (null === $name) { + $name = $this->defaultConnection; + } + + if (!isset($this->connections[$name])) { + throw new \InvalidArgumentException(sprintf('Doctrine %s Connection named "%s" does not exist.', $this->name, $name)); + } + + return $this->getService($this->connections[$name]); + } + + /** + * {@inheritdoc} + */ + public function getConnectionNames() + { + return $this->connections; + } + + /** + * {@inheritdoc} + */ + public function getConnections() + { + $connections = array(); + foreach ($this->connections as $name => $id) { + $connections[$name] = $this->getService($id); + } + + return $connections; + } + + /** + * {@inheritdoc} + */ + public function getDefaultConnectionName() + { + return $this->defaultConnection; + } + + /** + * {@inheritdoc} + */ + public function getDefaultManagerName() + { + return $this->defaultManager; + } + + /** + * {@inheritdoc} + * + * @throws \InvalidArgumentException + */ + public function getManager($name = null) + { + if (null === $name) { + $name = $this->defaultManager; + } + + if (!isset($this->managers[$name])) { + throw new \InvalidArgumentException(sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name)); + } + + return $this->getService($this->managers[$name]); + } + + /** + * {@inheritdoc} + */ + public function getManagerForClass($class) + { + // Check for namespace alias + if (strpos($class, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $class); + $class = $this->getAliasNamespace($namespaceAlias) . '\\' . $simpleClassName; + } + + $proxyClass = new \ReflectionClass($class); + if ($proxyClass->implementsInterface($this->proxyInterfaceName)) { + $class = $proxyClass->getParentClass()->getName(); + } + + foreach ($this->managers as $id) { + $manager = $this->getService($id); + + if (!$manager->getMetadataFactory()->isTransient($class)) { + return $manager; + } + } + } + + /** + * {@inheritdoc} + */ + public function getManagerNames() + { + return $this->managers; + } + + /** + * {@inheritdoc} + */ + public function getManagers() + { + $dms = array(); + foreach ($this->managers as $name => $id) { + $dms[$name] = $this->getService($id); + } + + return $dms; + } + + /** + * {@inheritdoc} + */ + public function getRepository($persistentObjectName, $persistentManagerName = null) + { + return $this->getManager($persistentManagerName)->getRepository($persistentObjectName); + } + + /** + * {@inheritdoc} + */ + public function resetManager($name = null) + { + if (null === $name) { + $name = $this->defaultManager; + } + + if (!isset($this->managers[$name])) { + throw new \InvalidArgumentException(sprintf('Doctrine %s Manager named "%s" does not exist.', $this->name, $name)); + } + + // force the creation of a new document manager + // if the current one is closed + $this->resetService($this->managers[$name]); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/ConnectionRegistry.php b/doctrine/common/lib/Doctrine/Common/Persistence/ConnectionRegistry.php new file mode 100644 index 00000000..7d6f0cfb --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/ConnectionRegistry.php @@ -0,0 +1,63 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +/** + * Contract covering connection for a Doctrine persistence layer ManagerRegistry class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Fabien Potencier + * @author Benjamin Eberlei + * @author Lukas Kahwe Smith + */ +interface ConnectionRegistry +{ + /** + * Gets the default connection name. + * + * @return string The default connection name + */ + function getDefaultConnectionName(); + + /** + * Gets the named connection. + * + * @param string $name The connection name (null for the default one) + * + * @return object + */ + function getConnection($name = null); + + /** + * Gets an array of all registered connections + * + * @return array An array of Connection instances + */ + function getConnections(); + + /** + * Gets all connection names. + * + * @return array An array of connection names + */ + function getConnectionNames(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Event/LifecycleEventArgs.php b/doctrine/common/lib/Doctrine/Common/Persistence/Event/LifecycleEventArgs.php new file mode 100644 index 00000000..2fb7c473 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Event/LifecycleEventArgs.php @@ -0,0 +1,77 @@ +. +*/ + +namespace Doctrine\Common\Persistence\Event; + +use Doctrine\Common\EventArgs; +use Doctrine\Common\Persistence\ObjectManager; + +/** + * Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions + * of entities. + * + * @link www.doctrine-project.org + * @since 2.2 + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class LifecycleEventArgs extends EventArgs +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var object + */ + private $entity; + + /** + * Constructor + * + * @param object $entity + * @param ObjectManager $objectManager + */ + public function __construct($entity, ObjectManager $objectManager) + { + $this->entity = $entity; + $this->objectManager = $objectManager; + } + + /** + * Retrieve associated Entity. + * + * @return object + */ + public function getEntity() + { + return $this->entity; + } + + /** + * Retrieve associated ObjectManager. + * + * @return ObjectManager + */ + public function getObjectManager() + { + return $this->objectManager; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Event/LoadClassMetadataEventArgs.php b/doctrine/common/lib/Doctrine/Common/Persistence/Event/LoadClassMetadataEventArgs.php new file mode 100644 index 00000000..b33c3cfb --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Event/LoadClassMetadataEventArgs.php @@ -0,0 +1,75 @@ +. + */ + +namespace Doctrine\Common\Persistence\Event; + +use Doctrine\Common\EventArgs; +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; + +/** + * Class that holds event arguments for a loadMetadata event. + * + * @author Jonathan H. Wage + * @since 2.2 + */ +class LoadClassMetadataEventArgs extends EventArgs +{ + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * Constructor. + * + * @param ClassMetadata $classMetadata + * @param ObjectManager $objectManager + */ + public function __construct(ClassMetadata $classMetadata, ObjectManager $objectManager) + { + $this->classMetadata = $classMetadata; + $this->objectManager = $objectManager; + } + + /** + * Retrieve associated ClassMetadata. + * + * @return ClassMetadata + */ + public function getClassMetadata() + { + return $this->classMetadata; + } + + /** + * Retrieve associated ObjectManager. + * + * @return ObjectManager + */ + public function getObjectManager() + { + return $this->objectManager; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Event/ManagerEventArgs.php b/doctrine/common/lib/Doctrine/Common/Persistence/Event/ManagerEventArgs.php new file mode 100644 index 00000000..f1393658 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Event/ManagerEventArgs.php @@ -0,0 +1,59 @@ +. +*/ + +namespace Doctrine\Common\Persistence\Event; + +use Doctrine\Common\Persistence\ObjectManager; + +/** + * Provides event arguments for the preFlush event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class ManagerEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * Constructor. + * + * @param ObjectManager $objectManager + */ + public function __construct(ObjectManager $objectManager) + { + $this->objectManager = $objectManager; + } + + /** + * Retrieve associated ObjectManager. + * + * @return ObjectManager + */ + public function getObjectManager() + { + return $this->objectManager; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Event/OnClearEventArgs.php b/doctrine/common/lib/Doctrine/Common/Persistence/Event/OnClearEventArgs.php new file mode 100644 index 00000000..18b65541 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Event/OnClearEventArgs.php @@ -0,0 +1,84 @@ +. + */ + +namespace Doctrine\Common\Persistence\Event; + +/** + * Provides event arguments for the onClear event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class OnClearEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var \Doctrine\Common\Persistence\ObjectManager + */ + private $objectManager; + + /** + * @var string + */ + private $entityClass; + + /** + * Constructor. + * + * @param \Doctrine\Common\Persistence\ObjectManager $objectManager + * @param string $entityClass Optional entity class + */ + public function __construct($objectManager, $entityClass = null) + { + $this->objectManager = $objectManager; + $this->entityClass = $entityClass; + } + + /** + * Retrieve associated ObjectManager. + * + * @return \Doctrine\Common\Persistence\ObjectManager + */ + public function getObjectManager() + { + return $this->objectManager; + } + + /** + * Name of the entity class that is cleared, or empty if all are cleared. + * + * @return string + */ + public function getEntityClass() + { + return $this->entityClass; + } + + /** + * Check if event clears all entities. + * + * @return bool + */ + public function clearsAllEntities() + { + return ($this->entityClass === null); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Event/PreUpdateEventArgs.php b/doctrine/common/lib/Doctrine/Common/Persistence/Event/PreUpdateEventArgs.php new file mode 100644 index 00000000..388533d3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Event/PreUpdateEventArgs.php @@ -0,0 +1,132 @@ +. + */ + +namespace Doctrine\Common\Persistence\Event; + +use Doctrine\Common\EventArgs, + Doctrine\Common\Persistence\ObjectManager; + +/** + * Class that holds event arguments for a preUpdate event. + * + * @author Guilherme Blanco + * @author Roman Borschel + * @author Benjamin Eberlei + * @since 2.2 + */ +class PreUpdateEventArgs extends LifecycleEventArgs +{ + /** + * @var array + */ + private $entityChangeSet; + + /** + * Constructor. + * + * @param object $entity + * @param ObjectManager $objectManager + * @param array $changeSet + */ + public function __construct($entity, ObjectManager $objectManager, array &$changeSet) + { + parent::__construct($entity, $objectManager); + + $this->entityChangeSet = &$changeSet; + } + + /** + * Retrieve entity changeset. + * + * @return array + */ + public function getEntityChangeSet() + { + return $this->entityChangeSet; + } + + /** + * Check if field has a changeset. + * + * @param string $field + * + * @return boolean + */ + public function hasChangedField($field) + { + return isset($this->entityChangeSet[$field]); + } + + /** + * Get the old value of the changeset of the changed field. + * + * @param string $field + * @return mixed + */ + public function getOldValue($field) + { + $this->assertValidField($field); + + return $this->entityChangeSet[$field][0]; + } + + /** + * Get the new value of the changeset of the changed field. + * + * @param string $field + * @return mixed + */ + public function getNewValue($field) + { + $this->assertValidField($field); + + return $this->entityChangeSet[$field][1]; + } + + /** + * Set the new value of this field. + * + * @param string $field + * @param mixed $value + */ + public function setNewValue($field, $value) + { + $this->assertValidField($field); + + $this->entityChangeSet[$field][1] = $value; + } + + /** + * Assert the field exists in changeset. + * + * @param string $field + * + * @throws \InvalidArgumentException + */ + private function assertValidField($field) + { + if ( ! isset($this->entityChangeSet[$field])) { + throw new \InvalidArgumentException(sprintf( + 'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.', + $field, + get_class($this->getEntity()) + )); + } + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/ManagerRegistry.php b/doctrine/common/lib/Doctrine/Common/Persistence/ManagerRegistry.php new file mode 100644 index 00000000..bdb23bd2 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/ManagerRegistry.php @@ -0,0 +1,112 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +/** + * Contract covering object managers for a Doctrine persistence layer ManagerRegistry class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Fabien Potencier + * @author Benjamin Eberlei + * @author Lukas Kahwe Smith + */ +interface ManagerRegistry extends ConnectionRegistry +{ + /** + * Gets the default object manager name. + * + * @return string The default object manager name + */ + function getDefaultManagerName(); + + /** + * Gets a named object manager. + * + * @param string $name The object manager name (null for the default one) + * + * @return \Doctrine\Common\Persistence\ObjectManager + */ + function getManager($name = null); + + /** + * Gets an array of all registered object managers + * + * @return \Doctrine\Common\Persistence\ObjectManager[] An array of ObjectManager instances + */ + function getManagers(); + + /** + * Resets a named object manager. + * + * This method is useful when an object manager has been closed + * because of a rollbacked transaction AND when you think that + * it makes sense to get a new one to replace the closed one. + * + * Be warned that you will get a brand new object manager as + * the existing one is not useable anymore. This means that any + * other object with a dependency on this object manager will + * hold an obsolete reference. You can inject the registry instead + * to avoid this problem. + * + * @param string $name The object manager name (null for the default one) + * + * @return \Doctrine\Common\Persistence\ObjectManager + */ + function resetManager($name = null); + + /** + * Resolves a registered namespace alias to the full namespace. + * + * This method looks for the alias in all registered object managers. + * + * @param string $alias The alias + * + * @return string The full namespace + */ + function getAliasNamespace($alias); + + /** + * Gets all connection names. + * + * @return array An array of connection names + */ + function getManagerNames(); + + /** + * Gets the ObjectRepository for an persistent object. + * + * @param string $persistentObject The name of the persistent object. + * @param string $persistentManagerName The object manager name (null for the default one) + * + * @return \Doctrine\Common\Persistence\ObjectRepository + */ + function getRepository($persistentObject, $persistentManagerName = null); + + /** + * Gets the object manager associated with a given class. + * + * @param string $class A persistent object class name + * + * @return \Doctrine\Common\Persistence\ObjectManager|null + */ + function getManagerForClass($class); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php new file mode 100644 index 00000000..1ace1ccb --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/AbstractClassMetadataFactory.php @@ -0,0 +1,383 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +use Doctrine\Common\Cache\Cache, + Doctrine\Common\Util\ClassUtils; + +/** + * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the + * metadata mapping informations of a class which describes how a class should be mapped + * to a relational database. + * + * This class was abstracted from the ORM ClassMetadataFactory + * + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +abstract class AbstractClassMetadataFactory implements ClassMetadataFactory +{ + /** + * Salt used by specific Object Manager implementation. + * + * @var string + */ + protected $cacheSalt = "\$CLASSMETADATA"; + + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $cacheDriver; + + /** + * @var array + */ + private $loadedMetadata = array(); + + /** + * @var bool + */ + protected $initialized = false; + + /** + * @var ReflectionService + */ + private $reflectionService; + + /** + * Sets the cache driver used by the factory to cache ClassMetadata instances. + * + * @param Doctrine\Common\Cache\Cache $cacheDriver + */ + public function setCacheDriver(Cache $cacheDriver = null) + { + $this->cacheDriver = $cacheDriver; + } + + /** + * Gets the cache driver used by the factory to cache ClassMetadata instances. + * + * @return Doctrine\Common\Cache\Cache + */ + public function getCacheDriver() + { + return $this->cacheDriver; + } + + /** + * Return an array of all the loaded metadata currently in memory. + * + * @return array + */ + public function getLoadedMetadata() + { + return $this->loadedMetadata; + } + + /** + * Forces the factory to load the metadata of all classes known to the underlying + * mapping driver. + * + * @return array The ClassMetadata instances of all mapped classes. + */ + public function getAllMetadata() + { + if ( ! $this->initialized) { + $this->initialize(); + } + + $driver = $this->getDriver(); + $metadata = array(); + foreach ($driver->getAllClassNames() as $className) { + $metadata[] = $this->getMetadataFor($className); + } + + return $metadata; + } + + /** + * Lazy initialization of this stuff, especially the metadata driver, + * since these are not needed at all when a metadata cache is active. + * + * @return void + */ + abstract protected function initialize(); + + /** + * Get the fully qualified class-name from the namespace alias. + * + * @param string $namespaceAlias + * @param string $simpleClassName + * @return string + */ + abstract protected function getFqcnFromAlias($namespaceAlias, $simpleClassName); + + /** + * Return the mapping driver implementation. + * + * @return \Doctrine\Common\Persistence\Mapping\Driver\MappingDriver + */ + abstract protected function getDriver(); + + /** + * Wakeup reflection after ClassMetadata gets unserialized from cache. + * + * @param ClassMetadata $class + * @param ReflectionService $reflService + * @return void + */ + abstract protected function wakeupReflection(ClassMetadata $class, ReflectionService $reflService); + + /** + * Initialize Reflection after ClassMetadata was constructed. + * + * @param ClassMetadata $class + * @param ReflectionService $reflService + * @return void + */ + abstract protected function initializeReflection(ClassMetadata $class, ReflectionService $reflService); + + /** + * Checks whether the class metadata is an entity. + * + * This method should false for mapped superclasses or + * embedded classes. + * + * @param ClassMetadata $class + * @return boolean + */ + abstract protected function isEntity(ClassMetadata $class); + + /** + * Gets the class metadata descriptor for a class. + * + * @param string $className The name of the class. + * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata + */ + public function getMetadataFor($className) + { + if (isset($this->loadedMetadata[$className])) { + return $this->loadedMetadata[$className]; + } + + $realClassName = $className; + + // Check for namespace alias + if (strpos($className, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $className); + $realClassName = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName); + } else { + $realClassName = ClassUtils::getRealClass($realClassName); + } + + if (isset($this->loadedMetadata[$realClassName])) { + // We do not have the alias name in the map, include it + $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; + + return $this->loadedMetadata[$realClassName]; + } + + if ($this->cacheDriver) { + if (($cached = $this->cacheDriver->fetch($realClassName . $this->cacheSalt)) !== false) { + $this->loadedMetadata[$realClassName] = $cached; + $this->wakeupReflection($cached, $this->getReflectionService()); + } else { + foreach ($this->loadMetadata($realClassName) as $loadedClassName) { + $this->cacheDriver->save( + $loadedClassName . $this->cacheSalt, $this->loadedMetadata[$loadedClassName], null + ); + } + } + } else { + $this->loadMetadata($realClassName); + } + + if ($className != $realClassName) { + // We do not have the alias name in the map, include it + $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; + } + + return $this->loadedMetadata[$className]; + } + + /** + * Checks whether the factory has the metadata for a class loaded already. + * + * @param string $className + * @return boolean TRUE if the metadata of the class in question is already loaded, FALSE otherwise. + */ + public function hasMetadataFor($className) + { + return isset($this->loadedMetadata[$className]); + } + + /** + * Sets the metadata descriptor for a specific class. + * + * NOTE: This is only useful in very special cases, like when generating proxy classes. + * + * @param string $className + * @param ClassMetadata $class + */ + public function setMetadataFor($className, $class) + { + $this->loadedMetadata[$className] = $class; + } + + /** + * Get array of parent classes for the given entity class + * + * @param string $name + * @return array $parentClasses + */ + protected function getParentClasses($name) + { + // Collect parent classes, ignoring transient (not-mapped) classes. + $parentClasses = array(); + foreach (array_reverse($this->getReflectionService()->getParentClasses($name)) as $parentClass) { + if ( ! $this->getDriver()->isTransient($parentClass)) { + $parentClasses[] = $parentClass; + } + } + return $parentClasses; + } + + /** + * Loads the metadata of the class in question and all it's ancestors whose metadata + * is still not loaded. + * + * @param string $name The name of the class for which the metadata should get loaded. + * + * @return array + */ + protected function loadMetadata($name) + { + if ( ! $this->initialized) { + $this->initialize(); + } + + $loaded = array(); + + $parentClasses = $this->getParentClasses($name); + $parentClasses[] = $name; + + // Move down the hierarchy of parent classes, starting from the topmost class + $parent = null; + $rootEntityFound = false; + $visited = array(); + $reflService = $this->getReflectionService(); + foreach ($parentClasses as $className) { + if (isset($this->loadedMetadata[$className])) { + $parent = $this->loadedMetadata[$className]; + if ($this->isEntity($parent)) { + $rootEntityFound = true; + array_unshift($visited, $className); + } + continue; + } + + $class = $this->newClassMetadataInstance($className); + $this->initializeReflection($class, $reflService); + + $this->doLoadMetadata($class, $parent, $rootEntityFound, $visited); + + $this->loadedMetadata[$className] = $class; + + $parent = $class; + + if ($this->isEntity($class)) { + $rootEntityFound = true; + array_unshift($visited, $className); + } + + $this->wakeupReflection($class, $reflService); + + $loaded[] = $className; + } + + return $loaded; + } + + /** + * Actually load the metadata from the underlying metadata + * + * @param ClassMetadata $class + * @param ClassMetadata|null $parent + * @param bool $rootEntityFound + * @param array $nonSuperclassParents classnames all parent classes that are not marked as mapped superclasses + * @return void + */ + abstract protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonSuperclassParents); + + /** + * Creates a new ClassMetadata instance for the given class name. + * + * @param string $className + * @return ClassMetadata + */ + abstract protected function newClassMetadataInstance($className); + + /** + * Check if this class is mapped by this Object Manager + ClassMetadata configuration + * + * @param $class + * @return bool + */ + public function isTransient($class) + { + if ( ! $this->initialized) { + $this->initialize(); + } + + // Check for namespace alias + if (strpos($class, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $class); + $class = $this->getFqcnFromAlias($namespaceAlias, $simpleClassName); + } + + return $this->getDriver()->isTransient($class); + } + + /** + * Set reflectionService. + * + * @param ReflectionService $reflectionService + */ + public function setReflectionService(ReflectionService $reflectionService) + { + $this->reflectionService = $reflectionService; + } + + /** + * Get the reflection service associated with this metadata factory. + * + * @return ReflectionService + */ + public function getReflectionService() + { + if ($this->reflectionService === null) { + $this->reflectionService = new RuntimeReflectionService(); + } + return $this->reflectionService; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadata.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadata.php new file mode 100644 index 00000000..4836bf87 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadata.php @@ -0,0 +1,165 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +/** + * Contract for a Doctrine persistence layer ClassMetadata class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.1 + * @author Benjamin Eberlei + * @author Jonathan Wage + */ +interface ClassMetadata +{ + /** + * Get fully-qualified class name of this persistent class. + * + * @return string + */ + function getName(); + + /** + * Gets the mapped identifier field name. + * + * The returned structure is an array of the identifier field names. + * + * @return array + */ + function getIdentifier(); + + /** + * Gets the ReflectionClass instance for this mapped class. + * + * @return \ReflectionClass + */ + function getReflectionClass(); + + /** + * Checks if the given field name is a mapped identifier for this class. + * + * @param string $fieldName + * @return boolean + */ + function isIdentifier($fieldName); + + /** + * Checks if the given field is a mapped property for this class. + * + * @param string $fieldName + * @return boolean + */ + function hasField($fieldName); + + /** + * Checks if the given field is a mapped association for this class. + * + * @param string $fieldName + * @return boolean + */ + function hasAssociation($fieldName); + + /** + * Checks if the given field is a mapped single valued association for this class. + * + * @param string $fieldName + * @return boolean + */ + function isSingleValuedAssociation($fieldName); + + /** + * Checks if the given field is a mapped collection valued association for this class. + * + * @param string $fieldName + * @return boolean + */ + function isCollectionValuedAssociation($fieldName); + + /** + * A numerically indexed list of field names of this persistent class. + * + * This array includes identifier fields if present on this class. + * + * @return array + */ + function getFieldNames(); + + /** + * Returns an array of identifier field names numerically indexed. + * + * @return array + */ + function getIdentifierFieldNames(); + + /** + * A numerically indexed list of association names of this persistent class. + * + * This array includes identifier associations if present on this class. + * + * @return array + */ + function getAssociationNames(); + + /** + * Returns a type name of this field. + * + * This type names can be implementation specific but should at least include the php types: + * integer, string, boolean, float/double, datetime. + * + * @param string $fieldName + * @return string + */ + function getTypeOfField($fieldName); + + /** + * Returns the target class name of the given association. + * + * @param string $assocName + * @return string + */ + function getAssociationTargetClass($assocName); + + /** + * Checks if the association is the inverse side of a bidirectional association + * + * @param string $assocName + * @return boolean + */ + function isAssociationInverseSide($assocName); + + /** + * Returns the target field of the owning side of the association + * + * @param string $assocName + * @return string + */ + function getAssociationMappedByTargetField($assocName); + + /** + * Return the identifier of this object as an array with field name as key. + * + * Has to return an empty array if no identifier isset. + * + * @param object $object + * @return array + */ + function getIdentifierValues($object); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadataFactory.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadataFactory.php new file mode 100644 index 00000000..3fa39bcb --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ClassMetadataFactory.php @@ -0,0 +1,74 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +/** + * Contract for a Doctrine persistence layer ClassMetadata class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.1 + * @author Benjamin Eberlei + * @author Jonathan Wage + */ +interface ClassMetadataFactory +{ + /** + * Forces the factory to load the metadata of all classes known to the underlying + * mapping driver. + * + * @return array The ClassMetadata instances of all mapped classes. + */ + function getAllMetadata(); + + /** + * Gets the class metadata descriptor for a class. + * + * @param string $className The name of the class. + * @return ClassMetadata + */ + function getMetadataFor($className); + + /** + * Checks whether the factory has the metadata for a class loaded already. + * + * @param string $className + * @return boolean TRUE if the metadata of the class in question is already loaded, FALSE otherwise. + */ + function hasMetadataFor($className); + + /** + * Sets the metadata descriptor for a specific class. + * + * @param string $className + * @param ClassMetadata $class + */ + function setMetadataFor($className, $class); + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped directly or as a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + function isTransient($className); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php new file mode 100644 index 00000000..1131add5 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/AnnotationDriver.php @@ -0,0 +1,214 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Cache\ArrayCache, + Doctrine\Common\Annotations\AnnotationReader, + Doctrine\Common\Annotations\AnnotationRegistry, + Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * The AnnotationDriver reads the mapping metadata from docblock annotations. + * + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +abstract class AnnotationDriver implements MappingDriver +{ + /** + * The AnnotationReader. + * + * @var AnnotationReader + */ + protected $reader; + + /** + * The paths where to look for mapping files. + * + * @var array + */ + protected $paths = array(); + + /** + * The file extension of mapping documents. + * + * @var string + */ + protected $fileExtension = '.php'; + + /** + * Cache for AnnotationDriver#getAllClassNames() + * + * @var array + */ + protected $classNames; + + /** + * Name of the entity annotations as keys + * + * @var array + */ + protected $entityAnnotationClasses = array(); + + /** + * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading + * docblock annotations. + * + * @param AnnotationReader $reader The AnnotationReader to use, duck-typed. + * @param string|array $paths One or multiple paths where mapping classes can be found. + */ + public function __construct($reader, $paths = null) + { + $this->reader = $reader; + if ($paths) { + $this->addPaths((array) $paths); + } + } + + /** + * Append lookup paths to metadata driver. + * + * @param array $paths + */ + public function addPaths(array $paths) + { + $this->paths = array_unique(array_merge($this->paths, $paths)); + } + + /** + * Retrieve the defined metadata lookup paths. + * + * @return array + */ + public function getPaths() + { + return $this->paths; + } + + /** + * Retrieve the current annotation reader + * + * @return AnnotationReader + */ + public function getReader() + { + return $this->reader; + } + + /** + * Get the file extension used to look for mapping files under + * + * @return string + */ + public function getFileExtension() + { + return $this->fileExtension; + } + + /** + * Set the file extension used to look for mapping files under + * + * @param string $fileExtension The file extension to set + * @return void + */ + public function setFileExtension($fileExtension) + { + $this->fileExtension = $fileExtension; + } + + /** + * Whether the class with the specified name is transient. Only non-transient + * classes, that is entities and mapped superclasses, should have their metadata loaded. + * + * A class is non-transient if it is annotated with an annotation + * from the {@see AnnotationDriver::entityAnnotationClasses}. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + $classAnnotations = $this->reader->getClassAnnotations(new \ReflectionClass($className)); + + foreach ($classAnnotations as $annot) { + if (isset($this->entityAnnotationClasses[get_class($annot)])) { + return false; + } + } + return true; + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames() + { + if ($this->classNames !== null) { + return $this->classNames; + } + + if (!$this->paths) { + throw MappingException::pathRequired(); + } + + $classes = array(); + $includedFiles = array(); + + foreach ($this->paths as $path) { + if ( ! is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RegexIterator( + new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ), + '/^.+' . str_replace('.', '\.', $this->fileExtension) . '$/i', + \RecursiveRegexIterator::GET_MATCH + ); + + foreach ($iterator as $file) { + $sourceFile = realpath($file[0]); + + require_once $sourceFile; + + $includedFiles[] = $sourceFile; + } + } + + $declared = get_declared_classes(); + + foreach ($declared as $className) { + $rc = new \ReflectionClass($className); + $sourceFile = $rc->getFileName(); + if (in_array($sourceFile, $includedFiles) && ! $this->isTransient($className)) { + $classes[] = $className; + } + } + + $this->classNames = $classes; + + return $classes; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/DefaultFileLocator.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/DefaultFileLocator.php new file mode 100644 index 00000000..0d61174f --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/DefaultFileLocator.php @@ -0,0 +1,170 @@ +. +*/ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * Locate the file that contains the metadata information for a given class name. + * + * This behavior is inpependent of the actual content of the file. It just detects + * the file which is responsible for the given class name. + * + * @author Benjamin Eberlei + * @author Johannes M. Schmitt + */ +class DefaultFileLocator implements FileLocator +{ + /** + * The paths where to look for mapping files. + * + * @var array + */ + protected $paths = array(); + + /** + * The file extension of mapping documents. + * + * @var string + */ + protected $fileExtension; + + /** + * Initializes a new FileDriver that looks in the given path(s) for mapping + * documents and operates in the specified operating mode. + * + * @param string|array $paths One or multiple paths where mapping documents can be found. + * @param string|null $fileExtension + */ + public function __construct($paths, $fileExtension = null) + { + $this->addPaths((array) $paths); + $this->fileExtension = $fileExtension; + } + + /** + * Append lookup paths to metadata driver. + * + * @param array $paths + */ + public function addPaths(array $paths) + { + $this->paths = array_unique(array_merge($this->paths, $paths)); + } + + /** + * Retrieve the defined metadata lookup paths. + * + * @return array + */ + public function getPaths() + { + return $this->paths; + } + + /** + * Get the file extension used to look for mapping files under + * + * @return string + */ + public function getFileExtension() + { + return $this->fileExtension; + } + + /** + * Set the file extension used to look for mapping files under + * + * @param string $fileExtension The file extension to set + * @return void + */ + public function setFileExtension($fileExtension) + { + $this->fileExtension = $fileExtension; + } + + /** + * {@inheritDoc} + */ + public function findMappingFile($className) + { + $fileName = str_replace('\\', '.', $className) . $this->fileExtension; + + // Check whether file exists + foreach ($this->paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $fileName)) { + return $path . DIRECTORY_SEPARATOR . $fileName; + } + } + + throw MappingException::mappingFileNotFound($className, $fileName); + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames($globalBasename) + { + $classes = array(); + + if ($this->paths) { + foreach ($this->paths as $path) { + if ( ! is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + $fileName = $file->getBasename($this->fileExtension); + + if ($fileName == $file->getBasename() || $fileName == $globalBasename) { + continue; + } + + // NOTE: All files found here means classes are not transient! + $classes[] = str_replace('.', '\\', $fileName); + } + } + } + + return $classes; + } + + /** + * {@inheritDoc} + */ + public function fileExists($className) + { + $fileName = str_replace('\\', '.', $className) . $this->fileExtension; + + // Check whether file exists + foreach ((array) $this->paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $fileName)) { + return true; + } + } + + return false; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileDriver.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileDriver.php new file mode 100644 index 00000000..b0a7685c --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileDriver.php @@ -0,0 +1,214 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * Base driver for file-based metadata drivers. + * + * A file driver operates in a mode where it loads the mapping files of individual + * classes on demand. This requires the user to adhere to the convention of 1 mapping + * file per class and the file names of the mapping files must correspond to the full + * class name, including namespace, with the namespace delimiters '\', replaced by dots '.'. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +abstract class FileDriver implements MappingDriver +{ + /** + * @var FileLocator + */ + protected $locator; + + /** + * @var array + */ + protected $classCache; + + /** + * @var string + */ + protected $globalBasename; + + /** + * Initializes a new FileDriver that looks in the given path(s) for mapping + * documents and operates in the specified operating mode. + * + * @param string|array|FileLocator $locator A FileLocator or one/multiple paths where mapping documents can be found. + * @param string $fileExtension + */ + public function __construct($locator, $fileExtension = null) + { + if ($locator instanceof FileLocator) { + $this->locator = $locator; + } else { + $this->locator = new DefaultFileLocator((array)$locator, $fileExtension); + } + } + + /** + * Set global basename + * + * @param string $file + */ + public function setGlobalBasename($file) + { + $this->globalBasename = $file; + } + + /** + * Retrieve global basename + * + * @return string + */ + public function getGlobalBasename() + { + return $this->globalBasename; + } + + /** + * Get the element of schema meta data for the class from the mapping file. + * This will lazily load the mapping file if it is not loaded yet + * + * @param string $className + * + * @throws MappingException + * @return array The element of schema meta data + */ + public function getElement($className) + { + if ($this->classCache === null) { + $this->initialize(); + } + + if (isset($this->classCache[$className])) { + return $this->classCache[$className]; + } + + $result = $this->loadMappingFile($this->locator->findMappingFile($className)); + if (!isset($result[$className])) { + throw MappingException::invalidMappingFile($className, str_replace('\\', '.', $className) . $this->locator->getFileExtension()); + } + + return $result[$className]; + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + if ($this->classCache === null) { + $this->initialize(); + } + + if (isset($this->classCache[$className])) { + return false; + } + + return !$this->locator->fileExists($className); + } + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + public function getAllClassNames() + { + if ($this->classCache === null) { + $this->initialize(); + } + + $classNames = (array)$this->locator->getAllClassNames($this->globalBasename); + if ($this->classCache) { + $classNames = array_merge(array_keys($this->classCache), $classNames); + } + return $classNames; + } + + /** + * Loads a mapping file with the given name and returns a map + * from class/entity names to their corresponding file driver elements. + * + * @param string $file The mapping file to load. + * @return array + */ + abstract protected function loadMappingFile($file); + + /** + * Initialize the class cache from all the global files. + * + * Using this feature adds a substantial performance hit to file drivers as + * more metadata has to be loaded into memory than might actually be + * necessary. This may not be relevant to scenarios where caching of + * metadata is in place, however hits very hard in scenarios where no + * caching is used. + * + * @return void + */ + protected function initialize() + { + $this->classCache = array(); + if (null !== $this->globalBasename) { + foreach ($this->locator->getPaths() as $path) { + $file = $path.'/'.$this->globalBasename.$this->locator->getFileExtension(); + if (is_file($file)) { + $this->classCache = array_merge( + $this->classCache, + $this->loadMappingFile($file) + ); + } + } + } + } + + /** + * Retrieve the locator used to discover mapping files by className + * + * @return FileLocator + */ + public function getLocator() + { + return $this->locator; + } + + /** + * Set the locator used to discover mapping files by className + * + * @param FileLocator $locator + */ + public function setLocator(FileLocator $locator) + { + $this->locator = $locator; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileLocator.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileLocator.php new file mode 100644 index 00000000..ec2b6065 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/FileLocator.php @@ -0,0 +1,71 @@ +. +*/ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +/** + * Locate the file that contains the metadata information for a given class name. + * + * This behavior is independent of the actual content of the file. It just detects + * the file which is responsible for the given class name. + * + * @author Benjamin Eberlei + * @author Johannes M. Schmitt + */ +interface FileLocator +{ + /** + * Locate mapping file for the given class name. + * + * @param string $className + * @return string + */ + function findMappingFile($className); + + /** + * Get all class names that are found with this file locator. + * + * @param string $globalBasename Passed to allow excluding the basename + * @return array + */ + function getAllClassNames($globalBasename); + + /** + * Check if a file can be found for this class name. + * + * @param string $className + * + * @return bool + */ + function fileExists($className); + + /** + * Get all the paths that this file locator looks for mapping files. + * + * @return array + */ + function getPaths(); + + /** + * Get the file extension that mapping files are suffixed with. + * + * @return string + */ + function getFileExtension(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriver.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriver.php new file mode 100644 index 00000000..955d831d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriver.php @@ -0,0 +1,56 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; + +/** + * Contract for metadata drivers. + * + * @since 2.2 + * @author Jonathan H. Wage + */ +interface MappingDriver +{ + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata $metadata + */ + function loadMetadataForClass($className, ClassMetadata $metadata); + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + function getAllClassNames(); + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + function isTransient($className); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php new file mode 100644 index 00000000..3b1049df --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/MappingDriverChain.php @@ -0,0 +1,168 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver, + Doctrine\Common\Persistence\Mapping\ClassMetadata, + Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * The DriverChain allows you to add multiple other mapping drivers for + * certain namespaces + * + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class MappingDriverChain implements MappingDriver +{ + /** + * The default driver + * + * @var MappingDriver + */ + private $defaultDriver; + + /** + * @var array + */ + private $drivers = array(); + + /** + * Get the default driver. + * + * @return MappingDriver|null + */ + public function getDefaultDriver() + { + return $this->defaultDriver; + } + + /** + * Set the default driver. + * + * @param MappingDriver $driver + */ + public function setDefaultDriver(MappingDriver $driver) + { + $this->defaultDriver = $driver; + } + + /** + * Add a nested driver. + * + * @param MappingDriver $nestedDriver + * @param string $namespace + */ + public function addDriver(MappingDriver $nestedDriver, $namespace) + { + $this->drivers[$namespace] = $nestedDriver; + } + + /** + * Get the array of nested drivers. + * + * @return array $drivers + */ + public function getDrivers() + { + return $this->drivers; + } + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadata $metadata + * + * @throws MappingException + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + /* @var $driver MappingDriver */ + foreach ($this->drivers as $namespace => $driver) { + if (strpos($className, $namespace) === 0) { + $driver->loadMetadataForClass($className, $metadata); + return; + } + } + + if (null !== $this->defaultDriver) { + $this->defaultDriver->loadMetadataForClass($className, $metadata); + return; + } + + throw MappingException::classNotFoundInNamespaces($className, array_keys($this->drivers)); + } + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + public function getAllClassNames() + { + $classNames = array(); + $driverClasses = array(); + + /* @var $driver MappingDriver */ + foreach ($this->drivers AS $namespace => $driver) { + $oid = spl_object_hash($driver); + + if (!isset($driverClasses[$oid])) { + $driverClasses[$oid] = $driver->getAllClassNames(); + } + + foreach ($driverClasses[$oid] AS $className) { + if (strpos($className, $namespace) === 0) { + $classNames[$className] = true; + } + } + } + + return array_keys($classNames); + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * + * This is only the case for non-transient classes either mapped as an Entity or MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + /* @var $driver MappingDriver */ + foreach ($this->drivers AS $namespace => $driver) { + if (strpos($className, $namespace) === 0) { + return $driver->isTransient($className); + } + } + + if ($this->defaultDriver !== null) { + return $this->defaultDriver->isTransient($className); + } + + return true; + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/PHPDriver.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/PHPDriver.php new file mode 100644 index 00000000..e0c86113 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/PHPDriver.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; + +/** + * The PHPDriver includes php files which just populate ClassMetadataInfo + * instances with plain php code + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class PHPDriver extends FileDriver +{ + /** + * {@inheritdoc} + */ + protected $metadata; + + /** + * {@inheritDoc} + */ + public function __construct($locator, $fileExtension = null) + { + $fileExtension = ".php"; + parent::__construct($locator, $fileExtension); + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + $this->metadata = $metadata; + $this->loadMappingFile($this->locator->findMappingFile($className)); + } + + /** + * {@inheritdoc} + */ + protected function loadMappingFile($file) + { + $metadata = $this->metadata; + include $file; + + return array($metadata->getName() => $metadata); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/StaticPHPDriver.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/StaticPHPDriver.php new file mode 100644 index 00000000..e3cea730 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/StaticPHPDriver.php @@ -0,0 +1,141 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * The StaticPHPDriver calls a static loadMetadata() method on your entity + * classes where you can manually populate the ClassMetadata instance. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.2 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class StaticPHPDriver implements MappingDriver +{ + /** + * Paths of entity directories. + * + * @var array + */ + private $paths = array(); + + /** + * Map of all class names. + * + * @var array + */ + private $classNames; + + /** + * Constructor + * + * @param array|string $paths + */ + public function __construct($paths) + { + $this->addPaths((array) $paths); + } + + /** + * Add paths + * + * @param array $paths + */ + public function addPaths(array $paths) + { + $this->paths = array_unique(array_merge($this->paths, $paths)); + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + $className::loadMetadata($metadata); + } + + /** + * {@inheritDoc} + * @todo Same code exists in AnnotationDriver, should we re-use it somehow or not worry about it? + */ + public function getAllClassNames() + { + if ($this->classNames !== null) { + return $this->classNames; + } + + if (!$this->paths) { + throw MappingException::pathRequired(); + } + + $classes = array(); + $includedFiles = array(); + + foreach ($this->paths as $path) { + if (!is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if ($file->getBasename('.php') == $file->getBasename()) { + continue; + } + + $sourceFile = realpath($file->getPathName()); + require_once $sourceFile; + $includedFiles[] = $sourceFile; + } + } + + $declared = get_declared_classes(); + + foreach ($declared as $className) { + $rc = new \ReflectionClass($className); + $sourceFile = $rc->getFileName(); + if (in_array($sourceFile, $includedFiles) && !$this->isTransient($className)) { + $classes[] = $className; + } + } + + $this->classNames = $classes; + + return $classes; + } + + /** + * {@inheritdoc} + */ + public function isTransient($className) + { + return ! method_exists($className, 'loadMetadata'); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/SymfonyFileLocator.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/SymfonyFileLocator.php new file mode 100644 index 00000000..9095187d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/Driver/SymfonyFileLocator.php @@ -0,0 +1,214 @@ +. +*/ + +namespace Doctrine\Common\Persistence\Mapping\Driver; + +use Doctrine\Common\Persistence\Mapping\MappingException; + +/** + * The Symfony File Locator makes a simplifying assumptions compared + * to the DefaultFileLocator. By assuming paths only contain entities of a certain + * namespace the mapping files consists of the short classname only. + * + * @author Fabien Potencier + * @author Benjamin Eberlei + * @license MIT + */ +class SymfonyFileLocator implements FileLocator +{ + /** + * The paths where to look for mapping files. + * + * @var array + */ + protected $paths = array(); + + /** + * A map of mapping directory path to namespace prefix used to expand class shortnames. + * + * @var array + */ + protected $prefixes = array(); + + /** + * File extension that is searched for. + * + * @var string + */ + protected $fileExtension; + + /** + * Constructor + * + * @param array $prefixes + * @param string|null $fileExtension + */ + public function __construct(array $prefixes, $fileExtension = null) + { + $this->addNamespacePrefixes($prefixes); + $this->fileExtension = $fileExtension; + } + + /** + * Add Namespace Prefixes + * + * @param array $prefixes + */ + public function addNamespacePrefixes(array $prefixes) + { + $this->prefixes = array_merge($this->prefixes, $prefixes); + $this->paths = array_merge($this->paths, array_keys($prefixes)); + } + + /** + * Get Namespace Prefixes + * + * @return array + */ + public function getNamespacePrefixes() + { + return $this->prefixes; + } + + /** + * {@inheritDoc} + */ + public function getPaths() + { + return $this->paths; + } + + /** + * {@inheritDoc} + */ + public function getFileExtension() + { + return $this->fileExtension; + } + + /** + * Set the file extension used to look for mapping files under + * + * @param string $fileExtension The file extension to set + * @return void + */ + public function setFileExtension($fileExtension) + { + $this->fileExtension = $fileExtension; + } + + /** + * {@inheritDoc} + */ + public function fileExists($className) + { + $defaultFileName = str_replace('\\', '.', $className).$this->fileExtension; + foreach ($this->paths as $path) { + if (!isset($this->prefixes[$path])) { + // global namespace class + if (is_file($path.DIRECTORY_SEPARATOR.$defaultFileName)) { + return true; + } + + continue; + } + + $prefix = $this->prefixes[$path]; + + if (0 !== strpos($className, $prefix.'\\')) { + continue; + } + + $filename = $path.'/'.strtr(substr($className, strlen($prefix)+1), '\\', '.').$this->fileExtension; + return is_file($filename); + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames($globalBasename = null) + { + $classes = array(); + + if ($this->paths) { + foreach ((array) $this->paths as $path) { + if (!is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + $fileName = $file->getBasename($this->fileExtension); + + if ($fileName == $file->getBasename() || $fileName == $globalBasename) { + continue; + } + + // NOTE: All files found here means classes are not transient! + if (isset($this->prefixes[$path])) { + $classes[] = $this->prefixes[$path].'\\'.str_replace('.', '\\', $fileName); + } else { + $classes[] = str_replace('.', '\\', $fileName); + } + } + } + } + + return $classes; + } + + /** + * {@inheritDoc} + */ + public function findMappingFile($className) + { + $defaultFileName = str_replace('\\', '.', $className).$this->fileExtension; + foreach ($this->paths as $path) { + if (!isset($this->prefixes[$path])) { + if (is_file($path.DIRECTORY_SEPARATOR.$defaultFileName)) { + return $path.DIRECTORY_SEPARATOR.$defaultFileName; + } + + continue; + } + + $prefix = $this->prefixes[$path]; + + if (0 !== strpos($className, $prefix.'\\')) { + continue; + } + + $filename = $path.'/'.strtr(substr($className, strlen($prefix)+1), '\\', '.').$this->fileExtension; + if (is_file($filename)) { + return $filename; + } + + throw MappingException::mappingFileNotFound($className, $filename); + } + + throw MappingException::mappingFileNotFound($className, substr($className, strrpos($className, '\\') + 1).$this->fileExtension); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/MappingException.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/MappingException.php new file mode 100644 index 00000000..c1e7ad57 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/MappingException.php @@ -0,0 +1,86 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +/** + * A MappingException indicates that something is wrong with the mapping setup. + * + * @since 2.2 + */ +class MappingException extends \Exception +{ + /** + * + * @param string $className + * @param array $namespaces + * + * @return MappingException + */ + public static function classNotFoundInNamespaces($className, $namespaces) + { + return new self("The class '" . $className . "' was not found in the ". + "chain configured namespaces " . implode(", ", $namespaces)); + } + + /** + * @return MappingException + */ + public static function pathRequired() + { + return new self("Specifying the paths to your entities is required ". + "in the AnnotationDriver to retrieve all class names."); + } + + /** + * @param string|null $path + * @return MappingException + */ + public static function fileMappingDriversRequireConfiguredDirectoryPath($path = null) + { + if ( ! empty($path)) { + $path = '[' . $path . ']'; + } + + return new self( + 'File mapping drivers must have a valid directory path, ' . + 'however the given path ' . $path . ' seems to be incorrect!' + ); + } + + /** + * @param string $entityName + * @param string $fileName + * @return MappingException + */ + public static function mappingFileNotFound($entityName, $fileName) + { + return new self("No mapping file found named '$fileName' for class '$entityName'."); + } + + /** + * @param string $entityName + * @param string $fileName + * @return MappingException + */ + public static function invalidMappingFile($entityName, $fileName) + { + return new self("Invalid mapping file '$fileName' for class '$entityName'."); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ReflectionService.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ReflectionService.php new file mode 100644 index 00000000..6d06ebb2 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/ReflectionService.php @@ -0,0 +1,79 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +/** + * Very simple reflection service abstraction. + * + * This is required inside metadata layers that may require either + * static or runtime reflection. + * + * @author Benjamin Eberlei + */ +interface ReflectionService +{ + /** + * Return an array of the parent classes (not interfaces) for the given class. + * + * @param string $class + * @return array + */ + function getParentClasses($class); + + /** + * Return the shortname of a class. + * + * @param string $class + * @return string + */ + function getClassShortName($class); + + /** + * @param string $class + * @return string + */ + function getClassNamespace($class); + + /** + * Return a reflection class instance or null + * + * @param string $class + * @return \ReflectionClass|null + */ + function getClass($class); + + /** + * Return an accessible property (setAccessible(true)) or null. + * + * @param string $class + * @param string $property + * @return \ReflectionProperty|null + */ + function getAccessibleProperty($class, $property); + + /** + * Check if the class have a public method with the given name. + * + * @param mixed $class + * @param mixed $method + * @return bool + */ + function hasPublicMethod($class, $method); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/RuntimeReflectionService.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/RuntimeReflectionService.php new file mode 100644 index 00000000..316acf65 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/RuntimeReflectionService.php @@ -0,0 +1,101 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +use ReflectionClass; +use ReflectionProperty; + +/** + * PHP Runtime Reflection Service + * + * @author Benjamin Eberlei + */ +class RuntimeReflectionService implements ReflectionService +{ + /** + * Return an array of the parent classes (not interfaces) for the given class. + * + * @param string $class + * @return array + */ + public function getParentClasses($class) + { + return class_parents($class); + } + + /** + * Return the shortname of a class. + * + * @param string $class + * @return string + */ + public function getClassShortName($class) + { + $r = new ReflectionClass($class); + return $r->getShortName(); + } + + /** + * @param string $class + * @return string + */ + public function getClassNamespace($class) + { + $r = new ReflectionClass($class); + return $r->getNamespaceName(); + } + + /** + * Return a reflection class instance or null + * + * @param string $class + * @return ReflectionClass|null + */ + public function getClass($class) + { + return new ReflectionClass($class); + } + + /** + * Return an accessible property (setAccessible(true)) or null. + * + * @param string $class + * @param string $property + * @return ReflectionProperty|null + */ + public function getAccessibleProperty($class, $property) + { + $property = new ReflectionProperty($class, $property); + $property->setAccessible(true); + return $property; + } + + /** + * Check if the class have a public method with the given name. + * + * @param mixed $class + * @param mixed $method + * @return bool + */ + public function hasPublicMethod($class, $method) + { + return method_exists($class, $method) && is_callable(array($class, $method)); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/StaticReflectionService.php b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/StaticReflectionService.php new file mode 100644 index 00000000..1270f271 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Mapping/StaticReflectionService.php @@ -0,0 +1,106 @@ +. + */ + +namespace Doctrine\Common\Persistence\Mapping; + +use ReflectionClass; +use ReflectionProperty; + +/** + * PHP Runtime Reflection Service + * + * @author Benjamin Eberlei + */ +class StaticReflectionService implements ReflectionService +{ + /** + * Return an array of the parent classes (not interfaces) for the given class. + * + * @param string $class + * @return array + */ + public function getParentClasses($class) + { + return array(); + } + + /** + * Return the shortname of a class. + * + * @param string $className + * @return string + */ + public function getClassShortName($className) + { + if (strpos($className, '\\') !== false) { + $className = substr($className, strrpos($className, "\\")+1); + } + return $className; + } + + /** + * Return the namespace of a class. + * + * @param string $className + * @return string + */ + public function getClassNamespace($className) + { + $namespace = ''; + if (strpos($className, '\\') !== false) { + $namespace = strrev(substr( strrev($className), strpos(strrev($className), '\\')+1 )); + } + return $namespace; + } + + /** + * Return a reflection class instance or null + * + * @param string $class + * @return ReflectionClass|null + */ + public function getClass($class) + { + return null; + } + + /** + * Return an accessible property (setAccessible(true)) or null. + * + * @param string $class + * @param string $property + * @return ReflectionProperty|null + */ + public function getAccessibleProperty($class, $property) + { + return null; + } + + /** + * Check if the class have a public method with the given name. + * + * @param mixed $class + * @param mixed $method + * @return bool + */ + public function hasPublicMethod($class, $method) + { + return method_exists($class, $method) && is_callable(array($class, $method)); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManager.php b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManager.php new file mode 100644 index 00000000..2bb87222 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManager.php @@ -0,0 +1,152 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +/** + * Contract for a Doctrine persistence layer ObjectManager class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.1 + * @author Benjamin Eberlei + * @author Jonathan Wage + */ +interface ObjectManager +{ + /** + * Finds a object by its identifier. + * + * This is just a convenient shortcut for getRepository($className)->find($id). + * + * @param string + * @param mixed + * @return object + */ + function find($className, $id); + + /** + * Tells the ObjectManager to make an instance managed and persistent. + * + * The object will be entered into the database as a result of the flush operation. + * + * NOTE: The persist operation always considers objects that are not yet known to + * this ObjectManager as NEW. Do not pass detached objects to the persist operation. + * + * @param object $object The instance to make managed and persistent. + */ + function persist($object); + + /** + * Removes an object instance. + * + * A removed object will be removed from the database as a result of the flush operation. + * + * @param object $object The object instance to remove. + */ + function remove($object); + + /** + * Merges the state of a detached object into the persistence context + * of this ObjectManager and returns the managed copy of the object. + * The object passed to merge will not become associated/managed with this ObjectManager. + * + * @param object $object + * @return object + */ + function merge($object); + + /** + * Clears the ObjectManager. All objects that are currently managed + * by this ObjectManager become detached. + * + * @param string $objectName if given, only objects of this type will get detached + */ + function clear($objectName = null); + + /** + * Detaches an object from the ObjectManager, causing a managed object to + * become detached. Unflushed changes made to the object if any + * (including removal of the object), will not be synchronized to the database. + * Objects which previously referenced the detached object will continue to + * reference it. + * + * @param object $object The object to detach. + */ + function detach($object); + + /** + * Refreshes the persistent state of an object from the database, + * overriding any local changes that have not yet been persisted. + * + * @param object $object The object to refresh. + */ + function refresh($object); + + /** + * Flushes all changes to objects that have been queued up to now to the database. + * This effectively synchronizes the in-memory state of managed objects with the + * database. + */ + function flush(); + + /** + * Gets the repository for a class. + * + * @param string $className + * @return \Doctrine\Common\Persistence\ObjectRepository + */ + function getRepository($className); + + /** + * Returns the ClassMetadata descriptor for a class. + * + * The class name must be the fully-qualified class name without a leading backslash + * (as it is returned by get_class($obj)). + * + * @param string $className + * @return \Doctrine\Common\Persistence\Mapping\ClassMetadata + */ + function getClassMetadata($className); + + /** + * Gets the metadata factory used to gather the metadata of classes. + * + * @return \Doctrine\Common\Persistence\Mapping\ClassMetadataFactory + */ + function getMetadataFactory(); + + /** + * Helper method to initialize a lazy loading proxy or persistent collection. + * + * This method is a no-op for other objects. + * + * @param object $obj + */ + function initializeObject($obj); + + /** + * Check if the object is part of the current UnitOfWork and therefore + * managed. + * + * @param object $object + * @return bool + */ + function contains($object); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManagerAware.php b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManagerAware.php new file mode 100644 index 00000000..69fba78d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectManagerAware.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; + +/** + * Makes a Persistent Objects aware of its own object-manager. + * + * Using this interface the managing object manager and class metadata instances + * are injected into the persistent object after construction. This allows + * you to implement ActiveRecord functionality on top of the persistance-ignorance + * that Doctrine propagates. + * + * Word of Warning: This is a very powerful hook to change how you can work with your domain models. + * Using this hook will break the Single Responsibility Principle inside your Domain Objects + * and increase the coupling of database and objects. + * + * Every ObjectManager has to implement this functionality itself. + * + * @author Benjamin Eberlei + */ +interface ObjectManagerAware +{ + /** + * Injects responsible ObjectManager and the ClassMetadata into this persistent object. + * + * @param ObjectManager $objectManager + * @param ClassMetadata $classMetadata + */ + public function injectObjectManager(ObjectManager $objectManager, ClassMetadata $classMetadata); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/ObjectRepository.php b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectRepository.php new file mode 100644 index 00000000..9a3e5b6f --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/ObjectRepository.php @@ -0,0 +1,78 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +/** + * Contract for a Doctrine persistence layer ObjectRepository class to implement. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.1 + * @author Benjamin Eberlei + * @author Jonathan Wage + */ +interface ObjectRepository +{ + /** + * Finds an object by its primary key / identifier. + * + * @param int $id The identifier. + * @return object The object. + */ + function find($id); + + /** + * Finds all objects in the repository. + * + * @return mixed The objects. + */ + function findAll(); + + /** + * Finds objects by a set of criteria. + * + * Optionally sorting and limiting details can be passed. An implementation may throw + * an UnexpectedValueException if certain values of the sorting or limiting details are + * not supported. + * + * @throws \UnexpectedValueException + * @param array $criteria + * @param array|null $orderBy + * @param int|null $limit + * @param int|null $offset + * @return mixed The objects. + */ + function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null); + + /** + * Finds a single object by a set of criteria. + * + * @param array $criteria + * @return object The object. + */ + function findOneBy(array $criteria); + + /** + * Returns the class name of the object managed by the repository + * + * @return string + */ + function getClassName(); +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/PersistentObject.php b/doctrine/common/lib/Doctrine/Common/Persistence/PersistentObject.php new file mode 100644 index 00000000..9fcc4cba --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/PersistentObject.php @@ -0,0 +1,244 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + +/** + * PersistentObject base class that implements getter/setter methods for all mapped fields and associations + * by overriding __call. + * + * This class is a forward compatible implementation of the PersistentObject trait. + * + * + * Limitations: + * + * 1. All persistent objects have to be associated with a single ObjectManager, multiple + * ObjectManagers are not supported. You can set the ObjectManager with `PersistentObject#setObjectManager()`. + * 2. Setters and getters only work if a ClassMetadata instance was injected into the PersistentObject. + * This is either done on `postLoad` of an object or by accessing the global object manager. + * 3. There are no hooks for setters/getters. Just implement the method yourself instead of relying on __call(). + * 4. Slower than handcoded implementations: An average of 7 method calls per access to a field and 11 for an association. + * 5. Only the inverse side associations get autoset on the owning side aswell. Setting objects on the owning side + * will not set the inverse side associations. + * + * @example + * + * PersistentObject::setObjectManager($em); + * + * class Foo extends PersistentObject + * { + * private $id; + * } + * + * $foo = new Foo(); + * $foo->getId(); // method exists through __call + * + * @author Benjamin Eberlei + */ +abstract class PersistentObject implements ObjectManagerAware +{ + /** + * @var ObjectManager + */ + private static $objectManager; + + /** + * @var ClassMetadata + */ + private $cm; + + /** + * Set the object manager responsible for all persistent object base classes. + * + * @param ObjectManager $objectManager + */ + static public function setObjectManager(ObjectManager $objectManager = null) + { + self::$objectManager = $objectManager; + } + + /** + * @return ObjectManager + */ + static public function getObjectManager() + { + return self::$objectManager; + } + + /** + * Inject Doctrine Object Manager + * + * @param ObjectManager $objectManager + * @param ClassMetadata $classMetadata + * + * @throws \RuntimeException + */ + public function injectObjectManager(ObjectManager $objectManager, ClassMetadata $classMetadata) + { + if ($objectManager !== self::$objectManager) { + throw new \RuntimeException("Trying to use PersistentObject with different ObjectManager instances. " . + "Was PersistentObject::setObjectManager() called?"); + } + + $this->cm = $classMetadata; + } + + /** + * Sets a persistent fields value. + * + * @param string $field + * @param array $args + * + * @throws \BadMethodCallException - When no persistent field exists by that name. + * @throws \InvalidArgumentException - When the wrong target object type is passed to an association + * @return void + */ + private function set($field, $args) + { + $this->initializeDoctrine(); + + if ($this->cm->hasField($field) && !$this->cm->isIdentifier($field)) { + $this->$field = $args[0]; + } else if ($this->cm->hasAssociation($field) && $this->cm->isSingleValuedAssociation($field)) { + $targetClass = $this->cm->getAssociationTargetClass($field); + if (!($args[0] instanceof $targetClass) && $args[0] !== null) { + throw new \InvalidArgumentException("Expected persistent object of type '".$targetClass."'"); + } + $this->$field = $args[0]; + $this->completeOwningSide($field, $targetClass, $args[0]); + } else { + throw new \BadMethodCallException("no field with name '".$field."' exists on '".$this->cm->getName()."'"); + } + } + + /** + * Get persistent field value. + * + * + * @param string $field + * + * @throws \BadMethodCallException - When no persistent field exists by that name. + * @return mixed + */ + private function get($field) + { + $this->initializeDoctrine(); + + if ( $this->cm->hasField($field) || $this->cm->hasAssociation($field) ) { + return $this->$field; + } else { + throw new \BadMethodCallException("no field with name '".$field."' exists on '".$this->cm->getName()."'"); + } + } + + /** + * If this is an inverse side association complete the owning side. + * + * @param string $field + * @param ClassMetadata $targetClass + * @param object $targetObject + */ + private function completeOwningSide($field, $targetClass, $targetObject) + { + // add this object on the owning side aswell, for obvious infinite recursion + // reasons this is only done when called on the inverse side. + if ($this->cm->isAssociationInverseSide($field)) { + $mappedByField = $this->cm->getAssociationMappedByTargetField($field); + $targetMetadata = self::$objectManager->getClassMetadata($targetClass); + + $setter = ($targetMetadata->isCollectionValuedAssociation($mappedByField) ? "add" : "set").$mappedByField; + $targetObject->$setter($this); + } + } + + /** + * Add an object to a collection + * + * @param string $field + * @param array $args + * + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + */ + private function add($field, $args) + { + $this->initializeDoctrine(); + + if ($this->cm->hasAssociation($field) && $this->cm->isCollectionValuedAssociation($field)) { + $targetClass = $this->cm->getAssociationTargetClass($field); + if (!($args[0] instanceof $targetClass)) { + throw new \InvalidArgumentException("Expected persistent object of type '".$targetClass."'"); + } + if (!($this->$field instanceof Collection)) { + $this->$field = new ArrayCollection($this->$field ?: array()); + } + $this->$field->add($args[0]); + $this->completeOwningSide($field, $targetClass, $args[0]); + } else { + throw new \BadMethodCallException("There is no method add".$field."() on ".$this->cm->getName()); + } + } + + /** + * Initialize Doctrine Metadata for this class. + * + * @throws \RuntimeException + * @return void + */ + private function initializeDoctrine() + { + if ($this->cm !== null) { + return; + } + + if (!self::$objectManager) { + throw new \RuntimeException("No runtime object manager set. Call PersistentObject#setObjectManager()."); + } + + $this->cm = self::$objectManager->getClassMetadata(get_class($this)); + } + + /** + * Magic method that implements + * + * @param string $method + * @param array $args + * + * @throws \BadMethodCallException + * @return mixed + */ + public function __call($method, $args) + { + $command = substr($method, 0, 3); + $field = lcfirst(substr($method, 3)); + if ($command == "set") { + $this->set($field, $args); + } else if ($command == "get") { + return $this->get($field); + } else if ($command == "add") { + $this->add($field, $args); + } else { + throw new \BadMethodCallException("There is no method ".$method." on ".$this->cm->getName()); + } + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Persistence/Proxy.php b/doctrine/common/lib/Doctrine/Common/Persistence/Proxy.php new file mode 100644 index 00000000..e25598c6 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Persistence/Proxy.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\Common\Persistence; + +/** + * Interface for proxy classes. + * + * @author Roman Borschel + * @since 2.2 + */ +interface Proxy +{ + /** + * Marker for Proxy class names. + * + * @var string + */ + const MARKER = '__CG__'; + + /** + * Length of the proxy marker + * + * @var int + */ + const MARKER_LENGTH = 6; + + /** + * Initialize this proxy if its not yet initialized. + * + * Acts as a no-op if already initialized. + * + * @return void + */ + public function __load(); + + /** + * Is this proxy initialized or not. + * + * @return bool + */ + public function __isInitialized(); +} diff --git a/doctrine/common/lib/Doctrine/Common/PropertyChangedListener.php b/doctrine/common/lib/Doctrine/Common/PropertyChangedListener.php new file mode 100644 index 00000000..55af1ec3 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/PropertyChangedListener.php @@ -0,0 +1,47 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Contract for classes that are potential listeners of a NotifyPropertyChanged + * implementor. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface PropertyChangedListener +{ + /** + * Notifies the listener of a property change. + * + * @param object $sender The object on which the property changed. + * @param string $propertyName The name of the property that changed. + * @param mixed $oldValue The old value of the property that changed. + * @param mixed $newValue The new value of the property that changed. + */ + function propertyChanged($sender, $propertyName, $oldValue, $newValue); +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/ClassFinderInterface.php b/doctrine/common/lib/Doctrine/Common/Reflection/ClassFinderInterface.php new file mode 100644 index 00000000..ae696076 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/ClassFinderInterface.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +/** + * Finds a class in a PSR-0 structure. + * + * @author Karoly Negyesi + */ +interface ClassFinderInterface +{ + /** + * Finds a class. + * + * @param string $class The name of the class. + * + * @return + * The name of the class or NULL if not found. + */ + public function findFile($class); +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/Psr0FindFile.php b/doctrine/common/lib/Doctrine/Common/Reflection/Psr0FindFile.php new file mode 100644 index 00000000..b6a5fd11 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/Psr0FindFile.php @@ -0,0 +1,83 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +/** + * Finds a class in a PSR-0 structure. + * + * @author Karoly Negyesi + */ +class Psr0FindFile implements ClassFinderInterface +{ + /** + * The PSR-0 prefixes. + * + * @var string + */ + protected $prefixes; + + /** + * @param string $prefixes + * An array of prefixes. Each key is a PHP namespace and each value is + * a list of directories. + */ + public function __construct($prefixes) + { + $this->prefixes = $prefixes; + } + + /** + * Finds a class. + * + * @param string $class The name of the class. + * + * @return + * The name of the class or NULL if not found. + */ + public function findFile($class) + { + $lastNsPos = strrpos($class, '\\'); + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + if (false !== $lastNsPos) { + // namespaced class name + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $lastNsPos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $lastNsPos + 1); + } else { + // PEAR-like class name + $classPath = null; + $className = $class; + } + + $classPath .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php'; + + foreach ($this->prefixes as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($dir . DIRECTORY_SEPARATOR . $classPath)) { + return $dir . DIRECTORY_SEPARATOR . $classPath; + } + } + } + } + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/ReflectionProviderInterface.php b/doctrine/common/lib/Doctrine/Common/Reflection/ReflectionProviderInterface.php new file mode 100644 index 00000000..a436a2d7 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/ReflectionProviderInterface.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +interface ReflectionProviderInterface +{ + /** + * Get the ReflectionClass equivalent for this class. + * + * @return ReflectionClass + */ + public function getReflectionClass(); + + /** + * Get the ReflectionClass equivalent for this class. + * + * @return ReflectionMethod + */ + public function getReflectionMethod($name); + + /** + * Get the ReflectionClass equivalent for this class. + * + * @return ReflectionMethod + */ + public function getReflectionProperty($name); +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionClass.php b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionClass.php new file mode 100644 index 00000000..12e45d57 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionClass.php @@ -0,0 +1,112 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +use ReflectionClass; +use ReflectionException; + +class StaticReflectionClass extends ReflectionClass +{ + /** + * The static reflection parser object. + * + * @var StaticReflectionParser + */ + private $staticReflectionParser; + + public function __construct(StaticReflectionParser $staticReflectionParser) + { + $this->staticReflectionParser = $staticReflectionParser; + } + + public function getName() + { + return $this->staticReflectionParser->getClassName(); + } + + public function getDocComment() + { + return $this->staticReflectionParser->getDocComment(); + } + + public function getNamespaceName() + { + return $this->staticReflectionParser->getNamespaceName(); + } + + public function getUseStatements() + { + return $this->staticReflectionParser->getUseStatements(); + } + + public function getMethod($name) + { + return $this->staticReflectionParser->getReflectionMethod($name); + } + + public function getProperty($name) + { + return $this->staticReflectionParser->getReflectionProperty($name); + } + + public static function export($argument, $return = false) { throw new ReflectionException('Method not implemented'); } + public function getConstant($name) { throw new ReflectionException('Method not implemented'); } + public function getConstants() { throw new ReflectionException('Method not implemented'); } + public function getConstructor() { throw new ReflectionException('Method not implemented'); } + public function getDefaultProperties() { throw new ReflectionException('Method not implemented'); } + public function getEndLine() { throw new ReflectionException('Method not implemented'); } + public function getExtension() { throw new ReflectionException('Method not implemented'); } + public function getExtensionName() { throw new ReflectionException('Method not implemented'); } + public function getFileName() { throw new ReflectionException('Method not implemented'); } + public function getInterfaceNames() { throw new ReflectionException('Method not implemented'); } + public function getInterfaces() { throw new ReflectionException('Method not implemented'); } + public function getMethods($filter = NULL) { throw new ReflectionException('Method not implemented'); } + public function getModifiers() { throw new ReflectionException('Method not implemented'); } + public function getParentClass() { throw new ReflectionException('Method not implemented'); } + public function getProperties($filter = NULL) { throw new ReflectionException('Method not implemented'); } + public function getShortName() { throw new ReflectionException('Method not implemented'); } + public function getStartLine() { throw new ReflectionException('Method not implemented'); } + public function getStaticProperties() { throw new ReflectionException('Method not implemented'); } + public function getStaticPropertyValue($name, $default = '') { throw new ReflectionException('Method not implemented'); } + public function getTraitAliases() { throw new ReflectionException('Method not implemented'); } + public function getTraitNames() { throw new ReflectionException('Method not implemented'); } + public function getTraits() { throw new ReflectionException('Method not implemented'); } + public function hasConstant($name) { throw new ReflectionException('Method not implemented'); } + public function hasMethod($name) { throw new ReflectionException('Method not implemented'); } + public function hasProperty($name) { throw new ReflectionException('Method not implemented'); } + public function implementsInterface($interface) { throw new ReflectionException('Method not implemented'); } + public function inNamespace() { throw new ReflectionException('Method not implemented'); } + public function isAbstract() { throw new ReflectionException('Method not implemented'); } + public function isCloneable() { throw new ReflectionException('Method not implemented'); } + public function isFinal() { throw new ReflectionException('Method not implemented'); } + public function isInstance($object) { throw new ReflectionException('Method not implemented'); } + public function isInstantiable() { throw new ReflectionException('Method not implemented'); } + public function isInterface() { throw new ReflectionException('Method not implemented'); } + public function isInternal() { throw new ReflectionException('Method not implemented'); } + public function isIterateable() { throw new ReflectionException('Method not implemented'); } + public function isSubclassOf($class) { throw new ReflectionException('Method not implemented'); } + public function isTrait() { throw new ReflectionException('Method not implemented'); } + public function isUserDefined() { throw new ReflectionException('Method not implemented'); } + public function newInstance($args) { throw new ReflectionException('Method not implemented'); } + public function newInstanceArgs(array $args = array()) { throw new ReflectionException('Method not implemented'); } + public function newInstanceWithoutConstructor() { throw new ReflectionException('Method not implemented'); } + public function setStaticPropertyValue($name, $value) { throw new ReflectionException('Method not implemented'); } + public function __toString() { throw new ReflectionException('Method not implemented'); } +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionMethod.php b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionMethod.php new file mode 100644 index 00000000..6482036e --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionMethod.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +use ReflectionMethod; +use ReflectionException; + +class StaticReflectionMethod extends ReflectionMethod +{ + /** + * The PSR-0 parser object. + * + * @var StaticReflectionParser + */ + protected $staticReflectionParser; + + /** + * The name of the method. + * + * @var string + */ + protected $methodName; + + public function __construct(StaticReflectionParser $staticReflectionParser, $methodName) + { + $this->staticReflectionParser = $staticReflectionParser; + $this->methodName = $methodName; + } + public function getName() + { + return $this->methodName; + } + protected function getStaticReflectionParser() + { + return $this->staticReflectionParser->getStaticReflectionParserForDeclaringClass('method', $this->methodName); + } + public function getDeclaringClass() + { + return $this->getStaticReflectionParser()->getReflectionClass(); + } + public function getNamespaceName() + { + return $this->getStaticReflectionParser()->getNamespaceName(); + } + public function getDocComment() + { + return $this->getStaticReflectionParser()->getDocComment('method', $this->methodName); + } + public function getUseStatements() + { + return $this->getStaticReflectionParser()->getUseStatements(); + } + public static function export($class, $name, $return = false) { throw new ReflectionException('Method not implemented'); } + public function getClosure($object) { throw new ReflectionException('Method not implemented'); } + public function getModifiers() { throw new ReflectionException('Method not implemented'); } + public function getPrototype() { throw new ReflectionException('Method not implemented'); } + public function invoke($object, $parameter = NULL) { throw new ReflectionException('Method not implemented'); } + public function invokeArgs($object, array $args) { throw new ReflectionException('Method not implemented'); } + public function isAbstract() { throw new ReflectionException('Method not implemented'); } + public function isConstructor() { throw new ReflectionException('Method not implemented'); } + public function isDestructor() { throw new ReflectionException('Method not implemented'); } + public function isFinal() { throw new ReflectionException('Method not implemented'); } + public function isPrivate() { throw new ReflectionException('Method not implemented'); } + public function isProtected() { throw new ReflectionException('Method not implemented'); } + public function isPublic() { throw new ReflectionException('Method not implemented'); } + public function isStatic() { throw new ReflectionException('Method not implemented'); } + public function setAccessible($accessible) { throw new ReflectionException('Method not implemented'); } + public function __toString() { throw new ReflectionException('Method not implemented'); } + public function getClosureThis() { throw new ReflectionException('Method not implemented'); } + public function getEndLine() { throw new ReflectionException('Method not implemented'); } + public function getExtension() { throw new ReflectionException('Method not implemented'); } + public function getExtensionName() { throw new ReflectionException('Method not implemented'); } + public function getFileName() { throw new ReflectionException('Method not implemented'); } + public function getNumberOfParameters() { throw new ReflectionException('Method not implemented'); } + public function getNumberOfRequiredParameters() { throw new ReflectionException('Method not implemented'); } + public function getParameters() { throw new ReflectionException('Method not implemented'); } + public function getShortName() { throw new ReflectionException('Method not implemented'); } + public function getStartLine() { throw new ReflectionException('Method not implemented'); } + public function getStaticVariables() { throw new ReflectionException('Method not implemented'); } + public function inNamespace() { throw new ReflectionException('Method not implemented'); } + public function isClosure() { throw new ReflectionException('Method not implemented'); } + public function isDeprecated() { throw new ReflectionException('Method not implemented'); } + public function isInternal() { throw new ReflectionException('Method not implemented'); } + public function isUserDefined() { throw new ReflectionException('Method not implemented'); } + public function returnsReference() { throw new ReflectionException('Method not implemented'); } +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php new file mode 100644 index 00000000..7f3e41fe --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionParser.php @@ -0,0 +1,282 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +use ReflectionException; +use Doctrine\Common\Annotations\TokenParser; + +/** + * Parses a file for namespaces/use/class declarations. + * + * @author Karoly Negyesi + */ +class StaticReflectionParser implements ReflectionProviderInterface +{ + + /** + * The name of the class. + * + * @var string + */ + protected $className; + + /** + * TRUE if the caller only wants class annotations. + * + * @var boolean. + */ + protected $classAnnotationOptimize; + + /** + * TRUE when the parser has ran. + * + * @var boolean + */ + protected $parsed = false; + + /** + * The namespace of the class + * + * @var string + */ + protected $namespace = ''; + + /** + * The use statements of this class. + * + * @var array + */ + protected $useStatements = array(); + + /** + * The docComment of the class. + * + * @var string + */ + protected $docComment = array( + 'class' => '', + 'property' => array(), + 'method' => array(), + ); + + /** + * The name of the class this class extends, if any. + * + * @var string + */ + protected $parentClassName = ''; + + /** + * The parent PSR-0 Parser. + * + * @var \Doctrine\Common\Annotations\StaticReflectionParser + */ + protected $parentStaticReflectionParser; + + /** + * Parses a class residing in a PSR-0 hierarchy. + * + * @param string $class + * The full, namespaced class name. + * @param ClassFinder $finder + * A ClassFinder object which finds the class. + * @param boolean $classAnnotationOptimize + * Only retrieve the class docComment. Presumes there is only one + * statement per line. + */ + public function __construct($className, $finder, $classAnnotationOptimize = false) + { + $this->className = ltrim($className, '\\'); + if ($lastNsPos = strrpos($this->className, '\\')) { + $this->namespace = substr($this->className, 0, $lastNsPos); + } + $this->finder = $finder; + $this->classAnnotationOptimize = $classAnnotationOptimize; + } + + protected function parse() + { + if ($this->parsed || !$fileName = $this->finder->findFile($this->className)) { + return; + } + $this->parsed = true; + $contents = file_get_contents($fileName); + if ($this->classAnnotationOptimize) { + if (preg_match("/(\A.*)^\s+(abstract|final)?\s+class\s+$className\s+{/sm", $contents, $matches)) { + $contents = $matches[1]; + } + } + $tokenParser = new TokenParser($contents); + $docComment = ''; + while ($token = $tokenParser->next(false)) { + if (is_array($token)) { + switch ($token[0]) { + case T_USE: + $this->useStatements = array_merge($this->useStatements, $tokenParser->parseUseStatement()); + break; + case T_DOC_COMMENT: + $docComment = $token[1]; + break; + case T_CLASS: + $this->docComment['class'] = $docComment; + $docComment = ''; + break; + case T_VAR: + case T_PRIVATE: + case T_PROTECTED: + case T_PUBLIC: + $token = $tokenParser->next(); + if ($token[0] === T_VARIABLE) { + $propertyName = substr($token[1], 1); + $this->docComment['property'][$propertyName] = $docComment; + continue 2; + } + if ($token[0] !== T_FUNCTION) { + // For example, it can be T_FINAL. + continue 2; + } + // No break. + case T_FUNCTION: + // The next string after function is the name, but + // there can be & before the function name so find the + // string. + while (($token = $tokenParser->next()) && $token[0] !== T_STRING); + $methodName = $token[1]; + $this->docComment['method'][$methodName] = $docComment; + $docComment = ''; + break; + case T_EXTENDS: + $this->parentClassName = $tokenParser->parseClass(); + $nsPos = strpos($this->parentClassName, '\\'); + $fullySpecified = false; + if ($nsPos === 0) { + $fullySpecified = true; + } else { + if ($nsPos) { + $prefix = strtolower(substr($this->parentClassName, 0, $nsPos)); + $postfix = substr($this->parentClassName, $nsPos); + } else { + $prefix = strtolower($this->parentClassName); + $postfix = ''; + } + foreach ($this->useStatements as $alias => $use) { + if ($alias == $prefix) { + $this->parentClassName = '\\' . $use . $postfix; + $fullySpecified = true; + } + } + } + if (!$fullySpecified) { + $this->parentClassName = '\\' . $this->namespace . '\\' . $this->parentClassName; + } + break; + } + } + } + } + + protected function getParentStaticReflectionParser() + { + if (empty($this->parentStaticReflectionParser)) { + $this->parentStaticReflectionParser = new static($this->parentClassName, $this->finder); + } + + return $this->parentStaticReflectionParser; + } + + public function getClassName() + { + return $this->className; + } + + public function getNamespaceName() + { + return $this->namespace; + } + + /** + * Get the ReflectionClass equivalent for this file / class. + */ + public function getReflectionClass() + { + return new StaticReflectionClass($this); + } + + /** + * Get the ReflectionMethod equivalent for the method of this file / class. + */ + public function getReflectionMethod($methodName) + { + return new StaticReflectionMethod($this, $methodName); + } + + /** + * Get the ReflectionProperty equivalent for the method of this file / class. + */ + public function getReflectionProperty($propertyName) + { + return new StaticReflectionProperty($this, $propertyName); + } + + /** + * Get the use statements from this file. + */ + public function getUseStatements() + { + $this->parse(); + + return $this->useStatements; + } + + /** + * Get docComment. + * + * @param string $type class, property or method. + * @param string $name Name of the property or method, not needed for class. + * + * @return string the doc comment or empty string if none. + */ + public function getDocComment($type = 'class', $name = '') + { + $this->parse(); + + return $name ? $this->docComment[$type][$name] : $this->docComment[$type]; + } + + /** + * Get the PSR-0 parser for the declaring class. + * + * @param string $type property or method. + * @param string $name Name of the property or method. + * + * @return StaticReflectionParser A static reflection parser for the declaring class. + */ + public function getStaticReflectionParserForDeclaringClass($type, $name) + { + $this->parse(); + if (isset($this->docComment[$type][$name])) { + return $this; + } + if (!empty($this->parentClassName)) { + return $this->getParentStaticReflectionParser()->getStaticReflectionParserForDeclaringClass($type, $name); + } + throw new ReflectionException('Invalid ' . $type . ' "' . $name . '"'); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionProperty.php b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionProperty.php new file mode 100644 index 00000000..7c6411a9 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Reflection/StaticReflectionProperty.php @@ -0,0 +1,77 @@ +. + */ + +namespace Doctrine\Common\Reflection; + +use ReflectionProperty; +use ReflectionException; + +class StaticReflectionProperty extends ReflectionProperty +{ + /** + * The PSR-0 parser object. + * + * @var StaticReflectionParser + */ + protected $staticReflectionParser; + + /** + * The name of the property. + * + * @var string + */ + protected $propertyName; + + public function __construct(StaticReflectionParser $staticReflectionParser, $propertyName) + { + $this->staticReflectionParser = $staticReflectionParser; + $this->propertyName = $propertyName; + } + public function getName() + { + return $this->propertyName; + } + protected function getStaticReflectionParser() + { + return $this->staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', $this->propertyName); + } + public function getDeclaringClass() + { + return $this->getStaticReflectionParser()->getReflectionClass(); + } + public function getDocComment() + { + return $this->getStaticReflectionParser()->getDocComment('property', $this->propertyName); + } + public function getUseStatements() + { + return $this->getStaticReflectionParser()->getUseStatements(); + } + public static function export ($class, $name, $return = false) { throw new ReflectionException('Method not implemented'); } + public function getModifiers() { throw new ReflectionException('Method not implemented'); } + public function getValue($object = NULL) { throw new ReflectionException('Method not implemented'); } + public function isDefault() { throw new ReflectionException('Method not implemented'); } + public function isPrivate() { throw new ReflectionException('Method not implemented'); } + public function isProtected() { throw new ReflectionException('Method not implemented'); } + public function isPublic() { throw new ReflectionException('Method not implemented'); } + public function isStatic() { throw new ReflectionException('Method not implemented'); } + public function setAccessible ($accessible) { throw new ReflectionException('Method not implemented'); } + public function setValue ($object, $value = NULL) { throw new ReflectionException('Method not implemented'); } + public function __toString() { throw new ReflectionException('Method not implemented'); } +} diff --git a/doctrine/common/lib/Doctrine/Common/Util/ClassUtils.php b/doctrine/common/lib/Doctrine/Common/Util/ClassUtils.php new file mode 100644 index 00000000..078a8dba --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Util/ClassUtils.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\Common\Util; + +use Doctrine\Common\Persistence\Proxy; + +/** + * Class and reflection related functionality for objects that + * might or not be proxy objects at the moment. + * + * @author Benjamin Eberlei + * @author Johannes Schmitt + */ +class ClassUtils +{ + /** + * Get the real class name of a class name that could be a proxy. + * + * @param string + * @return string + */ + public static function getRealClass($class) + { + if (false === $pos = strrpos($class, '\\'.Proxy::MARKER.'\\')) { + return $class; + } + + return substr($class, $pos + Proxy::MARKER_LENGTH + 2); + } + + /** + * Get the real class name of an object (even if its a proxy) + * + * @param object + * @return string + */ + public static function getClass($object) + { + return self::getRealClass(get_class($object)); + } + + /** + * Get the real parent class name of a class or object + * + * @param string + * @return string + */ + public static function getParentClass($className) + { + return get_parent_class( self::getRealClass( $className ) ); + } + + /** + * Create a new reflection class + * + * @param string + * @return \ReflectionClass + */ + public static function newReflectionClass($class) + { + return new \ReflectionClass( self::getRealClass( $class ) ); + } + + /** + * Create a new reflection object + * + * @param object + * @return \ReflectionObject + */ + public static function newReflectionObject($object) + { + return self::newReflectionClass( self::getClass( $object ) ); + } + + /** + * Given a class name and a proxy namespace return the proxy name. + * + * @param string $className + * @param string $proxyNamespace + * @return string + */ + public static function generateProxyClassName($className, $proxyNamespace) + { + return rtrim($proxyNamespace, '\\') . '\\'.Proxy::MARKER.'\\' . ltrim($className, '\\'); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Util/Debug.php b/doctrine/common/lib/Doctrine/Common/Util/Debug.php new file mode 100644 index 00000000..458e5c88 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Util/Debug.php @@ -0,0 +1,135 @@ +. + */ + +namespace Doctrine\Common\Util; + +/** + * Static class containing most used debug methods. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Giorgio Sironi + */ +final class Debug +{ + /** + * Private constructor (prevents from instantiation) + * + */ + private function __construct() {} + + /** + * Prints a dump of the public, protected and private properties of $var. + * + * @link http://xdebug.org/ + * @param mixed $var + * @param integer $maxDepth Maximum nesting level for object properties + * @param boolean $stripTags Flag that indicate if output should strip HTML tags + */ + public static function dump($var, $maxDepth = 2, $stripTags = true) + { + ini_set('html_errors', 'On'); + + if (extension_loaded('xdebug')) { + ini_set('xdebug.var_display_max_depth', $maxDepth); + } + + $var = self::export($var, $maxDepth++); + + ob_start(); + var_dump($var); + $dump = ob_get_contents(); + ob_end_clean(); + + echo ($stripTags ? strip_tags(html_entity_decode($dump)) : $dump); + + ini_set('html_errors', 'Off'); + } + + /** + * Export + * + * @param mixed $var + * @param int $maxDepth + * @return mixed + */ + public static function export($var, $maxDepth) + { + $return = null; + $isObj = is_object($var); + + if ($isObj && in_array('Doctrine\Common\Collections\Collection', class_implements($var))) { + $var = $var->toArray(); + } + + if ($maxDepth) { + if (is_array($var)) { + $return = array(); + + foreach ($var as $k => $v) { + $return[$k] = self::export($v, $maxDepth - 1); + } + } else if ($isObj) { + $return = new \stdclass(); + if ($var instanceof \DateTime) { + $return->__CLASS__ = "DateTime"; + $return->date = $var->format('c'); + $return->timezone = $var->getTimeZone()->getName(); + } else { + $reflClass = ClassUtils::newReflectionObject($var); + $return->__CLASS__ = ClassUtils::getClass($var); + + if ($var instanceof \Doctrine\Common\Persistence\Proxy) { + $return->__IS_PROXY__ = true; + $return->__PROXY_INITIALIZED__ = $var->__isInitialized(); + } + + foreach ($reflClass->getProperties() as $reflProperty) { + $name = $reflProperty->getName(); + + $reflProperty->setAccessible(true); + $return->$name = self::export($reflProperty->getValue($var), $maxDepth - 1); + } + } + } else { + $return = $var; + } + } else { + $return = is_object($var) ? get_class($var) + : (is_array($var) ? 'Array(' . count($var) . ')' : $var); + } + + return $return; + } + + /** + * Convert to string + * + * @param object $obj + * @return string + */ + public static function toString($obj) + { + return method_exists('__toString', $obj) ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Util/Inflector.php b/doctrine/common/lib/Doctrine/Common/Util/Inflector.php new file mode 100644 index 00000000..214ed574 --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Util/Inflector.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\Common\Util; + +/** + * Doctrine inflector has static methods for inflecting text + * + * The methods in these classes are from several different sources collected + * across several different php projects and several different authors. The + * original author names and emails are not known + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 1.0 + * @version $Revision: 3189 $ + * @author Konsta Vesterinen + * @author Jonathan H. Wage + */ +class Inflector +{ + /** + * Convert word in to the format for a Doctrine table name. Converts 'ModelName' to 'model_name' + * + * @param string $word Word to tableize + * @return string $word Tableized word + */ + public static function tableize($word) + { + return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word)); + } + + /** + * Convert a word in to the format for a Doctrine class name. Converts 'table_name' to 'TableName' + * + * @param string $word Word to classify + * @return string $word Classified word + */ + public static function classify($word) + { + return str_replace(" ", "", ucwords(strtr($word, "_-", " "))); + } + + /** + * Camelize a word. This uses the classify() method and turns the first character to lowercase + * + * @param string $word + * @return string $word + */ + public static function camelize($word) + { + return lcfirst(self::classify($word)); + } +} diff --git a/doctrine/common/lib/Doctrine/Common/Version.php b/doctrine/common/lib/Doctrine/Common/Version.php new file mode 100644 index 00000000..cca4894d --- /dev/null +++ b/doctrine/common/lib/Doctrine/Common/Version.php @@ -0,0 +1,55 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Class to store and retrieve the version of Doctrine + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Version +{ + /** + * Current Doctrine Version + */ + const VERSION = '2.3.0'; + + /** + * Compares a Doctrine version with the current one. + * + * @param string $version Doctrine version to compare. + * @return int Returns -1 if older, 0 if it is the same, 1 if version + * passed as argument is newer. + */ + public static function compare($version) + { + $currentVersion = str_replace(' ', '', strtolower(self::VERSION)); + $version = str_replace(' ', '', $version); + + return version_compare($version, $currentVersion); + } +} diff --git a/doctrine/common/phpunit.xml.dist b/doctrine/common/phpunit.xml.dist new file mode 100644 index 00000000..df5f06ea --- /dev/null +++ b/doctrine/common/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + ./tests/Doctrine/ + + + + + + ./lib/Doctrine/ + + + + + + performance + + + diff --git a/doctrine/common/tests/.gitignore b/doctrine/common/tests/.gitignore new file mode 100644 index 00000000..72104052 --- /dev/null +++ b/doctrine/common/tests/.gitignore @@ -0,0 +1,3 @@ +Doctrine/Tests/Proxies/ +Doctrine/Tests/ORM/Proxy/generated/ +Doctrine/Tests/ORM/Tools/Export/export diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AbstractReaderTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AbstractReaderTest.php new file mode 100644 index 00000000..4261e6b7 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AbstractReaderTest.php @@ -0,0 +1,517 @@ + array($testsRoot), + ); + $staticReflectionParser = new StaticReflectionParser($className, new Psr0FindFile($paths)); + return array( + 'native' => array(new ReflectionClass($className)), + 'static' => array($staticReflectionParser->getReflectionClass()), + ); + } + + /** + * @dataProvider getReflectionClass + */ + public function testAnnotations($class) + { + $reader = $this->getReader(); + $this->assertEquals(1, count($reader->getClassAnnotations($class))); + $this->assertInstanceOf($annotName = 'Doctrine\Tests\Common\Annotations\DummyAnnotation', $annot = $reader->getClassAnnotation($class, $annotName)); + $this->assertEquals("hello", $annot->dummyValue); + + $field1Prop = $class->getProperty('field1'); + $propAnnots = $reader->getPropertyAnnotations($field1Prop); + $this->assertEquals(1, count($propAnnots)); + $this->assertInstanceOf($annotName, $annot = $reader->getPropertyAnnotation($field1Prop, $annotName)); + $this->assertEquals("fieldHello", $annot->dummyValue); + + $getField1Method = $class->getMethod('getField1'); + $methodAnnots = $reader->getMethodAnnotations($getField1Method); + $this->assertEquals(1, count($methodAnnots)); + $this->assertInstanceOf($annotName, $annot = $reader->getMethodAnnotation($getField1Method, $annotName)); + $this->assertEquals(array(1, 2, "three"), $annot->value); + + $field2Prop = $class->getProperty('field2'); + $propAnnots = $reader->getPropertyAnnotations($field2Prop); + $this->assertEquals(1, count($propAnnots)); + $this->assertInstanceOf($annotName = 'Doctrine\Tests\Common\Annotations\DummyJoinTable', $joinTableAnnot = $reader->getPropertyAnnotation($field2Prop, $annotName)); + $this->assertEquals(1, count($joinTableAnnot->joinColumns)); + $this->assertEquals(1, count($joinTableAnnot->inverseJoinColumns)); + $this->assertTrue($joinTableAnnot->joinColumns[0] instanceof DummyJoinColumn); + $this->assertTrue($joinTableAnnot->inverseJoinColumns[0] instanceof DummyJoinColumn); + $this->assertEquals('col1', $joinTableAnnot->joinColumns[0]->name); + $this->assertEquals('col2', $joinTableAnnot->joinColumns[0]->referencedColumnName); + $this->assertEquals('col3', $joinTableAnnot->inverseJoinColumns[0]->name); + $this->assertEquals('col4', $joinTableAnnot->inverseJoinColumns[0]->referencedColumnName); + + $dummyAnnot = $reader->getMethodAnnotation($class->getMethod('getField1'), 'Doctrine\Tests\Common\Annotations\DummyAnnotation'); + $this->assertEquals('', $dummyAnnot->dummyValue); + $this->assertEquals(array(1, 2, 'three'), $dummyAnnot->value); + + $dummyAnnot = $reader->getPropertyAnnotation($class->getProperty('field1'), 'Doctrine\Tests\Common\Annotations\DummyAnnotation'); + $this->assertEquals('fieldHello', $dummyAnnot->dummyValue); + + $classAnnot = $reader->getClassAnnotation($class, 'Doctrine\Tests\Common\Annotations\DummyAnnotation'); + $this->assertEquals('hello', $classAnnot->dummyValue); + } + + public function testAnnotationsWithValidTargets() + { + $reader = $this->getReader(); + $class = new ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithValidAnnotationTarget'); + + $this->assertEquals(1,count($reader->getClassAnnotations($class))); + $this->assertEquals(1,count($reader->getPropertyAnnotations($class->getProperty('foo')))); + $this->assertEquals(1,count($reader->getMethodAnnotations($class->getMethod('someFunction')))); + $this->assertEquals(1,count($reader->getPropertyAnnotations($class->getProperty('nested')))); + } + + public function testAnnotationsWithVarType() + { + $reader = $this->getReader(); + $class = new ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithVarType'); + + $this->assertEquals(1,count($fooAnnot = $reader->getPropertyAnnotations($class->getProperty('foo')))); + $this->assertEquals(1,count($barAnnot = $reader->getMethodAnnotations($class->getMethod('bar')))); + + $this->assertInternalType('string', $fooAnnot[0]->string); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', $barAnnot[0]->annotation); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Semantical Error] Annotation @AnnotationTargetPropertyMethod is not allowed to be declared on class Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtClass. You may only use this annotation on these code elements: METHOD, PROPERTY + */ + public function testClassWithInvalidAnnotationTargetAtClassDocBlock() + { + $reader = $this->getReader(); + $reader->getClassAnnotations(new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtClass')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Semantical Error] Annotation @AnnotationTargetClass is not allowed to be declared on property Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtProperty::$foo. You may only use this annotation on these code elements: CLASS + */ + public function testClassWithInvalidAnnotationTargetAtPropertyDocBlock() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtProperty', 'foo')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Semantical Error] Annotation @AnnotationTargetAnnotation is not allowed to be declared on property Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtProperty::$bar. You may only use this annotation on these code elements: ANNOTATION + */ + public function testClassWithInvalidNestedAnnotationTargetAtPropertyDocBlock() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtProperty', 'bar')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Semantical Error] Annotation @AnnotationTargetClass is not allowed to be declared on method Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtMethod::functionName(). You may only use this annotation on these code elements: CLASS + */ + public function testClassWithInvalidAnnotationTargetAtMethodDocBlock() + { + $reader = $this->getReader(); + $reader->getMethodAnnotations(new \ReflectionMethod('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtMethod', 'functionName')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 24 in class @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithTargetSyntaxError. + */ + public function testClassWithAnnotationWithTargetSyntaxErrorAtClassDocBlock() + { + $reader = $this->getReader(); + $reader->getClassAnnotations(new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithTargetSyntaxError')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 24 in class @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithTargetSyntaxError. + */ + public function testClassWithAnnotationWithTargetSyntaxErrorAtPropertyDocBlock() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithTargetSyntaxError','foo')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 24 in class @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithTargetSyntaxError. + */ + public function testClassWithAnnotationWithTargetSyntaxErrorAtMethodDocBlock() + { + $reader = $this->getReader(); + $reader->getMethodAnnotations(new \ReflectionMethod('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithTargetSyntaxError','bar')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Type Error] Attribute "string" of @AnnotationWithVarType declared on property Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithVarType::$invalidProperty expects a(n) string, but got integer. + */ + public function testClassWithPropertyInvalidVarTypeError() + { + $reader = $this->getReader(); + $class = new ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithVarType'); + + $reader->getPropertyAnnotations($class->getProperty('invalidProperty')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Type Error] Attribute "annotation" of @AnnotationWithVarType declared on method Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithVarType::invalidMethod() expects a(n) Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll, but got an instance of Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation. + */ + public function testClassWithMethodInvalidVarTypeError() + { + $reader = $this->getReader(); + $class = new ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithAnnotationWithVarType'); + + $reader->getMethodAnnotations($class->getMethod('invalidMethod')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 18 in class Doctrine\Tests\Common\Annotations\DummyClassSyntaxError. + */ + public function testClassSyntaxErrorContext() + { + $reader = $this->getReader(); + $reader->getClassAnnotations(new \ReflectionClass('Doctrine\Tests\Common\Annotations\DummyClassSyntaxError')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 18 in method Doctrine\Tests\Common\Annotations\DummyClassMethodSyntaxError::foo(). + */ + public function testMethodSyntaxErrorContext() + { + $reader = $this->getReader(); + $reader->getMethodAnnotations(new \ReflectionMethod('Doctrine\Tests\Common\Annotations\DummyClassMethodSyntaxError', 'foo')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 18 in property Doctrine\Tests\Common\Annotations\DummyClassPropertySyntaxError::$foo. + */ + public function testPropertySyntaxErrorContext() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\DummyClassPropertySyntaxError', 'foo')); + } + + /** + * @group regression + */ + public function testMultipleAnnotationsOnSameLine() + { + $reader = $this->getReader(); + $annots = $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\DummyClass2', 'id')); + $this->assertEquals(3, count($annots)); + } + + public function testNonAnnotationProblem() + { + $reader = $this->getReader(); + + $this->assertNotNull($annot = $reader->getPropertyAnnotation(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\DummyClassNonAnnotationProblem', 'foo'), $name = 'Doctrine\Tests\Common\Annotations\DummyAnnotation')); + $this->assertInstanceOf($name, $annot); + } + + public function testImportWithConcreteAnnotation() + { + $reader = $this->getReader(); + $property = new \ReflectionProperty('Doctrine\Tests\Common\Annotations\TestImportWithConcreteAnnotation', 'field'); + $annotations = $reader->getPropertyAnnotations($property); + $this->assertEquals(1, count($annotations)); + $this->assertNotNull($reader->getPropertyAnnotation($property, 'Doctrine\Tests\Common\Annotations\DummyAnnotation')); + } + + public function testImportWithInheritance() + { + $reader = $this->getReader(); + + $class = new TestParentClass(); + $ref = new \ReflectionClass($class); + + $childAnnotations = $reader->getPropertyAnnotations($ref->getProperty('child')); + $this->assertEquals(1, count($childAnnotations)); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Foo\Name', reset($childAnnotations)); + + $parentAnnotations = $reader->getPropertyAnnotations($ref->getProperty('parent')); + $this->assertEquals(1, count($parentAnnotations)); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Bar\Name', reset($parentAnnotations)); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage The annotation "@NameFoo" in property Doctrine\Tests\Common\Annotations\TestAnnotationNotImportedClass::$field was never imported. + */ + public function testImportDetectsNotImportedAnnotation() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\TestAnnotationNotImportedClass', 'field')); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage The annotation "@Foo\Bar\Name" in property Doctrine\Tests\Common\Annotations\TestNonExistentAnnotationClass::$field was never imported. + */ + public function testImportDetectsNonExistentAnnotation() + { + $reader = $this->getReader(); + $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\TestNonExistentAnnotationClass', 'field')); + } + + public function testTopLevelAnnotation() + { + $reader = $this->getReader(); + $annotations = $reader->getPropertyAnnotations(new \ReflectionProperty('Doctrine\Tests\Common\Annotations\TestTopLevelAnnotationClass', 'field')); + + $this->assertEquals(1, count($annotations)); + $this->assertInstanceOf('\TopLevelAnnotation', reset($annotations)); + } + + public function testIgnoresAnnotationsNotPrefixedWithWhitespace() + { + $reader = $this->getReader(); + + $annotation = $reader->getClassAnnotation(new \ReflectionClass(new TestIgnoresNonAnnotationsClass()), 'Doctrine\Tests\Common\Annotations\Name'); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Name', $annotation); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage The class "Doctrine\Tests\Common\Annotations\Fixtures\NoAnnotation" is not annotated with @Annotation. Are you sure this class can be used as annotation? If so, then you need to add @Annotation to the _class_ doc comment of "Doctrine\Tests\Common\Annotations\Fixtures\NoAnnotation". If it is indeed no annotation, then you need to add @IgnoreAnnotation("NoAnnotation") to the _class_ doc comment of class Doctrine\Tests\Common\Annotations\Fixtures\InvalidAnnotationUsageClass. + */ + public function testErrorWhenInvalidAnnotationIsUsed() + { + $reader = $this->getReader(); + $ref = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\InvalidAnnotationUsageClass'); + $reader->getClassAnnotations($ref); + } + + public function testInvalidAnnotationUsageButIgnoredClass() + { + $reader = $this->getReader(); + $ref = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\InvalidAnnotationUsageButIgnoredClass'); + $annots = $reader->getClassAnnotations($ref); + + $this->assertEquals(2, count($annots)); + } + + /** + * @group DDC-1660 + * @group regression + */ + public function testInvalidAnnotationButIgnored() + { + $reader = $this->getReader(); + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassDDC1660'); + + $this->assertTrue(class_exists('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Version')); + $this->assertCount(0, $reader->getClassAnnotations($class)); + $this->assertCount(0, $reader->getMethodAnnotations($class->getMethod('bar'))); + $this->assertCount(0, $reader->getPropertyAnnotations($class->getProperty('foo'))); + } + + abstract protected function getReader(); +} + +/** + * @parseAnnotation("var") + * @author Johannes M. Schmitt + * + */ +class TestParseAnnotationClass +{ + /** + * @var + */ + private $field; +} + +/** + * @Name + * @author Johannes M. Schmitt + */ +class TestIgnoresNonAnnotationsClass +{ +} + +class TestTopLevelAnnotationClass +{ + /** + * @\TopLevelAnnotation + */ + private $field; +} + +class TestNonExistentAnnotationClass +{ + /** + * @Foo\Bar\Name + */ + private $field; +} + +class TestAnnotationNotImportedClass +{ + /** + * @NameFoo + */ + private $field; +} + +class TestChildClass +{ + /** + * @\Doctrine\Tests\Common\Annotations\Foo\Name(name = "foo") + */ + protected $child; +} + +class TestParentClass extends TestChildClass +{ + /** + * @\Doctrine\Tests\Common\Annotations\Bar\Name(name = "bar") + */ + private $parent; +} + +class TestImportWithConcreteAnnotation +{ + /** + * @DummyAnnotation(dummyValue = "bar") + */ + private $field; +} + +/** + * @ignoreAnnotation("var") + */ +class DummyClass2 { + /** + * @DummyId @DummyColumn(type="integer") @DummyGeneratedValue + * @var integer + */ + private $id; +} + +/** @Annotation */ +class DummyId extends \Doctrine\Common\Annotations\Annotation {} +/** @Annotation */ +class DummyColumn extends \Doctrine\Common\Annotations\Annotation { + public $type; +} +/** @Annotation */ +class DummyGeneratedValue extends \Doctrine\Common\Annotations\Annotation {} +/** @Annotation */ +class DummyAnnotation extends \Doctrine\Common\Annotations\Annotation { + public $dummyValue; +} +/** @Annotation */ +class DummyJoinColumn extends \Doctrine\Common\Annotations\Annotation { + public $name; + public $referencedColumnName; +} +/** @Annotation */ +class DummyJoinTable extends \Doctrine\Common\Annotations\Annotation { + public $name; + public $joinColumns; + public $inverseJoinColumns; +} + +/** + * @DummyAnnotation(@) + */ +class DummyClassSyntaxError +{ + +} + +class DummyClassMethodSyntaxError +{ + /** + * @DummyAnnotation(@) + */ + public function foo() + { + + } +} + +class DummyClassPropertySyntaxError +{ + /** + * @DummyAnnotation(@) + */ + public $foo; +} + +/** + * @ignoreAnnotation({"since", "var"}) + */ +class DummyClassNonAnnotationProblem +{ + /** + * @DummyAnnotation + * + * @var \Test + * @since 0.1 + */ + public $foo; +} + + +/** +* @DummyAnnotation Foo bar +*/ +class DummyClassWithEmail +{ + +} + +namespace Doctrine\Tests\Common\Annotations\Foo; + +/** @Annotation */ +class Name extends \Doctrine\Common\Annotations\Annotation +{ + public $name; +} + +namespace Doctrine\Tests\Common\Annotations\Bar; + +/** @Annotation */ +class Name extends \Doctrine\Common\Annotations\Annotation +{ + public $name; +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php new file mode 100644 index 00000000..d2cc6678 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/AnnotationReaderTest.php @@ -0,0 +1,13 @@ +getMock('Doctrine\Common\Cache\Cache'); + $cache + ->expects($this->at(0)) + ->method('fetch') + ->with($this->equalTo($cacheKey)) + ->will($this->returnValue(array())) + ; + $cache + ->expects($this->at(1)) + ->method('fetch') + ->with($this->equalTo('[C]'.$cacheKey)) + ->will($this->returnValue(time() - 10)) + ; + $cache + ->expects($this->at(2)) + ->method('save') + ->with($this->equalTo($cacheKey)) + ; + $cache + ->expects($this->at(3)) + ->method('save') + ->with($this->equalTo('[C]'.$cacheKey)) + ; + + $reader = new CachedReader(new AnnotationReader(), $cache, true); + $route = new Route(); + $route->pattern = '/someprefix'; + $this->assertEquals(array($route), $reader->getClassAnnotations(new \ReflectionClass($name))); + } + + protected function getReader() + { + $this->cache = new ArrayCache(); + return new CachedReader(new AnnotationReader(), $this->cache); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php new file mode 100644 index 00000000..03a55c80 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocLexerTest.php @@ -0,0 +1,137 @@ +setInput("@Name"); + $this->assertNull($lexer->token); + $this->assertNull($lexer->lookahead); + + $this->assertTrue($lexer->moveNext()); + $this->assertNull($lexer->token); + $this->assertEquals('@', $lexer->lookahead['value']); + + $this->assertTrue($lexer->moveNext()); + $this->assertEquals('@', $lexer->token['value']); + $this->assertEquals('Name', $lexer->lookahead['value']); + + $this->assertFalse($lexer->moveNext()); + } + + public function testScannerTokenizesDocBlockWhitConstants() + { + $lexer = new DocLexer(); + $docblock = '@AnnotationWithConstants(PHP_EOL, ClassWithConstants::SOME_VALUE, \Doctrine\Tests\Common\Annotations\Fixtures\IntefaceWithConstants::SOME_VALUE)'; + + $tokens = array ( + array( + 'value' => '@', + 'position' => 0, + 'type' => DocLexer::T_AT, + ), + array( + 'value' => 'AnnotationWithConstants', + 'position' => 1, + 'type' => DocLexer::T_IDENTIFIER, + ), + array( + 'value' => '(', + 'position' => 24, + 'type' => DocLexer::T_OPEN_PARENTHESIS, + ), + array( + 'value' => 'PHP_EOL', + 'position' => 25, + 'type' => DocLexer::T_IDENTIFIER, + ), + array( + 'value' => ',', + 'position' => 32, + 'type' => DocLexer::T_COMMA, + ), + array( + 'value' => 'ClassWithConstants::SOME_VALUE', + 'position' => 34, + 'type' => DocLexer::T_IDENTIFIER, + ), + array( + 'value' => ',', + 'position' => 64, + 'type' => DocLexer::T_COMMA, + ), + array( + 'value' => '\\Doctrine\\Tests\\Common\\Annotations\\Fixtures\\IntefaceWithConstants::SOME_VALUE', + 'position' => 66, + 'type' => DocLexer::T_IDENTIFIER, + ), + array( + 'value' => ')', + 'position' => 143, + 'type' => DocLexer::T_CLOSE_PARENTHESIS, + ) + + ); + + $lexer->setInput($docblock); + + foreach ($tokens as $expected) { + $lexer->moveNext(); + $lookahead = $lexer->lookahead; + $this->assertEquals($expected['value'], $lookahead['value']); + $this->assertEquals($expected['type'], $lookahead['type']); + $this->assertEquals($expected['position'], $lookahead['position']); + } + + $this->assertFalse($lexer->moveNext()); + } + + + public function testScannerTokenizesDocBlockWhitInvalidIdentifier() + { + $lexer = new DocLexer(); + $docblock = '@Foo\3.42'; + + $tokens = array ( + array( + 'value' => '@', + 'position' => 0, + 'type' => DocLexer::T_AT, + ), + array( + 'value' => 'Foo', + 'position' => 1, + 'type' => DocLexer::T_IDENTIFIER, + ), + array( + 'value' => '\\', + 'position' => 4, + 'type' => DocLexer::T_NAMESPACE_SEPARATOR, + ), + array( + 'value' => 3.42, + 'position' => 5, + 'type' => DocLexer::T_FLOAT, + ) + ); + + $lexer->setInput($docblock); + + foreach ($tokens as $expected) { + $lexer->moveNext(); + $lookahead = $lexer->lookahead; + $this->assertEquals($expected['value'], $lookahead['value']); + $this->assertEquals($expected['type'], $lookahead['type']); + $this->assertEquals($expected['position'], $lookahead['position']); + } + + $this->assertFalse($lexer->moveNext()); + } + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php new file mode 100644 index 00000000..595e4195 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DocParserTest.php @@ -0,0 +1,1208 @@ +createTestParser(); + + // Nested arrays with nested annotations + $result = $parser->parse('@Name(foo={1,2, {"key"=@Name}})'); + $annot = $result[0]; + + $this->assertTrue($annot instanceof Name); + $this->assertNull($annot->value); + $this->assertEquals(3, count($annot->foo)); + $this->assertEquals(1, $annot->foo[0]); + $this->assertEquals(2, $annot->foo[1]); + $this->assertTrue(is_array($annot->foo[2])); + + $nestedArray = $annot->foo[2]; + $this->assertTrue(isset($nestedArray['key'])); + $this->assertTrue($nestedArray['key'] instanceof Name); + } + + public function testBasicAnnotations() + { + $parser = $this->createTestParser(); + + // Marker annotation + $result = $parser->parse("@Name"); + $annot = $result[0]; + $this->assertTrue($annot instanceof Name); + $this->assertNull($annot->value); + $this->assertNull($annot->foo); + + // Associative arrays + $result = $parser->parse('@Name(foo={"key1" = "value1"})'); + $annot = $result[0]; + $this->assertNull($annot->value); + $this->assertTrue(is_array($annot->foo)); + $this->assertTrue(isset($annot->foo['key1'])); + + // Numerical arrays + $result = $parser->parse('@Name({2="foo", 4="bar"})'); + $annot = $result[0]; + $this->assertTrue(is_array($annot->value)); + $this->assertEquals('foo', $annot->value[2]); + $this->assertEquals('bar', $annot->value[4]); + $this->assertFalse(isset($annot->value[0])); + $this->assertFalse(isset($annot->value[1])); + $this->assertFalse(isset($annot->value[3])); + + // Multiple values + $result = $parser->parse('@Name(@Name, @Name)'); + $annot = $result[0]; + + $this->assertTrue($annot instanceof Name); + $this->assertTrue(is_array($annot->value)); + $this->assertTrue($annot->value[0] instanceof Name); + $this->assertTrue($annot->value[1] instanceof Name); + + // Multiple types as values + $result = $parser->parse('@Name(foo="Bar", @Name, {"key1"="value1", "key2"="value2"})'); + $annot = $result[0]; + + $this->assertTrue($annot instanceof Name); + $this->assertTrue(is_array($annot->value)); + $this->assertTrue($annot->value[0] instanceof Name); + $this->assertTrue(is_array($annot->value[1])); + $this->assertEquals('value1', $annot->value[1]['key1']); + $this->assertEquals('value2', $annot->value[1]['key2']); + + // Complete docblock + $docblock = <<parse($docblock); + $this->assertEquals(1, count($result)); + $annot = $result[0]; + $this->assertTrue($annot instanceof Name); + $this->assertEquals("bar", $annot->foo); + $this->assertNull($annot->value); + } + + public function testNamespacedAnnotations() + { + $parser = new DocParser; + $parser->setIgnoreNotImportedAnnotations(true); + + $docblock = << + * @Doctrine\Tests\Common\Annotations\Name(foo="bar") + * @ignore + */ +DOCBLOCK; + + $result = $parser->parse($docblock); + $this->assertEquals(1, count($result)); + $annot = $result[0]; + $this->assertTrue($annot instanceof Name); + $this->assertEquals("bar", $annot->foo); + } + + /** + * @group debug + */ + public function testTypicalMethodDocBlock() + { + $parser = $this->createTestParser(); + + $docblock = <<parse($docblock); + $this->assertEquals(2, count($result)); + $this->assertTrue(isset($result[0])); + $this->assertTrue(isset($result[1])); + $annot = $result[0]; + $this->assertTrue($annot instanceof Name); + $this->assertEquals("bar", $annot->foo); + $marker = $result[1]; + $this->assertTrue($marker instanceof Marker); + } + + + public function testAnnotationWithoutConstructor() + { + $parser = $this->createTestParser(); + + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertNotNull($annot); + $this->assertTrue($annot instanceof SomeAnnotationClassNameWithoutConstructor); + + $this->assertNull($annot->name); + $this->assertNotNull($annot->data); + $this->assertEquals($annot->data, "Some data"); + + + + +$docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertNotNull($annot); + $this->assertTrue($annot instanceof SomeAnnotationClassNameWithoutConstructor); + + $this->assertEquals($annot->name, "Some Name"); + $this->assertEquals($annot->data, "Some data"); + + + + +$docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertEquals($annot->data, "Some data"); + $this->assertNull($annot->name); + + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertEquals($annot->name, "Some name"); + $this->assertNull($annot->data); + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertEquals($annot->data, "Some data"); + $this->assertNull($annot->name); + + + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertEquals($annot->name, "Some name"); + $this->assertEquals($annot->data, "Some data"); + + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $annot = $result[0]; + + $this->assertEquals($annot->name, "Some name"); + $this->assertEquals($annot->data, "Some data"); + + $docblock = <<parse($docblock); + $this->assertEquals(count($result), 1); + $this->assertTrue($result[0] instanceof SomeAnnotationClassNameWithoutConstructorAndProperties); + } + + public function testAnnotationTarget() + { + + $parser = new DocParser; + $parser->setImports(array( + '__NAMESPACE__' => 'Doctrine\Tests\Common\Annotations\Fixtures', + )); + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithValidAnnotationTarget'); + + + $context = 'class ' . $class->getName(); + $docComment = $class->getDocComment(); + + $parser->setTarget(Target::TARGET_CLASS); + $this->assertNotNull($parser->parse($docComment,$context)); + + + $property = $class->getProperty('foo'); + $docComment = $property->getDocComment(); + $context = 'property ' . $class->getName() . "::\$" . $property->getName(); + + $parser->setTarget(Target::TARGET_PROPERTY); + $this->assertNotNull($parser->parse($docComment,$context)); + + + + $method = $class->getMethod('someFunction'); + $docComment = $property->getDocComment(); + $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; + + $parser->setTarget(Target::TARGET_METHOD); + $this->assertNotNull($parser->parse($docComment,$context)); + + + try { + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtClass'); + $context = 'class ' . $class->getName(); + $docComment = $class->getDocComment(); + + $parser->setTarget(Target::TARGET_CLASS); + $parser->parse($class->getDocComment(),$context); + + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertNotNull($exc->getMessage()); + } + + + try { + + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtMethod'); + $method = $class->getMethod('functionName'); + $docComment = $method->getDocComment(); + $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; + + $parser->setTarget(Target::TARGET_METHOD); + $parser->parse($docComment,$context); + + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertNotNull($exc->getMessage()); + } + + + try { + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassWithInvalidAnnotationTargetAtProperty'); + $property = $class->getProperty('foo'); + $docComment = $property->getDocComment(); + $context = 'property ' . $class->getName() . "::\$" . $property->getName(); + + $parser->setTarget(Target::TARGET_PROPERTY); + $parser->parse($docComment,$context); + + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertNotNull($exc->getMessage()); + } + + } + + public function getAnnotationVarTypeProviderValid() + { + //({attribute name}, {attribute value}) + return array( + // mixed type + array('mixed', '"String Value"'), + array('mixed', 'true'), + array('mixed', 'false'), + array('mixed', '1'), + array('mixed', '1.2'), + array('mixed', '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll'), + + // boolean type + array('boolean', 'true'), + array('boolean', 'false'), + + // alias for internal type boolean + array('bool', 'true'), + array('bool', 'false'), + + // integer type + array('integer', '0'), + array('integer', '1'), + array('integer', '123456789'), + array('integer', '9223372036854775807'), + + // alias for internal type double + array('float', '0.1'), + array('float', '1.2'), + array('float', '123.456'), + + // string type + array('string', '"String Value"'), + array('string', '"true"'), + array('string', '"123"'), + + // array type + array('array', '{@AnnotationExtendsAnnotationTargetAll}'), + array('array', '{@AnnotationExtendsAnnotationTargetAll,@AnnotationExtendsAnnotationTargetAll}'), + + array('arrayOfIntegers', '1'), + array('arrayOfIntegers', '{1}'), + array('arrayOfIntegers', '{1,2,3,4}'), + array('arrayOfAnnotations', '@AnnotationExtendsAnnotationTargetAll'), + array('arrayOfAnnotations', '{@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll}'), + array('arrayOfAnnotations', '{@AnnotationExtendsAnnotationTargetAll, @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll}'), + + // annotation instance + array('annotation', '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll'), + array('annotation', '@AnnotationExtendsAnnotationTargetAll'), + ); + } + + public function getAnnotationVarTypeProviderInvalid() + { + //({attribute name}, {type declared type}, {attribute value} , {given type or class}) + return array( + // boolean type + array('boolean','boolean','1','integer'), + array('boolean','boolean','1.2','double'), + array('boolean','boolean','"str"','string'), + array('boolean','boolean','{1,2,3}','array'), + array('boolean','boolean','@Name', 'an instance of Doctrine\Tests\Common\Annotations\Name'), + + // alias for internal type boolean + array('bool','bool', '1','integer'), + array('bool','bool', '1.2','double'), + array('bool','bool', '"str"','string'), + array('bool','bool', '{"str"}','array'), + + // integer type + array('integer','integer', 'true','boolean'), + array('integer','integer', 'false','boolean'), + array('integer','integer', '1.2','double'), + array('integer','integer', '"str"','string'), + array('integer','integer', '{"str"}','array'), + array('integer','integer', '{1,2,3,4}','array'), + + // alias for internal type double + array('float','float', 'true','boolean'), + array('float','float', 'false','boolean'), + array('float','float', '123','integer'), + array('float','float', '"str"','string'), + array('float','float', '{"str"}','array'), + array('float','float', '{12.34}','array'), + array('float','float', '{1,2,3}','array'), + + // string type + array('string','string', 'true','boolean'), + array('string','string', 'false','boolean'), + array('string','string', '12','integer'), + array('string','string', '1.2','double'), + array('string','string', '{"str"}','array'), + array('string','string', '{1,2,3,4}','array'), + + // annotation instance + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', 'true','boolean'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', 'false','boolean'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '12','integer'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '1.2','double'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{"str"}','array'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{1,2,3,4}','array'), + array('annotation','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '@Name','an instance of Doctrine\Tests\Common\Annotations\Name'), + ); + } + + public function getAnnotationVarTypeArrayProviderInvalid() + { + //({attribute name}, {type declared type}, {attribute value} , {given type or class}) + return array( + array('arrayOfIntegers','integer', 'true','boolean'), + array('arrayOfIntegers','integer', 'false','boolean'), + array('arrayOfIntegers','integer', '{true,true}','boolean'), + array('arrayOfIntegers','integer', '{1,true}','boolean'), + array('arrayOfIntegers','integer', '{1,2,1.2}','double'), + array('arrayOfIntegers','integer', '{1,2,"str"}','string'), + + + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', 'true','boolean'), + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', 'false','boolean'), + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll,true}','boolean'), + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll,true}','boolean'), + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll,1.2}','double'), + array('arrayOfAnnotations','Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll', '{@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll,@AnnotationExtendsAnnotationTargetAll,"str"}','string'), + ); + } + + /** + * @dataProvider getAnnotationVarTypeProviderValid + */ + public function testAnnotationWithVarType($attribute, $value) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::$invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + $result = $parser->parse($docblock, $context); + + $this->assertTrue(sizeof($result) === 1); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType', $result[0]); + $this->assertNotNull($result[0]->$attribute); + } + + /** + * @dataProvider getAnnotationVarTypeProviderInvalid + */ + public function testAnnotationWithVarTypeError($attribute,$type,$value,$given) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + try { + $parser->parse($docblock, $context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains("[Type Error] Attribute \"$attribute\" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType declared on property SomeClassName::invalidProperty. expects a(n) $type, but got $given.", $exc->getMessage()); + } + } + + + /** + * @dataProvider getAnnotationVarTypeArrayProviderInvalid + */ + public function testAnnotationWithVarTypeArrayError($attribute,$type,$value,$given) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + try { + $parser->parse($docblock, $context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains("[Type Error] Attribute \"$attribute\" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithVarType declared on property SomeClassName::invalidProperty. expects either a(n) $type, or an array of {$type}s, but got $given.", $exc->getMessage()); + } + } + + /** + * @dataProvider getAnnotationVarTypeProviderValid + */ + public function testAnnotationWithAttributes($attribute, $value) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::$invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + $result = $parser->parse($docblock, $context); + + $this->assertTrue(sizeof($result) === 1); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes', $result[0]); + $getter = "get".ucfirst($attribute); + $this->assertNotNull($result[0]->$getter()); + } + + /** + * @dataProvider getAnnotationVarTypeProviderInvalid + */ + public function testAnnotationWithAttributesError($attribute,$type,$value,$given) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + try { + $parser->parse($docblock, $context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains("[Type Error] Attribute \"$attribute\" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes declared on property SomeClassName::invalidProperty. expects a(n) $type, but got $given.", $exc->getMessage()); + } + } + + + /** + * @dataProvider getAnnotationVarTypeArrayProviderInvalid + */ + public function testAnnotationWithAttributesWithVarTypeArrayError($attribute,$type,$value,$given) + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $docblock = sprintf('@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes(%s = %s)',$attribute, $value); + $parser->setTarget(Target::TARGET_PROPERTY); + + try { + $parser->parse($docblock, $context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains("[Type Error] Attribute \"$attribute\" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithAttributes declared on property SomeClassName::invalidProperty. expects either a(n) $type, or an array of {$type}s, but got $given.", $exc->getMessage()); + } + } + + public function testAnnotationWithRequiredAttributes() + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $parser->setTarget(Target::TARGET_PROPERTY); + + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes("Some Value", annot = @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation)'; + $result = $parser->parse($docblock); + + $this->assertTrue(sizeof($result) === 1); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes', $result[0]); + $this->assertEquals("Some Value",$result[0]->getValue()); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation', $result[0]->getAnnot()); + + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes("Some Value")'; + try { + $result = $parser->parse($docblock,$context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains('Attribute "annot" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes declared on property SomeClassName::invalidProperty. expects a(n) Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation. This value should not be null.', $exc->getMessage()); + } + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes(annot = @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation)'; + try { + $result = $parser->parse($docblock,$context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains('Attribute "value" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributes declared on property SomeClassName::invalidProperty. expects a(n) string. This value should not be null.', $exc->getMessage()); + } + + } + + public function testAnnotationWithRequiredAttributesWithoutContructor() + { + $parser = $this->createTestParser(); + $context = 'property SomeClassName::invalidProperty.'; + $parser->setTarget(Target::TARGET_PROPERTY); + + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor("Some Value", annot = @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation)'; + $result = $parser->parse($docblock); + + $this->assertTrue(sizeof($result) === 1); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor', $result[0]); + $this->assertEquals("Some Value", $result[0]->value); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation', $result[0]->annot); + + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor("Some Value")'; + try { + $result = $parser->parse($docblock,$context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains('Attribute "annot" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor declared on property SomeClassName::invalidProperty. expects a(n) Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation. This value should not be null.', $exc->getMessage()); + } + + $docblock = '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor(annot = @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation)'; + try { + $result = $parser->parse($docblock,$context); + $this->fail(); + } catch (\Doctrine\Common\Annotations\AnnotationException $exc) { + $this->assertContains('Attribute "value" of @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithRequiredAttributesWithoutContructor declared on property SomeClassName::invalidProperty. expects a(n) string. This value should not be null.', $exc->getMessage()); + } + + } + + public function getConstantsProvider() + { + $provider[] = array( + '@AnnotationWithConstants(PHP_EOL)', + PHP_EOL + ); + $provider[] = array( + '@AnnotationWithConstants(AnnotationWithConstants::INTEGER)', + AnnotationWithConstants::INTEGER + ); + $provider[] = array( + '@Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithConstants(AnnotationWithConstants::STRING)', + AnnotationWithConstants::STRING + ); + $provider[] = array( + '@AnnotationWithConstants(Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithConstants::FLOAT)', + AnnotationWithConstants::FLOAT + ); + $provider[] = array( + '@AnnotationWithConstants(ClassWithConstants::SOME_VALUE)', + ClassWithConstants::SOME_VALUE + ); + $provider[] = array( + '@AnnotationWithConstants(Doctrine\Tests\Common\Annotations\Fixtures\ClassWithConstants::SOME_VALUE)', + ClassWithConstants::SOME_VALUE + ); + $provider[] = array( + '@AnnotationWithConstants(IntefaceWithConstants::SOME_VALUE)', + IntefaceWithConstants::SOME_VALUE + ); + $provider[] = array( + '@AnnotationWithConstants(\Doctrine\Tests\Common\Annotations\Fixtures\IntefaceWithConstants::SOME_VALUE)', + IntefaceWithConstants::SOME_VALUE + ); + $provider[] = array( + '@AnnotationWithConstants({AnnotationWithConstants::STRING, AnnotationWithConstants::INTEGER, AnnotationWithConstants::FLOAT})', + array(AnnotationWithConstants::STRING, AnnotationWithConstants::INTEGER, AnnotationWithConstants::FLOAT) + ); + $provider[] = array( + '@AnnotationWithConstants({ + AnnotationWithConstants::STRING = AnnotationWithConstants::INTEGER + })', + array(AnnotationWithConstants::STRING => AnnotationWithConstants::INTEGER) + ); + $provider[] = array( + '@AnnotationWithConstants({ + Doctrine\Tests\Common\Annotations\Fixtures\IntefaceWithConstants::SOME_KEY = AnnotationWithConstants::INTEGER + })', + array(IntefaceWithConstants::SOME_KEY => AnnotationWithConstants::INTEGER) + ); + $provider[] = array( + '@AnnotationWithConstants({ + \Doctrine\Tests\Common\Annotations\Fixtures\IntefaceWithConstants::SOME_KEY = AnnotationWithConstants::INTEGER + })', + array(IntefaceWithConstants::SOME_KEY => AnnotationWithConstants::INTEGER) + ); + $provider[] = array( + '@AnnotationWithConstants({ + AnnotationWithConstants::STRING = AnnotationWithConstants::INTEGER, + ClassWithConstants::SOME_KEY = ClassWithConstants::SOME_VALUE, + Doctrine\Tests\Common\Annotations\Fixtures\ClassWithConstants::SOME_KEY = IntefaceWithConstants::SOME_VALUE + })', + array( + AnnotationWithConstants::STRING => AnnotationWithConstants::INTEGER, + ClassWithConstants::SOME_KEY => ClassWithConstants::SOME_VALUE, + ClassWithConstants::SOME_KEY => IntefaceWithConstants::SOME_VALUE + ) + ); + return $provider; + } + + /** + * @dataProvider getConstantsProvider + */ + public function testSupportClassConstants($docblock, $expected) + { + $parser = $this->createTestParser(); + $parser->setImports(array( + 'classwithconstants' => 'Doctrine\Tests\Common\Annotations\Fixtures\ClassWithConstants', + 'intefacewithconstants' => 'Doctrine\Tests\Common\Annotations\Fixtures\IntefaceWithConstants', + 'annotationwithconstants' => 'Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithConstants' + )); + + $result = $parser->parse($docblock); + $this->assertInstanceOf('\Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithConstants', $annotation = $result[0]); + $this->assertEquals($expected, $annotation->value); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage The annotation @SomeAnnotationClassNameWithoutConstructorAndProperties declared on does not accept any values, but got {"value":"Foo"}. + */ + public function testWithoutConstructorWhenIsNotDefaultValue() + { + $parser = $this->createTestParser(); + $docblock = <<setTarget(Target::TARGET_CLASS); + $parser->parse($docblock); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage The annotation @SomeAnnotationClassNameWithoutConstructorAndProperties declared on does not accept any values, but got {"value":"Foo"}. + */ + public function testWithoutConstructorWhenHasNoProperties() + { + $parser = $this->createTestParser(); + $docblock = <<setTarget(Target::TARGET_CLASS); + $parser->parse($docblock); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected namespace separator or identifier, got ')' at position 24 in class @Doctrine\Tests\Common\Annotations\Fixtures\AnnotationWithTargetSyntaxError. + */ + public function testAnnotationTargetSyntaxError() + { + $parser = $this->createTestParser(); + $context = 'class ' . 'SomeClassName'; + $docblock = <<setTarget(Target::TARGET_CLASS); + $parser->parse($docblock,$context); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Invalid Target "Foo". Available targets: [ALL, CLASS, METHOD, PROPERTY, ANNOTATION] + */ + public function testAnnotationWithInvalidTargetDeclarationError() + { + $parser = $this->createTestParser(); + $context = 'class ' . 'SomeClassName'; + $docblock = <<setTarget(Target::TARGET_CLASS); + $parser->parse($docblock,$context); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage @Target expects either a string value, or an array of strings, "NULL" given. + */ + public function testAnnotationWithTargetEmptyError() + { + $parser = $this->createTestParser(); + $context = 'class ' . 'SomeClassName'; + $docblock = <<setTarget(Target::TARGET_CLASS); + $parser->parse($docblock,$context); + } + + /** + * @group DDC-575 + */ + public function testRegressionDDC575() + { + $parser = $this->createTestParser(); + + $docblock = <<parse($docblock); + + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Name", $result[0]); + + $docblock = <<parse($docblock); + + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Name", $result[0]); + } + + /** + * @group DDC-77 + */ + public function testAnnotationWithoutClassIsIgnoredWithoutWarning() + { + $parser = new DocParser(); + $parser->setIgnoreNotImportedAnnotations(true); + $result = $parser->parse("@param"); + + $this->assertEquals(0, count($result)); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected PlainValue, got ''' at position 10. + */ + public function testAnnotationDontAcceptSingleQuotes() + { + $parser = $this->createTestParser(); + $parser->parse("@Name(foo='bar')"); + } + + /** + * @group DCOM-41 + */ + public function testAnnotationDoesntThrowExceptionWhenAtSignIsNotFollowedByIdentifier() + { + $parser = new DocParser(); + $result = $parser->parse("'@'"); + + $this->assertEquals(0, count($result)); + } + + /** + * @group DCOM-41 + * @expectedException Doctrine\Common\Annotations\AnnotationException + */ + public function testAnnotationThrowsExceptionWhenAtSignIsNotFollowedByIdentifierInNestedAnnotation() + { + $parser = new DocParser(); + $result = $parser->parse("@Doctrine\Tests\Common\Annotations\Name(@')"); + } + + /** + * @group DCOM-56 + */ + public function testAutoloadAnnotation() + { + $this->assertFalse(class_exists('Doctrine\Tests\Common\Annotations\Fixture\Annotation\Autoload', false), 'Pre-condition: Doctrine\Tests\Common\Annotations\Fixture\Annotation\Autoload not allowed to be loaded.'); + + $parser = new DocParser(); + + AnnotationRegistry::registerAutoloadNamespace('Doctrine\Tests\Common\Annotations\Fixtures\Annotation', __DIR__ . '/../../../../'); + + $parser->setImports(array( + 'autoload' => 'Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload', + )); + $annotations = $parser->parse('@Autoload'); + + $this->assertEquals(1, count($annotations)); + $this->assertInstanceOf('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Autoload', $annotations[0]); + } + + public function createTestParser() + { + $parser = new DocParser(); + $parser->setIgnoreNotImportedAnnotations(true); + $parser->setImports(array( + 'name' => 'Doctrine\Tests\Common\Annotations\Name', + '__NAMESPACE__' => 'Doctrine\Tests\Common\Annotations', + )); + + return $parser; + } + + /** + * @group DDC-78 + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage Expected PlainValue, got ''' at position 10 in class \Doctrine\Tests\Common\Annotations\Name + */ + public function testSyntaxErrorWithContextDescription() + { + $parser = $this->createTestParser(); + $parser->parse("@Name(foo='bar')", "class \Doctrine\Tests\Common\Annotations\Name"); + } + + /** + * @group DDC-183 + */ + public function testSyntaxErrorWithUnknownCharacters() + { + $docblock = <<setInput(trim($docblock, '/ *')); + //var_dump($lexer); + + try { + $parser = $this->createTestParser(); + $result = $parser->parse($docblock); + } catch (Exception $e) { + $this->fail($e->getMessage()); + } + } + + /** + * @group DCOM-14 + */ + public function testIgnorePHPDocThrowTag() + { + $docblock = <<createTestParser(); + $result = $parser->parse($docblock); + } catch (Exception $e) { + $this->fail($e->getMessage()); + } + } + + /** + * @group DCOM-38 + */ + public function testCastInt() + { + $parser = $this->createTestParser(); + + $result = $parser->parse("@Name(foo=1234)"); + $annot = $result[0]; + $this->assertInternalType('int', $annot->foo); + } + + /** + * @group DCOM-38 + */ + public function testCastNegativeInt() + { + $parser = $this->createTestParser(); + + $result = $parser->parse("@Name(foo=-1234)"); + $annot = $result[0]; + $this->assertInternalType('int', $annot->foo); + } + + /** + * @group DCOM-38 + */ + public function testCastFloat() + { + $parser = $this->createTestParser(); + + $result = $parser->parse("@Name(foo=1234.345)"); + $annot = $result[0]; + $this->assertInternalType('float', $annot->foo); + } + + /** + * @group DCOM-38 + */ + public function testCastNegativeFloat() + { + $parser = $this->createTestParser(); + + $result = $parser->parse("@Name(foo=-1234.345)"); + $annot = $result[0]; + $this->assertInternalType('float', $annot->foo); + + $result = $parser->parse("@Marker(-1234.345)"); + $annot = $result[0]; + $this->assertInternalType('float', $annot->value); + } + + public function testReservedKeywordsInAnnotations() + { + $parser = $this->createTestParser(); + + $result = $parser->parse('@Doctrine\Tests\Common\Annotations\True'); + $this->assertTrue($result[0] instanceof True); + $result = $parser->parse('@Doctrine\Tests\Common\Annotations\False'); + $this->assertTrue($result[0] instanceof False); + $result = $parser->parse('@Doctrine\Tests\Common\Annotations\Null'); + $this->assertTrue($result[0] instanceof Null); + + $result = $parser->parse('@True'); + $this->assertTrue($result[0] instanceof True); + $result = $parser->parse('@False'); + $this->assertTrue($result[0] instanceof False); + $result = $parser->parse('@Null'); + $this->assertTrue($result[0] instanceof Null); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Creation Error] The annotation @SomeAnnotationClassNameWithoutConstructor declared on some class does not have a property named "invalidaProperty". Available properties: data, name + */ + public function testSetValuesExeption() + { + $docblock = <<createTestParser()->parse($docblock, 'some class'); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Syntax Error] Expected Doctrine\Common\Annotations\DocLexer::T_IDENTIFIER or Doctrine\Common\Annotations\DocLexer::T_TRUE or Doctrine\Common\Annotations\DocLexer::T_FALSE or Doctrine\Common\Annotations\DocLexer::T_NULL, got '3.42' at position 5. + */ + public function testInvalidIdentifierInAnnotation() + { + $parser = $this->createTestParser(); + $parser->parse('@Foo\3.42'); + } + + public function testTrailingCommaIsAllowed() + { + $parser = $this->createTestParser(); + + $annots = $parser->parse('@Name({ + "Foo", + "Bar", + })'); + $this->assertEquals(1, count($annots)); + $this->assertEquals(array('Foo', 'Bar'), $annots[0]->value); + } + + public function testDefaultAnnotationValueIsNotOverwritten() + { + $parser = $this->createTestParser(); + + $annots = $parser->parse('@Doctrine\Tests\Common\Annotations\Fixtures\Annotation\AnnotWithDefaultValue'); + $this->assertEquals(1, count($annots)); + $this->assertEquals('bar', $annots[0]->foo); + } + + public function testArrayWithColon() + { + $parser = $this->createTestParser(); + + $annots = $parser->parse('@Name({"foo": "bar"})'); + $this->assertEquals(1, count($annots)); + $this->assertEquals(array('foo' => 'bar'), $annots[0]->value); + } + + /** + * @expectedException Doctrine\Common\Annotations\AnnotationException + * @expectedExceptionMessage [Semantical Error] Couldn't find constant foo. + */ + public function testInvalidContantName() + { + $parser = $this->createTestParser(); + $parser->parse('@Name(foo: "bar")'); + } +} + +/** @Annotation */ +class SomeAnnotationClassNameWithoutConstructor +{ + public $data; + public $name; +} + +/** @Annotation */ +class SomeAnnotationWithConstructorWithoutParams +{ + function __construct() + { + $this->data = "Some data"; + } + public $data; + public $name; +} + +/** @Annotation */ +class SomeAnnotationClassNameWithoutConstructorAndProperties{} + +/** + * @Annotation + * @Target("Foo") + */ +class AnnotationWithInvalidTargetDeclaration{} + +/** + * @Annotation + * @Target + */ +class AnnotationWithTargetEmpty{} + +/** @Annotation */ +class AnnotationExtendsAnnotationTargetAll extends \Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll +{ +} + +/** @Annotation */ +class Name extends \Doctrine\Common\Annotations\Annotation { + public $foo; +} + +/** @Annotation */ +class Marker { + public $value; +} + +/** @Annotation */ +class True {} + +/** @Annotation */ +class False {} + +/** @Annotation */ +class Null {} + +namespace Doctrine\Tests\Common\Annotations\FooBar; + +/** @Annotation */ +class Name extends \Doctrine\Common\Annotations\Annotation { +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DummyClass.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DummyClass.php new file mode 100644 index 00000000..17223f68 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/DummyClass.php @@ -0,0 +1,48 @@ +cacheDir = sys_get_temp_dir() . "/annotations_". uniqid(); + @mkdir($this->cacheDir); + return new FileCacheReader(new AnnotationReader(), $this->cacheDir); + } + + public function tearDown() + { + foreach (glob($this->cacheDir.'/*.php') AS $file) { + unlink($file); + } + rmdir($this->cacheDir); + } + + /** + * @group DCOM-81 + */ + public function testAttemptToCreateAnnotationCacheDir() + { + $this->cacheDir = sys_get_temp_dir() . "/not_existed_dir_". uniqid(); + + $this->assertFalse(is_dir($this->cacheDir)); + + $cache = new FileCacheReader(new AnnotationReader(), $this->cacheDir); + + $this->assertTrue(is_dir($this->cacheDir)); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/AnnotWithDefaultValue.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/AnnotWithDefaultValue.php new file mode 100644 index 00000000..44108e19 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/AnnotWithDefaultValue.php @@ -0,0 +1,10 @@ +roles = $values['value']; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Template.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Template.php new file mode 100644 index 00000000..b507e602 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Template.php @@ -0,0 +1,14 @@ +name = isset($values['value']) ? $values['value'] : null; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Version.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Version.php new file mode 100644 index 00000000..09ef0317 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/Annotation/Version.php @@ -0,0 +1,11 @@ +"), + @Attribute("annotation", type = "Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll"), + @Attribute("arrayOfAnnotations", type = "array"), + }) + */ +final class AnnotationWithAttributes +{ + + public final function __construct(array $data) + { + foreach ($data as $key => $value) { + $this->$key = $value; + } + } + + private $mixed; + private $boolean; + private $bool; + private $float; + private $string; + private $integer; + private $array; + private $annotation; + private $arrayOfIntegers; + private $arrayOfAnnotations; + + /** + * @return mixed + */ + public function getMixed() + { + return $this->mixed; + } + + /** + * @return boolean + */ + public function getBoolean() + { + return $this->boolean; + } + + /** + * @return bool + */ + public function getBool() + { + return $this->bool; + } + + /** + * @return float + */ + public function getFloat() + { + return $this->float; + } + + /** + * @return string + */ + public function getString() + { + return $this->string; + } + + public function getInteger() + { + return $this->integer; + } + + /** + * @return array + */ + public function getArray() + { + return $this->array; + } + + /** + * @return Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAll + */ + public function getAnnotation() + { + return $this->annotation; + } + + /** + * @return array + */ + public function getArrayOfIntegers() + { + return $this->arrayOfIntegers; + } + + /** + * @return array + */ + public function getArrayOfAnnotations() + { + return $this->arrayOfAnnotations; + } + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithConstants.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithConstants.php new file mode 100644 index 00000000..9c94558b --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithConstants.php @@ -0,0 +1,20 @@ + $value) { + $this->$key = $value; + } + } + + /** + * @var string + */ + private $value; + + /** + * + * @var Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation + */ + private $annot; + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @return Doctrine\Tests\Common\Annotations\Fixtures\AnnotationTargetAnnotation + */ + public function getAnnot() + { + return $this->annot; + } + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithRequiredAttributesWithoutContructor.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithRequiredAttributesWithoutContructor.php new file mode 100644 index 00000000..bf458ee7 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/AnnotationWithRequiredAttributesWithoutContructor.php @@ -0,0 +1,24 @@ + + */ + public $arrayOfIntegers; + + /** + * @var array + */ + public $arrayOfAnnotations; + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassDDC1660.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassDDC1660.php new file mode 100644 index 00000000..4e652e13 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassDDC1660.php @@ -0,0 +1,30 @@ +events->filter(function ($item) use ($year, $month, $day) { + $leftDate = new \DateTime($year.'-'.$month.'-'.$day.' 00:00'); + $rigthDate = new \DateTime($year.'-'.$month.'-'.$day.' +1 day 00:00'); + return ( ( $leftDate <= $item->getDateStart() ) && ( $item->getDateStart() < $rigthDate ) ); + + } + ); + return $extractEvents; + } + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithConstants.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithConstants.php new file mode 100644 index 00000000..055e245c --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/ClassWithConstants.php @@ -0,0 +1,10 @@ + + */ +class Controller +{ + /** + * @Route("/", name="_demo") + * @Template() + */ + public function indexAction() + { + return array(); + } + + /** + * @Route("/hello/{name}", name="_demo_hello") + * @Template() + */ + public function helloAction($name) + { + return array('name' => $name); + } + + /** + * @Route("/contact", name="_demo_contact") + * @Template() + */ + public function contactAction() + { + $form = ContactForm::create($this->get('form.context'), 'contact'); + + $form->bind($this->container->get('request'), $form); + if ($form->isValid()) { + $form->send($this->get('mailer')); + + $this->get('session')->setFlash('notice', 'Message sent!'); + + return new RedirectResponse($this->generateUrl('_demo')); + } + + return array('form' => $form); + } + + /** + * Creates the ACL for the passed object identity + * + * @param ObjectIdentityInterface $oid + * @return void + */ + private function createObjectIdentity(ObjectIdentityInterface $oid) + { + $classId = $this->createOrRetrieveClassId($oid->getType()); + + $this->connection->executeQuery($this->getInsertObjectIdentitySql($oid->getIdentifier(), $classId, true)); + } + + /** + * Returns the primary key for the passed class type. + * + * If the type does not yet exist in the database, it will be created. + * + * @param string $classType + * @return integer + */ + private function createOrRetrieveClassId($classType) + { + if (false !== $id = $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn()) { + return $id; + } + + $this->connection->executeQuery($this->getInsertClassSql($classType)); + + return $this->connection->executeQuery($this->getSelectClassIdSql($classType))->fetchColumn(); + } + + /** + * Returns the primary key for the passed security identity. + * + * If the security identity does not yet exist in the database, it will be + * created. + * + * @param SecurityIdentityInterface $sid + * @return integer + */ + private function createOrRetrieveSecurityIdentityId(SecurityIdentityInterface $sid) + { + if (false !== $id = $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn()) { + return $id; + } + + $this->connection->executeQuery($this->getInsertSecurityIdentitySql($sid)); + + return $this->connection->executeQuery($this->getSelectSecurityIdentityIdSql($sid))->fetchColumn(); + } + + /** + * Deletes all ACEs for the given object identity primary key. + * + * @param integer $oidPK + * @return void + */ + private function deleteAccessControlEntries($oidPK) + { + $this->connection->executeQuery($this->getDeleteAccessControlEntriesSql($oidPK)); + } + + /** + * Deletes the object identity from the database. + * + * @param integer $pk + * @return void + */ + private function deleteObjectIdentity($pk) + { + $this->connection->executeQuery($this->getDeleteObjectIdentitySql($pk)); + } + + /** + * Deletes all entries from the relations table from the database. + * + * @param integer $pk + * @return void + */ + private function deleteObjectIdentityRelations($pk) + { + $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk)); + } + + /** + * This regenerates the ancestor table which is used for fast read access. + * + * @param AclInterface $acl + * @return void + */ + private function regenerateAncestorRelations(AclInterface $acl) + { + $pk = $acl->getId(); + $this->connection->executeQuery($this->getDeleteObjectIdentityRelationsSql($pk)); + $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $pk)); + + $parentAcl = $acl->getParentAcl(); + while (null !== $parentAcl) { + $this->connection->executeQuery($this->getInsertObjectIdentityRelationSql($pk, $parentAcl->getId())); + + $parentAcl = $parentAcl->getParentAcl(); + } + } + + /** + * This processes changes on an ACE related property (classFieldAces, or objectFieldAces). + * + * @param string $name + * @param array $changes + * @return void + */ + private function updateFieldAceProperty($name, array $changes) + { + $sids = new \SplObjectStorage(); + $classIds = new \SplObjectStorage(); + $currentIds = array(); + foreach ($changes[1] as $field => $new) { + for ($i=0,$c=count($new); $i<$c; $i++) { + $ace = $new[$i]; + + if (null === $ace->getId()) { + if ($sids->contains($ace->getSecurityIdentity())) { + $sid = $sids->offsetGet($ace->getSecurityIdentity()); + } else { + $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity()); + } + + $oid = $ace->getAcl()->getObjectIdentity(); + if ($classIds->contains($oid)) { + $classId = $classIds->offsetGet($oid); + } else { + $classId = $this->createOrRetrieveClassId($oid->getType()); + } + + $objectIdentityId = $name === 'classFieldAces' ? null : $ace->getAcl()->getId(); + + $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, $field, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure())); + $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, $field, $i))->fetchColumn(); + $this->loadedAces[$aceId] = $ace; + + $aceIdProperty = new \ReflectionProperty('Symfony\Component\Security\Acl\Domain\Entry', 'id'); + $aceIdProperty->setAccessible(true); + $aceIdProperty->setValue($ace, intval($aceId)); + } else { + $currentIds[$ace->getId()] = true; + } + } + } + + foreach ($changes[0] as $old) { + for ($i=0,$c=count($old); $i<$c; $i++) { + $ace = $old[$i]; + + if (!isset($currentIds[$ace->getId()])) { + $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId())); + unset($this->loadedAces[$ace->getId()]); + } + } + } + } + + /** + * This processes changes on an ACE related property (classAces, or objectAces). + * + * @param string $name + * @param array $changes + * @return void + */ + private function updateAceProperty($name, array $changes) + { + list($old, $new) = $changes; + + $sids = new \SplObjectStorage(); + $classIds = new \SplObjectStorage(); + $currentIds = array(); + for ($i=0,$c=count($new); $i<$c; $i++) { + $ace = $new[$i]; + + if (null === $ace->getId()) { + if ($sids->contains($ace->getSecurityIdentity())) { + $sid = $sids->offsetGet($ace->getSecurityIdentity()); + } else { + $sid = $this->createOrRetrieveSecurityIdentityId($ace->getSecurityIdentity()); + } + + $oid = $ace->getAcl()->getObjectIdentity(); + if ($classIds->contains($oid)) { + $classId = $classIds->offsetGet($oid); + } else { + $classId = $this->createOrRetrieveClassId($oid->getType()); + } + + $objectIdentityId = $name === 'classAces' ? null : $ace->getAcl()->getId(); + + $this->connection->executeQuery($this->getInsertAccessControlEntrySql($classId, $objectIdentityId, null, $i, $sid, $ace->getStrategy(), $ace->getMask(), $ace->isGranting(), $ace->isAuditSuccess(), $ace->isAuditFailure())); + $aceId = $this->connection->executeQuery($this->getSelectAccessControlEntryIdSql($classId, $objectIdentityId, null, $i))->fetchColumn(); + $this->loadedAces[$aceId] = $ace; + + $aceIdProperty = new \ReflectionProperty($ace, 'id'); + $aceIdProperty->setAccessible(true); + $aceIdProperty->setValue($ace, intval($aceId)); + } else { + $currentIds[$ace->getId()] = true; + } + } + + for ($i=0,$c=count($old); $i<$c; $i++) { + $ace = $old[$i]; + + if (!isset($currentIds[$ace->getId()])) { + $this->connection->executeQuery($this->getDeleteAccessControlEntrySql($ace->getId())); + unset($this->loadedAces[$ace->getId()]); + } + } + } + + /** + * Persists the changes which were made to ACEs to the database. + * + * @param \SplObjectStorage $aces + * @return void + */ + private function updateAces(\SplObjectStorage $aces) + { + foreach ($aces as $ace) { + $propertyChanges = $aces->offsetGet($ace); + $sets = array(); + + if (isset($propertyChanges['mask'])) { + $sets[] = sprintf('mask = %d', $propertyChanges['mask'][1]); + } + if (isset($propertyChanges['strategy'])) { + $sets[] = sprintf('granting_strategy = %s', $this->connection->quote($propertyChanges['strategy'])); + } + if (isset($propertyChanges['aceOrder'])) { + $sets[] = sprintf('ace_order = %d', $propertyChanges['aceOrder'][1]); + } + if (isset($propertyChanges['auditSuccess'])) { + $sets[] = sprintf('audit_success = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditSuccess'][1])); + } + if (isset($propertyChanges['auditFailure'])) { + $sets[] = sprintf('audit_failure = %s', $this->connection->getDatabasePlatform()->convertBooleans($propertyChanges['auditFailure'][1])); + } + + $this->connection->executeQuery($this->getUpdateAccessControlEntrySql($ace->getId(), $sets)); + } + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/DifferentNamespacesPerFileWithClassAsFirst.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/DifferentNamespacesPerFileWithClassAsFirst.php new file mode 100644 index 00000000..bda2cc21 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/DifferentNamespacesPerFileWithClassAsFirst.php @@ -0,0 +1,15 @@ +test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test2() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test3() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test4() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test5() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test6() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test7() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test8() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test9() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test10() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test11() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test12() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test13() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test14() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test15() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test16() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test17() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test18() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test19() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test20() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test21() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test22() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test23() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test24() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test25() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test26() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test27() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test28() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test29() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test30() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test31() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test32() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test33() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test34() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test35() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test36() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test37() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test38() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test39() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NoAnnotation.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NoAnnotation.php new file mode 100644 index 00000000..1dae104a --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/NoAnnotation.php @@ -0,0 +1,5 @@ +test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test2() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test3() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test4() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test5() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test6() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test7() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test8() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test9() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test10() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test11() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test12() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test13() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test14() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test15() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test16() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test17() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test18() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test19() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test20() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test21() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test22() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test23() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test24() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test25() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test26() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test27() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test28() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test29() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test30() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test31() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test32() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test33() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test34() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test35() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test36() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test37() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + + } + + public function test38() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } + + public function test39() + { + echo $this->test1; + echo $this->test2; + echo $this->test3; + $array = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + foreach ($array as $key => $value) { + echo $key . ' => ' . $value; + } + + $val = (string)self::TEST1; + $val .= (string)self::TEST2; + $val .= (string)self::TEST3; + $val .= (string)self::TEST4; + $val .= (string)self::TEST5; + $val .= (string)self::TEST6; + $val .= (string)self::TEST7; + $val .= (string)self::TEST8; + $val .= (string)self::TEST9; + + strtolower($val); + + return $val; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/TestInterface.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/TestInterface.php new file mode 100644 index 00000000..58c5e6af --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Fixtures/TestInterface.php @@ -0,0 +1,13 @@ +getMethod(); + + $time = microtime(true); + for ($i=0,$c=500; $i<$c; $i++) { + $reader->getMethodAnnotations($method); + } + $time = microtime(true) - $time; + + $this->printResults('cached reader (in-memory)', $time, $c); + } + + /** + * @group performance + */ + public function testCachedReadPerformanceWithFileCache() + { + $method = $this->getMethod(); + + // prime cache + $reader = new FileCacheReader(new AnnotationReader(), sys_get_temp_dir()); + $reader->getMethodAnnotations($method); + + $time = microtime(true); + for ($i=0,$c=500; $i<$c; $i++) { + $reader = new FileCacheReader(new AnnotationReader(), sys_get_temp_dir()); + $reader->getMethodAnnotations($method); + clearstatcache(); + } + $time = microtime(true) - $time; + + $this->printResults('cached reader (file)', $time, $c); + } + + /** + * @group performance + */ + public function testReadPerformance() + { + $method = $this->getMethod(); + + $time = microtime(true); + for ($i=0,$c=150; $i<$c; $i++) { + $reader = new AnnotationReader(); + $reader->getMethodAnnotations($method); + } + $time = microtime(true) - $time; + + $this->printResults('reader', $time, $c); + } + + /** + * @group performance + */ + public function testDocParsePerformance() + { + $imports = array( + 'ignorephpdoc' => 'Annotations\Annotation\IgnorePhpDoc', + 'ignoreannotation' => 'Annotations\Annotation\IgnoreAnnotation', + 'route' => 'Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Route', + 'template' => 'Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Template', + '__NAMESPACE__' => 'Doctrine\Tests\Common\Annotations\Fixtures', + ); + $ignored = array( + 'access', 'author', 'copyright', 'deprecated', 'example', 'ignore', + 'internal', 'link', 'see', 'since', 'tutorial', 'version', 'package', + 'subpackage', 'name', 'global', 'param', 'return', 'staticvar', + 'static', 'var', 'throws', 'inheritdoc', + ); + + $method = $this->getMethod(); + $methodComment = $method->getDocComment(); + $classComment = $method->getDeclaringClass()->getDocComment(); + + $time = microtime(true); + for ($i=0,$c=200; $i<$c; $i++) { + $parser = new DocParser(); + $parser->setImports($imports); + $parser->setIgnoredAnnotationNames($ignored); + $parser->setIgnoreNotImportedAnnotations(true); + + $parser->parse($methodComment); + $parser->parse($classComment); + } + $time = microtime(true) - $time; + + $this->printResults('doc-parser', $time, $c); + } + + /** + * @group performance + */ + public function testDocLexerPerformance() + { + $method = $this->getMethod(); + $methodComment = $method->getDocComment(); + $classComment = $method->getDeclaringClass()->getDocComment(); + + $time = microtime(true); + for ($i=0,$c=500; $i<$c; $i++) { + $lexer = new DocLexer(); + $lexer->setInput($methodComment); + $lexer->setInput($classComment); + } + $time = microtime(true) - $time; + + $this->printResults('doc-lexer', $time, $c); + } + + /** + * @group performance + */ + public function testPhpParserPerformanceWithShortCut() + { + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\NamespacedSingleClassLOC1000'); + + $time = microtime(true); + for ($i=0,$c=500; $i<$c; $i++) { + $parser = new PhpParser(); + $parser->parseClass($class); + } + $time = microtime(true) - $time; + + $this->printResults('doc-parser-with-short-cut', $time, $c); + } + + /** + * @group performance + */ + public function testPhpParserPerformanceWithoutShortCut() + { + $class = new \ReflectionClass('SingleClassLOC1000'); + + $time = microtime(true); + for ($i=0,$c=500; $i<$c; $i++) { + $parser = new PhpParser(); + $parser->parseClass($class); + } + $time = microtime(true) - $time; + + $this->printResults('doc-parser-without-short-cut', $time, $c); + } + + private function getMethod() + { + return new \ReflectionMethod('Doctrine\Tests\Common\Annotations\Fixtures\Controller', 'helloAction'); + } + + private function printResults($test, $time, $iterations) + { + if (0 == $iterations) { + throw new \InvalidArgumentException('$iterations cannot be zero.'); + } + + $title = $test." results:\n"; + $iterationsText = sprintf("Iterations: %d\n", $iterations); + $totalTime = sprintf("Total Time: %.3f s\n", $time); + $iterationTime = sprintf("Time per iteration: %.3f ms\n", $time/$iterations * 1000); + + $max = max(strlen($title), strlen($iterationTime)) - 1; + + echo "\n".str_repeat('-', $max)."\n"; + echo $title; + echo str_repeat('=', $max)."\n"; + echo $iterationsText; + echo $totalTime; + echo $iterationTime; + echo str_repeat('-', $max)."\n"; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/PhpParserTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/PhpParserTest.php new file mode 100644 index 00000000..cf81116c --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/PhpParserTest.php @@ -0,0 +1,194 @@ +assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + ), $parser->parseClass($class)); + } + + public function testParseClassWithMultipleImportsInUseStatement() + { + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\MultipleImportsInUseStatement'); + $parser = new PhpParser(); + + $this->assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + ), $parser->parseClass($class)); + } + + public function testParseClassWhenNotUserDefined() + { + $parser = new PhpParser(); + $this->assertEquals(array(), $parser->parseClass(new \ReflectionClass('\stdClass'))); + } + + public function testParseClassWhenClassIsNotNamespaced() + { + $parser = new PhpParser(); + $class = new ReflectionClass('\AnnotationsTestsFixturesNonNamespacedClass'); + + $this->assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testParseClassWhenClassIsInterface() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\TestInterface'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + ), $parser->parseClass($class)); + } + + public function testClassWithFullyQualifiedUseStatements() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\ClassWithFullyQualifiedUseStatements'); + + $this->assertEquals(array( + 'secure' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => '\\' . __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testNamespaceAndClassCommentedOut() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\NamespaceAndClassCommentedOut'); + + $this->assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testEqualNamespacesPerFileWithClassAsFirst() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\EqualNamespacesPerFileWithClassAsFirst'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + ), $parser->parseClass($class)); + } + + public function testEqualNamespacesPerFileWithClassAsLast() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\EqualNamespacesPerFileWithClassAsLast'); + + $this->assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testDifferentNamespacesPerFileWithClassAsFirst() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\DifferentNamespacesPerFileWithClassAsFirst'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + ), $parser->parseClass($class)); + } + + public function testDifferentNamespacesPerFileWithClassAsLast() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\DifferentNamespacesPerFileWithClassAsLast'); + + $this->assertEquals(array( + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testGlobalNamespacesPerFileWithClassAsFirst() + { + $parser = new PhpParser(); + $class = new \ReflectionClass('\GlobalNamespacesPerFileWithClassAsFirst'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + ), $parser->parseClass($class)); + } + + public function testGlobalNamespacesPerFileWithClassAsLast() + { + $parser = new PhpParser(); + $class = new ReflectionClass('\GlobalNamespacesPerFileWithClassAsLast'); + + $this->assertEquals(array( + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testNamespaceWithClosureDeclaration() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\NamespaceWithClosureDeclaration'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + public function testIfPointerResetsOnMultipleParsingTries() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\NamespaceWithClosureDeclaration'); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + + $this->assertEquals(array( + 'secure' => __NAMESPACE__ . '\Fixtures\Annotation\Secure', + 'route' => __NAMESPACE__ . '\Fixtures\Annotation\Route', + 'template' => __NAMESPACE__ . '\Fixtures\Annotation\Template', + ), $parser->parseClass($class)); + } + + /** + * @group DCOM-97 + * @group regression + */ + public function testClassWithClosure() + { + $parser = new PhpParser(); + $class = new ReflectionClass(__NAMESPACE__ . '\Fixtures\ClassWithClosure'); + + $this->assertEquals(array( + 'annotationtargetall' => __NAMESPACE__ . '\Fixtures\AnnotationTargetAll', + 'annotationtargetannotation' => __NAMESPACE__ . '\Fixtures\AnnotationTargetAnnotation', + ), $parser->parseClass($class)); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php new file mode 100644 index 00000000..1c68ca62 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/SimpleAnnotationReaderTest.php @@ -0,0 +1,97 @@ +getReader(); + $class = new \ReflectionClass('Doctrine\Tests\Common\Annotations\Fixtures\ClassDDC1660'); + + $this->assertTrue(class_exists('Doctrine\Tests\Common\Annotations\Fixtures\Annotation\Version')); + $this->assertCount(1, $reader->getClassAnnotations($class)); + $this->assertCount(1, $reader->getMethodAnnotations($class->getMethod('bar'))); + $this->assertCount(1, $reader->getPropertyAnnotations($class->getProperty('foo'))); + } + + protected function getReader() + { + $reader = new SimpleAnnotationReader(); + $reader->addNamespace(__NAMESPACE__); + $reader->addNamespace(__NAMESPACE__ . '\Fixtures'); + $reader->addNamespace(__NAMESPACE__ . '\Fixtures\Annotation'); + + return $reader; + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM55Test.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM55Test.php new file mode 100644 index 00000000..a7b9e2f2 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM55Test.php @@ -0,0 +1,65 @@ +getClassAnnotations($class); + } + + public function testAnnotation() + { + $class = new \ReflectionClass(__NAMESPACE__ . '\\DCOM55Consumer'); + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $annots = $reader->getClassAnnotations($class); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf(__NAMESPACE__.'\\DCOM55Annotation', $annots[0]); + } + + public function testParseAnnotationDocblocks() + { + $class = new \ReflectionClass(__NAMESPACE__ . '\\DCOM55Annotation'); + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $annots = $reader->getClassAnnotations($class); + + $this->assertEquals(0, count($annots)); + } +} + +/** + * @Controller + */ +class Dummy +{ + +} + +/** + * @Annotation + */ +class DCOM55Annotation +{ + +} + +/** + * @DCOM55Annotation + */ +class DCOM55Consumer +{ + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php new file mode 100644 index 00000000..708bcc99 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/Ticket/DCOM58Entity.php @@ -0,0 +1,8 @@ +getClassAnnotations(new \ReflectionClass(__NAMESPACE__."\MappedClass")); + + foreach ($result as $annot) { + $classAnnotations[get_class($annot)] = $annot; + } + + $this->assertTrue(!isset($classAnnotations['']), 'Class "xxx" is not a valid entity or mapped super class.'); + } + + public function testIssueGlobalNamespace() + { + $docblock = "@Entity"; + $parser = new \Doctrine\Common\Annotations\DocParser(); + $parser->setImports(array( + "__NAMESPACE__" =>"Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping" + )); + + $annots = $parser->parse($docblock); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]); + } + + public function testIssueNamespaces() + { + $docblock = "@Entity"; + $parser = new \Doctrine\Common\Annotations\DocParser(); + $parser->addNamespace("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM"); + + $annots = $parser->parse($docblock); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Entity", $annots[0]); + } + + public function testIssueMultipleNamespaces() + { + $docblock = "@Entity"; + $parser = new \Doctrine\Common\Annotations\DocParser(); + $parser->addNamespace("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping"); + $parser->addNamespace("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM"); + + $annots = $parser->parse($docblock); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]); + } + + public function testIssueWithNamespacesOrImports() + { + $docblock = "@Entity"; + $parser = new \Doctrine\Common\Annotations\DocParser(); + $annots = $parser->parse($docblock); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf("Entity", $annots[0]); + $this->assertEquals(1, count($annots)); + } + + + public function testIssueSimpleAnnotationReader() + { + $reader = new \Doctrine\Common\Annotations\SimpleAnnotationReader(); + $reader->addNamespace('Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping'); + $annots = $reader->getClassAnnotations(new \ReflectionClass(__NAMESPACE__."\MappedClass")); + + $this->assertEquals(1, count($annots)); + $this->assertInstanceOf("Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping\Entity", $annots[0]); + } + +} + +/** + * @Entity + */ +class MappedClass +{ + +} + + +namespace Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM\Mapping; +/** +* @Annotation +*/ +class Entity +{ + +} + +namespace Doctrine\Tests\Common\Annotations\Ticket\Doctrine\ORM; +/** +* @Annotation +*/ +class Entity +{ + +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Annotations/TopLevelAnnotation.php b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/TopLevelAnnotation.php new file mode 100644 index 00000000..ff3ca376 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Annotations/TopLevelAnnotation.php @@ -0,0 +1,8 @@ +markTestSkipped('The ' . __CLASS__ .' requires the use of APC'); + } + } + + protected function _getCacheDriver() + { + return new ApcCache(); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/ArrayCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/ArrayCacheTest.php new file mode 100644 index 00000000..6cad8915 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/ArrayCacheTest.php @@ -0,0 +1,21 @@ +_getCacheDriver(); + $stats = $cache->getStats(); + + $this->assertNull($stats); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/CacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/CacheTest.php new file mode 100644 index 00000000..1bbc1650 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/CacheTest.php @@ -0,0 +1,91 @@ +_getCacheDriver(); + + // Test save + $cache->save('test_key', 'testing this out'); + + // Test contains to test that save() worked + $this->assertTrue($cache->contains('test_key')); + + // Test fetch + $this->assertEquals('testing this out', $cache->fetch('test_key')); + + // Test delete + $cache->save('test_key2', 'test2'); + $cache->delete('test_key2'); + $this->assertFalse($cache->contains('test_key2')); + } + + public function testObjects() + { + $cache = $this->_getCacheDriver(); + + // Fetch/save test with objects (Is cache driver serializes/unserializes objects correctly ?) + $cache->save('test_object_key', new \ArrayObject()); + $this->assertTrue($cache->fetch('test_object_key') instanceof \ArrayObject); + } + + public function testDeleteAll() + { + $cache = $this->_getCacheDriver(); + $cache->save('test_key1', '1'); + $cache->save('test_key2', '2'); + $cache->deleteAll(); + + $this->assertFalse($cache->contains('test_key1')); + $this->assertFalse($cache->contains('test_key2')); + } + + public function testFlushAll() + { + $cache = $this->_getCacheDriver(); + $cache->save('test_key1', '1'); + $cache->save('test_key2', '2'); + $cache->flushAll(); + + $this->assertFalse($cache->contains('test_key1')); + $this->assertFalse($cache->contains('test_key2')); + } + + public function testNamespace() + { + $cache = $this->_getCacheDriver(); + $cache->setNamespace('test_'); + $cache->save('key1', 'test'); + + $this->assertTrue($cache->contains('key1')); + + $cache->setNamespace('test2_'); + + $this->assertFalse($cache->contains('key1')); + } + + /** + * @group DCOM-43 + */ + public function testGetStats() + { + $cache = $this->_getCacheDriver(); + $stats = $cache->getStats(); + + $this->assertArrayHasKey(Cache::STATS_HITS, $stats); + $this->assertArrayHasKey(Cache::STATS_MISSES, $stats); + $this->assertArrayHasKey(Cache::STATS_UPTIME, $stats); + $this->assertArrayHasKey(Cache::STATS_MEMORY_USAGE, $stats); + $this->assertArrayHasKey(Cache::STATS_MEMORY_AVAILIABLE, $stats); + } + + /** + * @return \Doctrine\Common\Cache\CacheProvider + */ + abstract protected function _getCacheDriver(); +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/FilesystemCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/FilesystemCacheTest.php new file mode 100644 index 00000000..153d2e5c --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/FilesystemCacheTest.php @@ -0,0 +1,97 @@ +assertFalse(is_dir($dir)); + + $this->driver = new FilesystemCache($dir); + $this->assertTrue(is_dir($dir)); + + return $this->driver; + } + + public function testLifetime() + { + $cache = $this->_getCacheDriver(); + + // Test save + $cache->save('test_key', 'testing this out', 10); + + // Test contains to test that save() worked + $this->assertTrue($cache->contains('test_key')); + + // Test fetch + $this->assertEquals('testing this out', $cache->fetch('test_key')); + + // access private methods + $getFilename = new \ReflectionMethod($cache, 'getFilename'); + $getNamespacedId = new \ReflectionMethod($cache, 'getNamespacedId'); + + $getFilename->setAccessible(true); + $getNamespacedId->setAccessible(true); + + $id = $getNamespacedId->invoke($cache, 'test_key'); + $filename = $getFilename->invoke($cache, $id); + + $data = ''; + $lifetime = 0; + $resource = fopen($filename, "r"); + + if (false !== ($line = fgets($resource))) { + $lifetime = (integer) $line; + } + + while (false !== ($line = fgets($resource))) { + $data .= $line; + } + + $this->assertNotEquals(0, $lifetime, "previous lifetime could not be loaded"); + + // update lifetime + $lifetime = $lifetime - 20; + file_put_contents($filename, $lifetime . PHP_EOL . $data); + + // test expired data + $this->assertFalse($cache->contains('test_key')); + $this->assertFalse($cache->fetch('test_key')); + } + + public function testGetStats() + { + $cache = $this->_getCacheDriver(); + $stats = $cache->getStats(); + + $this->assertNull($stats); + } + + public function tearDown() + { + $dir = $this->driver->getDirectory(); + $ext = $this->driver->getExtension(); + $iterator = new \RecursiveDirectoryIterator($dir); + + foreach (new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST) as $file) { + if ($file->isFile()) { + @unlink($file->getRealPath()); + } else { + @rmdir($file->getRealPath()); + } + } + } + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcacheCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcacheCacheTest.php new file mode 100644 index 00000000..36c180c9 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcacheCacheTest.php @@ -0,0 +1,45 @@ +_memcache = new \Memcache; + $ok = @$this->_memcache->connect('localhost', 11211); + if (!$ok) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of memcache'); + } + } else { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of memcache'); + } + } + + public function testNoExpire() { + $cache = $this->_getCacheDriver(); + $cache->save('noexpire', 'value', 0); + sleep(1); + $this->assertTrue($cache->contains('noexpire'), 'Memcache provider should support no-expire'); + } + + public function testLongLifetime() + { + $cache = $this->_getCacheDriver(); + $cache->save('key', 'value', 30 * 24 * 3600 + 1); + $this->assertTrue($cache->contains('key'), 'Memcache provider should support TTL > 30 days'); + } + + protected function _getCacheDriver() + { + $driver = new MemcacheCache(); + $driver->setMemcache($this->_memcache); + return $driver; + } + +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcachedCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcachedCacheTest.php new file mode 100644 index 00000000..ecbe5a60 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/MemcachedCacheTest.php @@ -0,0 +1,48 @@ +memcached = new \Memcached(); + $this->memcached->setOption(\Memcached::OPT_COMPRESSION, false); + $this->memcached->addServer('127.0.0.1', 11211); + + $fh = @fsockopen('127.0.0.1', 11211); + if (!$fh) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of memcache'); + } + } else { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of memcache'); + } + } + + public function testNoExpire() { + $cache = $this->_getCacheDriver(); + $cache->save('noexpire', 'value', 0); + sleep(1); + $this->assertTrue($cache->contains('noexpire'), 'Memcache provider should support no-expire'); + } + + public function testLongLifetime() + { + $cache = $this->_getCacheDriver(); + $cache->save('key', 'value', 30 * 24 * 3600 + 1); + + $this->assertTrue($cache->contains('key'), 'Memcached provider should support TTL > 30 days'); + } + + protected function _getCacheDriver() + { + $driver = new MemcachedCache(); + $driver->setMemcached($this->memcached); + return $driver; + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/PhpFileCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/PhpFileCacheTest.php new file mode 100644 index 00000000..5085f466 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/PhpFileCacheTest.php @@ -0,0 +1,149 @@ +assertFalse(is_dir($dir)); + + $this->driver = new PhpFileCache($dir); + $this->assertTrue(is_dir($dir)); + + return $this->driver; + } + + public function testObjects() + { + $this->markTestSkipped('PhpFileCache does not support saving objects that dont implement __set_state()'); + } + + public function testLifetime() + { + $cache = $this->_getCacheDriver(); + + // Test save + $cache->save('test_key', 'testing this out', 10); + + // Test contains to test that save() worked + $this->assertTrue($cache->contains('test_key')); + + // Test fetch + $this->assertEquals('testing this out', $cache->fetch('test_key')); + + // access private methods + $getFilename = new \ReflectionMethod($cache, 'getFilename'); + $getNamespacedId = new \ReflectionMethod($cache, 'getNamespacedId'); + + $getFilename->setAccessible(true); + $getNamespacedId->setAccessible(true); + + $id = $getNamespacedId->invoke($cache, 'test_key'); + $path = $getFilename->invoke($cache, $id); + $value = include $path; + + // update lifetime + $value['lifetime'] = $value['lifetime'] - 20; + file_put_contents($path, 'assertFalse($cache->contains('test_key')); + $this->assertFalse($cache->fetch('test_key')); + } + + public function testImplementsSetState() + { + $cache = $this->_getCacheDriver(); + + // Test save + $cache->save('test_set_state', new SetStateClass(array(1,2,3))); + + //Test __set_state call + $this->assertCount(0, SetStateClass::$values); + + // Test fetch + $value = $cache->fetch('test_set_state'); + $this->assertInstanceOf('Doctrine\Tests\Common\Cache\SetStateClass', $value); + $this->assertEquals(array(1,2,3), $value->getValue()); + + //Test __set_state call + $this->assertCount(1, SetStateClass::$values); + + // Test contains + $this->assertTrue($cache->contains('test_set_state')); + } + + public function testNotImplementsSetState() + { + $cache = $this->_getCacheDriver(); + + $this->setExpectedException('InvalidArgumentException'); + $cache->save('test_not_set_state', new NotSetStateClass(array(1,2,3))); + } + + public function testGetStats() + { + $cache = $this->_getCacheDriver(); + $stats = $cache->getStats(); + + $this->assertNull($stats); + } + + public function tearDown() + { + if (!$this->driver) { + return; + } + + $dir = $this->driver->getDirectory(); + $ext = $this->driver->getExtension(); + $iterator = new \RecursiveDirectoryIterator($dir); + + foreach (new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST) as $file) { + if ($file->isFile()) { + @unlink($file->getRealPath()); + } else { + @rmdir($file->getRealPath()); + } + } + } + +} + +class NotSetStateClass +{ + private $value; + + public function __construct($value) + { + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } +} + +class SetStateClass extends NotSetStateClass +{ + public static $values = array(); + + public static function __set_state($data) + { + self::$values = $data; + return new self($data['value']); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/RedisCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/RedisCacheTest.php new file mode 100644 index 00000000..45bbc752 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/RedisCacheTest.php @@ -0,0 +1,30 @@ +_redis = new \Redis(); + $ok = @$this->_redis->connect('127.0.0.1'); + if (!$ok) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of redis'); + } + } else { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of redis'); + } + } + + protected function _getCacheDriver() + { + $driver = new RedisCache(); + $driver->setRedis($this->_redis); + return $driver; + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/WinCacheCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/WinCacheCacheTest.php new file mode 100644 index 00000000..cb363df9 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/WinCacheCacheTest.php @@ -0,0 +1,20 @@ +markTestSkipped('The ' . __CLASS__ .' requires the use of Wincache'); + } + } + + protected function _getCacheDriver() + { + return new WincacheCache(); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/XcacheCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/XcacheCacheTest.php new file mode 100644 index 00000000..62598487 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/XcacheCacheTest.php @@ -0,0 +1,20 @@ +markTestSkipped('The ' . __CLASS__ .' requires the use of xcache'); + } + } + + protected function _getCacheDriver() + { + return new XcacheCache(); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Cache/ZendDataCacheTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Cache/ZendDataCacheTest.php new file mode 100644 index 00000000..cd66e157 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Cache/ZendDataCacheTest.php @@ -0,0 +1,28 @@ +markTestSkipped('The ' . __CLASS__ .' requires the use of Zend Data Cache which only works in apache2handler SAPI'); + } + } + + public function testGetStats() + { + $cache = $this->_getCacheDriver(); + $stats = $cache->getStats(); + + $this->assertNull($stats); + } + + protected function _getCacheDriver() + { + return new ZendDataCache(); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest.php b/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest.php new file mode 100644 index 00000000..567cf918 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest.php @@ -0,0 +1,45 @@ +setIncludePath(__DIR__); + $classLoader->setFileExtension('.class.php'); + $classLoader->setNamespaceSeparator('_'); + + $this->assertTrue($classLoader->canLoadClass('ClassLoaderTest_ClassA')); + $this->assertTrue($classLoader->canLoadClass('ClassLoaderTest_ClassB')); + $this->assertTrue($classLoader->canLoadClass('ClassLoaderTest_ClassC')); + $this->assertFalse($classLoader->canLoadClass('OtherClass')); + $this->assertEquals($classLoader->loadClass('ClassLoaderTest_ClassA'), true); + $this->assertEquals($classLoader->loadClass('ClassLoaderTest_ClassB'), true); + $this->assertEquals($classLoader->loadClass('ClassLoaderTest_ClassC'), true); + } + + public function testClassExists() + { + $this->assertFalse(ClassLoader::classExists('ClassLoaderTest\ClassD')); + $badLoader = function($className) { + require __DIR__ . '/ClassLoaderTest/ClassD.php'; + return true; + }; + spl_autoload_register($badLoader); + $this->assertTrue(ClassLoader::classExists('ClassLoaderTest\ClassD')); + spl_autoload_unregister($badLoader); + } + + public function testGetClassLoader() + { + $cl = new ClassLoader('ClassLoaderTest', __DIR__); + $cl->register(); + $this->assertTrue(ClassLoader::getClassLoader('ClassLoaderTest\ClassD') instanceof \Doctrine\Common\ClassLoader); + $this->assertNull(ClassLoader::getClassLoader('This\Class\Does\Not\Exist')); + $cl->unregister(); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassA.class.php b/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassA.class.php new file mode 100644 index 00000000..85546547 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/ClassLoaderTest/ClassA.class.php @@ -0,0 +1,6 @@ +. + */ + +namespace Doctrine\Tests\Common\Collections; + +use Doctrine\Common\Collections\Expr\ClosureExpressionVisitor; +use Doctrine\Common\Collections\ExpressionBuilder; + +/** + * @group DDC-1637 + */ +class ClosureExpressionVisitorTest extends \PHPUnit_Framework_TestCase +{ + private $visitor; + private $builder; + + public function setUp() + { + $this->visitor = new ClosureExpressionVisitor(); + $this->builder = new ExpressionBuilder(); + } + + public function testWalkEqualsComparison() + { + $closure = $this->visitor->walkComparison($this->builder->eq("foo", 1)); + + $this->assertTrue($closure(new TestObject(1))); + $this->assertFalse($closure(new TestObject(2))); + } + + public function testWalkNotEqualsComparison() + { + $closure = $this->visitor->walkComparison($this->builder->neq("foo", 1)); + + $this->assertFalse($closure(new TestObject(1))); + $this->assertTrue($closure(new TestObject(2))); + } + + public function testWalkLessThanComparison() + { + $closure = $this->visitor->walkComparison($this->builder->lt("foo", 1)); + + $this->assertFalse($closure(new TestObject(1))); + $this->assertTrue($closure(new TestObject(0))); + } + + public function testWalkLessThanEqualsComparison() + { + $closure = $this->visitor->walkComparison($this->builder->lte("foo", 1)); + + $this->assertFalse($closure(new TestObject(2))); + $this->assertTrue($closure(new TestObject(1))); + $this->assertTrue($closure(new TestObject(0))); + } + + public function testWalkGreaterThanEqualsComparison() + { + $closure = $this->visitor->walkComparison($this->builder->gte("foo", 1)); + + $this->assertTrue($closure(new TestObject(2))); + $this->assertTrue($closure(new TestObject(1))); + $this->assertFalse($closure(new TestObject(0))); + } + + public function testWalkGreaterThanComparison() + { + $closure = $this->visitor->walkComparison($this->builder->gt("foo", 1)); + + $this->assertTrue($closure(new TestObject(2))); + $this->assertFalse($closure(new TestObject(1))); + $this->assertFalse($closure(new TestObject(0))); + } + + public function testWalkInComparison() + { + $closure = $this->visitor->walkComparison($this->builder->in("foo", array(1, 2, 3))); + + $this->assertTrue($closure(new TestObject(2))); + $this->assertTrue($closure(new TestObject(1))); + $this->assertFalse($closure(new TestObject(0))); + } + + public function testWalkNotInComparison() + { + $closure = $this->visitor->walkComparison($this->builder->notIn("foo", array(1, 2, 3))); + + $this->assertFalse($closure(new TestObject(1))); + $this->assertFalse($closure(new TestObject(2))); + $this->assertTrue($closure(new TestObject(0))); + $this->assertTrue($closure(new TestObject(4))); + } + + public function testWalkAndCompositeExpression() + { + $closure = $this->visitor->walkCompositeExpression( + $this->builder->andX( + $this->builder->eq("foo", 1), + $this->builder->eq("bar", 1) + ) + ); + + $this->assertTrue($closure(new TestObject(1, 1))); + $this->assertFalse($closure(new TestObject(1, 0))); + $this->assertFalse($closure(new TestObject(0, 1))); + $this->assertFalse($closure(new TestObject(0, 0))); + } + + public function testWalkOrCompositeExpression() + { + $closure = $this->visitor->walkCompositeExpression( + $this->builder->orX( + $this->builder->eq("foo", 1), + $this->builder->eq("bar", 1) + ) + ); + + $this->assertTrue($closure(new TestObject(1, 1))); + $this->assertTrue($closure(new TestObject(1, 0))); + $this->assertTrue($closure(new TestObject(0, 1))); + $this->assertFalse($closure(new TestObject(0, 0))); + } + + public function testSortByFieldAscending() + { + $objects = array(new TestObject("b"), new TestObject("a"), new TestObject("c")); + $sort = ClosureExpressionVisitor::sortByField("foo"); + + usort($objects, $sort); + + $this->assertEquals("a", $objects[0]->getFoo()); + $this->assertEquals("b", $objects[1]->getFoo()); + $this->assertEquals("c", $objects[2]->getFoo()); + } + + public function testSortByFieldDescending() + { + $objects = array(new TestObject("b"), new TestObject("a"), new TestObject("c")); + $sort = ClosureExpressionVisitor::sortByField("foo", -1); + + usort($objects, $sort); + + $this->assertEquals("c", $objects[0]->getFoo()); + $this->assertEquals("b", $objects[1]->getFoo()); + $this->assertEquals("a", $objects[2]->getFoo()); + } + + public function testSortDelegate() + { + $objects = array(new TestObject("a", "c"), new TestObject("a", "b"), new TestObject("a", "a")); + $sort = ClosureExpressionVisitor::sortByField("bar", 1); + $sort = ClosureExpressionVisitor::sortByField("foo", 1, $sort); + + usort($objects, $sort); + + $this->assertEquals("a", $objects[0]->getBar()); + $this->assertEquals("b", $objects[1]->getBar()); + $this->assertEquals("c", $objects[2]->getBar()); + } +} + +class TestObject +{ + private $foo; + private $bar; + + public function __construct($foo = null, $bar = null) + { + $this->foo = $foo; + $this->bar = $bar; + } + + public function getFoo() + { + return $this->foo; + } + + public function getBar() + { + return $this->bar; + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Collections/CollectionTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Collections/CollectionTest.php new file mode 100644 index 00000000..280efa38 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Collections/CollectionTest.php @@ -0,0 +1,251 @@ +_coll = new \Doctrine\Common\Collections\ArrayCollection; + } + + public function testIssetAndUnset() + { + $this->assertFalse(isset($this->_coll[0])); + $this->_coll->add('testing'); + $this->assertTrue(isset($this->_coll[0])); + unset($this->_coll[0]); + $this->assertFalse(isset($this->_coll[0])); + } + + public function testToString() + { + $this->_coll->add('testing'); + $this->assertTrue(is_string((string) $this->_coll)); + } + + public function testRemovingNonExistentEntryReturnsNull() + { + $this->assertEquals(null, $this->_coll->remove('testing_does_not_exist')); + } + + public function testExists() + { + $this->_coll->add("one"); + $this->_coll->add("two"); + $exists = $this->_coll->exists(function($k, $e) { return $e == "one"; }); + $this->assertTrue($exists); + $exists = $this->_coll->exists(function($k, $e) { return $e == "other"; }); + $this->assertFalse($exists); + } + + public function testMap() + { + $this->_coll->add(1); + $this->_coll->add(2); + $res = $this->_coll->map(function($e) { return $e * 2; }); + $this->assertEquals(array(2, 4), $res->toArray()); + } + + public function testFilter() + { + $this->_coll->add(1); + $this->_coll->add("foo"); + $this->_coll->add(3); + $res = $this->_coll->filter(function($e) { return is_numeric($e); }); + $this->assertEquals(array(0 => 1, 2 => 3), $res->toArray()); + } + + public function testFirstAndLast() + { + $this->_coll->add('one'); + $this->_coll->add('two'); + + $this->assertEquals($this->_coll->first(), 'one'); + $this->assertEquals($this->_coll->last(), 'two'); + } + + public function testArrayAccess() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + + $this->assertEquals($this->_coll[0], 'one'); + $this->assertEquals($this->_coll[1], 'two'); + + unset($this->_coll[0]); + $this->assertEquals($this->_coll->count(), 1); + } + + public function testContainsKey() + { + $this->_coll[5] = 'five'; + $this->assertTrue($this->_coll->containsKey(5)); + } + + public function testContains() + { + $this->_coll[0] = 'test'; + $this->assertTrue($this->_coll->contains('test')); + } + + public function testSearch() + { + $this->_coll[0] = 'test'; + $this->assertEquals(0, $this->_coll->indexOf('test')); + } + + public function testGet() + { + $this->_coll[0] = 'test'; + $this->assertEquals('test', $this->_coll->get(0)); + } + + public function testGetKeys() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->assertEquals(array(0, 1), $this->_coll->getKeys()); + } + + public function testGetValues() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->assertEquals(array('one', 'two'), $this->_coll->getValues()); + } + + public function testCount() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->assertEquals($this->_coll->count(), 2); + $this->assertEquals(count($this->_coll), 2); + } + + public function testForAll() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->assertEquals($this->_coll->forAll(function($k, $e) { return is_string($e); }), true); + $this->assertEquals($this->_coll->forAll(function($k, $e) { return is_array($e); }), false); + } + + public function testPartition() + { + $this->_coll[] = true; + $this->_coll[] = false; + $partition = $this->_coll->partition(function($k, $e) { return $e == true; }); + $this->assertEquals($partition[0][0], true); + $this->assertEquals($partition[1][0], false); + } + + public function testClear() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->_coll->clear(); + $this->assertEquals($this->_coll->isEmpty(), true); + } + + public function testRemove() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $el = $this->_coll->remove(0); + + $this->assertEquals('one', $el); + $this->assertEquals($this->_coll->contains('one'), false); + $this->assertNull($this->_coll->remove(0)); + } + + public function testRemoveElement() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + + $this->assertTrue($this->_coll->removeElement('two')); + $this->assertFalse($this->_coll->contains('two')); + $this->assertFalse($this->_coll->removeElement('two')); + } + + public function testSlice() + { + $this->_coll[] = 'one'; + $this->_coll[] = 'two'; + $this->_coll[] = 'three'; + + $slice = $this->_coll->slice(0, 1); + $this->assertInternalType('array', $slice); + $this->assertEquals(array('one'), $slice); + + $slice = $this->_coll->slice(1); + $this->assertEquals(array(1 => 'two', 2 => 'three'), $slice); + + $slice = $this->_coll->slice(1, 1); + $this->assertEquals(array(1 => 'two'), $slice); + } + + public function fillMatchingFixture() + { + $std1 = new \stdClass(); + $std1->foo = "bar"; + $this->_coll[] = $std1; + + $std2 = new \stdClass(); + $std2->foo = "baz"; + $this->_coll[] = $std2; + } + + /** + * @group DDC-1637 + */ + public function testMatching() + { + $this->fillMatchingFixture(); + + $col = $this->_coll->matching(new Criteria(Criteria::expr()->eq("foo", "bar"))); + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $col); + $this->assertNotSame($col, $this->_coll); + $this->assertEquals(1, count($col)); + } + + /** + * @group DDC-1637 + */ + public function testMatchingOrdering() + { + $this->fillMatchingFixture(); + + $col = $this->_coll->matching(new Criteria(null, array('foo' => 'DESC'))); + + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $col); + $this->assertNotSame($col, $this->_coll); + $this->assertEquals(2, count($col)); + $this->assertEquals('baz', $col[0]->foo); + $this->assertEquals('bar', $col[1]->foo); + } + + /** + * @group DDC-1637 + */ + public function testMatchingSlice() + { + $this->fillMatchingFixture(); + + $col = $this->_coll->matching(new Criteria(null, null, 1, 1)); + + $this->assertInstanceOf('Doctrine\Common\Collections\Collection', $col); + $this->assertNotSame($col, $this->_coll); + $this->assertEquals(1, count($col)); + $this->assertEquals('baz', $col[0]->foo); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Collections/CriteriaTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Collections/CriteriaTest.php new file mode 100644 index 00000000..03fb6cae --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Collections/CriteriaTest.php @@ -0,0 +1,82 @@ +assertInstanceOf("Doctrine\Common\Collections\Criteria", $criteria); + } + + public function testConstructor() + { + $expr = new Comparison("field", "=", "value"); + $criteria = new Criteria($expr, array("foo" => "ASC"), 10, 20); + + $this->assertSame($expr, $criteria->getWhereExpression()); + $this->assertEquals(array("foo" => "ASC"), $criteria->getOrderings()); + $this->assertEquals(10, $criteria->getFirstResult()); + $this->assertEquals(20, $criteria->getMaxResults()); + } + + public function testWhere() + { + $expr = new Comparison("field", "=", "value"); + $criteria = new Criteria(); + + $criteria->where($expr); + + $this->assertSame($expr, $criteria->getWhereExpression()); + } + + public function testAndWhere() + { + $expr = new Comparison("field", "=", "value"); + $criteria = new Criteria(); + + $criteria->where($expr); + $expr = $criteria->getWhereExpression(); + $criteria->andWhere($expr); + + $where = $criteria->getWhereExpression(); + $this->assertInstanceOf('Doctrine\Common\Collections\Expr\CompositeExpression', $where); + + $this->assertEquals(CompositeExpression::TYPE_AND, $where->getType()); + $this->assertSame(array($expr, $expr), $where->getExpressionList()); + } + + public function testOrWhere() + { + $expr = new Comparison("field", "=", "value"); + $criteria = new Criteria(); + + $criteria->where($expr); + $expr = $criteria->getWhereExpression(); + $criteria->orWhere($expr); + + $where = $criteria->getWhereExpression(); + $this->assertInstanceOf('Doctrine\Common\Collections\Expr\CompositeExpression', $where); + + $this->assertEquals(CompositeExpression::TYPE_OR, $where->getType()); + $this->assertSame(array($expr, $expr), $where->getExpressionList()); + } + + public function testOrderings() + { + $criteria = Criteria::create() + ->orderBy(array("foo" => "ASC")); + + $this->assertEquals(array("foo" => "ASC"), $criteria->getOrderings()); + } + + public function testExpr() + { + $this->assertInstanceOf('Doctrine\Common\Collections\ExpressionBuilder', Criteria::expr()); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Collections/ExpressionBuilderTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Collections/ExpressionBuilderTest.php new file mode 100644 index 00000000..11bb8a4b --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Collections/ExpressionBuilderTest.php @@ -0,0 +1,113 @@ +builder = new ExpressionBuilder(); + } + + public function testAndX() + { + $expr = $this->builder->andX($this->builder->eq("a", "b")); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\CompositeExpression", $expr); + $this->assertEquals(CompositeExpression::TYPE_AND, $expr->getType()); + } + + public function testOrX() + { + $expr = $this->builder->orX($this->builder->eq("a", "b")); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\CompositeExpression", $expr); + $this->assertEquals(CompositeExpression::TYPE_OR, $expr->getType()); + } + + public function testInvalidAndXArgument() + { + $this->setExpectedException("RuntimeException"); + $this->builder->andX("foo"); + } + + public function testEq() + { + $expr = $this->builder->eq("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::EQ, $expr->getOperator()); + } + + public function testNeq() + { + $expr = $this->builder->neq("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::NEQ, $expr->getOperator()); + } + + public function testLt() + { + $expr = $this->builder->lt("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::LT, $expr->getOperator()); + } + + public function testGt() + { + $expr = $this->builder->gt("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::GT, $expr->getOperator()); + } + + public function testGte() + { + $expr = $this->builder->gte("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::GTE, $expr->getOperator()); + } + + public function testLte() + { + $expr = $this->builder->lte("a", "b"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::LTE, $expr->getOperator()); + } + + public function testIn() + { + $expr = $this->builder->in("a", array("b")); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::IN, $expr->getOperator()); + } + + public function testNotIn() + { + $expr = $this->builder->notIn("a", array("b")); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::NIN, $expr->getOperator()); + } + + public function testIsNull() + { + $expr = $this->builder->isNull("a"); + + $this->assertInstanceOf("Doctrine\Common\Collections\Expr\Comparison", $expr); + $this->assertEquals(Comparison::IS, $expr->getOperator()); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/DoctrineExceptionTest.php b/doctrine/common/tests/Doctrine/Tests/Common/DoctrineExceptionTest.php new file mode 100644 index 00000000..e69de29b diff --git a/doctrine/common/tests/Doctrine/Tests/Common/EventManagerTest.php b/doctrine/common/tests/Doctrine/Tests/Common/EventManagerTest.php new file mode 100644 index 00000000..2b11b20e --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/EventManagerTest.php @@ -0,0 +1,88 @@ +_eventManager = new EventManager; + $this->_preFooInvoked = false; + $this->_postFooInvoked = false; + } + + public function testInitialState() + { + $this->assertEquals(array(), $this->_eventManager->getListeners()); + $this->assertFalse($this->_eventManager->hasListeners(self::preFoo)); + $this->assertFalse($this->_eventManager->hasListeners(self::postFoo)); + } + + public function testAddEventListener() + { + $this->_eventManager->addEventListener(array('preFoo', 'postFoo'), $this); + $this->assertTrue($this->_eventManager->hasListeners(self::preFoo)); + $this->assertTrue($this->_eventManager->hasListeners(self::postFoo)); + $this->assertEquals(1, count($this->_eventManager->getListeners(self::preFoo))); + $this->assertEquals(1, count($this->_eventManager->getListeners(self::postFoo))); + $this->assertEquals(2, count($this->_eventManager->getListeners())); + } + + public function testDispatchEvent() + { + $this->_eventManager->addEventListener(array('preFoo', 'postFoo'), $this); + $this->_eventManager->dispatchEvent(self::preFoo); + $this->assertTrue($this->_preFooInvoked); + $this->assertFalse($this->_postFooInvoked); + } + + public function testRemoveEventListener() + { + $this->_eventManager->addEventListener(array('preBar'), $this); + $this->assertTrue($this->_eventManager->hasListeners(self::preBar)); + $this->_eventManager->removeEventListener(array('preBar'), $this); + $this->assertFalse($this->_eventManager->hasListeners(self::preBar)); + } + + public function testAddEventSubscriber() + { + $eventSubscriber = new TestEventSubscriber(); + $this->_eventManager->addEventSubscriber($eventSubscriber); + $this->assertTrue($this->_eventManager->hasListeners(self::preFoo)); + $this->assertTrue($this->_eventManager->hasListeners(self::postFoo)); + } + + /* Listener methods */ + + public function preFoo(EventArgs $e) + { + $this->_preFooInvoked = true; + } + + public function postFoo(EventArgs $e) + { + $this->_postFooInvoked = true; + } +} + +class TestEventSubscriber implements \Doctrine\Common\EventSubscriber +{ + public function getSubscribedEvents() + { + return array('preFoo', 'postFoo'); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ChainDriverTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ChainDriverTest.php new file mode 100644 index 00000000..66ad7629 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ChainDriverTest.php @@ -0,0 +1,130 @@ +getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $chain = new MappingDriverChain(); + + $driver1 = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $driver1->expects($this->never()) + ->method('loadMetadataForClass'); + $driver1->expectS($this->never()) + ->method('isTransient'); + + $driver2 = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $driver2->expects($this->at(0)) + ->method('loadMetadataForClass') + ->with($this->equalTo($className), $this->equalTo($classMetadata)); + $driver2->expects($this->at(1)) + ->method('isTransient') + ->with($this->equalTo($className)) + ->will($this->returnValue( true )); + + $chain->addDriver($driver1, 'Doctrine\Tests\Models\Company'); + $chain->addDriver($driver2, 'Doctrine\Tests\Common\Persistence\Mapping'); + + $chain->loadMetadataForClass($className, $classMetadata); + + $this->assertTrue( $chain->isTransient($className) ); + } + + public function testLoadMetadata_NoDelegatorFound_ThrowsMappingException() + { + $className = 'Doctrine\Tests\Common\Persistence\Mapping\DriverChainEntity'; + $classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $chain = new MappingDriverChain(); + + $this->setExpectedException('Doctrine\Common\Persistence\Mapping\MappingException'); + $chain->loadMetadataForClass($className, $classMetadata); + } + + public function testGatherAllClassNames() + { + $className = 'Doctrine\Tests\Common\Persistence\Mapping\DriverChainEntity'; + $classMetadata = $this->getMock('Doctrine\Common\Peristence\ClassMetadata'); + + $chain = new MappingDriverChain(); + + $driver1 = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $driver1->expects($this->once()) + ->method('getAllClassNames') + ->will($this->returnValue(array('Doctrine\Tests\Models\Company\Foo'))); + + $driver2 = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $driver2->expects($this->once()) + ->method('getAllClassNames') + ->will($this->returnValue(array('Doctrine\Tests\ORM\Mapping\Bar', 'Doctrine\Tests\ORM\Mapping\Baz', 'FooBarBaz'))); + + $chain->addDriver($driver1, 'Doctrine\Tests\Models\Company'); + $chain->addDriver($driver2, 'Doctrine\Tests\ORM\Mapping'); + + $this->assertEquals(array( + 'Doctrine\Tests\Models\Company\Foo', + 'Doctrine\Tests\ORM\Mapping\Bar', + 'Doctrine\Tests\ORM\Mapping\Baz' + ), $chain->getAllClassNames()); + } + + /** + * @group DDC-706 + */ + public function testIsTransient() + { + $driver1 = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $chain = new MappingDriverChain(); + $chain->addDriver($driver1, 'Doctrine\Tests\Models\CMS'); + + $this->assertTrue($chain->isTransient('stdClass'), "stdClass isTransient"); + } + + /** + * @group DDC-1412 + */ + public function testDefaultDriver() + { + $companyDriver = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $dafaultDriver = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $entityClassName = 'Doctrine\Tests\ORM\Mapping\DriverChainEntity'; + $managerClassName = 'Doctrine\Tests\Models\Company\CompanyManager'; + $chain = new MappingDriverChain(); + + $companyDriver->expects($this->never()) + ->method('loadMetadataForClass'); + $companyDriver->expects($this->once()) + ->method('isTransient') + ->with($this->equalTo($managerClassName)) + ->will($this->returnValue(false)); + + $dafaultDriver->expects($this->never()) + ->method('loadMetadataForClass'); + $dafaultDriver->expects($this->once()) + ->method('isTransient') + ->with($this->equalTo($entityClassName)) + ->will($this->returnValue(true)); + + $this->assertNull($chain->getDefaultDriver()); + + $chain->setDefaultDriver($dafaultDriver); + $chain->addDriver($companyDriver, 'Doctrine\Tests\Models\Company'); + + $this->assertSame($dafaultDriver, $chain->getDefaultDriver()); + + $this->assertTrue($chain->isTransient($entityClassName)); + $this->assertFalse($chain->isTransient($managerClassName)); + } +} + +class DriverChainEntity +{ + +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ClassMetadataFactoryTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ClassMetadataFactoryTest.php new file mode 100644 index 00000000..bc1559af --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/ClassMetadataFactoryTest.php @@ -0,0 +1,139 @@ +getMock('Doctrine\Common\Persistence\Mapping\Driver\MappingDriver'); + $metadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $this->cmf = new TestClassMetadataFactory($driver, $metadata); + } + + public function testGetCacheDriver() + { + $this->assertNull($this->cmf->getCacheDriver()); + $cache = new ArrayCache(); + $this->cmf->setCacheDriver($cache); + $this->assertSame($cache, $this->cmf->getCacheDriver()); + } + + public function testGetMetadataFor() + { + $metadata = $this->cmf->getMetadataFor('stdClass'); + + $this->assertInstanceOf('Doctrine\Common\Persistence\Mapping\ClassMetadata', $metadata); + $this->assertTrue($this->cmf->hasMetadataFor('stdClass')); + } + + public function testGetParentMetadata() + { + $metadata = $this->cmf->getMetadataFor(__NAMESPACE__ . '\ChildEntity'); + + $this->assertInstanceOf('Doctrine\Common\Persistence\Mapping\ClassMetadata', $metadata); + $this->assertTrue($this->cmf->hasMetadataFor(__NAMESPACE__ . '\ChildEntity')); + $this->assertTrue($this->cmf->hasMetadataFor(__NAMESPACE__ . '\RootEntity')); + } + + public function testGetCachedMetadata() + { + $metadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $cache = new ArrayCache(); + $cache->save(__NAMESPACE__. '\ChildEntity$CLASSMETADATA', $metadata); + + $this->cmf->setCacheDriver($cache); + + $loadedMetadata = $this->cmf->getMetadataFor(__NAMESPACE__ . '\ChildEntity'); + $this->assertSame($loadedMetadata, $metadata); + } + + public function testCacheGetMetadataFor() + { + $cache = new ArrayCache(); + $this->cmf->setCacheDriver($cache); + + $loadedMetadata = $this->cmf->getMetadataFor(__NAMESPACE__ . '\ChildEntity'); + + $this->assertSame($loadedMetadata, $cache->fetch(__NAMESPACE__. '\ChildEntity$CLASSMETADATA')); + } + + public function testGetAliasedMetadata() + { + $loadedMetadata = $this->cmf->getMetadataFor('prefix:ChildEntity'); + + $this->assertTrue($this->cmf->hasMetadataFor(__NAMESPACE__ . '\ChildEntity')); + $this->assertTrue($this->cmf->hasMetadataFor('prefix:ChildEntity')); + } +} + +class TestClassMetadataFactory extends AbstractClassMetadataFactory +{ + public $driver; + public $metadata; + + public function __construct($driver, $metadata) + { + $this->driver = $driver; + $this->metadata = $metadata; + } + + protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonSuperclassParents) + { + + } + + protected function getFqcnFromAlias($namespaceAlias, $simpleClassName) + { + return __NAMESPACE__ . '\\' . $simpleClassName; + } + + protected function initialize() + { + + } + + protected function newClassMetadataInstance($className) + { + return $this->metadata; + } + + protected function getDriver() + { + return $this->driver; + } + protected function wakeupReflection(ClassMetadata $class, ReflectionService $reflService) + { + } + + protected function initializeReflection(ClassMetadata $class, ReflectionService $reflService) + { + } + + protected function isEntity(ClassMetadata $class) + { + return true; + } +} + +class RootEntity +{ + +} + +class ChildEntity extends RootEntity +{ + +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/DefaultFileLocatorTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/DefaultFileLocatorTest.php new file mode 100644 index 00000000..37072de6 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/DefaultFileLocatorTest.php @@ -0,0 +1,90 @@ +assertEquals(array($path), $locator->getPaths()); + + $locator = new DefaultFileLocator($path); + $this->assertEquals(array($path), $locator->getPaths()); + } + + public function testGetFileExtension() + { + $locator = new DefaultFileLocator(array(), ".yml"); + $this->assertEquals(".yml", $locator->getFileExtension()); + $locator->setFileExtension(".xml"); + $this->assertEquals(".xml", $locator->getFileExtension()); + } + + public function testUniquePaths() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path, $path)); + $this->assertEquals(array($path), $locator->getPaths()); + } + + public function testFindMappingFile() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path), ".yml"); + + $this->assertEquals(__DIR__ . '/_files' . DIRECTORY_SEPARATOR . 'stdClass.yml', $locator->findMappingFile('stdClass')); + } + + public function testFindMappingFileNotFound() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path), ".yml"); + + $this->setExpectedException( + 'Doctrine\Common\Persistence\Mapping\MappingException', + "No mapping file found named 'stdClass2.yml' for class 'stdClass2'" + ); + $locator->findMappingFile('stdClass2'); + } + + public function testGetAllClassNames() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path), ".yml"); + $classes = $locator->getAllClassNames(null); + sort($classes); + + $this->assertEquals(array('global', 'stdClass'), $classes); + $this->assertEquals(array('stdClass'), $locator->getAllClassNames("global")); + } + + public function testGetAllClassNamesNonMatchingFileExtension() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path), ".xml"); + $this->assertEquals(array(), $locator->getAllClassNames("global")); + } + + public function testFileExists() + { + $path = __DIR__ . "/_files"; + + $locator = new DefaultFileLocator(array($path), ".yml"); + + $this->assertTrue($locator->fileExists("stdClass")); + $this->assertFalse($locator->fileExists("stdClass2")); + $this->assertTrue($locator->fileExists("global")); + $this->assertFalse($locator->fileExists("global2")); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/FileDriverTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/FileDriverTest.php new file mode 100644 index 00000000..020c2424 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/FileDriverTest.php @@ -0,0 +1,142 @@ +assertNull($driver->getGlobalBasename()); + + $driver->setGlobalBasename("global"); + $this->assertEquals("global", $driver->getGlobalBasename()); + } + + public function testGetElementFromGlobalFile() + { + $driver = new TestFileDriver($this->newLocator()); + $driver->setGlobalBasename("global"); + + $element = $driver->getElement('stdGlobal'); + + $this->assertEquals('stdGlobal', $element); + } + + public function testGetElementFromFile() + { + $locator = $this->newLocator(); + $locator->expects($this->once()) + ->method('findMappingFile') + ->with($this->equalTo('stdClass')) + ->will($this->returnValue(__DIR__ . '/_files/stdClass.yml')); + + $driver = new TestFileDriver($locator); + + $this->assertEquals('stdClass', $driver->getElement('stdClass')); + } + + public function testGetAllClassNamesGlobalBasename() + { + $driver = new TestFileDriver($this->newLocator()); + $driver->setGlobalBasename("global"); + + $classNames = $driver->getAllClassNames(); + + $this->assertEquals(array('stdGlobal', 'stdGlobal2'), $classNames); + } + + public function testGetAllClassNamesFromMappingFile() + { + $locator = $this->newLocator(); + $locator->expects($this->any()) + ->method('getAllClassNames') + ->with($this->equalTo(null)) + ->will($this->returnValue(array('stdClass'))); + $driver = new TestFileDriver($locator); + + $classNames = $driver->getAllClassNames(); + + $this->assertEquals(array('stdClass'), $classNames); + } + + public function testGetAllClassNamesBothSources() + { + $locator = $this->newLocator(); + $locator->expects($this->any()) + ->method('getAllClassNames') + ->with($this->equalTo('global')) + ->will($this->returnValue(array('stdClass'))); + $driver = new TestFileDriver($locator); + $driver->setGlobalBasename("global"); + + $classNames = $driver->getAllClassNames(); + + $this->assertEquals(array('stdGlobal', 'stdGlobal2', 'stdClass'), $classNames); + } + + public function testIsNotTransient() + { + $locator = $this->newLocator(); + $locator->expects($this->once()) + ->method('fileExists') + ->with($this->equalTo('stdClass')) + ->will($this->returnValue( true )); + + $driver = new TestFileDriver($locator); + $driver->setGlobalBasename("global"); + + $this->assertFalse($driver->isTransient('stdClass')); + $this->assertFalse($driver->isTransient('stdGlobal')); + $this->assertFalse($driver->isTransient('stdGlobal2')); + } + + public function testIsTransient() + { + $locator = $this->newLocator(); + $locator->expects($this->once()) + ->method('fileExists') + ->with($this->equalTo('stdClass2')) + ->will($this->returnValue( false )); + + $driver = new TestFileDriver($locator); + + $this->assertTrue($driver->isTransient('stdClass2')); + } + + public function testNonLocatorFallback() + { + $driver = new TestFileDriver(__DIR__ . '/_files', '.yml'); + $this->assertTrue($driver->isTransient('stdClass2')); + $this->assertFalse($driver->isTransient('stdClass')); + } + + private function newLocator() + { + $locator = $this->getMock('Doctrine\Common\Persistence\Mapping\Driver\FileLocator'); + $locator->expects($this->any())->method('getFileExtension')->will($this->returnValue('.yml')); + $locator->expects($this->any())->method('getPaths')->will($this->returnValue(array(__DIR__ . "/_files"))); + return $locator; + } +} + +class TestFileDriver extends FileDriver +{ + protected function loadMappingFile($file) + { + if (strpos($file, "global.yml") !== false) { + return array('stdGlobal' => 'stdGlobal', 'stdGlobal2' => 'stdGlobal2'); + } + return array('stdClass' => 'stdClass'); + } + + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/PHPDriverTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/PHPDriverTest.php new file mode 100644 index 00000000..8fc4d807 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/PHPDriverTest.php @@ -0,0 +1,18 @@ +getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $metadata->expects($this->once())->method('getFieldNames'); + + $driver = new PHPDriver(array(__DIR__ . "/_files")); + $driver->loadMetadataForClass('TestEntity', $metadata); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/RuntimeReflectionServiceTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/RuntimeReflectionServiceTest.php new file mode 100644 index 00000000..91af21a6 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/RuntimeReflectionServiceTest.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\Tests\Common\Persistence\Mapping; + +use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService; + +/** + * @group DCOM-93 + */ +class RuntimeReflectionServiceTest extends \PHPUnit_Framework_TestCase +{ + private $reflectionService; + + public function setUp() + { + $this->reflectionService = new RuntimeReflectionService(); + } + + public function testShortname() + { + $this->assertEquals("RuntimeReflectionServiceTest", $this->reflectionService->getClassShortName(__CLASS__)); + } + + public function testClassNamespaceName() + { + $this->assertEquals("Doctrine\Tests\Common\Persistence\Mapping", $this->reflectionService->getClassNamespace(__CLASS__)); + } + + public function testGetParentClasses() + { + $classes = $this->reflectionService->getParentClasses(__CLASS__); + $this->assertTrue(count($classes) >= 1, "The test class ".__CLASS__." should have at least one parent."); + } + + public function testGetReflectionClass() + { + $class = $this->reflectionService->getClass(__CLASS__); + $this->assertInstanceOf("ReflectionClass", $class); + } + + public function testGetMethods() + { + $this->assertTrue($this->reflectionService->hasPublicMethod(__CLASS__, "testGetMethods")); + $this->assertFalse($this->reflectionService->hasPublicMethod(__CLASS__, "testGetMethods2")); + } + + public function testGetAccessibleProperty() + { + $reflProp = $this->reflectionService->getAccessibleProperty(__CLASS__, "reflectionService"); + $this->assertInstanceOf("ReflectionProperty", $reflProp); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticPHPDriverTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticPHPDriverTest.php new file mode 100644 index 00000000..9f1c5684 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticPHPDriverTest.php @@ -0,0 +1,35 @@ +getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $metadata->expects($this->once())->method('getFieldNames'); + + $driver = new StaticPHPDriver(array(__DIR__)); + $driver->loadMetadataForClass(__NAMESPACE__ . '\\TestEntity', $metadata); + } + + public function testGetAllClassNames() + { + $driver = new StaticPHPDriver(array(__DIR__)); + $classNames = $driver->getAllClassNames(); + + $this->assertContains( + 'Doctrine\Tests\Common\Persistence\Mapping\TestEntity', $classNames); + } +} + +class TestEntity +{ + static public function loadMetadata($metadata) + { + $metadata->getFieldNames(); + } +} \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticReflectionServiceTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticReflectionServiceTest.php new file mode 100644 index 00000000..f8324fa7 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/StaticReflectionServiceTest.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\Tests\Common\Persistence\Mapping; + +use Doctrine\Common\Persistence\Mapping\StaticReflectionService; + +/** + * @group DCOM-93 + */ +class StaticReflectionServiceTest extends \PHPUnit_Framework_TestCase +{ + private $reflectionService; + + public function setUp() + { + $this->reflectionService = new StaticReflectionService(); + } + + public function testShortname() + { + $this->assertEquals("StaticReflectionServiceTest", $this->reflectionService->getClassShortName(__CLASS__)); + } + + public function testClassNamespaceName() + { + $this->assertEquals("Doctrine\Tests\Common\Persistence\Mapping", $this->reflectionService->getClassNamespace(__CLASS__)); + } + + public function testGetParentClasses() + { + $classes = $this->reflectionService->getParentClasses(__CLASS__); + $this->assertTrue(count($classes) == 0, "The test class ".__CLASS__." should have no parents according to static reflection."); + } + + public function testGetReflectionClass() + { + $class = $this->reflectionService->getClass(__CLASS__); + $this->assertNull($class); + } + + public function testGetMethods() + { + $this->assertTrue($this->reflectionService->hasPublicMethod(__CLASS__, "testGetMethods")); + $this->assertFalse($this->reflectionService->hasPublicMethod(__CLASS__, "testGetMethods2")); + } + + public function testGetAccessibleProperty() + { + $reflProp = $this->reflectionService->getAccessibleProperty(__CLASS__, "reflectionService"); + $this->assertNull($reflProp); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/SymfonyFileLocatorTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/SymfonyFileLocatorTest.php new file mode 100644 index 00000000..b51162e6 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/SymfonyFileLocatorTest.php @@ -0,0 +1,88 @@ + $prefix)); + $this->assertEquals(array($path), $locator->getPaths()); + + $locator = new SymfonyFileLocator(array($path => $prefix)); + $this->assertEquals(array($path), $locator->getPaths()); + } + + public function testGetPrefixes() + { + $path = __DIR__ . "/_files"; + $prefix = "Foo"; + + $locator = new SymfonyFileLocator(array($path => $prefix)); + $this->assertEquals(array($path => $prefix), $locator->getNamespacePrefixes()); + } + + public function testGetFileExtension() + { + $locator = new SymfonyFileLocator(array(), ".yml"); + $this->assertEquals(".yml", $locator->getFileExtension()); + $locator->setFileExtension(".xml"); + $this->assertEquals(".xml", $locator->getFileExtension()); + } + + public function testFileExists() + { + $path = __DIR__ . "/_files"; + $prefix = "Foo"; + + $locator = new SymfonyFileLocator(array($path => $prefix), ".yml"); + + $this->assertTrue($locator->fileExists("Foo\stdClass")); + $this->assertTrue($locator->fileExists("Foo\global")); + $this->assertFalse($locator->fileExists("Foo\stdClass2")); + $this->assertFalse($locator->fileExists("Foo\global2")); + } + + public function testGetAllClassNames() + { + $path = __DIR__ . "/_files"; + $prefix = "Foo"; + + $locator = new SymfonyFileLocator(array($path => $prefix), ".yml"); + $classes = $locator->getAllClassNames(null); + sort($classes); + + $this->assertEquals(array("Foo\\global", "Foo\\stdClass"), $classes); + $this->assertEquals(array("Foo\\stdClass"), $locator->getAllClassNames("global")); + } + + public function testFindMappingFile() + { + $path = __DIR__ . "/_files"; + $prefix = "Foo"; + + $locator = new SymfonyFileLocator(array($path => $prefix), ".yml"); + + $this->assertEquals(__DIR__ . "/_files/stdClass.yml", $locator->findMappingFile("Foo\\stdClass")); + } + + public function testFindMappingFileNotFound() + { + $path = __DIR__ . "/_files"; + $prefix = "Foo"; + + $locator = new SymfonyFileLocator(array($path => $prefix), ".yml"); + + $this->setExpectedException( + "Doctrine\Common\Persistence\Mapping\MappingException", + "No mapping file found named '".__DIR__."/_files/stdClass2.yml' for class 'Foo\stdClass2'." + ); + $locator->findMappingFile("Foo\\stdClass2"); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/TestEntity.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/TestEntity.php new file mode 100644 index 00000000..d0e9976a --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/TestEntity.php @@ -0,0 +1,3 @@ +getFieldNames(); \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/global.yml b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/global.yml new file mode 100644 index 00000000..30d74d25 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/global.yml @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/stdClass.yml b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/stdClass.yml new file mode 100644 index 00000000..30d74d25 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/Mapping/_files/stdClass.yml @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Persistence/PersistentObjectTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/PersistentObjectTest.php new file mode 100644 index 00000000..a0f77b50 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Persistence/PersistentObjectTest.php @@ -0,0 +1,247 @@ +cm = new TestObjectMetadata; + $this->om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); + $this->om->expects($this->any())->method('getClassMetadata') + ->will($this->returnValue($this->cm)); + $this->object = new TestObject; + PersistentObject::setObjectManager($this->om); + $this->object->injectObjectManager($this->om, $this->cm); + } + + public function testGetObjectManager() + { + $this->assertSame($this->om, PersistentObject::getObjectManager()); + } + + public function testNonMatchingObjectManager() + { + $this->setExpectedException('RuntimeException'); + $om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); + $this->object->injectObjectManager($om, $this->cm); + } + + public function testGetField() + { + $this->assertEquals('beberlei', $this->object->getName()); + } + + public function testSetField() + { + $this->object->setName("test"); + $this->assertEquals("test", $this->object->getName()); + } + + public function testGetIdentifier() + { + $this->assertEquals(1, $this->object->getId()); + } + + public function testSetIdentifier() + { + $this->setExpectedException('BadMethodCallException'); + $this->object->setId(2); + } + + public function testSetUnknownField() + { + $this->setExpectedException('BadMethodCallException'); + $this->object->setUnknown("test"); + } + + public function testGetUnknownField() + { + $this->setExpectedException('BadMethodCallException'); + $this->object->getUnknown(); + } + + public function testGetToOneAssociation() + { + $this->assertNull($this->object->getParent()); + } + + public function testSetToOneAssociation() + { + $parent = new TestObject(); + $this->object->setParent($parent); + $this->assertSame($parent, $this->object->getParent($parent)); + } + + public function testSetInvalidToOneAssocation() + { + $parent = new \stdClass(); + + $this->setExpectedException('InvalidArgumentException'); + $this->object->setParent($parent); + } + + public function testSetToOneAssociationNull() + { + $parent = new TestObject(); + $this->object->setParent($parent); + $this->object->setParent(null); + $this->assertNull($this->object->getParent()); + } + + public function testAddToManyAssocation() + { + $child = new TestObject(); + $this->object->addChildren($child); + + $this->assertSame($this->object, $child->getParent()); + $this->assertEquals(1, count($this->object->getChildren())); + + $child = new TestObject(); + $this->object->addChildren($child); + + $this->assertEquals(2, count($this->object->getChildren())); + } + + public function testAddInvalidToManyAssocation() + { + $this->setExpectedException('InvalidArgumentException'); + $this->object->addChildren(new \stdClass()); + } + + public function testNoObjectManagerSet() + { + PersistentObject::setObjectManager(null); + $child = new TestObject(); + + $this->setExpectedException('RuntimeException'); + $child->setName("test"); + } + + public function testInvalidMethod() + { + $this->setExpectedException('BadMethodCallException'); + $this->object->asdf(); + } + + public function testAddInvalidCollection() + { + $this->setExpectedException('BadMethodCallException'); + $this->object->addAsdf(new \stdClass()); + } +} + +class TestObject extends PersistentObject +{ + protected $id = 1; + protected $name = 'beberlei'; + protected $parent; + protected $children; +} + +class TestObjectMetadata implements ClassMetadata +{ + + public function getAssociationMappedByTargetField($assocName) + { + $assoc = array('children' => 'parent'); + return $assoc[$assocName]; + } + + public function getAssociationNames() + { + return array('parent', 'children'); + } + + public function getAssociationTargetClass($assocName) + { + return __NAMESPACE__ . '\TestObject'; + } + + public function getFieldNames() + { + return array('id', 'name'); + } + + public function getIdentifier() + { + return array('id'); + } + + public function getName() + { + return __NAMESPACE__ . '\TestObject'; + } + + public function getReflectionClass() + { + return new \ReflectionClass($this->getName()); + } + + public function getTypeOfField($fieldName) + { + $types = array('id' => 'integer', 'name' => 'string'); + return $types[$fieldName]; + } + + public function hasAssociation($fieldName) + { + return in_array($fieldName, array('parent', 'children')); + } + + public function hasField($fieldName) + { + return in_array($fieldName, array('id', 'name')); + } + + public function isAssociationInverseSide($assocName) + { + return ($assocName === 'children'); + } + + public function isCollectionValuedAssociation($fieldName) + { + return ($fieldName === 'children'); + } + + public function isIdentifier($fieldName) + { + return $fieldName === 'id'; + } + + public function isSingleValuedAssociation($fieldName) + { + return $fieldName === 'parent'; + } + + public function getIdentifierValues($entity) + { + + } + + public function getIdentifierFieldNames() + { + + } + + public function initializeReflection(ReflectionService $reflService) + { + + } + + public function wakeupReflection(ReflectionService $reflService) + { + + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Reflection/DeeperNamespaceParent.php b/doctrine/common/tests/Doctrine/Tests/Common/Reflection/DeeperNamespaceParent.php new file mode 100644 index 00000000..72faa778 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Reflection/DeeperNamespaceParent.php @@ -0,0 +1,7 @@ + array($testsRoot), + ); + $noParentClassName = 'Doctrine\\Tests\\Common\\Reflection\\NoParent'; + $staticReflectionParser = new StaticReflectionParser($noParentClassName, new Psr0FindFile($paths)); + $declaringClassName = $staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', 'test')->getClassName(); + $this->assertEquals($noParentClassName, $declaringClassName); + + $className = 'Doctrine\\Tests\\Common\\Reflection\\FullyClassifiedParent'; + $staticReflectionParser = new StaticReflectionParser($className, new Psr0FindFile($paths)); + $declaringClassName = $staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', 'test')->getClassName(); + $this->assertEquals($noParentClassName, $declaringClassName); + + $className = 'Doctrine\\Tests\\Common\\Reflection\\SameNamespaceParent'; + $staticReflectionParser = new StaticReflectionParser($className, new Psr0FindFile($paths)); + $declaringClassName = $staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', 'test')->getClassName(); + $this->assertEquals($noParentClassName, $declaringClassName); + + $dummyParentClassName = 'Doctrine\\Tests\\Common\\Reflection\\Dummies\\NoParent'; + + $className = 'Doctrine\\Tests\\Common\\Reflection\\DeeperNamespaceParent'; + $staticReflectionParser = new StaticReflectionParser($className, new Psr0FindFile($paths)); + $declaringClassName = $staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', 'test')->getClassName(); + $this->assertEquals($dummyParentClassName, $declaringClassName); + + $className = 'Doctrine\\Tests\\Common\\Reflection\\UseParent'; + $staticReflectionParser = new StaticReflectionParser($className, new Psr0FindFile($paths)); + $declaringClassName = $staticReflectionParser->getStaticReflectionParserForDeclaringClass('property', 'test')->getClassName(); + $this->assertEquals($dummyParentClassName, $declaringClassName); + + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Reflection/UseParent.php b/doctrine/common/tests/Doctrine/Tests/Common/Reflection/UseParent.php new file mode 100644 index 00000000..dd512d4d --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Reflection/UseParent.php @@ -0,0 +1,9 @@ +assertEquals($expectedClassName, ClassUtils::getRealClass($className)); + } + + /** + * @dataProvider dataGetClass + */ + public function testGetClass( $className, $expectedClassName ) + { + $object = new $className(); + $this->assertEquals($expectedClassName, ClassUtils::getClass($object)); + } + + public function testGetParentClass() + { + $parentClass = ClassUtils::getParentClass( 'MyProject\Proxies\__CG__\OtherProject\Proxies\__CG__\Doctrine\Tests\Common\Util\ChildObject' ); + $this->assertEquals('stdClass', $parentClass); + } + + public function testGenerateProxyClassName() + { + $this->assertEquals( 'Proxies\__CG__\stdClass', ClassUtils::generateProxyClassName( 'stdClass', 'Proxies' ) ); + } + + /** + * @dataProvider dataGetClass + */ + public function testNewReflectionClass( $className, $expectedClassName ) + { + $reflClass = ClassUtils::newReflectionClass( $className ); + $this->assertEquals( $expectedClassName, $reflClass->getName() ); + } + + /** + * @dataProvider dataGetClass + */ + public function testNewReflectionObject( $className, $expectedClassName ) + { + $object = new $className; + $reflClass = ClassUtils::newReflectionObject( $object ); + $this->assertEquals( $expectedClassName, $reflClass->getName() ); + } + } + + class ChildObject extends \stdClass + { + } +} + +namespace MyProject\Proxies\__CG__ +{ + class stdClass extends \stdClass + { + } +} + +namespace MyProject\Proxies\__CG__\Doctrine\Tests\Common\Util +{ + class ChildObject extends \Doctrine\Tests\Common\Util\ChildObject + { + } +} + +namespace MyProject\Proxies\__CG__\OtherProject\Proxies\__CG__ +{ + class stdClass extends \MyProject\Proxies\__CG__\stdClass + { + } +} + +namespace MyProject\Proxies\__CG__\OtherProject\Proxies\__CG__\Doctrine\Tests\Common\Util +{ + class ChildObject extends \MyProject\Proxies\__CG__\Doctrine\Tests\Common\Util\ChildObject + { + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/Common/Util/DebugTest.php b/doctrine/common/tests/Doctrine/Tests/Common/Util/DebugTest.php new file mode 100644 index 00000000..b4e9eed2 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/Common/Util/DebugTest.php @@ -0,0 +1,27 @@ +foo = "bar"; + $obj->bar = 1234; + + $var = Debug::export($obj, 2); + $this->assertEquals( "stdClass", $var->__CLASS__ ); + } + + public function testExportDateTime() + { + $obj = new \DateTime( "2010-10-10 10:10:10" ); + + $var = Debug::export( $obj, 2 ); + $this->assertEquals( "DateTime", $var->__CLASS__ ); + } +} diff --git a/doctrine/common/tests/Doctrine/Tests/DoctrineTestCase.php b/doctrine/common/tests/Doctrine/Tests/DoctrineTestCase.php new file mode 100644 index 00000000..e8323d29 --- /dev/null +++ b/doctrine/common/tests/Doctrine/Tests/DoctrineTestCase.php @@ -0,0 +1,10 @@ + + */ +class NativePhpunitTask extends Task +{ + private $test; + private $testfile; + private $testdirectory; + private $configuration = null; + private $coverageClover = null; + private $junitlogfile = null; + private $haltonfailure = true; + private $haltonerror = true; + + public function setTestdirectory($directory) { + $this->testdirectory = $directory; + } + + public function setTest($test) { + $this->test = $test; + } + + public function setTestfile($testfile) { + $this->testfile = $testfile; + } + + public function setJunitlogfile($junitlogfile) { + if (strlen($junitlogfile) == 0) { + $junitlogfile = NULL; + } + + $this->junitlogfile = $junitlogfile; + } + + public function setConfiguration($configuration) { + if (strlen($configuration) == 0) { + $configuration = NULL; + } + + $this->configuration = $configuration; + } + + public function setCoverageClover($coverageClover) { + if (strlen($coverageClover) == 0) { + $coverageClover = NULL; + } + + $this->coverageClover = $coverageClover; + } + + public function setHaltonfailure($haltonfailures) { + $this->haltonfailure = $haltonfailures; + } + + public function setHaltonerror($haltonerrors) { + $this->haltonerror = $haltonerrors; + } + + public function init() + { + require_once "PHPUnit/Runner/Version.php"; + $version = PHPUnit_Runner_Version::id(); + + if (version_compare($version, '3.4.0') < 0) { + throw new BuildException("NativePHPUnitTask requires PHPUnit version >= 3.2.0", $this->getLocation()); + } + + require_once 'PHPUnit/Util/Filter.php'; + + // point PHPUnit_MAIN_METHOD define to non-existing method + if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'PHPUnitTask::undefined'); + } + } + + public function main() + { + if (!is_dir(realpath($this->testdirectory))) { + throw new BuildException("NativePHPUnitTask requires a Test Directory path given, '".$this->testdirectory."' given."); + } + set_include_path(realpath($this->testdirectory) . PATH_SEPARATOR . get_include_path()); + + $printer = new NativePhpunitPrinter(); + + $arguments = array( + 'configuration' => $this->configuration, + 'coverageClover' => $this->coverageClover, + 'junitLogfile' => $this->junitlogfile, + 'printer' => $printer, + ); + + $runner = new PHPUnit_TextUI_TestRunner(); + $suite = $runner->getTest($this->test, $this->testfile, true); + + try { + $result = $runner->doRun($suite, $arguments); + /* @var $result PHPUnit_Framework_TestResult */ + + if ( ($this->haltonfailure && $result->failureCount() > 0) || ($this->haltonerror && $result->errorCount() > 0) ) { + throw new BuildException("PHPUnit: ".$result->failureCount()." Failures and ".$result->errorCount()." Errors, ". + "last failure message: ".$printer->getMessages()); + } + + $this->log("PHPUnit Success: ".count($result->passed())." tests passed, no ". + "failures (".$result->skippedCount()." skipped, ".$result->notImplementedCount()." not implemented)"); + + // Hudson for example doesn't like the backslash in class names + if (file_exists($this->coverageClover)) { + $this->log("Generated Clover Coverage XML to: ".$this->coverageClover); + $content = file_get_contents($this->coverageClover); + $content = str_replace("\\", ".", $content); + file_put_contents($this->coverageClover, $content); + unset($content); + } + + } catch(\Exception $e) { + throw new BuildException("NativePhpunitTask failed: ".$e->getMessage()); + } + } +} + +class NativePhpunitPrinter extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener +{ + private $_messages = array(); + + public function write($buffer) + { + // do nothing + } + + public function getMessages() + { + return $this->_messages; + } + + /** + * An error occurred. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + */ + public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->_messages[] = "Test ERROR: ".$test->getName().": ".$e->getMessage(); + } + + /** + * A failure occurred. + * + * @param PHPUnit_Framework_Test $test + * @param PHPUnit_Framework_AssertionFailedError $e + * @param float $time + */ + public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) + { + $this->_messages[] = "Test FAILED: ".$test->getName().": ".$e->getMessage(); + } + + /** + * Incomplete test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + */ + public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) + { + + } + + /** + * Skipped test. + * + * @param PHPUnit_Framework_Test $test + * @param Exception $e + * @param float $time + * @since Method available since Release 3.0.0 + */ + public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) + { + + } + + /** + * A test suite started. + * + * @param PHPUnit_Framework_TestSuite $suite + * @since Method available since Release 2.2.0 + */ + public function startTestSuite(PHPUnit_Framework_TestSuite $suite) + { + + } + + /** + * A test suite ended. + * + * @param PHPUnit_Framework_TestSuite $suite + * @since Method available since Release 2.2.0 + */ + public function endTestSuite(PHPUnit_Framework_TestSuite $suite) + { + + } + + /** + * A test started. + * + * @param PHPUnit_Framework_Test $test + */ + public function startTest(PHPUnit_Framework_Test $test) + { + + } + + /** + * A test ended. + * + * @param PHPUnit_Framework_Test $test + * @param float $time + */ + public function endTest(PHPUnit_Framework_Test $test, $time) + { + + } +} diff --git a/doctrine/common/tests/README.markdown b/doctrine/common/tests/README.markdown new file mode 100644 index 00000000..e6f1703d --- /dev/null +++ b/doctrine/common/tests/README.markdown @@ -0,0 +1,27 @@ +# Running the Doctrine 2 Testsuite + +## Running tests + +Execute PHPUnit in the root folder of your doctrine-common clone. + + phpunit + +## Testing Lock-Support + +The Lock support in Doctrine 2 is tested using Gearman, which allows to run concurrent tasks in parallel. +Install Gearman with PHP as follows: + +1. Go to http://www.gearman.org and download the latest Gearman Server +2. Compile it and then call ldconfig +3. Start it up "gearmand -vvvv" +4. Install pecl/gearman by calling "gearman-beta" + +You can then go into tests/ and start up two workers: + + php Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php + +Then run the locking test-suite: + + phpunit --configuration Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php + +This can run considerable time, because it is using sleep() to test for the timing ranges of locks. \ No newline at end of file diff --git a/doctrine/dbal/.gitignore b/doctrine/dbal/.gitignore new file mode 100644 index 00000000..a9b57aaa --- /dev/null +++ b/doctrine/dbal/.gitignore @@ -0,0 +1,6 @@ +build/ +logs/ +reports/ +dist/ +download/ +lib/Doctrine/Common/ diff --git a/doctrine/dbal/.gitmodules b/doctrine/dbal/.gitmodules new file mode 100644 index 00000000..00dc3fa1 --- /dev/null +++ b/doctrine/dbal/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/vendor/doctrine-common"] + path = lib/vendor/doctrine-common + url = git://github.com/doctrine/common.git +[submodule "lib/vendor/Symfony/Component/Console"] + path = lib/vendor/Symfony/Component/Console + url = git://github.com/symfony/Console.git +[submodule "lib/vendor/doctrine-build-common"] + path = lib/vendor/doctrine-build-common + url = git://github.com/doctrine/doctrine-build-common.git diff --git a/doctrine/dbal/.travis.yml b/doctrine/dbal/.travis.yml new file mode 100644 index 00000000..6cdd9ff6 --- /dev/null +++ b/doctrine/dbal/.travis.yml @@ -0,0 +1,21 @@ +language: php + +php: + - 5.3 + - 5.4 +env: + - DB=mysql + - DB=pgsql + - DB=sqlite + - DB=mysqli + +before_script: + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests_tmp;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests;' -U postgres; fi" + - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests_tmp;' -U postgres; fi" + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" + - sh -c "if [ '$DB' = 'mysqli' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" + - git submodule update --init + +script: phpunit --configuration tests/travis/$DB.travis.xml diff --git a/doctrine/dbal/LICENSE b/doctrine/dbal/LICENSE new file mode 100644 index 00000000..4a91f0bf --- /dev/null +++ b/doctrine/dbal/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006-2012 Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doctrine/dbal/README.md b/doctrine/dbal/README.md new file mode 100644 index 00000000..c88f9e67 --- /dev/null +++ b/doctrine/dbal/README.md @@ -0,0 +1,14 @@ +# Doctrine DBAL + +Powerful database abstraction layer with many features for database schema introspection, schema management and PDO abstraction. + +* Master: [![Build Status](https://secure.travis-ci.org/doctrine/dbal.png?branch=master)](http://travis-ci.org/doctrine/dbal) +* 2.2: [![Build Status](https://secure.travis-ci.org/doctrine/dbal.png?branch=2.2)](http://travis-ci.org/doctrine/dbal) +* 2.1.x: [![Build Status](https://secure.travis-ci.org/doctrine/dbal.png?branch=2.1.x)](http://travis-ci.org/doctrine/dbal) + +## More resources: + +* [Website](http://www.doctrine-project.org) +* [Documentation](http://www.doctrine-project.org/projects/dbal/current/docs/en) +* [Issue Tracker](http://www.doctrine-project.org/jira/browse/DBAL) +* [Downloads](http://github.com/doctrine/dbal/downloads) diff --git a/doctrine/dbal/UPGRADE b/doctrine/dbal/UPGRADE new file mode 100644 index 00000000..36ca80ee --- /dev/null +++ b/doctrine/dbal/UPGRADE @@ -0,0 +1,148 @@ +# Upgrade to 2.3 + +## Oracle Session Init now sets Numeric Character + +Before 2.3 the Oracle Session Init did not care about the numeric character of the Session. +This could lead to problems on non english locale systems that required a comma as a floating +point seperator in Oracle. Since 2.3, using the Oracle Session Init on connection start the +client session will be altered to set the numeric character to ".,": + + ALTER SESSION SET NLS_NUMERIC_CHARACTERS = '.,' + +See [DBAL-345](http://www.doctrine-project.org/jira/browse/DBAL-345) for more details. + +## Doctrine\DBAL\Connection and Doctrine\DBAL\Statement + +The query related methods including but not limited to executeQuery, exec, query, and executeUpdate +now wrap the driver exceptions such as PDOException with DBALException to add more debugging +information such as the executed SQL statement, and any bound parameters. + +If you want to retrieve the driver specific exception, you can retrieve it by calling the +``getPrevious()`` method on DBALException. + +Before: + + catch(\PDOException $ex) { + // ... + } + +After: + + catch(\Doctrine\DBAL\DBALException $ex) { + $pdoException = $ex->getPrevious(); + // ... + } + +## Doctrine\DBAL\Connection#setCharsetSQL() removed + +This method only worked on MySQL and it is considered unsafe on MySQL to use SET NAMES UTF-8 instead +of setting the charset directly on connection already. Replace this behavior with the +connection charset option: + +Before: + + $conn = DriverManager::getConnection(array(..)); + $conn->setCharset('UTF8'); + +After: + + $conn = DriverManager::getConnection(array('charset' => 'UTF8', ..)); + +## Doctrine\DBAL\Schema\Table#renameColumn() removed + +Doctrine\DBAL\Schema\Table#renameColumn() was removed, because it drops and recreates +the column instead. There is no fix available, because a schema diff +cannot reliably detect if a column was renamed or one column was created +and another one dropped. + +You should use explicit SQL ALTER TABLE statements to change columns names. + +## Schema Filter paths + +The Filter Schema assets expression is not wrapped in () anymore for the regexp automatically. + +Before: + + $config->setFilterSchemaAssetsExpression('foo'); + +After: + + $config->setFilterSchemaAssetsExpression('(foo)'); + +## Creating MySQL Tables now defaults to UTF-8 + +If you are creating a new MySQL Table through the Doctrine API, charset/collate are +now set to 'utf8'/'utf8_unicode_ci' by default. Previously the MySQL server defaults were used. + +# Upgrade to 2.2 + +## Doctrine\DBAL\Connection#insert and Doctrine\DBAL\Connnection#update + +Both methods now accept an optional last parameter $types with binding types of the values passed. +This can potentially break child classes that have overwritten one of these methods. + +## Doctrine\DBAL\Connection#executeQuery + +Doctrine\DBAL\Connection#executeQuery() got a new last parameter "QueryCacheProfile $qcp" + +## Doctrine\DBAL\Driver\Statement split + +The Driver statement was split into a ResultStatement and the normal statement extending from it. +This seperates the configuration and the retrieval API from a statement. + +## MsSql Platform/SchemaManager renamed + +The MsSqlPlatform was renamed to SQLServerPlatform, the MsSqlSchemaManager was renamed +to SQLServerSchemaManager. + +## Cleanup SQLServer Platform version mess + +DBAL 2.1 and before were actually only compatible to SQL Server 2008, not earlier versions. +Still other parts of the platform did use old features instead of newly introduced datatypes +in SQL Server 2005. Starting with DBAL 2.2 you can pick the Doctrine abstraction exactly +matching your SQL Server version. + +The PDO SqlSrv driver now uses the new `SQLServer2008Platform` as default platform. +This platform uses new features of SQL Server as of version 2008. This also includes a switch +in the used fields for "text" and "blob" field types to: + + "text" => "VARCHAR(MAX)" + "blob" => "VARBINARY(MAX)" + +Additionally `SQLServerPlatform` in DBAL 2.1 and before used "DATE", "TIME" and "DATETIME2" for dates. +This types are only available since version 2008 and the introduction of an explicit +SQLServer 2008 platform makes this dependency explicit. + +An `SQLServer2005Platform` was also introduced to differentiate the features between +versions 2003, earlier and 2005. + +With this change the `SQLServerPlatform` now throws an exception for using limit queries +with an offset, since SQLServer 2003 and lower do not support this feature. + +To use the old SQL Server Platform, because you are using SQL Server 2003 and below use +the following configuration code: + + use Doctrine\DBAL\DriverManager; + use Doctrine\DBAL\Platforms\SQLServerPlatform; + use Doctrine\DBAL\Platforms\SQLServer2005Platform; + + // You are using SQL Server 2003 or earlier + $conn = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlsrv', + 'platform' => new SQLServerPlatform() + // .. additional parameters + )); + + // You are using SQL Server 2005 + $conn = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlsrv', + 'platform' => new SQLServer2005Platform() + // .. additional parameters + )); + + // You are using SQL Server 2008 + $conn = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlsrv', + // 2008 is default platform + // .. additional parameters + )); diff --git a/doctrine/dbal/bin/doctrine-dbal b/doctrine/dbal/bin/doctrine-dbal new file mode 100644 index 00000000..7ca0142c --- /dev/null +++ b/doctrine/dbal/bin/doctrine-dbal @@ -0,0 +1,4 @@ +#!/usr/bin/env php +register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Symfony'); +$classLoader->register(); + +$configFile = getcwd() . DIRECTORY_SEPARATOR . 'cli-config.php'; + +$helperSet = null; +if (file_exists($configFile)) { + if ( ! is_readable($configFile)) { + trigger_error( + 'Configuration file [' . $configFile . '] does not have read permission.', E_ERROR + ); + } + + require $configFile; + + foreach ($GLOBALS as $helperSetCandidate) { + if ($helperSetCandidate instanceof \Symfony\Component\Console\Helper\HelperSet) { + $helperSet = $helperSetCandidate; + break; + } + } +} + +$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet(); + +$cli = new \Symfony\Component\Console\Application('Doctrine Command Line Interface', Doctrine\DBAL\Version::VERSION); +$cli->setCatchExceptions(true); +$cli->setHelperSet($helperSet); +$cli->addCommands(array( + // DBAL Commands + new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), + new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), + new \Doctrine\DBAL\Tools\Console\Command\ReservedWordsCommand(), + +)); +$cli->run(); diff --git a/doctrine/dbal/bin/doctrine.php b/doctrine/dbal/bin/doctrine.php new file mode 100644 index 00000000..b017703c --- /dev/null +++ b/doctrine/dbal/bin/doctrine.php @@ -0,0 +1,42 @@ +register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Symfony'); +$classLoader->register(); + +$configFile = getcwd() . DIRECTORY_SEPARATOR . 'cli-config.php'; + +$helperSet = null; +if (file_exists($configFile)) { + if ( ! is_readable($configFile)) { + trigger_error( + 'Configuration file [' . $configFile . '] does not have read permission.', E_ERROR + ); + } + + require $configFile; + + foreach ($GLOBALS as $helperSetCandidate) { + if ($helperSetCandidate instanceof \Symfony\Component\Console\Helper\HelperSet) { + $helperSet = $helperSetCandidate; + break; + } + } +} + +$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet(); + +$cli = new \Symfony\Component\Console\Application('Doctrine Command Line Interface', Doctrine\DBAL\Version::VERSION); +$cli->setCatchExceptions(true); +$cli->setHelperSet($helperSet); +$cli->addCommands(array( + // DBAL Commands + new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), + new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), + +)); +$cli->run(); diff --git a/doctrine/dbal/build.properties b/doctrine/dbal/build.properties new file mode 100644 index 00000000..12b5e8fc --- /dev/null +++ b/doctrine/dbal/build.properties @@ -0,0 +1,10 @@ +# Project Name +project.name=DoctrineDBAL + +# Dependency minimum versions +dependencies.common=2.0.1 +dependencies.sfconsole=2.0.0 + +# Version class and file +project.version_class = Doctrine\DBAL\Version +project.version_file = lib/Doctrine/DBAL/Version.php diff --git a/doctrine/dbal/build.xml b/doctrine/dbal/build.xml new file mode 100644 index 00000000..0fb05ff5 --- /dev/null +++ b/doctrine/dbal/build.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${project.name} + Doctrine Database Abstraction Layer + pear.doctrine-project.org + The Doctrine DBAL package is the database abstraction layer used to power the ORM package. + + + + + LGPL + + + - + + + + + + + script + Doctrine/Common/ + Doctrine/Symfony/ + + + + + + + diff --git a/doctrine/dbal/composer.json b/doctrine/dbal/composer.json new file mode 100644 index 00000000..ae2409b1 --- /dev/null +++ b/doctrine/dbal/composer.json @@ -0,0 +1,26 @@ +{ + "name": "doctrine/dbal", + "type": "library","version":"2.3.1", + "description": "Database Abstraction Layer", + "keywords": ["dbal", "database", "persistence", "queryobject"], + "homepage": "http://www.doctrine-project.org", + "license": "MIT", + "authors": [ + {"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"}, + {"name": "Roman Borschel", "email": "roman@code-factory.org"}, + {"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"}, + {"name": "Jonathan Wage", "email": "jonwage@gmail.com"} + ], + "require": { + "php": ">=5.3.2", + "doctrine/common": "2.3.*" + }, + "autoload": { + "psr-0": { "Doctrine\\DBAL": "lib/" } + }, + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + } +} diff --git a/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md b/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md new file mode 100644 index 00000000..16e65d90 --- /dev/null +++ b/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md @@ -0,0 +1,94 @@ +# Azure Federations + +Implementing Federations inside a new Doctrine Sharding Extension. Some extensions to the DBAL and ORM core have to be done to get this working. + +1. DBAL (Database Abstraction Layer) + +* Add support for Database Schema Operations + * CREATE FEDERATION + * CREATE TABLE ... FEDERATED ON + * Add support to create a multi-tenent schema from any given schema +* Add API to pick a shard based on distribution key and atomic value +* Add API to ask about federations, federation members and so on. +* Add Sharding Abstraction + * If a shard is picked via distribution key and atomic value fire queries against this only + * Or query the global database. + +2. ORM (Object-Relational Mapper) + +* Federation Key has to be part of the clustered index of the table + * Test with a pure Multi-Tenent App with Filtering = ON (TaskList) + * Test with sharded app (Weather) + +## Implementation Details + +SQL Azure requires one and exactly one clustered index. It makes no difference if the primary key +or any other key is the clustered index. Sharding requires an external ID generation (no auto-increment) +such as GUIDs. GUIDs have negative properties with regard to clustered index performance, so that +typically you would add a "created" timestamp for example that holds the clustered index instead +of making the GUID a clustered index. + +## Example API: + + @@@ php + 'tcp:dbname.database.windows.net', + 'sharding' => array( + 'federationName' => 'Orders_Federation', + 'distributionKey' => 'CustID', + 'distributionType' => 'integer', + 'filteringEnabled' => false, + ), + // ... + ); + + $conn = DriverManager::getConnection($dbParams); + $shardManager = $conn->getShardManager(); + + // Example 1: query against root database + $sql = "SELECT * FROM Products"; + $rows = $conn->executeQuery($sql); + + // Example 2: query against the selected shard with CustomerId = 100 + $aCustomerID = 100; + $shardManager->selectShard($aCustomerID); // Using Default federationName and distributionKey + // Query: "USE FEDERATION Orders_Federation (CustID = $aCustomerID) WITH RESET, FILTERING OFF;" + + $sql = "SELECT * FROM Customers"; + $rows = $conn->executeQuery($sql); + + // Example 3: Reset API to root database again + $shardManager->selectGlobal(); + +## ID Generation + +With sharding all the ids have to be generated for global uniqueness. There are three strategies for this. + +1. Use GUIDs as described here http://blogs.msdn.com/b/cbiyikoglu/archive/2011/06/20/id-generation-in-federations-identity-sequences-and-guids-uniqueidentifier.aspx +2. Having a central table that is accessed with a second connection to generate sequential ids +3. Using natural keys from the domain. + +The second approach has the benefit of having numerical primary keys, however also a central failure location. The third strategy can seldom be used, because the domains dont allow this. Identity columns cannot be used at all. + + @@@ php + 'dbname.database.windows.net', + // ... + ); + $conn = DriverManager::getConnection($dbParams); + + $idGenerator = new TableHiLoIdGenerator($conn, 'id_table_name', $multiplicator = 1); + // only once, create this table + $idGenerator->createTable(); + + $nextId = $idGenerator->generateId('for_table_name'); + $nextOtherId = $idGenerator->generateId('for_other_table'); + +The connection for the table generator has to be a different one than the one used for the main app to avoid transaction clashes. diff --git a/doctrine/dbal/docs/design/SHARDING.md b/doctrine/dbal/docs/design/SHARDING.md new file mode 100644 index 00000000..24e6cef3 --- /dev/null +++ b/doctrine/dbal/docs/design/SHARDING.md @@ -0,0 +1,74 @@ +# Doctrine Shards + +Doctrine Extension to support horizontal sharding in the Doctrine ORM. + +## Idea + +Implement sharding inside Doctrine at a level that is as unobtrusive to the developer as possible. + +Problems to tackle: + +1. Where to send INSERT statements? +2. How to generate primary keys? +3. How to pick shards for update, delete statements? +4. How to pick shards for select operations? +5. How to merge select queries that span multiple shards? +6. How to handle/prevent multi-shard queries that cannot be merged (GROUP BY)? +7. How to handle non-sharded data? (static metadata tables for example) +8. How to handle multiple connections? +9. Implementation on the DBAL or ORM level? + +## Roadmap + +Version 1: DBAL 2.3 (Multi-Tenant Apps) + + 1. ID Generation support (in DBAL + ORM done) + 2. Multi-Tenant Support: Either pick a global metadata database or exactly one shard. + 3. Fan-out queries over all shards (or a subset) by result appending + +Version 2: ORM related (complex): + + 4. ID resolving (Pick shard for a new ID) + 5. Query resolving (Pick shards a query should send to) + 6. Shard resolving (Pick shards an ID could be on) + 7. Transactions + 8. Read Only objects + +## Technical Requirements for Database Schemas + +Sharded tables require the sharding-distribution key as one of their columns. This will affect your code compared to a normalized db-schema. If you have a Blog <-> BlogPost <-> PostComments entity setup sharded by `blog_id` then even the PostComment table needs this column, even if an "unsharded", normalized DB-Schema does not need this information. + +## Implementation Details + +Assumptions: + +* For querying you either want to query ALL or just exactly one shard. +* IDs for ALL sharded tables have to be unique across all shards. +* Non-shareded data is replicated between all shards. They redundantly keep the information available. This is necessary so join queries on shards to reference data work. +* If you retrieve an object A from a shard, then all references and collections of this object reside on the same shard. +* The database schema on all shards is the same (or compatible) + +### SQL Azure Federations + +SQL Azure is a special case, points 1, 2, 3, 4, 7 and 8 are partly handled on the database level. This makes it a perfect test-implementation for just the subset of features in points 5-6. However there need to be a way to configure SchemaTool to generate the correct Schema on SQL Azure. + +* SELECT Operations: The most simple assumption is to always query all shards unless the user specifies otherwise explicitly. +* Queries can be merged in PHP code, this obviously does not work for DISTINCT, GROUP BY and ORDER BY queries. + +### Generic Sharding + +More features are necessary to implement sharding on the PHP level, independent from database support: + +1. Configuration of multiple connections, one connection = one shard. +2. Primary Key Generation mechanisms (UUID, central table, sequence emulation) + +## Primary Use-Cases + +1. Multi-Tenant Applications + +These are easier to support as you have some value to determine the shard id for the whole request very early on. +Here also queries can always be limited to a single shard. + +2. Scale-Out by some attribute (Round-Robin?) + +This strategy requires access to multiple shards in a single request based on the data accessed. diff --git a/doctrine/dbal/docs/examples/sharding/README.md b/doctrine/dbal/docs/examples/sharding/README.md new file mode 100644 index 00000000..73ba195b --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/README.md @@ -0,0 +1,25 @@ +# Sharding with SQLAzure Example + +This example demonstrates Sharding with SQL Azure Federations. + +## Requirements + +1. Windows Azure Account +2. SQL Azure Database +3. Composer for dependencies + +## Install + + composer install + +Change "examples/sharding/bootstrap.php" to contain Database connection. + +## Order to execute Scripts + +1. create_schema.php +2. view_federation_members.php +3. insert_data.php +4. split_federation.php +5. insert_data_after_split.php +6. query_filtering_off.php +7. query_filtering_on.php diff --git a/doctrine/dbal/docs/examples/sharding/bootstrap.php b/doctrine/dbal/docs/examples/sharding/bootstrap.php new file mode 100644 index 00000000..ad62aa79 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/bootstrap.php @@ -0,0 +1,25 @@ + 'SalesDB', + 'host' => 'tcp:dbname.windows.net', + 'user' => 'user@dbname', + 'password' => 'XXX', + 'sharding' => array( + 'federationName' => 'Orders_Federation', + 'distributionKey' => 'CustId', + 'distributionType' => 'integer', + ) +); + +if ($config['host'] == "tcp:dbname.windows.net") { + die("You have to change the configuration to your Azure account.\n"); +} + +$conn = DriverManager::getConnection($config); +$shardManager = new SQLAzureShardManager($conn); diff --git a/doctrine/dbal/docs/examples/sharding/composer.json b/doctrine/dbal/docs/examples/sharding/composer.json new file mode 100644 index 00000000..214f9221 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "doctrine/dbal": "*", + "doctrine/shards": "0.3" + } +} diff --git a/doctrine/dbal/docs/examples/sharding/create_schema.php b/doctrine/dbal/docs/examples/sharding/create_schema.php new file mode 100644 index 00000000..cc4c8a7b --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/create_schema.php @@ -0,0 +1,50 @@ +createTable('Products'); +$products->addColumn('ProductID', 'integer'); +$products->addColumn('SupplierID', 'integer'); +$products->addColumn('ProductName', 'string'); +$products->addColumn('Price', 'decimal', array('scale' => 2, 'precision' => 12)); +$products->setPrimaryKey(array('ProductID')); +$products->addOption('azure.federated', true); + +$customers = $schema->createTable('Customers'); +$customers->addColumn('CustomerID', 'integer'); +$customers->addColumn('CompanyName', 'string'); +$customers->addColumn('FirstName', 'string'); +$customers->addColumn('LastName', 'string'); +$customers->setPrimaryKey(array('CustomerID')); +$customers->addOption('azure.federated', true); +$customers->addOption('azure.federatedOnColumnName', 'CustomerID'); + +$orders = $schema->createTable('Orders'); +$orders->addColumn('CustomerID', 'integer'); +$orders->addColumn('OrderID', 'integer'); +$orders->addColumn('OrderDate', 'datetime'); +$orders->setPrimaryKey(array('CustomerID', 'OrderID')); +$orders->addOption('azure.federated', true); +$orders->addOption('azure.federatedOnColumnName', 'CustomerID'); + +$orderItems = $schema->createTable('OrderItems'); +$orderItems->addColumn('CustomerID', 'integer'); +$orderItems->addColumn('OrderID', 'integer'); +$orderItems->addColumn('ProductID', 'integer'); +$orderItems->addColumn('Quantity', 'integer'); +$orderItems->setPrimaryKey(array('CustomerID', 'OrderID', 'ProductID')); +$orderItems->addOption('azure.federated', true); +$orderItems->addOption('azure.federatedOnColumnName', 'CustomerID'); + +// Create the Schema + Federation: +$synchronizer = new SQLAzureSchemaSynchronizer($conn, $shardManager); + +// Or jut look at the SQL: +echo implode("\n", $synchronizer->getCreateSchema($schema)); + +$synchronizer->createSchema($schema); diff --git a/doctrine/dbal/docs/examples/sharding/insert_data.php b/doctrine/dbal/docs/examples/sharding/insert_data.php new file mode 100644 index 00000000..57aeda6c --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/insert_data.php @@ -0,0 +1,132 @@ +selectShard(0); + +$conn->insert("Products", array( + "ProductID" => 386, + "SupplierID" => 1001, + "ProductName" => 'Titanium Extension Bracket Left Hand', + "Price" => 5.25, +)); +$conn->insert("Products", array( + "ProductID" => 387, + "SupplierID" => 1001, + "ProductName" => 'Titanium Extension Bracket Right Hand', + "Price" => 5.25, +)); +$conn->insert("Products", array( + "ProductID" => 388, + "SupplierID" => 1001, + "ProductName" => 'Fusion Generator Module 5 kV', + "Price" => 10.50, +)); +$conn->insert("Products", array( + "ProductID" => 389, + "SupplierID" => 1001, + "ProductName" => 'Bypass Filter 400 MHz Low Pass', + "Price" => 10.50, +)); + +$conn->insert("Customers", array( + 'CustomerID' => 10, + 'CompanyName' => 'Van Nuys', + 'FirstName' => 'Catherine', + 'LastName' => 'Abel', +)); +$conn->insert("Customers", array( + 'CustomerID' => 20, + 'CompanyName' => 'Abercrombie', + 'FirstName' => 'Kim', + 'LastName' => 'Branch', +)); +$conn->insert("Customers", array( + 'CustomerID' => 30, + 'CompanyName' => 'Contoso', + 'FirstName' => 'Frances', + 'LastName' => 'Adams', +)); +$conn->insert("Customers", array( + 'CustomerID' => 40, + 'CompanyName' => 'A. Datum Corporation', + 'FirstName' => 'Mark', + 'LastName' => 'Harrington', +)); +$conn->insert("Customers", array( + 'CustomerID' => 50, + 'CompanyName' => 'Adventure Works', + 'FirstName' => 'Keith', + 'LastName' => 'Harris', +)); +$conn->insert("Customers", array( + 'CustomerID' => 60, + 'CompanyName' => 'Alpine Ski House', + 'FirstName' => 'Wilson', + 'LastName' => 'Pais', +)); +$conn->insert("Customers", array( + 'CustomerID' => 70, + 'CompanyName' => 'Baldwin Museum of Science', + 'FirstName' => 'Roger', + 'LastName' => 'Harui', +)); +$conn->insert("Customers", array( + 'CustomerID' => 80, + 'CompanyName' => 'Blue Yonder Airlines', + 'FirstName' => 'Pilar', + 'LastName' => 'Pinilla', +)); +$conn->insert("Customers", array( + 'CustomerID' => 90, + 'CompanyName' => 'City Power & Light', + 'FirstName' => 'Kari', + 'LastName' => 'Hensien', +)); +$conn->insert("Customers", array( + 'CustomerID' => 100, + 'CompanyName' => 'Coho Winery', + 'FirstName' => 'Peter', + 'LastName' => 'Brehm', +)); + +$conn->executeUpdate(" + DECLARE @orderId INT + + DECLARE @customerId INT + + SET @orderId = 10 + SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Hensien' and FirstName = 'Kari' + + INSERT INTO Orders (CustomerId, OrderId, OrderDate) + VALUES (@customerId, @orderId, GetDate()) + + INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity) + VALUES (@customerId, @orderId, 388, 4) + + SET @orderId = 20 + SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Harui' and FirstName = 'Roger' + + INSERT INTO Orders (CustomerId, OrderId, OrderDate) + VALUES (@customerId, @orderId, GetDate()) + + INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity) + VALUES (@customerId, @orderId, 389, 2) + + SET @orderId = 30 + SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Brehm' and FirstName = 'Peter' + + INSERT INTO Orders (CustomerId, OrderId, OrderDate) + VALUES (@customerId, @orderId, GetDate()) + + INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity) + VALUES (@customerId, @orderId, 387, 3) + + SET @orderId = 40 + SELECT @customerId = CustomerId FROM Customers WHERE LastName = 'Pais' and FirstName = 'Wilson' + + INSERT INTO Orders (CustomerId, OrderId, OrderDate) + VALUES (@customerId, @orderId, GetDate()) + + INSERT INTO OrderItems (CustomerID, OrderID, ProductID, Quantity) + VALUES (@customerId, @orderId, 388, 1)"); diff --git a/doctrine/dbal/docs/examples/sharding/insert_data_aftersplit.php b/doctrine/dbal/docs/examples/sharding/insert_data_aftersplit.php new file mode 100644 index 00000000..312e90b2 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/insert_data_aftersplit.php @@ -0,0 +1,27 @@ +selectShard($newCustomerId); + +$conn->insert("Customers", array( + "CustomerID" => $newCustomerId, + "CompanyName" => "Microsoft", + "FirstName" => "Brian", + "LastName" => "Swan", +)); + +$conn->insert("Orders", array( + "CustomerID" => 55, + "OrderID" => 37, + "OrderDate" => date('Y-m-d H:i:s'), +)); + +$conn->insert("OrderItems", array( + "CustomerID" => 55, + "OrderID" => 37, + "ProductID" => 387, + "Quantity" => 1, +)); diff --git a/doctrine/dbal/docs/examples/sharding/query_filtering_off.php b/doctrine/dbal/docs/examples/sharding/query_filtering_off.php new file mode 100644 index 00000000..c0b24fa0 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/query_filtering_off.php @@ -0,0 +1,8 @@ +selectShard(0); + +$data = $conn->fetchAll('SELECT * FROM Customers'); +print_r($data); diff --git a/doctrine/dbal/docs/examples/sharding/query_filtering_on.php b/doctrine/dbal/docs/examples/sharding/query_filtering_on.php new file mode 100644 index 00000000..e7d9e145 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/query_filtering_on.php @@ -0,0 +1,9 @@ +setFilteringEnabled(true); +$shardManager->selectShard(55); + +$data = $conn->fetchAll('SELECT * FROM Customers'); +print_r($data); diff --git a/doctrine/dbal/docs/examples/sharding/split_federation.php b/doctrine/dbal/docs/examples/sharding/split_federation.php new file mode 100644 index 00000000..ff681edf --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/split_federation.php @@ -0,0 +1,5 @@ +splitFederation(60); diff --git a/doctrine/dbal/docs/examples/sharding/view_federation_members.php b/doctrine/dbal/docs/examples/sharding/view_federation_members.php new file mode 100644 index 00000000..497e4df6 --- /dev/null +++ b/doctrine/dbal/docs/examples/sharding/view_federation_members.php @@ -0,0 +1,8 @@ +getShards(); +foreach ($shards as $shard) { + print_r($shard); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Cache/ArrayStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Cache/ArrayStatement.php new file mode 100644 index 00000000..8ad167bc --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Cache/ArrayStatement.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +use Doctrine\DBAL\Driver\ResultStatement; +use PDO; + +class ArrayStatement implements \IteratorAggregate, ResultStatement +{ + private $data; + private $columnCount = 0; + private $num = 0; + private $defaultFetchMode = PDO::FETCH_BOTH; + + public function __construct(array $data) + { + $this->data = $data; + if (count($data)) { + $this->columnCount = count($data[0]); + } + } + + public function closeCursor() + { + unset ($this->data); + } + + public function columnCount() + { + return $this->columnCount; + } + + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + if ($arg2 !== null || $arg3 !== null) { + throw new \InvalidArgumentException("Caching layer does not support 2nd/3rd argument to setFetchMode()"); + } + + $this->defaultFetchMode = $fetchMode; + } + + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + public function fetch($fetchMode = null) + { + if (isset($this->data[$this->num])) { + $row = $this->data[$this->num++]; + $fetchMode = $fetchMode ?: $this->defaultFetchMode; + if ($fetchMode === PDO::FETCH_ASSOC) { + return $row; + } else if ($fetchMode === PDO::FETCH_NUM) { + return array_values($row); + } else if ($fetchMode === PDO::FETCH_BOTH) { + return array_merge($row, array_values($row)); + } else if ($fetchMode === PDO::FETCH_COLUMN) { + return reset($row); + } else { + throw new \InvalidArgumentException("Invalid fetch-style given for fetching result."); + } + } + return false; + } + + public function fetchAll($fetchMode = null) + { + $rows = array(); + while ($row = $this->fetch($fetchMode)) { + $rows[] = $row; + } + return $rows; + } + + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + if (!isset($row[$columnIndex])) { + // TODO: verify this is correct behavior + return false; + } + return $row[$columnIndex]; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Cache/CacheException.php b/doctrine/dbal/lib/Doctrine/DBAL/Cache/CacheException.php new file mode 100644 index 00000000..dd274779 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Cache/CacheException.php @@ -0,0 +1,37 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +/** + * @author Benjamin Eberlei + * @since 2.2 + */ +class CacheException extends \Doctrine\DBAL\DBALException +{ + static public function noCacheKey() + { + return new self("No cache key was set."); + } + + static public function noResultDriverConfigured() + { + return new self("Trying to cache a query but no result driver is configured."); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Cache/QueryCacheProfile.php b/doctrine/dbal/lib/Doctrine/DBAL/Cache/QueryCacheProfile.php new file mode 100644 index 00000000..54c34b9b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Cache/QueryCacheProfile.php @@ -0,0 +1,131 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +use Doctrine\Common\Cache\Cache; + +/** + * Query Cache Profile handles the data relevant for query caching. + * + * It is a value object, setter methods return NEW instances. + * + * @author Benjamin Eberlei + */ +class QueryCacheProfile +{ + /** + * @var Cache + */ + private $resultCacheDriver; + /** + * @var int + */ + private $lifetime = 0; + /** + * @var string + */ + private $cacheKey; + + /** + * @param int $lifetime + * @param string $cacheKey + * @param Cache $resultCache + */ + public function __construct($lifetime = 0, $cacheKey = null, Cache $resultCache = null) + { + $this->lifetime = $lifetime; + $this->cacheKey = $cacheKey; + $this->resultCacheDriver = $resultCache; + } + + /** + * @return Cache + */ + public function getResultCacheDriver() + { + return $this->resultCacheDriver; + } + + /** + * @return int + */ + public function getLifetime() + { + return $this->lifetime; + } + + /** + * @return string + */ + public function getCacheKey() + { + if ($this->cacheKey === null) { + throw CacheException::noCacheKey(); + } + return $this->cacheKey; + } + + /** + * Generate the real cache key from query, params and types. + * + * @param string $query + * @param array $params + * @param array $types + * @return array + */ + public function generateCacheKeys($query, $params, $types) + { + $realCacheKey = $query . "-" . serialize($params) . "-" . serialize($types); + // should the key be automatically generated using the inputs or is the cache key set? + if ($this->cacheKey === null) { + $cacheKey = sha1($realCacheKey); + } else { + $cacheKey = $this->cacheKey; + } + return array($cacheKey, $realCacheKey); + } + + /** + * @param Cache $cache + * @return QueryCacheProfile + */ + public function setResultCacheDriver(Cache $cache) + { + return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache); + } + + /** + * @param string|null $cacheKey + * @return QueryCacheProfile + */ + public function setCacheKey($cacheKey) + { + return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCacheDriver); + } + + /** + * @param int $lifetime + * @return QueryCacheProfile + */ + public function setLifetime($lifetime) + { + return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCacheDriver); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php new file mode 100644 index 00000000..f118e7c5 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Cache/ResultCacheStatement.php @@ -0,0 +1,239 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +use Doctrine\DBAL\Driver\Statement; +use Doctrine\DBAL\Driver\ResultStatement; +use Doctrine\DBAL\Connection; +use Doctrine\Common\Cache\Cache; +use PDO; + +/** + * Cache statement for SQL results. + * + * A result is saved in multiple cache keys, there is the originally specified + * cache key which is just pointing to result rows by key. The following things + * have to be ensured: + * + * 1. lifetime of the original key has to be longer than that of all the individual rows keys + * 2. if any one row key is missing the query has to be re-executed. + * + * Also you have to realize that the cache will load the whole result into memory at once to ensure 2. + * This means that the memory usage for cached results might increase by using this feature. + */ +class ResultCacheStatement implements \IteratorAggregate, ResultStatement +{ + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $resultCache; + + /** + * + * @var string + */ + private $cacheKey; + + /** + * @var string + */ + private $realKey; + + /** + * @var int + */ + private $lifetime; + + /** + * @var Doctrine\DBAL\Driver\Statement + */ + private $statement; + + /** + * Did we reach the end of the statement? + * + * @var bool + */ + private $emptied = false; + + /** + * @var array + */ + private $data; + + /** + * @var int + */ + private $defaultFetchMode = PDO::FETCH_BOTH; + + /** + * @param Statement $stmt + * @param Cache $resultCache + * @param string $cacheKey + * @param string $realKey + * @param int $lifetime + */ + public function __construct(Statement $stmt, Cache $resultCache, $cacheKey, $realKey, $lifetime) + { + $this->statement = $stmt; + $this->resultCache = $resultCache; + $this->cacheKey = $cacheKey; + $this->realKey = $realKey; + $this->lifetime = $lifetime; + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + public function closeCursor() + { + $this->statement->closeCursor(); + if ($this->emptied && $this->data !== null) { + $data = $this->resultCache->fetch($this->cacheKey); + if ( ! $data) { + $data = array(); + } + $data[$this->realKey] = $this->data; + + $this->resultCache->save($this->cacheKey, $data, $this->lifetime); + unset($this->data); + } + } + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + public function columnCount() + { + return $this->statement->columnCount(); + } + + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + $this->defaultFetchMode = $fetchMode; + } + + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchMode Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @return mixed + */ + public function fetch($fetchMode = null) + { + if ($this->data === null) { + $this->data = array(); + } + + $row = $this->statement->fetch(PDO::FETCH_ASSOC); + if ($row) { + $this->data[] = $row; + + $fetchMode = $fetchMode ?: $this->defaultFetchMode; + + if ($fetchMode == PDO::FETCH_ASSOC) { + return $row; + } else if ($fetchMode == PDO::FETCH_NUM) { + return array_values($row); + } else if ($fetchMode == PDO::FETCH_BOTH) { + return array_merge($row, array_values($row)); + } else if ($fetchMode == PDO::FETCH_COLUMN) { + return reset($row); + } else { + throw new \InvalidArgumentException("Invalid fetch-style given for caching result."); + } + } + $this->emptied = true; + return false; + } + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchMode Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @return array + */ + public function fetchAll($fetchMode = null) + { + $rows = array(); + while ($row = $this->fetch($fetchMode)) { + $rows[] = $row; + } + return $rows; + } + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + if (!isset($row[$columnIndex])) { + // TODO: verify this is correct behavior + return false; + } + return $row[$columnIndex]; + } + + /** + * rowCount + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * executed by the corresponding object. + * + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be + * relied on for portable applications. + * + * @return integer Returns the number of rows. + */ + public function rowCount() + { + return $this->statement->rowCount(); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Configuration.php b/doctrine/dbal/lib/Doctrine/DBAL/Configuration.php new file mode 100644 index 00000000..53f7b5e7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Configuration.php @@ -0,0 +1,113 @@ +. + */ + +namespace Doctrine\DBAL; + +use Doctrine\DBAL\Logging\SQLLogger; +use Doctrine\Common\Cache\Cache; + +/** + * Configuration container for the Doctrine DBAL. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @internal When adding a new configuration option just write a getter/setter + * pair and add the option to the _attributes array with a proper default value. + */ +class Configuration +{ + /** + * The attributes that are contained in the configuration. + * Values are default values. + * + * @var array + */ + protected $_attributes = array(); + + /** + * Sets the SQL logger to use. Defaults to NULL which means SQL logging is disabled. + * + * @param SQLLogger $logger + */ + public function setSQLLogger(SQLLogger $logger = null) + { + $this->_attributes['sqlLogger'] = $logger; + } + + /** + * Gets the SQL logger that is used. + * + * @return SQLLogger + */ + public function getSQLLogger() + { + return isset($this->_attributes['sqlLogger']) ? + $this->_attributes['sqlLogger'] : null; + } + + /** + * Gets the cache driver implementation that is used for query result caching. + * + * @return \Doctrine\Common\Cache\Cache + */ + public function getResultCacheImpl() + { + return isset($this->_attributes['resultCacheImpl']) ? + $this->_attributes['resultCacheImpl'] : null; + } + + /** + * Sets the cache driver implementation that is used for query result caching. + * + * @param \Doctrine\Common\Cache\Cache $cacheImpl + */ + public function setResultCacheImpl(Cache $cacheImpl) + { + $this->_attributes['resultCacheImpl'] = $cacheImpl; + } + + /** + * Filter schema assets expression. + * + * Only include tables/sequences matching the filter expression regexp in + * schema instances generated for the active connection when calling + * {AbstractSchemaManager#createSchema()}. + * + * @param string $filterExpression + */ + public function setFilterSchemaAssetsExpression($filterExpression) + { + $this->_attributes['filterSchemaAssetsExpression'] = $filterExpression; + } + + /** + * Return filter schema assets expression. + * + * @return string|null + */ + public function getFilterSchemaAssetsExpression() + { + if (isset($this->_attributes['filterSchemaAssetsExpression'])) { + return $this->_attributes['filterSchemaAssetsExpression']; + } + return null; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Connection.php new file mode 100644 index 00000000..4aeb1dc7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Connection.php @@ -0,0 +1,1308 @@ +. + */ + +namespace Doctrine\DBAL; + +use PDO, Closure, Exception, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Driver\Connection as DriverConnection, + Doctrine\Common\EventManager, + Doctrine\DBAL\DBALException, + Doctrine\DBAL\Cache\ResultCacheStatement, + Doctrine\DBAL\Cache\QueryCacheProfile, + Doctrine\DBAL\Cache\ArrayStatement, + Doctrine\DBAL\Cache\CacheException; + +/** + * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like + * events, transaction isolation levels, configuration, emulated transaction nesting, + * lazy connecting and more. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Konsta Vesterinen + * @author Lukas Smith (MDB2 library) + * @author Benjamin Eberlei + */ +class Connection implements DriverConnection +{ + /** + * Constant for transaction isolation level READ UNCOMMITTED. + */ + const TRANSACTION_READ_UNCOMMITTED = 1; + + /** + * Constant for transaction isolation level READ COMMITTED. + */ + const TRANSACTION_READ_COMMITTED = 2; + + /** + * Constant for transaction isolation level REPEATABLE READ. + */ + const TRANSACTION_REPEATABLE_READ = 3; + + /** + * Constant for transaction isolation level SERIALIZABLE. + */ + const TRANSACTION_SERIALIZABLE = 4; + + /** + * Represents an array of ints to be expanded by Doctrine SQL parsing. + * + * @var int + */ + const PARAM_INT_ARRAY = 101; + + /** + * Represents an array of strings to be expanded by Doctrine SQL parsing. + * + * @var int + */ + const PARAM_STR_ARRAY = 102; + + /** + * Offset by which PARAM_* constants are detected as arrays of the param type. + * + * @var int + */ + const ARRAY_PARAM_OFFSET = 100; + + /** + * The wrapped driver connection. + * + * @var \Doctrine\DBAL\Driver\Connection + */ + protected $_conn; + + /** + * @var \Doctrine\DBAL\Configuration + */ + protected $_config; + + /** + * @var \Doctrine\Common\EventManager + */ + protected $_eventManager; + + /** + * @var \Doctrine\DBAL\Query\Expression\ExpressionBuilder + */ + protected $_expr; + + /** + * Whether or not a connection has been established. + * + * @var boolean + */ + private $_isConnected = false; + + /** + * The transaction nesting level. + * + * @var integer + */ + private $_transactionNestingLevel = 0; + + /** + * The currently active transaction isolation level. + * + * @var integer + */ + private $_transactionIsolationLevel; + + /** + * If nested transations should use savepoints + * + * @var integer + */ + private $_nestTransactionsWithSavepoints; + + /** + * The parameters used during creation of the Connection instance. + * + * @var array + */ + private $_params = array(); + + /** + * The DatabasePlatform object that provides information about the + * database platform used by the connection. + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + /** + * The schema manager. + * + * @var \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + protected $_schemaManager; + + /** + * The used DBAL driver. + * + * @var \Doctrine\DBAL\Driver + */ + protected $_driver; + + /** + * Flag that indicates whether the current transaction is marked for rollback only. + * + * @var boolean + */ + private $_isRollbackOnly = false; + + private $_defaultFetchMode = PDO::FETCH_ASSOC; + + /** + * Initializes a new instance of the Connection class. + * + * @param array $params The connection parameters. + * @param Driver $driver + * @param Configuration $config + * @param EventManager $eventManager + */ + public function __construct(array $params, Driver $driver, Configuration $config = null, + EventManager $eventManager = null) + { + $this->_driver = $driver; + $this->_params = $params; + + if (isset($params['pdo'])) { + $this->_conn = $params['pdo']; + $this->_isConnected = true; + } + + // Create default config and event manager if none given + if ( ! $config) { + $config = new Configuration(); + } + + if ( ! $eventManager) { + $eventManager = new EventManager(); + } + + $this->_config = $config; + $this->_eventManager = $eventManager; + + $this->_expr = new Query\Expression\ExpressionBuilder($this); + + if ( ! isset($params['platform'])) { + $this->_platform = $driver->getDatabasePlatform(); + } else if ($params['platform'] instanceof Platforms\AbstractPlatform) { + $this->_platform = $params['platform']; + } else { + throw DBALException::invalidPlatformSpecified(); + } + + $this->_platform->setEventManager($eventManager); + + $this->_transactionIsolationLevel = $this->_platform->getDefaultTransactionIsolationLevel(); + } + + /** + * Gets the parameters used during instantiation. + * + * @return array $params + */ + public function getParams() + { + return $this->_params; + } + + /** + * Gets the name of the database this Connection is connected to. + * + * @return string $database + */ + public function getDatabase() + { + return $this->_driver->getDatabase($this); + } + + /** + * Gets the hostname of the currently connected database. + * + * @return string + */ + public function getHost() + { + return isset($this->_params['host']) ? $this->_params['host'] : null; + } + + /** + * Gets the port of the currently connected database. + * + * @return mixed + */ + public function getPort() + { + return isset($this->_params['port']) ? $this->_params['port'] : null; + } + + /** + * Gets the username used by this connection. + * + * @return string + */ + public function getUsername() + { + return isset($this->_params['user']) ? $this->_params['user'] : null; + } + + /** + * Gets the password used by this connection. + * + * @return string + */ + public function getPassword() + { + return isset($this->_params['password']) ? $this->_params['password'] : null; + } + + /** + * Gets the DBAL driver instance. + * + * @return \Doctrine\DBAL\Driver + */ + public function getDriver() + { + return $this->_driver; + } + + /** + * Gets the Configuration used by the Connection. + * + * @return \Doctrine\DBAL\Configuration + */ + public function getConfiguration() + { + return $this->_config; + } + + /** + * Gets the EventManager used by the Connection. + * + * @return \Doctrine\Common\EventManager + */ + public function getEventManager() + { + return $this->_eventManager; + } + + /** + * Gets the DatabasePlatform for the connection. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_platform; + } + + /** + * Gets the ExpressionBuilder for the connection. + * + * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder + */ + public function getExpressionBuilder() + { + return $this->_expr; + } + + /** + * Establishes the connection with the database. + * + * @return boolean TRUE if the connection was successfully established, FALSE if + * the connection is already open. + */ + public function connect() + { + if ($this->_isConnected) return false; + + $driverOptions = isset($this->_params['driverOptions']) ? + $this->_params['driverOptions'] : array(); + $user = isset($this->_params['user']) ? $this->_params['user'] : null; + $password = isset($this->_params['password']) ? + $this->_params['password'] : null; + + $this->_conn = $this->_driver->connect($this->_params, $user, $password, $driverOptions); + $this->_isConnected = true; + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + $eventArgs = new Event\ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * setFetchMode + * + * @param integer $fetchMode + */ + public function setFetchMode($fetchMode) + { + $this->_defaultFetchMode = $fetchMode; + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as an associative array. + * + * @param string $statement The SQL query. + * @param array $params The query parameters. + * @return array + */ + public function fetchAssoc($statement, array $params = array()) + { + return $this->executeQuery($statement, $params)->fetch(PDO::FETCH_ASSOC); + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as a numerically indexed array. + * + * @param string $statement sql query to be executed + * @param array $params prepared statement params + * @return array + */ + public function fetchArray($statement, array $params = array()) + { + return $this->executeQuery($statement, $params)->fetch(PDO::FETCH_NUM); + } + + /** + * Prepares and executes an SQL query and returns the value of a single column + * of the first row of the result. + * + * @param string $statement sql query to be executed + * @param array $params prepared statement params + * @param int $colnum 0-indexed column number to retrieve + * @return mixed + */ + public function fetchColumn($statement, array $params = array(), $colnum = 0) + { + return $this->executeQuery($statement, $params)->fetchColumn($colnum); + } + + /** + * Whether an actual connection to the database is established. + * + * @return boolean + */ + public function isConnected() + { + return $this->_isConnected; + } + + /** + * Checks whether a transaction is currently active. + * + * @return boolean TRUE if a transaction is currently active, FALSE otherwise. + */ + public function isTransactionActive() + { + return $this->_transactionNestingLevel > 0; + } + + /** + * Executes an SQL DELETE statement on a table. + * + * @param string $tableName The name of the table on which to delete. + * @param array $identifier The deletion criteria. An associative array containing column-value pairs. + * @return integer The number of affected rows. + */ + public function delete($tableName, array $identifier) + { + $this->connect(); + + $criteria = array(); + + foreach (array_keys($identifier) as $columnName) { + $criteria[] = $columnName . ' = ?'; + } + + $query = 'DELETE FROM ' . $tableName . ' WHERE ' . implode(' AND ', $criteria); + + return $this->executeUpdate($query, array_values($identifier)); + } + + /** + * Closes the connection. + * + * @return void + */ + public function close() + { + unset($this->_conn); + + $this->_isConnected = false; + } + + /** + * Sets the transaction isolation level. + * + * @param integer $level The level to set. + * @return integer + */ + public function setTransactionIsolation($level) + { + $this->_transactionIsolationLevel = $level; + + return $this->executeUpdate($this->_platform->getSetTransactionIsolationSQL($level)); + } + + /** + * Gets the currently active transaction isolation level. + * + * @return integer The current transaction isolation level. + */ + public function getTransactionIsolation() + { + return $this->_transactionIsolationLevel; + } + + /** + * Executes an SQL UPDATE statement on a table. + * + * @param string $tableName The name of the table to update. + * @param array $data + * @param array $identifier The update criteria. An associative array containing column-value pairs. + * @param array $types Types of the merged $data and $identifier arrays in that order. + * @return integer The number of affected rows. + */ + public function update($tableName, array $data, array $identifier, array $types = array()) + { + $this->connect(); + $set = array(); + foreach ($data as $columnName => $value) { + $set[] = $columnName . ' = ?'; + } + + $params = array_merge(array_values($data), array_values($identifier)); + + $sql = 'UPDATE ' . $tableName . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', array_keys($identifier)) + . ' = ?'; + + return $this->executeUpdate($sql, $params, $types); + } + + /** + * Inserts a table row with specified data. + * + * @param string $tableName The name of the table to insert data into. + * @param array $data An associative array containing column-value pairs. + * @param array $types Types of the inserted data. + * @return integer The number of affected rows. + */ + public function insert($tableName, array $data, array $types = array()) + { + $this->connect(); + + // column names are specified as array keys + $cols = array(); + $placeholders = array(); + + foreach ($data as $columnName => $value) { + $cols[] = $columnName; + $placeholders[] = '?'; + } + + $query = 'INSERT INTO ' . $tableName + . ' (' . implode(', ', $cols) . ')' + . ' VALUES (' . implode(', ', $placeholders) . ')'; + + return $this->executeUpdate($query, array_values($data), $types); + } + + /** + * Quote a string so it can be safely used as a table or column name, even if + * it is a reserved name. + * + * Delimiting style depends on the underlying database platform that is being used. + * + * NOTE: Just because you CAN use quoted identifiers does not mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str The name to be quoted. + * @return string The quoted name. + */ + public function quoteIdentifier($str) + { + return $this->_platform->quoteIdentifier($str); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input Parameter to be quoted. + * @param string $type Type of the parameter. + * @return string The quoted parameter. + */ + public function quote($input, $type = null) + { + $this->connect(); + + list($value, $bindingType) = $this->getBindingInfo($input, $type); + return $this->_conn->quote($value, $bindingType); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array. + * + * @param string $sql The SQL query. + * @param array $params The query parameters. + * @return array + */ + public function fetchAll($sql, array $params = array()) + { + return $this->executeQuery($sql, $params)->fetchAll(); + } + + /** + * Prepares an SQL statement. + * + * @param string $statement The SQL statement to prepare. + * @return \Doctrine\DBAL\Driver\Statement The prepared statement. + */ + public function prepare($statement) + { + $this->connect(); + + try { + $stmt = new Statement($statement, $this); + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, $statement); + } + + $stmt->setFetchMode($this->_defaultFetchMode); + + return $stmt; + } + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parameterized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters to bind to the query, if any. + * @param array $types The types the previous parameters are in. + * @param QueryCacheProfile $qcp + * @return \Doctrine\DBAL\Driver\Statement The executed statement. + * @internal PERF: Directly prepares a driver statement, not a wrapper. + */ + public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null) + { + if ($qcp !== null) { + return $this->executeCacheQuery($query, $params, $types, $qcp); + } + + $this->connect(); + + $logger = $this->_config->getSQLLogger(); + if ($logger) { + $logger->startQuery($query, $params, $types); + } + + try { + if ($params) { + list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types); + + $stmt = $this->_conn->prepare($query); + if ($types) { + $this->_bindTypedValues($stmt, $params, $types); + $stmt->execute(); + } else { + $stmt->execute($params); + } + } else { + $stmt = $this->_conn->query($query); + } + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, $query, $this->resolveParams($params, $types)); + } + + $stmt->setFetchMode($this->_defaultFetchMode); + + if ($logger) { + $logger->stopQuery(); + } + + return $stmt; + } + + /** + * Execute a caching query and + * + * @param string $query + * @param array $params + * @param array $types + * @param QueryCacheProfile $qcp + * @return \Doctrine\DBAL\Driver\ResultStatement + */ + public function executeCacheQuery($query, $params, $types, QueryCacheProfile $qcp) + { + $resultCache = $qcp->getResultCacheDriver() ?: $this->_config->getResultCacheImpl(); + if ( ! $resultCache) { + throw CacheException::noResultDriverConfigured(); + } + + list($cacheKey, $realKey) = $qcp->generateCacheKeys($query, $params, $types); + + // fetch the row pointers entry + if ($data = $resultCache->fetch($cacheKey)) { + // is the real key part of this row pointers map or is the cache only pointing to other cache keys? + if (isset($data[$realKey])) { + $stmt = new ArrayStatement($data[$realKey]); + } else if (array_key_exists($realKey, $data)) { + $stmt = new ArrayStatement(array()); + } + } + + if (!isset($stmt)) { + $stmt = new ResultCacheStatement($this->executeQuery($query, $params, $types), $resultCache, $cacheKey, $realKey, $qcp->getLifetime()); + } + + $stmt->setFetchMode($this->_defaultFetchMode); + + return $stmt; + } + + /** + * Executes an, optionally parameterized, SQL query and returns the result, + * applying a given projection/transformation function on each row of the result. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters, if any. + * @param Closure $mapper The transformation function that is applied on each row. + * The function receives a single paramater, an array, that + * represents a row of the result set. + * @return mixed The projected result of the query. + */ + public function project($query, array $params, Closure $function) + { + $result = array(); + $stmt = $this->executeQuery($query, $params ?: array()); + + while ($row = $stmt->fetch()) { + $result[] = $function($row); + } + + $stmt->closeCursor(); + + return $result; + } + + /** + * Executes an SQL statement, returning a result set as a Statement object. + * + * @param string $statement + * @param integer $fetchType + * @return \Doctrine\DBAL\Driver\Statement + */ + public function query() + { + $this->connect(); + + $args = func_get_args(); + + $logger = $this->_config->getSQLLogger(); + if ($logger) { + $logger->startQuery($args[0]); + } + + try { + $statement = call_user_func_array(array($this->_conn, 'query'), $args); + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, func_get_arg(0)); + } + + $statement->setFetchMode($this->_defaultFetchMode); + + if ($logger) { + $logger->stopQuery(); + } + + return $statement; + } + + /** + * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters + * and returns the number of affected rows. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param string $query The SQL query. + * @param array $params The query parameters. + * @param array $types The parameter types. + * @return integer The number of affected rows. + * @internal PERF: Directly prepares a driver statement, not a wrapper. + */ + public function executeUpdate($query, array $params = array(), array $types = array()) + { + $this->connect(); + + $logger = $this->_config->getSQLLogger(); + if ($logger) { + $logger->startQuery($query, $params, $types); + } + + try { + if ($params) { + list($query, $params, $types) = SQLParserUtils::expandListParameters($query, $params, $types); + + $stmt = $this->_conn->prepare($query); + if ($types) { + $this->_bindTypedValues($stmt, $params, $types); + $stmt->execute(); + } else { + $stmt->execute($params); + } + $result = $stmt->rowCount(); + } else { + $result = $this->_conn->exec($query); + } + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, $query, $this->resolveParams($params, $types)); + } + + if ($logger) { + $logger->stopQuery(); + } + + return $result; + } + + /** + * Execute an SQL statement and return the number of affected rows. + * + * @param string $statement + * @return integer The number of affected rows. + */ + public function exec($statement) + { + $this->connect(); + + $logger = $this->_config->getSQLLogger(); + if ($logger) { + $logger->startQuery($statement); + } + + try { + $result = $this->_conn->exec($statement); + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, $statement); + } + + if ($logger) { + $logger->stopQuery(); + } + + return $result; + } + + /** + * Returns the current transaction nesting level. + * + * @return integer The nesting level. A value of 0 means there's no active transaction. + */ + public function getTransactionNestingLevel() + { + return $this->_transactionNestingLevel; + } + + /** + * Fetch the SQLSTATE associated with the last database operation. + * + * @return integer The last error code. + */ + public function errorCode() + { + $this->connect(); + return $this->_conn->errorCode(); + } + + /** + * Fetch extended error information associated with the last database operation. + * + * @return array The last error information. + */ + public function errorInfo() + { + $this->connect(); + return $this->_conn->errorInfo(); + } + + /** + * Returns the ID of the last inserted row, or the last value from a sequence object, + * depending on the underlying driver. + * + * Note: This method may not return a meaningful or consistent result across different drivers, + * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY + * columns or sequences. + * + * @param string $seqName Name of the sequence object from which the ID should be returned. + * @return string A string representation of the last inserted ID. + */ + public function lastInsertId($seqName = null) + { + $this->connect(); + return $this->_conn->lastInsertId($seqName); + } + + /** + * Executes a function in a transaction. + * + * The function gets passed this Connection instance as an (optional) parameter. + * + * If an exception occurs during execution of the function or transaction commit, + * the transaction is rolled back and the exception re-thrown. + * + * @param Closure $func The function to execute transactionally. + */ + public function transactional(Closure $func) + { + $this->beginTransaction(); + try { + $func($this); + $this->commit(); + } catch (Exception $e) { + $this->rollback(); + throw $e; + } + } + + /** + * Set if nested transactions should use savepoints + * + * @param boolean $nestTransactionsWithSavepoints + * @return void + */ + public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints) + { + if ($this->_transactionNestingLevel > 0) { + throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction(); + } + + if ( ! $this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_nestTransactionsWithSavepoints = $nestTransactionsWithSavepoints; + } + + /** + * Get if nested transactions should use savepoints + * + * @return boolean + */ + public function getNestTransactionsWithSavepoints() + { + return $this->_nestTransactionsWithSavepoints; + } + + /** + * Returns the savepoint name to use for nested transactions are false if they are not supported + * "savepointFormat" parameter is not set + * + * @return mixed a string with the savepoint name or false + */ + protected function _getNestedTransactionSavePointName() + { + return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel; + } + + /** + * Starts a transaction by suspending auto-commit mode. + * + * @return void + */ + public function beginTransaction() + { + $this->connect(); + + ++$this->_transactionNestingLevel; + + $logger = $this->_config->getSQLLogger(); + + if ($this->_transactionNestingLevel == 1) { + if ($logger) { + $logger->startQuery('"START TRANSACTION"'); + } + $this->_conn->beginTransaction(); + if ($logger) { + $logger->stopQuery(); + } + } else if ($this->_nestTransactionsWithSavepoints) { + if ($logger) { + $logger->startQuery('"SAVEPOINT"'); + } + $this->createSavepoint($this->_getNestedTransactionSavePointName()); + if ($logger) { + $logger->stopQuery(); + } + } + } + + /** + * Commits the current transaction. + * + * @return void + * @throws ConnectionException If the commit failed due to no active transaction or + * because the transaction was marked for rollback only. + */ + public function commit() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + if ($this->_isRollbackOnly) { + throw ConnectionException::commitFailedRollbackOnly(); + } + + $this->connect(); + + $logger = $this->_config->getSQLLogger(); + + if ($this->_transactionNestingLevel == 1) { + if ($logger) { + $logger->startQuery('"COMMIT"'); + } + $this->_conn->commit(); + if ($logger) { + $logger->stopQuery(); + } + } else if ($this->_nestTransactionsWithSavepoints) { + if ($logger) { + $logger->startQuery('"RELEASE SAVEPOINT"'); + } + $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); + if ($logger) { + $logger->stopQuery(); + } + } + + --$this->_transactionNestingLevel; + } + + /** + * Cancel any database changes done during the current transaction. + * + * this method can be listened with onPreTransactionRollback and onTransactionRollback + * eventlistener methods + * + * @throws ConnectionException If the rollback operation failed. + */ + public function rollBack() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + + $this->connect(); + + $logger = $this->_config->getSQLLogger(); + + if ($this->_transactionNestingLevel == 1) { + if ($logger) { + $logger->startQuery('"ROLLBACK"'); + } + $this->_transactionNestingLevel = 0; + $this->_conn->rollback(); + $this->_isRollbackOnly = false; + if ($logger) { + $logger->stopQuery(); + } + } else if ($this->_nestTransactionsWithSavepoints) { + if ($logger) { + $logger->startQuery('"ROLLBACK TO SAVEPOINT"'); + } + $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); + --$this->_transactionNestingLevel; + if ($logger) { + $logger->stopQuery(); + } + } else { + $this->_isRollbackOnly = true; + --$this->_transactionNestingLevel; + } + } + + /** + * createSavepoint + * creates a new savepoint + * + * @param string $savepoint name of a savepoint to set + * @return void + */ + public function createSavepoint($savepoint) + { + if ( ! $this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_conn->exec($this->_platform->createSavePoint($savepoint)); + } + + /** + * releaseSavePoint + * releases given savepoint + * + * @param string $savepoint name of a savepoint to release + * @return void + */ + public function releaseSavepoint($savepoint) + { + if ( ! $this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + if ($this->_platform->supportsReleaseSavepoints()) { + $this->_conn->exec($this->_platform->releaseSavePoint($savepoint)); + } + } + + /** + * rollbackSavePoint + * releases given savepoint + * + * @param string $savepoint name of a savepoint to rollback to + * @return void + */ + public function rollbackSavepoint($savepoint) + { + if ( ! $this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_conn->exec($this->_platform->rollbackSavePoint($savepoint)); + } + + /** + * Gets the wrapped driver connection. + * + * @return \Doctrine\DBAL\Driver\Connection + */ + public function getWrappedConnection() + { + $this->connect(); + + return $this->_conn; + } + + /** + * Gets the SchemaManager that can be used to inspect or change the + * database schema through the connection. + * + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + public function getSchemaManager() + { + if ( ! $this->_schemaManager) { + $this->_schemaManager = $this->_driver->getSchemaManager($this); + } + + return $this->_schemaManager; + } + + /** + * Marks the current transaction so that the only possible + * outcome for the transaction to be rolled back. + * + * @throws ConnectionException If no transaction is active. + */ + public function setRollbackOnly() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + $this->_isRollbackOnly = true; + } + + /** + * Check whether the current transaction is marked for rollback only. + * + * @return boolean + * @throws ConnectionException If no transaction is active. + */ + public function isRollbackOnly() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + return $this->_isRollbackOnly; + } + + /** + * Converts a given value to its database representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * @return mixed The converted value. + */ + public function convertToDatabaseValue($value, $type) + { + return Type::getType($type)->convertToDatabaseValue($value, $this->_platform); + } + + /** + * Converts a given value to its PHP representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * @return mixed The converted type. + */ + public function convertToPHPValue($value, $type) + { + return Type::getType($type)->convertToPHPValue($value, $this->_platform); + } + + /** + * Binds a set of parameters, some or all of which are typed with a PDO binding type + * or DBAL mapping type, to a given statement. + * + * @param string $stmt The statement to bind the values to. + * @param array $params The map/list of named/positional parameters. + * @param array $types The parameter types (PDO binding types or DBAL mapping types). + * @internal Duck-typing used on the $stmt parameter to support driver statements as well as + * raw PDOStatement instances. + */ + private function _bindTypedValues($stmt, array $params, array $types) + { + // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO. + if (is_int(key($params))) { + // Positional parameters + $typeOffset = array_key_exists(0, $types) ? -1 : 0; + $bindIndex = 1; + foreach ($params as $value) { + $typeIndex = $bindIndex + $typeOffset; + if (isset($types[$typeIndex])) { + $type = $types[$typeIndex]; + list($value, $bindingType) = $this->getBindingInfo($value, $type); + $stmt->bindValue($bindIndex, $value, $bindingType); + } else { + $stmt->bindValue($bindIndex, $value); + } + ++$bindIndex; + } + } else { + // Named parameters + foreach ($params as $name => $value) { + if (isset($types[$name])) { + $type = $types[$name]; + list($value, $bindingType) = $this->getBindingInfo($value, $type); + $stmt->bindValue($name, $value, $bindingType); + } else { + $stmt->bindValue($name, $value); + } + } + } + } + + /** + * Gets the binding type of a given type. The given type can be a PDO or DBAL mapping type. + * + * @param mixed $value The value to bind + * @param mixed $type The type to bind (PDO or DBAL) + * @return array [0] => the (escaped) value, [1] => the binding type + */ + private function getBindingInfo($value, $type) + { + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->_platform); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type; // PDO::PARAM_* constants + } + return array($value, $bindingType); + } + + /** + * Resolves the parameters to a format which can be displayed. + * + * @internal This is a purely internal method. If you rely on this method, you are advised to + * copy/paste the code as this method may change, or be removed without prior notice. + * + * @param array $params + * @param array $types + * + * @return array + */ + public function resolveParams(array $params, array $types) + { + $resolvedParams = array(); + + // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO. + if (is_int(key($params))) { + // Positional parameters + $typeOffset = array_key_exists(0, $types) ? -1 : 0; + $bindIndex = 1; + foreach ($params as $value) { + $typeIndex = $bindIndex + $typeOffset; + if (isset($types[$typeIndex])) { + $type = $types[$typeIndex]; + list($value,) = $this->getBindingInfo($value, $type); + $resolvedParams[$bindIndex] = $value; + } else { + $resolvedParams[$bindIndex] = $value; + } + ++$bindIndex; + } + } else { + // Named parameters + foreach ($params as $name => $value) { + if (isset($types[$name])) { + $type = $types[$name]; + list($value,) = $this->getBindingInfo($value, $type); + $resolvedParams[$name] = $value; + } else { + $resolvedParams[$name] = $value; + } + } + } + + return $resolvedParams; + } + + /** + * Create a new instance of a SQL query builder. + * + * @return \Doctrine\DBAL\Query\QueryBuilder + */ + public function createQueryBuilder() + { + return new Query\QueryBuilder($this); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/ConnectionException.php b/doctrine/dbal/lib/Doctrine/DBAL/ConnectionException.php new file mode 100644 index 00000000..4b41ef24 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/ConnectionException.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Doctrine\DBAL\ConnectionException + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 4628 $ + * @author Jonathan H. Wage . + */ + +namespace Doctrine\DBAL\Connections; + + +use Doctrine\DBAL\Connection, + Doctrine\DBAL\Driver, + Doctrine\DBAL\Configuration, + Doctrine\Common\EventManager, + Doctrine\DBAL\Event\ConnectionEventArgs, + Doctrine\DBAL\Events; + +/** + * Master-Slave Connection + * + * Connection can be used with master-slave setups. + * + * Important for the understanding of this connection should be how and when + * it picks the slave or master. + * + * 1. Slave if master was never picked before and ONLY if 'getWrappedConnection' + * or 'executeQuery' is used. + * 2. Master picked when 'exec', 'executeUpdate', 'insert', 'delete', 'update', 'createSavepoint', + * 'releaseSavepoint', 'beginTransaction', 'rollback', 'commit', 'query' or + * 'prepare' is called. + * 3. If master was picked once during the lifetime of the connection it will always get picked afterwards. + * 4. One slave connection is randomly picked ONCE during a request. + * + * ATTENTION: You can write to the slave with this connection if you execute a write query without + * opening up a transaction. For example: + * + * $conn = DriverManager::getConnection(...); + * $conn->executeQuery("DELETE FROM table"); + * + * Be aware that Connection#executeQuery is a method specifically for READ + * operations only. + * + * This connection is limited to slave operations using the + * Connection#executeQuery operation only, because it wouldn't be compatible + * with the ORM or SchemaManager code otherwise. Both use all the other + * operations in a context where writes could happen to a slave, which makes + * this restricted approach necessary. + * + * You can manually connect to the master at any time by calling: + * + * $conn->connect('master'); + * + * Instantiation through the DriverManager looks like: + * + * @example + * + * $conn = DriverManager::getConnection(array( + * 'wrapperClass' => 'Doctrine\DBAL\Connections\MasterSlaveConnection', + * 'driver' => 'pdo_mysql', + * 'master' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), + * 'slaves' => array( + * array('user' => 'slave1', 'password', 'host' => '', 'dbname' => ''), + * array('user' => 'slave2', 'password', 'host' => '', 'dbname' => ''), + * ) + * )); + * + * You can also pass 'driverOptions' and any other documented option to each of this drivers to pass additional information. + * + * @author Lars Strojny + * @author Benjamin Eberlei + */ +class MasterSlaveConnection extends Connection +{ + /** + * Master and slave connection (one of the randomly picked slaves) + * + * @var Doctrine\DBAL\Driver\Connection[] + */ + protected $connections = array('master' => null, 'slave' => null); + + /** + * You can keep the slave connection and then switch back to it + * during the request if you know what you are doing. + * + * @var bool + */ + protected $keepSlave = false; + + /** + * Create Master Slave Connection + * + * @param array $params + * @param Driver $driver + * @param Configuration $config + * @param EventManager $eventManager + */ + public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $eventManager = null) + { + if ( !isset($params['slaves']) || !isset($params['master']) ) { + throw new \InvalidArgumentException('master or slaves configuration missing'); + } + if ( count($params['slaves']) == 0 ) { + throw new \InvalidArgumentException('You have to configure at least one slaves.'); + } + + $params['master']['driver'] = $params['driver']; + foreach ($params['slaves'] as $slaveKey => $slave) { + $params['slaves'][$slaveKey]['driver'] = $params['driver']; + } + + $this->keepSlave = isset($params['keepSlave']) ? (bool)$params['keepSlave'] : false; + + parent::__construct($params, $driver, $config, $eventManager); + } + + /** + * Check if the connection is currently towards the master or not. + * + * @return bool + */ + public function isConnectedToMaster() + { + return $this->_conn !== null && $this->_conn === $this->connections['master']; + } + + /** + * {@inheritDoc} + */ + public function connect($connectionName = null) + { + $requestedConnectionChange = ($connectionName !== null); + $connectionName = $connectionName ?: 'slave'; + + if ( $connectionName !== 'slave' && $connectionName !== 'master' ) { + throw new \InvalidArgumentException("Invalid option to connect(), only master or slave allowed."); + } + + // If we have a connection open, and this is not an explicit connection + // change request, then abort right here, because we are already done. + // This prevents writes to the slave in case of "keepSlave" option enabled. + if ($this->_conn && !$requestedConnectionChange) { + return false; + } + + $forceMasterAsSlave = false; + + if ($this->getTransactionNestingLevel() > 0) { + $connectionName = 'master'; + $forceMasterAsSlave = true; + } + + if ($this->connections[$connectionName]) { + if ($forceMasterAsSlave) { + $this->connections['slave'] = $this->_conn = $this->connections['master']; + } else { + $this->_conn = $this->connections[$connectionName]; + } + return false; + } + + if ($connectionName === 'master') { + // Set slave connection to master to avoid invalid reads + if ($this->connections['slave'] && ! $this->keepSlave) { + unset($this->connections['slave']); + } + + $this->connections['master'] = $this->_conn = $this->connectTo($connectionName); + + if ( ! $this->keepSlave) { + $this->connections['slave'] = $this->connections['master']; + } + } else { + $this->connections['slave'] = $this->_conn = $this->connectTo($connectionName); + } + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + $eventArgs = new ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * Connect to a specific connection + * + * @param string $connectionName + * @return Driver + */ + protected function connectTo($connectionName) + { + $params = $this->getParams(); + + $driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array(); + + $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); + + $user = isset($connectionParams['user']) ? $connectionParams['user'] : null; + $password = isset($connectionParams['password']) ? $connectionParams['password'] : null; + + return $this->_driver->connect($connectionParams, $user, $password, $driverOptions); + } + + protected function chooseConnectionConfiguration($connectionName, $params) + { + if ($connectionName === 'master') { + return $params['master']; + } + + return $params['slaves'][array_rand($params['slaves'])]; + } + + /** + * {@inheritDoc} + */ + public function executeUpdate($query, array $params = array(), array $types = array()) + { + $this->connect('master'); + return parent::executeUpdate($query, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->connect('master'); + return parent::beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + $this->connect('master'); + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + $this->connect('master'); + return parent::rollBack(); + } + + /** + * {@inheritDoc} + */ + public function delete($tableName, array $identifier) + { + $this->connect('master'); + return parent::delete($tableName, $identifier); + } + + /** + * {@inheritDoc} + */ + public function update($tableName, array $data, array $identifier, array $types = array()) + { + $this->connect('master'); + return parent::update($tableName, $data, $identifier, $types); + } + + /** + * {@inheritDoc} + */ + public function insert($tableName, array $data, array $types = array()) + { + $this->connect('master'); + return parent::insert($tableName, $data, $types); + } + + /** + * {@inheritDoc} + */ + public function exec($statement) + { + $this->connect('master'); + return parent::exec($statement); + } + + /** + * {@inheritDoc} + */ + public function createSavepoint($savepoint) + { + $this->connect('master'); + + return parent::createSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function releaseSavepoint($savepoint) + { + $this->connect('master'); + + return parent::releaseSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function rollbackSavepoint($savepoint) + { + $this->connect('master'); + + return parent::rollbackSavepoint($savepoint); + } + + public function query() + { + $this->connect('master'); + + $args = func_get_args(); + + $logger = $this->getConfiguration()->getSQLLogger(); + if ($logger) { + $logger->startQuery($args[0]); + } + + $statement = call_user_func_array(array($this->_conn, 'query'), $args); + + if ($logger) { + $logger->stopQuery(); + } + + return $statement; + } + + public function prepare($statement) + { + $this->connect('master'); + + return parent::prepare($statement); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php b/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php new file mode 100644 index 00000000..c8e430b0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php @@ -0,0 +1,106 @@ +getMessage(); + + return new self($msg, 0, $driverEx); + } + + public static function invalidWrapperClass($wrapperClass) + { + return new self("The given 'wrapperClass' ".$wrapperClass." has to be a ". + "subtype of \Doctrine\DBAL\Connection."); + } + + public static function invalidDriverClass($driverClass) + { + return new self("The given 'driverClass' ".$driverClass." has to implement the ". + "\Doctrine\DBAL\Driver interface."); + } + + /** + * @param string $tableName + * @return DBALException + */ + public static function invalidTableName($tableName) + { + return new self("Invalid table name specified: ".$tableName); + } + + /** + * @param string $tableName + * @return DBALException + */ + public static function noColumnsSpecifiedForTable($tableName) + { + return new self("No columns specified for table ".$tableName); + } + + public static function limitOffsetInvalid() + { + return new self("Invalid Offset in Limit Query, it has to be larger or equal to 0."); + } + + public static function typeExists($name) + { + return new self('Type '.$name.' already exists.'); + } + + public static function unknownColumnType($name) + { + return new self('Unknown column type "'.$name.'" requested. Any Doctrine type that you use has ' . + 'to be registered with \Doctrine\DBAL\Types\Type::addType(). You can get a list of all the ' . + 'known types with \Doctrine\DBAL\Types\Type::getTypeMap(). If this error occurs during database ' . + 'introspection then you might have forgot to register all database types for a Doctrine Type. Use ' . + 'AbstractPlatform#registerDoctrineTypeMapping() or have your custom types implement ' . + 'Type#getMappedDatabaseTypes(). If the type name is empty you might ' . + 'have a problem with the cache or forgot some mapping information.' + ); + } + + public static function typeNotFound($name) + { + return new self('Type to be overwritten '.$name.' does not exist.'); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver.php new file mode 100644 index 00000000..83649902 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Driver interface. + * Interface that all DBAL drivers must implement. + * + * @since 2.0 + */ +interface Driver +{ + /** + * Attempts to create a connection with the database. + * + * @param array $params All connection parameters passed by the user. + * @param string $username The username to use when connecting. + * @param string $password The password to use when connecting. + * @param array $driverOptions The driver options to use when connecting. + * @return \Doctrine\DBAL\Driver\Connection The database connection. + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()); + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform(); + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param \Doctrine\DBAL\Connection $conn + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + public function getSchemaManager(Connection $conn); + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName(); + + /** + * Get the name of the database connected to for this driver. + * + * @param \Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(Connection $conn); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php new file mode 100644 index 00000000..a618487b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Connection.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +/** + * Connection interface. + * Driver connections must implement this interface. + * + * This resembles (a subset of) the PDO interface. + * + * @since 2.0 + */ +interface Connection +{ + function prepare($prepareString); + function query(); + function quote($input, $type=\PDO::PARAM_STR); + function exec($statement); + function lastInsertId($name = null); + function beginTransaction(); + function commit(); + function rollBack(); + function errorCode(); + function errorInfo(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Connection.php new file mode 100644 index 00000000..2b46c992 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Connection.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\DBAL\Driver\DrizzlePDOMySql; + +/** + * @author Kim Hemsø Rasmussen + */ +class Connection extends \Doctrine\DBAL\Driver\PDOConnection +{ + /** + * {@inheritdoc} + */ + public function quote($value, $type=\PDO::PARAM_STR) + { + if (\PDO::PARAM_BOOL === $type) { + if ($value) { + return 'true'; + } else { + return 'false'; + } + } + return parent::quote($value, $type); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Driver.php new file mode 100644 index 00000000..8030bbc4 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/DrizzlePDOMySql/Driver.php @@ -0,0 +1,99 @@ +. + */ + +namespace Doctrine\DBAL\Driver\DrizzlePDOMySql; + +/** + * Drizzle driver using PDO MySql. + * + * @author Kim Hemsø Rasmussen + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * {@inheritdoc} + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + $conn = new Connection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + return $conn; + } + + /** + * Constructs the Drizzle MySql PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'mysql:'; + if (isset($params['host']) && $params['host'] != '') { + $dsn .= 'host=' . $params['host'] . ';'; + } + if (isset($params['port'])) { + $dsn .= 'port=' . $params['port'] . ';'; + } + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ';'; + } + if (isset($params['unix_socket'])) { + $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; + } + + return $dsn; + } + + /** + * {@inheritdoc} + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\DrizzlePlatform(); + } + + /** + * {@inheritdoc} + */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\DrizzleSchemaManager($conn); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'drizzle_pdo_mysql'; + } + + /** + * {@inheritdoc} + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php new file mode 100644 index 00000000..c1c2212f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php @@ -0,0 +1,115 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +class DB2Connection implements \Doctrine\DBAL\Driver\Connection +{ + private $_conn = null; + + public function __construct(array $params, $username, $password, $driverOptions = array()) + { + $isPersistant = (isset($params['persistent']) && $params['persistent'] == true); + + if ($isPersistant) { + $this->_conn = db2_pconnect($params['dbname'], $username, $password, $driverOptions); + } else { + $this->_conn = db2_connect($params['dbname'], $username, $password, $driverOptions); + } + if ( ! $this->_conn) { + throw new DB2Exception(db2_conn_errormsg()); + } + } + + public function prepare($sql) + { + $stmt = @db2_prepare($this->_conn, $sql); + if ( ! $stmt) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return new DB2Statement($stmt); + } + + public function query() + { + $args = func_get_args(); + $sql = $args[0]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + public function quote($input, $type=\PDO::PARAM_STR) + { + $input = db2_escape_string($input); + if ($type == \PDO::PARAM_INT ) { + return $input; + } else { + return "'".$input."'"; + } + } + + public function exec($statement) + { + $stmt = $this->prepare($statement); + $stmt->execute(); + return $stmt->rowCount(); + } + + public function lastInsertId($name = null) + { + return db2_last_insert_id($this->_conn); + } + + public function beginTransaction() + { + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_OFF); + } + + public function commit() + { + if (!db2_commit($this->_conn)) { + throw new DB2Exception(db2_conn_errormsg($this->_conn)); + } + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_ON); + } + + public function rollBack() + { + if (!db2_rollback($this->_conn)) { + throw new DB2Exception(db2_conn_errormsg($this->_conn)); + } + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_ON); + } + + public function errorCode() + { + return db2_conn_error($this->_conn); + } + + public function errorInfo() + { + return array( + 0 => db2_conn_errormsg($this->_conn), + 1 => $this->errorCode(), + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php new file mode 100644 index 00000000..82c71e5d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php @@ -0,0 +1,111 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +use Doctrine\DBAL\Driver, + Doctrine\DBAL\Connection; + +/** + * IBM DB2 Driver + * + * @since 2.0 + * @author Benjamin Eberlei + */ +class DB2Driver implements Driver +{ + /** + * Attempts to create a connection with the database. + * + * @param array $params All connection parameters passed by the user. + * @param string $username The username to use when connecting. + * @param string $password The password to use when connecting. + * @param array $driverOptions The driver options to use when connecting. + * @return \Doctrine\DBAL\Driver\Connection The database connection. + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + if ( ! isset($params['protocol'])) { + $params['protocol'] = 'TCPIP'; + } + + if ($params['host'] !== 'localhost' && $params['host'] != '127.0.0.1') { + // if the host isn't localhost, use extended connection params + $params['dbname'] = 'DRIVER={IBM DB2 ODBC DRIVER}' . + ';DATABASE=' . $params['dbname'] . + ';HOSTNAME=' . $params['host'] . + ';PROTOCOL=' . $params['protocol'] . + ';UID=' . $username . + ';PWD=' . $password .';'; + if (isset($params['port'])) { + $params['dbname'] .= 'PORT=' . $params['port']; + } + + $username = null; + $password = null; + } + + return new DB2Connection($params, $username, $password, $driverOptions); + } + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\DB2Platform; + } + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param \Doctrine\DBAL\Connection $conn + * @return \Doctrine\DBAL\Schema\DB2SchemaManager + */ + public function getSchemaManager(Connection $conn) + { + return new \Doctrine\DBAL\Schema\DB2SchemaManager($conn); + } + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName() + { + return 'ibm_db2'; + } + + /** + * Get the name of the database connected to for this driver. + * + * @param \Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php new file mode 100644 index 00000000..3d076582 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php @@ -0,0 +1,27 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +class DB2Exception extends \Exception +{ + +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php new file mode 100644 index 00000000..9a43f9fa --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php @@ -0,0 +1,214 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +use \Doctrine\DBAL\Driver\Statement; + +class DB2Statement implements \IteratorAggregate, Statement +{ + private $_stmt = null; + + private $_bindParam = array(); + + private $_defaultFetchMode = \PDO::FETCH_BOTH; + + /** + * DB2_BINARY, DB2_CHAR, DB2_DOUBLE, or DB2_LONG + * @var array + */ + static private $_typeMap = array( + \PDO::PARAM_INT => DB2_LONG, + \PDO::PARAM_STR => DB2_CHAR, + ); + + public function __construct($stmt) + { + $this->_stmt = $stmt; + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = null) + { + return $this->bindParam($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function bindParam($column, &$variable, $type = null, $length = null) + { + $this->_bindParam[$column] =& $variable; + + if ($type && isset(self::$_typeMap[$type])) { + $type = self::$_typeMap[$type]; + } else { + $type = DB2_CHAR; + } + + if (!db2_bind_param($this->_stmt, $column, "variable", DB2_PARAM_IN, $type)) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return true; + } + + /** + * {@inheritdoc} + */ + public function closeCursor() + { + if ( ! $this->_stmt) { + return false; + } + + $this->_bindParam = array(); + db2_free_result($this->_stmt); + $ret = db2_free_stmt($this->_stmt); + $this->_stmt = false; + return $ret; + } + + /** + * {@inheritdoc} + */ + public function columnCount() + { + if ( ! $this->_stmt) { + return false; + } + return db2_num_fields($this->_stmt); + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + return db2_stmt_error(); + } + + /** + * {@inheritdoc} + */ + public function errorInfo() + { + return array( + 0 => db2_stmt_errormsg(), + 1 => db2_stmt_error(), + ); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null) + { + if ( ! $this->_stmt) { + return false; + } + + /*$retval = true; + if ($params !== null) { + $retval = @db2_execute($this->_stmt, $params); + } else { + $retval = @db2_execute($this->_stmt); + }*/ + if ($params === null) { + ksort($this->_bindParam); + $params = array_values($this->_bindParam); + } + $retval = @db2_execute($this->_stmt, $params); + + if ($retval === false) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return $retval; + } + + /** + * {@inheritdoc} + */ + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + $this->_defaultFetchMode = $fetchMode; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->_defaultFetchMode; + switch ($fetchMode) { + case \PDO::FETCH_BOTH: + return db2_fetch_both($this->_stmt); + case \PDO::FETCH_ASSOC: + return db2_fetch_assoc($this->_stmt); + case \PDO::FETCH_NUM: + return db2_fetch_array($this->_stmt); + default: + throw new DB2Exception("Given Fetch-Style " . $fetchMode . " is not supported."); + } + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchMode = null) + { + $rows = array(); + while ($row = $this->fetch($fetchMode)) { + $rows[] = $row; + } + return $rows; + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(\PDO::FETCH_NUM); + if ($row && isset($row[$columnIndex])) { + return $row[$columnIndex]; + } + return false; + } + + /** + * {@inheritdoc} + */ + public function rowCount() + { + return (@db2_num_rows($this->_stmt))?:0; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Driver.php new file mode 100644 index 00000000..60defba7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/Driver.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\DBAL\Driver\Mysqli; + +use Doctrine\DBAL\Driver as DriverInterface; + +/** + * @author Kim Hemsø Rasmussen + */ +class Driver implements DriverInterface +{ + /** + * {@inheritdoc} + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new MysqliConnection($params, $username, $password, $driverOptions); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'mysqli'; + } + + /** + * {@inheritdoc} + */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\MySqlSchemaManager($conn); + } + + /** + * {@inheritdoc} + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\MySqlPlatform(); + } + + /** + * {@inheritdoc} + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php new file mode 100644 index 00000000..7ffa2ca8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliConnection.php @@ -0,0 +1,146 @@ +. + */ + +namespace Doctrine\DBAL\Driver\Mysqli; + +use Doctrine\DBAL\Driver\Connection as Connection; + +/** + * @author Kim Hemsø Rasmussen + */ +class MysqliConnection implements Connection +{ + /** + * @var \mysqli + */ + private $_conn; + + public function __construct(array $params, $username, $password, array $driverOptions = array()) + { + $port = isset($params['port']) ? $params['port'] : ini_get('mysqli.default_port'); + $socket = isset($params['unix_socket']) ? $params['unix_socket'] : ini_get('mysqli.default_socket'); + + $this->_conn = mysqli_init(); + if ( ! $this->_conn->real_connect($params['host'], $username, $password, $params['dbname'], $port, $socket)) { + throw new MysqliException($this->_conn->connect_error, $this->_conn->connect_errno); + } + + if (isset($params['charset'])) { + $this->_conn->set_charset($params['charset']); + } + } + + /** + * Retrieve mysqli native resource handle. + * + * Could be used if part of your application is not using DBAL + * + * @return \mysqli + */ + public function getWrappedResourceHandle() + { + return $this->_conn; + } + + /** + * {@inheritdoc} + */ + public function prepare($prepareString) + { + return new MysqliStatement($this->_conn, $prepareString); + } + + /** + * {@inheritdoc} + */ + public function query() + { + $args = func_get_args(); + $sql = $args[0]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + /** + * {@inheritdoc} + */ + public function quote($input, $type=\PDO::PARAM_STR) + { + return "'". $this->_conn->escape_string($input) ."'"; + } + + /** + * {@inheritdoc} + */ + public function exec($statement) + { + $this->_conn->query($statement); + return $this->_conn->affected_rows; + } + + /** + * {@inheritdoc} + */ + public function lastInsertId($name = null) + { + return $this->_conn->insert_id; + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + $this->_conn->query('START TRANSACTION'); + return true; + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return $this->_conn->commit(); + } + + /** + * {@inheritdoc}non-PHPdoc) + */ + public function rollBack() + { + return $this->_conn->rollback(); + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + return $this->_conn->errno; + } + + /** + * {@inheritdoc} + */ + public function errorInfo() + { + return $this->_conn->error; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliException.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliException.php new file mode 100644 index 00000000..139ce8fa --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliException.php @@ -0,0 +1,26 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\Mysqli; + +/** + * @author Kim Hemsø Rasmussen + */ +class MysqliException extends \Exception +{} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php new file mode 100644 index 00000000..2eaa8fd8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php @@ -0,0 +1,342 @@ +. + */ + +namespace Doctrine\DBAL\Driver\Mysqli; + +use Doctrine\DBAL\Driver\Statement; +use PDO; + +/** + * @author Kim Hemsø Rasmussen + */ +class MysqliStatement implements \IteratorAggregate, Statement +{ + protected static $_paramTypeMap = array( + PDO::PARAM_STR => 's', + PDO::PARAM_BOOL => 'i', + PDO::PARAM_NULL => 's', + PDO::PARAM_INT => 'i', + PDO::PARAM_LOB => 's' // TODO Support LOB bigger then max package size. + ); + + protected $_conn; + protected $_stmt; + + /** + * @var null|false|array + */ + protected $_columnNames; + + /** + * @var null|array + */ + protected $_rowBindedValues; + + /** + * @var array + */ + protected $_bindedValues; + + /** + * Contains ref values for bindValue() + * + * @var array + */ + protected $_values = array(); + + protected $_defaultFetchMode = PDO::FETCH_BOTH; + + public function __construct(\mysqli $conn, $prepareString) + { + $this->_conn = $conn; + $this->_stmt = $conn->prepare($prepareString); + if (false === $this->_stmt) { + throw new MysqliException($this->_conn->error, $this->_conn->errno); + } + + $paramCount = $this->_stmt->param_count; + if (0 < $paramCount) { + // Index 0 is types + // Need to init the string else php think we are trying to access it as a array. + $bindedValues = array(0 => str_repeat('s', $paramCount)); + $null = null; + for ($i = 1; $i < $paramCount; $i++) { + $bindedValues[] =& $null; + } + $this->_bindedValues = $bindedValues; + } + } + + /** + * {@inheritdoc} + */ + public function bindParam($column, &$variable, $type = null, $length = null) + { + if (null === $type) { + $type = 's'; + } else { + if (isset(self::$_paramTypeMap[$type])) { + $type = self::$_paramTypeMap[$type]; + } else { + throw new MysqliException("Unkown type: '{$type}'"); + } + } + + $this->_bindedValues[$column] =& $variable; + $this->_bindedValues[0][$column - 1] = $type; + return true; + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = null) + { + if (null === $type) { + $type = 's'; + } else { + if (isset(self::$_paramTypeMap[$type])) { + $type = self::$_paramTypeMap[$type]; + } else { + throw new MysqliException("Unknown type: '{$type}'"); + } + } + + $this->_values[$param] = $value; + $this->_bindedValues[$param] =& $this->_values[$param]; + $this->_bindedValues[0][$param - 1] = $type; + return true; + } + + /** + * {@inheritdoc} + */ + public function execute($params = null) + { + if (null !== $this->_bindedValues) { + if (null !== $params) { + if ( ! $this->_bindValues($params)) { + throw new MysqliException($this->_stmt->error, $this->_stmt->errno); + } + } else { + if (!call_user_func_array(array($this->_stmt, 'bind_param'), $this->_bindedValues)) { + throw new MysqliException($this->_stmt->error, $this->_stmt->errno); + } + } + } + + if ( ! $this->_stmt->execute()) { + throw new MysqliException($this->_stmt->error, $this->_stmt->errno); + } + + if (null === $this->_columnNames) { + $meta = $this->_stmt->result_metadata(); + if (false !== $meta) { + $columnNames = array(); + foreach ($meta->fetch_fields() as $col) { + $columnNames[] = $col->name; + } + $meta->free(); + + $this->_columnNames = $columnNames; + $this->_rowBindedValues = array_fill(0, count($columnNames), NULL); + + $refs = array(); + foreach ($this->_rowBindedValues as $key => &$value) { + $refs[$key] =& $value; + } + + if (!call_user_func_array(array($this->_stmt, 'bind_result'), $refs)) { + throw new MysqliException($this->_stmt->error, $this->_stmt->errno); + } + } else { + $this->_columnNames = false; + } + } + + // We have a result. + if (false !== $this->_columnNames) { + $this->_stmt->store_result(); + } + return true; + } + + /** + * Bind a array of values to bound parameters + * + * @param array $values + * @return boolean + */ + private function _bindValues($values) + { + $params = array(); + $types = str_repeat('s', count($values)); + $params[0] = $types; + + foreach ($values as &$v) { + $params[] =& $v; + } + return call_user_func_array(array($this->_stmt, 'bind_param'), $params); + } + + /** + * @return boolean|array + */ + private function _fetch() + { + $ret = $this->_stmt->fetch(); + + if (true === $ret) { + $values = array(); + foreach ($this->_rowBindedValues as $v) { + // Mysqli converts them to a scalar type it can fit in. + $values[] = null === $v ? null : (string)$v; + } + return $values; + } + return $ret; + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchMode = null) + { + $values = $this->_fetch(); + if (null === $values) { + return null; + } + + if (false === $values) { + throw new MysqliException($this->_stmt->error, $this->_stmt->errno); + } + + $fetchMode = $fetchMode ?: $this->_defaultFetchMode; + + switch ($fetchMode) { + case PDO::FETCH_NUM: + return $values; + + case PDO::FETCH_ASSOC: + return array_combine($this->_columnNames, $values); + + case PDO::FETCH_BOTH: + $ret = array_combine($this->_columnNames, $values); + $ret += $values; + return $ret; + + default: + throw new MysqliException("Unknown fetch type '{$fetchMode}'"); + } + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->_defaultFetchMode; + + $rows = array(); + if (PDO::FETCH_COLUMN == $fetchMode) { + while (($row = $this->fetchColumn()) !== false) { + $rows[] = $row; + } + } else { + while (($row = $this->fetch($fetchMode)) !== null) { + $rows[] = $row; + } + } + + return $rows; + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + if (null === $row) { + return false; + } + return $row[$columnIndex]; + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + return $this->_stmt->errno; + } + + /** + * {@inheritdoc} + */ + public function errorInfo() + { + return $this->_stmt->error; + } + + /** + * {@inheritdoc} + */ + public function closeCursor() + { + $this->_stmt->free_result(); + return true; + } + + /** + * {@inheritdoc} + */ + public function rowCount() + { + if (false === $this->_columnNames) { + return $this->_stmt->affected_rows; + } + return $this->_stmt->num_rows; + } + + /** + * {@inheritdoc} + */ + public function columnCount() + { + return $this->_stmt->field_count; + } + + /** + * {@inheritdoc} + */ + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + $this->_defaultFetchMode = $fetchMode; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Driver.php new file mode 100644 index 00000000..d512610e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/Driver.php @@ -0,0 +1,99 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +use Doctrine\DBAL\Platforms; + +/** + * A Doctrine DBAL driver for the Oracle OCI8 PHP extensions. + * + * @author Roman Borschel + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new OCI8Connection( + $username, + $password, + $this->_constructDsn($params), + isset($params['charset']) ? $params['charset'] : null, + isset($params['sessionMode']) ? $params['sessionMode'] : OCI_DEFAULT, + isset($params['persistent']) ? $params['persistent'] : false + ); + } + + /** + * Constructs the Oracle DSN. + * + * @return string The DSN. + */ + protected function _constructDsn(array $params) + { + $dsn = ''; + if (isset($params['host']) && $params['host'] != '') { + $dsn .= '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' . + '(HOST=' . $params['host'] . ')'; + + if (isset($params['port'])) { + $dsn .= '(PORT=' . $params['port'] . ')'; + } else { + $dsn .= '(PORT=1521)'; + } + + if (isset($params['service']) && $params['service'] == true) { + $dsn .= '))(CONNECT_DATA=(SERVICE_NAME=' . $params['dbname'] . '))'; + } else { + $dsn .= '))(CONNECT_DATA=(SID=' . $params['dbname'] . '))'; + } + if (isset($params['pooled']) && $params['pooled'] == true) { + $dsn .= '(SERVER=POOLED)'; + } + $dsn .= ')'; + } else { + $dsn .= $params['dbname']; + } + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\OraclePlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\OracleSchemaManager($conn); + } + + public function getName() + { + return 'oci8'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['user']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php new file mode 100644 index 00000000..bc74787e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php @@ -0,0 +1,200 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +use Doctrine\DBAL\Platforms\OraclePlatform; + +/** + * OCI8 implementation of the Connection interface. + * + * @since 2.0 + */ +class OCI8Connection implements \Doctrine\DBAL\Driver\Connection +{ + /** + * @var resource + */ + protected $dbh; + + /** + * @var int + */ + protected $executeMode = OCI_COMMIT_ON_SUCCESS; + + /** + * Create a Connection to an Oracle Database using oci8 extension. + * + * @param string $username + * @param string $password + * @param string $db + */ + public function __construct($username, $password, $db, $charset = null, $sessionMode = OCI_DEFAULT, $persistent = false) + { + if (!defined('OCI_NO_AUTO_COMMIT')) { + define('OCI_NO_AUTO_COMMIT', 0); + } + + $this->dbh = $persistent + ? @oci_pconnect($username, $password, $db, $charset, $sessionMode) + : @oci_connect($username, $password, $db, $charset, $sessionMode); + + if ( ! $this->dbh) { + throw OCI8Exception::fromErrorInfo(oci_error()); + } + } + + /** + * Create a non-executed prepared statement. + * + * @param string $prepareString + * @return OCI8Statement + */ + public function prepare($prepareString) + { + return new OCI8Statement($this->dbh, $prepareString, $this); + } + + /** + * @param string $sql + * @return OCI8Statement + */ + public function query() + { + $args = func_get_args(); + $sql = $args[0]; + //$fetchMode = $args[1]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + /** + * Quote input value. + * + * @param mixed $input + * @param int $type PDO::PARAM* + * @return mixed + */ + public function quote($value, $type=\PDO::PARAM_STR) + { + if (is_int($value) || is_float($value)) { + return $value; + } + $value = str_replace("'", "''", $value); + return "'" . addcslashes($value, "\000\n\r\\\032") . "'"; + } + + /** + * + * @param string $statement + * @return int + */ + public function exec($statement) + { + $stmt = $this->prepare($statement); + $stmt->execute(); + return $stmt->rowCount(); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name === null) { + return false; + } + + OraclePlatform::assertValidIdentifier($name); + + $sql = 'SELECT ' . $name . '.CURRVAL FROM DUAL'; + $stmt = $this->query($sql); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if ($result === false || !isset($result['CURRVAL'])) { + throw new OCI8Exception("lastInsertId failed: Query was executed but no result was returned."); + } + + return (int) $result['CURRVAL']; + } + + /** + * Return the current execution mode. + */ + public function getExecuteMode() + { + return $this->executeMode; + } + + /** + * Start a transactiom + * + * Oracle has to explicitly set the autocommit mode off. That means + * after connection, a commit or rollback there is always automatically + * opened a new transaction. + * + * @return bool + */ + public function beginTransaction() + { + $this->executeMode = OCI_NO_AUTO_COMMIT; + return true; + } + + /** + * @throws OCI8Exception + * @return bool + */ + public function commit() + { + if (!oci_commit($this->dbh)) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + $this->executeMode = OCI_COMMIT_ON_SUCCESS; + return true; + } + + /** + * @throws OCI8Exception + * @return bool + */ + public function rollBack() + { + if (!oci_rollback($this->dbh)) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + $this->executeMode = OCI_COMMIT_ON_SUCCESS; + return true; + } + + public function errorCode() + { + $error = oci_error($this->dbh); + if ($error !== false) { + $error = $error['code']; + } + return $error; + } + + public function errorInfo() + { + return oci_error($this->dbh); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php new file mode 100644 index 00000000..adeb13f0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php @@ -0,0 +1,30 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\OCI8; + +class OCI8Exception extends \Exception +{ + static public function fromErrorInfo($error) + { + return new self($error['message'], $error['code']); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php new file mode 100644 index 00000000..8bf3121c --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php @@ -0,0 +1,268 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +use PDO; +use IteratorAggregate; +use Doctrine\DBAL\Driver\Statement; + +/** + * The OCI8 implementation of the Statement interface. + * + * @since 2.0 + * @author Roman Borschel + */ +class OCI8Statement implements \IteratorAggregate, Statement +{ + /** Statement handle. */ + protected $_dbh; + protected $_sth; + protected $_conn; + protected static $_PARAM = ':param'; + protected static $fetchModeMap = array( + PDO::FETCH_BOTH => OCI_BOTH, + PDO::FETCH_ASSOC => OCI_ASSOC, + PDO::FETCH_NUM => OCI_NUM, + PDO::PARAM_LOB => OCI_B_BLOB, + PDO::FETCH_COLUMN => OCI_NUM, + ); + protected $_defaultFetchMode = PDO::FETCH_BOTH; + protected $_paramMap = array(); + + /** + * Creates a new OCI8Statement that uses the given connection handle and SQL statement. + * + * @param resource $dbh The connection handle. + * @param string $statement The SQL statement. + */ + public function __construct($dbh, $statement, OCI8Connection $conn) + { + list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement); + $this->_sth = oci_parse($dbh, $statement); + $this->_dbh = $dbh; + $this->_paramMap = $paramMap; + $this->_conn = $conn; + } + + /** + * Convert positional (?) into named placeholders (:param) + * + * Oracle does not support positional parameters, hence this method converts all + * positional parameters into artificially named parameters. Note that this conversion + * is not perfect. All question marks (?) in the original statement are treated as + * placeholders and converted to a named parameter. + * + * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral. + * Question marks inside literal strings are therefore handled correctly by this method. + * This comes at a cost, the whole sql statement has to be looped over. + * + * @todo extract into utility class in Doctrine\DBAL\Util namespace + * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements. + * @param string $statement The SQL statement to convert. + * @return string + */ + static public function convertPositionalToNamedPlaceholders($statement) + { + $count = 1; + $inLiteral = false; // a valid query never starts with quotes + $stmtLen = strlen($statement); + $paramMap = array(); + for ($i = 0; $i < $stmtLen; $i++) { + if ($statement[$i] == '?' && !$inLiteral) { + // real positional parameter detected + $paramMap[$count] = ":param$count"; + $len = strlen($paramMap[$count]); + $statement = substr_replace($statement, ":param$count", $i, 1); + $i += $len-1; // jump ahead + $stmtLen = strlen($statement); // adjust statement length + ++$count; + } else if ($statement[$i] == "'" || $statement[$i] == '"') { + $inLiteral = ! $inLiteral; // switch state! + } + } + + return array($statement, $paramMap); + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = null) + { + return $this->bindParam($param, $value, $type, null); + } + + /** + * {@inheritdoc} + */ + public function bindParam($column, &$variable, $type = null,$length = null) + { + $column = isset($this->_paramMap[$column]) ? $this->_paramMap[$column] : $column; + + if ($type == \PDO::PARAM_LOB) { + $lob = oci_new_descriptor($this->_dbh, OCI_D_LOB); + $lob->writeTemporary($variable, OCI_TEMP_BLOB); + + return oci_bind_by_name($this->_sth, $column, $lob, -1, OCI_B_BLOB); + } else { + return oci_bind_by_name($this->_sth, $column, $variable); + } + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + public function closeCursor() + { + return oci_free_statement($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function columnCount() + { + return oci_num_fields($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + $error = oci_error($this->_sth); + if ($error !== false) { + $error = $error['code']; + } + return $error; + } + + /** + * {@inheritdoc} + */ + public function errorInfo() + { + return oci_error($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null) + { + if ($params) { + $hasZeroIndex = array_key_exists(0, $params); + foreach ($params as $key => $val) { + if ($hasZeroIndex && is_numeric($key)) { + $this->bindValue($key + 1, $val); + } else { + $this->bindValue($key, $val); + } + } + } + + $ret = @oci_execute($this->_sth, $this->_conn->getExecuteMode()); + if ( ! $ret) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + return $ret; + } + + /** + * {@inheritdoc} + */ + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + $this->_defaultFetchMode = $fetchMode; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->_defaultFetchMode; + if ( ! isset(self::$fetchModeMap[$fetchMode])) { + throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode); + } + + return oci_fetch_array($this->_sth, self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->_defaultFetchMode; + if ( ! isset(self::$fetchModeMap[$fetchMode])) { + throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode); + } + + $result = array(); + if (self::$fetchModeMap[$fetchMode] === OCI_BOTH) { + while ($row = $this->fetch($fetchMode)) { + $result[] = $row; + } + } else { + $fetchStructure = OCI_FETCHSTATEMENT_BY_ROW; + if ($fetchMode == PDO::FETCH_COLUMN) { + $fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN; + } + + oci_fetch_all($this->_sth, $result, 0, -1, + self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS); + + if ($fetchMode == PDO::FETCH_COLUMN) { + $result = $result[0]; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + return isset($row[$columnIndex]) ? $row[$columnIndex] : false; + } + + /** + * {@inheritdoc} + */ + public function rowCount() + { + return oci_num_rows($this->_sth); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php new file mode 100644 index 00000000..4595e5a4 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php @@ -0,0 +1,40 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use \PDO; + +/** + * PDO implementation of the Connection interface. + * Used by all PDO-based drivers. + * + * @since 2.0 + */ +class PDOConnection extends PDO implements Connection +{ + public function __construct($dsn, $user = null, $password = null, array $options = null) + { + parent::__construct($dsn, $user, $password, $options); + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Doctrine\DBAL\Driver\PDOStatement', array())); + $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOIbm/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOIbm/Driver.php new file mode 100644 index 00000000..de307577 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOIbm/Driver.php @@ -0,0 +1,126 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\PDOIbm; + +use Doctrine\DBAL\Connection; + +/** + * Driver for the PDO IBM extension + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * Attempts to establish a connection with the underlying driver. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return \Doctrine\DBAL\Driver\Connection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + $conn = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + return $conn; + } + + /** + * Constructs the MySql PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'ibm:'; + if (isset($params['host'])) { + $dsn .= 'HOSTNAME=' . $params['host'] . ';'; + } + if (isset($params['port'])) { + $dsn .= 'PORT=' . $params['port'] . ';'; + } + $dsn .= 'PROTOCOL=TCPIP;'; + if (isset($params['dbname'])) { + $dsn .= 'DATABASE=' . $params['dbname'] . ';'; + } + + return $dsn; + } + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\DB2Platform; + } + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param \Doctrine\DBAL\Connection $conn + * @return \Doctrine\DBAL\Schema\DB2SchemaManager + */ + public function getSchemaManager(Connection $conn) + { + return new \Doctrine\DBAL\Schema\DB2SchemaManager($conn); + } + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName() + { + return 'pdo_ibm'; + } + + /** + * Get the name of the database connected to for this driver. + * + * @param \Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php new file mode 100644 index 00000000..eeb67279 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOMySql/Driver.php @@ -0,0 +1,102 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOMySql; + +use Doctrine\DBAL\Connection; + +/** + * PDO MySql driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * Attempts to establish a connection with the underlying driver. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return \Doctrine\DBAL\Driver\Connection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + $conn = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + return $conn; + } + + /** + * Constructs the MySql PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'mysql:'; + if (isset($params['host']) && $params['host'] != '') { + $dsn .= 'host=' . $params['host'] . ';'; + } + if (isset($params['port'])) { + $dsn .= 'port=' . $params['port'] . ';'; + } + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ';'; + } + if (isset($params['unix_socket'])) { + $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; + } + if (isset($params['charset'])) { + $dsn .= 'charset=' . $params['charset'] . ';'; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\MySqlPlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\MySqlSchemaManager($conn); + } + + public function getName() + { + return 'pdo_mysql'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + + if (isset($params['dbname'])) { + return $params['dbname']; + } + return $conn->query('SELECT DATABASE()')->fetchColumn(); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOOracle/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOOracle/Driver.php new file mode 100644 index 00000000..cb2e6b03 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOOracle/Driver.php @@ -0,0 +1,98 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOOracle; + +use Doctrine\DBAL\Platforms; + +/** + * PDO Oracle driver + * + * WARNING: This driver gives us segfauls in our testsuites on CLOB and other + * stuff. PDO Oracle is not maintained by Oracle or anyone in the PHP community, + * which leads us to the recommendation to use the "oci8" driver to connect + * to Oracle instead. + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Oracle PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'oci:'; + if (isset($params['host']) && $params['host'] != '') { + $dsn .= 'dbname=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' . + '(HOST=' . $params['host'] . ')'; + + if (isset($params['port'])) { + $dsn .= '(PORT=' . $params['port'] . ')'; + } else { + $dsn .= '(PORT=1521)'; + } + + if (isset($params['service']) && $params['service'] == true) { + $dsn .= '))(CONNECT_DATA=(SERVICE_NAME=' . $params['dbname'] . ')))'; + } else { + $dsn .= '))(CONNECT_DATA=(SID=' . $params['dbname'] . ')))'; + } + } else { + $dsn .= 'dbname=' . $params['dbname']; + } + + if (isset($params['charset'])) { + $dsn .= ';charset=' . $params['charset']; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\OraclePlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\OracleSchemaManager($conn); + } + + public function getName() + { + return 'pdo_oracle'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['user']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php new file mode 100644 index 00000000..951406dd --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOPgSql/Driver.php @@ -0,0 +1,70 @@ +_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Postgres PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'pgsql:'; + if (isset($params['host']) && $params['host'] != '') { + $dsn .= 'host=' . $params['host'] . ' '; + } + if (isset($params['port']) && $params['port'] != '') { + $dsn .= 'port=' . $params['port'] . ' '; + } + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ' '; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\PostgreSqlPlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\PostgreSqlSchemaManager($conn); + } + + public function getName() + { + return 'pdo_pgsql'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlite/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlite/Driver.php new file mode 100644 index 00000000..903d9994 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlite/Driver.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlite; + +/** + * The PDO Sqlite driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * @var array + */ + protected $_userDefinedFunctions = array( + 'sqrt' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfSqrt'), 'numArgs' => 1), + 'mod' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfMod'), 'numArgs' => 2), + 'locate' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfLocate'), 'numArgs' => -1), + ); + + /** + * Tries to establish a database connection to SQLite. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return \Doctrine\DBAL\Driver\PDOConnection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + if (isset($driverOptions['userDefinedFunctions'])) { + $this->_userDefinedFunctions = array_merge( + $this->_userDefinedFunctions, $driverOptions['userDefinedFunctions']); + unset($driverOptions['userDefinedFunctions']); + } + + $pdo = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + + foreach ($this->_userDefinedFunctions as $fn => $data) { + $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']); + } + + return $pdo; + } + + /** + * Constructs the Sqlite PDO DSN. + * + * @return string The DSN. + * @override + */ + protected function _constructPdoDsn(array $params) + { + $dsn = 'sqlite:'; + if (isset($params['path'])) { + $dsn .= $params['path']; + } else if (isset($params['memory'])) { + $dsn .= ':memory:'; + } + + return $dsn; + } + + /** + * Gets the database platform that is relevant for this driver. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\SqlitePlatform(); + } + + /** + * Gets the schema manager that is relevant for this driver. + * + * @param \Doctrine\DBAL\Connection $conn + * @return \Doctrine\DBAL\Schema\SqliteSchemaManager + */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\SqliteSchemaManager($conn); + } + + public function getName() + { + return 'pdo_sqlite'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return isset($params['path']) ? $params['path'] : null; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php new file mode 100644 index 00000000..01a5769f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlsrv; + +/** + * Sqlsrv Connection implementation. + * + * @since 2.0 + */ +class Connection extends \Doctrine\DBAL\Driver\PDOConnection implements \Doctrine\DBAL\Driver\Connection +{ + /** + * @override + */ + public function quote($value, $type=\PDO::PARAM_STR) + { + $val = parent::quote($value, $type); + + // Fix for a driver version terminating all values with null byte + if (strpos($val, "\0") !== false) { + $val = substr($val, 0, -1); + } + + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php new file mode 100644 index 00000000..7072b5fa --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php @@ -0,0 +1,87 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlsrv; + +/** + * The PDO-based Sqlsrv driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new Connection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Sqlsrv PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + } + + if (isset($params['port']) && !empty($params['port'])) { + $dsn .= ',' . $params['port']; + } + + if (isset($params['dbname'])) {; + $dsn .= ';Database=' . $params['dbname']; + } + + if (isset($params['MultipleActiveResultSets'])) { + $dsn .= '; MultipleActiveResultSets=' . ($params['MultipleActiveResultSets'] ? 'true' : 'false'); + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\SQLServer2008Platform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\SQLServerSchemaManager($conn); + } + + public function getName() + { + return 'pdo_sqlsrv'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php new file mode 100644 index 00000000..2673ae9d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +/** + * The PDO implementation of the Statement interface. + * Used by all PDO-based drivers. + * + * @since 2.0 + */ +class PDOStatement extends \PDOStatement implements Statement +{ + private function __construct() {} + + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + // This thin wrapper is necessary to shield against the weird signature + // of PDOStatement::setFetchMode(): even if the second and third + // parameters are optional, PHP will not let us remove it from this + // declaration. + if ($arg2 === null && $arg3 === null) { + return parent::setFetchMode($fetchMode); + } + + if ($arg3 === null) { + return parent::setFetchMode($fetchMode, $arg2); + } + + return parent::setFetchMode($fetchMode, $arg2, $arg3); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php new file mode 100644 index 00000000..d0780482 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php @@ -0,0 +1,92 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use PDO; + +/** + * Interface for the reading part of a prepare statement only. + * + * @author Benjamin Eberlei + */ +interface ResultStatement extends \Traversable +{ + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function closeCursor(); + + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + function columnCount(); + + /** + * setFetchMode + * Set the fetch mode to use while iterating this statement. + * + * @param integer $fetchMode + */ + function setFetchMode($fetchMode, $arg2 = null, $arg3 = null); + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchMode Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @return mixed + */ + function fetch($fetchMode = null); + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchMode Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @return array + */ + function fetchAll($fetchMode = null); + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + function fetchColumn($columnIndex = 0); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php new file mode 100644 index 00000000..364f811e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php @@ -0,0 +1,71 @@ +. + */ + +namespace Doctrine\DBAL\Driver\SQLSrv; + +/** + * Driver for ext/sqlsrv + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + if (!isset($params['host'])) { + throw new SQLSrvException("Missing 'host' in configuration for sqlsrv driver."); + } + if (!isset($params['dbname'])) { + throw new SQLSrvException("Missing 'dbname' in configuration for sqlsrv driver."); + } + + $serverName = $params['host']; + if (isset($params['port'])) { + $serverName .= ', ' . $params['port']; + } + $driverOptions['Database'] = $params['dbname']; + $driverOptions['UID'] = $username; + $driverOptions['PWD'] = $password; + + if (!isset($driverOptions['ReturnDatesAsStrings'])) { + $driverOptions['ReturnDatesAsStrings'] = 1; + } + + return new SQLSrvConnection($serverName, $driverOptions); + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\SQLServer2008Platform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\SQLServerSchemaManager($conn); + } + + public function getName() + { + return 'sqlsrv'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php new file mode 100644 index 00000000..3f13c38a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\DBAL\Driver\SQLSrv; + +/** + * Last Id Data Container + * + * @since 2.3 + * @author Benjamin Eberlei + */ +class LastInsertId +{ + private $id; + + public function setId($id) + { + $this->id = $id; + } + + public function getId() + { + return $this->id; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php new file mode 100644 index 00000000..5faca1fb --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php @@ -0,0 +1,160 @@ +. + */ + +namespace Doctrine\DBAL\Driver\SQLSrv; + +/** + * SQL Server implementation for the Connection interface. + * + * @since 2.3 + * @author Benjamin Eberlei + */ +class SQLSrvConnection implements \Doctrine\DBAL\Driver\Connection +{ + /** + * @var resource + */ + protected $conn; + + /** + * @var LastInsertId + */ + protected $lastInsertId; + + + public function __construct($serverName, $connectionOptions) + { + $this->conn = sqlsrv_connect($serverName, $connectionOptions); + if ( ! $this->conn) { + throw SQLSrvException::fromSqlSrvErrors(); + } + $this->lastInsertId = new LastInsertId(); + } + + /** + * {@inheritDoc} + */ + public function prepare($sql) + { + return new SQLSrvStatement($this->conn, $sql, $this->lastInsertId); + } + + /** + * {@inheritDoc} + */ + public function query() + { + $args = func_get_args(); + $sql = $args[0]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + /** + * {@inheritDoc} + * @license New BSD, code from Zend Framework + */ + public function quote($value, $type=\PDO::PARAM_STR) + { + if (is_int($value)) { + return $value; + } else if (is_float($value)) { + return sprintf('%F', $value); + } + + return "'" . str_replace("'", "''", $value) . "'"; + } + + /** + * {@inheritDoc} + */ + public function exec($statement) + { + $stmt = $this->prepare($statement); + $stmt->execute(); + return $stmt->rowCount(); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + $sql = "SELECT IDENT_CURRENT(".$this->quote($name).") AS LastInsertId"; + $stmt = $this->prepare($sql); + $stmt->execute(); + + return $stmt->fetchColumn(); + } + + return $this->lastInsertId->getId(); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + if ( ! sqlsrv_begin_transaction($this->conn)) { + throw SQLSrvException::fromSqlSrvErrors(); + } + } + + /** + * {@inheritDoc} + */ + public function commit() + { + if ( ! sqlsrv_commit($this->conn)) { + throw SQLSrvException::fromSqlSrvErrors(); + } + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + if ( ! sqlsrv_rollback($this->conn)) { + throw SQLSrvException::fromSqlSrvErrors(); + } + } + + /** + * {@inheritDoc} + */ + public function errorCode() + { + $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); + if ($errors) { + return $errors[0]['code']; + } + return false; + } + + /** + * {@inheritDoc} + */ + public function errorInfo() + { + return sqlsrv_errors(SQLSRV_ERR_ERRORS); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php new file mode 100644 index 00000000..6a129444 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\DBAL\Driver\SQLSrv; + +class SQLSrvException extends \Doctrine\DBAL\DBALException +{ + /** + * Helper method to turn sql server errors into exception. + * + * @return SQLSrvException + */ + static public function fromSqlSrvErrors() + { + $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); + $message = ""; + foreach ($errors as $error) { + $message .= "SQLSTATE [".$error['SQLSTATE'].", ".$error['code']."]: ". $error['message']."\n"; + } + if ( ! $message) { + $message = "SQL Server error occured but no error message was retrieved from driver."; + } + + return new self(rtrim($message)); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php new file mode 100644 index 00000000..1adf266a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php @@ -0,0 +1,250 @@ +. + */ + +namespace Doctrine\DBAL\Driver\SQLSrv; + +use PDO; +use IteratorAggregate; +use Doctrine\DBAL\Driver\Statement; + +/** + * SQL Server Statement + * + * @since 2.3 + * @author Benjamin Eberlei + */ +class SQLSrvStatement implements IteratorAggregate, Statement +{ + /** + * SQLSRV Resource + * + * @var resource + */ + private $conn; + + /** + * SQL Statement to execute + * + * @var string + */ + private $sql; + + /** + * SQLSRV Statement Resource + * + * @var resource + */ + private $stmt; + + /** + * Parameters to bind + * + * @var array + */ + private $params = array(); + + /** + * Translations + * + * @var array + */ + private static $fetchMap = array( + PDO::FETCH_BOTH => SQLSRV_FETCH_BOTH, + PDO::FETCH_ASSOC => SQLSRV_FETCH_ASSOC, + PDO::FETCH_NUM => SQLSRV_FETCH_NUMERIC, + ); + + /** + * Fetch Style + * + * @param int + */ + private $defaultFetchMode = PDO::FETCH_BOTH; + + /** + * @var int|null + */ + private $lastInsertId; + + /** + * Append to any INSERT query to retrieve the last insert id. + * + * @var string + */ + const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;'; + + public function __construct($conn, $sql, $lastInsertId = null) + { + $this->conn = $conn; + $this->sql = $sql; + + if (stripos($sql, 'INSERT INTO ') === 0) { + $this->sql .= self::LAST_INSERT_ID_SQL; + $this->lastInsertId = $lastInsertId; + } + } + + public function bindValue($param, $value, $type = null) + { + return $this->bindParam($param, $value, $type,null); + } + + /** + * {@inheritdoc} + */ + public function bindParam($column, &$variable, $type = null, $length = null) + { + if (!is_numeric($column)) { + throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead."); + } + + if ($type === \PDO::PARAM_LOB) { + $this->params[$column-1] = array($variable, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max')); + } else { + $this->params[$column-1] = $variable; + } + } + + public function closeCursor() + { + if ($this->stmt) { + sqlsrv_free_stmt($this->stmt); + } + } + + public function columnCount() + { + return sqlsrv_num_fields($this->stmt); + } + + /** + * {@inheritDoc} + */ + public function errorCode() + { + $errors = sqlsrv_errors(SQLSRV_ERR_ERRORS); + if ($errors) { + return $errors[0]['code']; + } + return false; + } + + /** + * {@inheritDoc} + */ + public function errorInfo() + { + return sqlsrv_errors(SQLSRV_ERR_ERRORS); + } + + public function execute($params = null) + { + if ($params) { + $hasZeroIndex = array_key_exists(0, $params); + foreach ($params as $key => $val) { + $key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key; + $this->bindValue($key, $val); + } + } + + $this->stmt = sqlsrv_query($this->conn, $this->sql, $this->params); + if ( ! $this->stmt) { + throw SQLSrvException::fromSqlSrvErrors(); + } + + if ($this->lastInsertId) { + sqlsrv_next_result($this->stmt); + sqlsrv_fetch($this->stmt); + $this->lastInsertId->setId( sqlsrv_get_field($this->stmt, 0) ); + } + } + + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + $this->defaultFetchMode = $fetchMode; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->defaultFetchMode; + if (isset(self::$fetchMap[$fetchMode])) { + return sqlsrv_fetch_array($this->stmt, self::$fetchMap[$fetchMode]); + } else if ($fetchMode == PDO::FETCH_OBJ || $fetchMode == PDO::FETCH_CLASS) { + $className = null; + $ctorArgs = null; + if (func_num_args() >= 2) { + $args = func_get_args(); + $className = $args[1]; + $ctorArgs = (isset($args[2])) ? $args[2] : array(); + } + return sqlsrv_fetch_object($this->stmt, $className, $ctorArgs); + } + + throw new SQLSrvException("Fetch mode is not supported!"); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchMode = null) + { + $className = null; + $ctorArgs = null; + if (func_num_args() >= 2) { + $args = func_get_args(); + $className = $args[1]; + $ctorArgs = (isset($args[2])) ? $args[2] : array(); + } + + $rows = array(); + while ($row = $this->fetch($fetchMode, $className, $ctorArgs)) { + $rows[] = $row; + } + return $rows; + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + return $row[$columnIndex]; + } + + /** + * {@inheritdoc} + */ + public function rowCount() + { + return sqlsrv_rows_affected($this->stmt); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php new file mode 100644 index 00000000..718614d8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/Statement.php @@ -0,0 +1,125 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use \PDO; + +/** + * Statement interface. + * Drivers must implement this interface. + * + * This resembles (a subset of) the PDOStatement interface. + * + * @author Konsta Vesterinen + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + */ +interface Statement extends ResultStatement +{ + /** + * Binds a value to a corresponding named or positional + * placeholder in the SQL statement that was used to prepare the statement. + * + * @param mixed $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $value The value to bind to the parameter. + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindValue($param, $value, $type = null); + + /** + * Binds a PHP variable to a corresponding named or question mark placeholder in the + * SQL statement that was use to prepare the statement. Unlike PDOStatement->bindValue(), + * the variable is bound as a reference and will only be evaluated at the time + * that PDOStatement->execute() is called. + * + * Most parameters are input parameters, that is, parameters that are + * used in a read-only fashion to build up the query. Some drivers support the invocation + * of stored procedures that return data as output parameters, and some also as input/output + * parameters that both send in data and are updated to receive it. + * + * @param mixed $column Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter. + * + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. To return + * an INOUT parameter from a stored procedure, use the bitwise OR operator to set the + * PDO::PARAM_INPUT_OUTPUT bits for the data_type parameter. + * @param integer $length You must specify maxlength when using an OUT bind so that PHP allocates enough memory to hold the returned value. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindParam($column, &$variable, $type = null, $length = null); + + /** + * errorCode + * Fetch the SQLSTATE associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorCode() + * @return string error code string + */ + function errorCode(); + + /** + * errorInfo + * Fetch extended error information associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorInfo() + * @return array error info array + */ + function errorInfo(); + + /** + * Executes a prepared statement + * + * If the prepared statement included parameter markers, you must either: + * call PDOStatement->bindParam() to bind PHP variables to the parameter markers: + * bound variables pass their value as input and receive the output value, + * if any, of their associated parameter markers or pass an array of input-only + * parameter values + * + * + * @param array $params An array of values with as many elements as there are + * bound parameters in the SQL statement being executed. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function execute($params = null); + + /** + * rowCount + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * executed by the corresponding object. + * + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be + * relied on for portable applications. + * + * @return integer Returns the number of rows. + */ + function rowCount(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/DriverManager.php b/doctrine/dbal/lib/Doctrine/DBAL/DriverManager.php new file mode 100644 index 00000000..7cdde951 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/DriverManager.php @@ -0,0 +1,176 @@ +. + */ + +namespace Doctrine\DBAL; + +use Doctrine\Common\EventManager; + +/** + * Factory for creating Doctrine\DBAL\Connection instances. + * + * @author Roman Borschel + * @since 2.0 + */ +final class DriverManager +{ + /** + * List of supported drivers and their mappings to the driver classes. + * + * To add your own driver use the 'driverClass' parameter to + * {@link DriverManager::getConnection()}. + * + * @var array + */ + private static $_driverMap = array( + 'pdo_mysql' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', + 'pdo_sqlite' => 'Doctrine\DBAL\Driver\PDOSqlite\Driver', + 'pdo_pgsql' => 'Doctrine\DBAL\Driver\PDOPgSql\Driver', + 'pdo_oci' => 'Doctrine\DBAL\Driver\PDOOracle\Driver', + 'oci8' => 'Doctrine\DBAL\Driver\OCI8\Driver', + 'ibm_db2' => 'Doctrine\DBAL\Driver\IBMDB2\DB2Driver', + 'pdo_ibm' => 'Doctrine\DBAL\Driver\PDOIbm\Driver', + 'pdo_sqlsrv' => 'Doctrine\DBAL\Driver\PDOSqlsrv\Driver', + 'mysqli' => 'Doctrine\DBAL\Driver\Mysqli\Driver', + 'drizzle_pdo_mysql' => 'Doctrine\DBAL\Driver\DrizzlePDOMySql\Driver', + 'sqlsrv' => 'Doctrine\DBAL\Driver\SQLSrv\Driver', + ); + + /** Private constructor. This class cannot be instantiated. */ + private function __construct() { } + + /** + * Creates a connection object based on the specified parameters. + * This method returns a Doctrine\DBAL\Connection which wraps the underlying + * driver connection. + * + * $params must contain at least one of the following. + * + * Either 'driver' with one of the following values: + * + * pdo_mysql + * pdo_sqlite + * pdo_pgsql + * pdo_oci (unstable) + * pdo_sqlsrv + * pdo_ibm (unstable) + * pdo_sqlsrv + * mysqli + * sqlsrv + * ibm_db2 (unstable) + * drizzle_pdo_mysql + * + * OR 'driverClass' that contains the full class name (with namespace) of the + * driver class to instantiate. + * + * Other (optional) parameters: + * + * user (string): + * The username to use when connecting. + * + * password (string): + * The password to use when connecting. + * + * driverOptions (array): + * Any additional driver-specific options for the driver. These are just passed + * through to the driver. + * + * pdo: + * You can pass an existing PDO instance through this parameter. The PDO + * instance will be wrapped in a Doctrine\DBAL\Connection. + * + * wrapperClass: + * You may specify a custom wrapper class through the 'wrapperClass' + * parameter but this class MUST inherit from Doctrine\DBAL\Connection. + * + * driverClass: + * The driver class to use. + * + * @param array $params The parameters. + * @param \Doctrine\DBAL\Configuration The configuration to use. + * @param \Doctrine\Common\EventManager The event manager to use. + * @return \Doctrine\DBAL\Connection + */ + public static function getConnection( + array $params, + Configuration $config = null, + EventManager $eventManager = null) + { + // create default config and event manager, if not set + if ( ! $config) { + $config = new Configuration(); + } + if ( ! $eventManager) { + $eventManager = new EventManager(); + } + + // check for existing pdo object + if (isset($params['pdo']) && ! $params['pdo'] instanceof \PDO) { + throw DBALException::invalidPdoInstance(); + } else if (isset($params['pdo'])) { + $params['pdo']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $params['driver'] = 'pdo_' . $params['pdo']->getAttribute(\PDO::ATTR_DRIVER_NAME); + } else { + self::_checkParams($params); + } + if (isset($params['driverClass'])) { + $className = $params['driverClass']; + } else { + $className = self::$_driverMap[$params['driver']]; + } + + $driver = new $className(); + + $wrapperClass = 'Doctrine\DBAL\Connection'; + if (isset($params['wrapperClass'])) { + if (is_subclass_of($params['wrapperClass'], $wrapperClass)) { + $wrapperClass = $params['wrapperClass']; + } else { + throw DBALException::invalidWrapperClass($params['wrapperClass']); + } + } + + return new $wrapperClass($params, $driver, $config, $eventManager); + } + + /** + * Checks the list of parameters. + * + * @param array $params + */ + private static function _checkParams(array $params) + { + // check existance of mandatory parameters + + // driver + if ( ! isset($params['driver']) && ! isset($params['driverClass'])) { + throw DBALException::driverRequired(); + } + + // check validity of parameters + + // driver + if ( isset($params['driver']) && ! isset(self::$_driverMap[$params['driver']])) { + throw DBALException::unknownDriver($params['driver'], array_keys(self::$_driverMap)); + } + + if (isset($params['driverClass']) && ! in_array('Doctrine\DBAL\Driver', class_implements($params['driverClass'], true))) { + throw DBALException::invalidDriverClass($params['driverClass']); + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/ConnectionEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/ConnectionEventArgs.php new file mode 100644 index 00000000..f4cb1cd8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/ConnectionEventArgs.php @@ -0,0 +1,79 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\Common\EventArgs, + Doctrine\DBAL\Connection; + +/** + * Event Arguments used when a Driver connection is established inside Doctrine\DBAL\Connection. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ConnectionEventArgs extends EventArgs +{ + /** + * @var Connection + */ + private $_connection = null; + + public function __construct(Connection $connection) + { + $this->_connection = $connection; + } + + /** + * @return \Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return \Doctrine\DBAL\Driver + */ + public function getDriver() + { + return $this->_connection->getDriver(); + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_connection->getDatabasePlatform(); + } + + /** + * @return \Doctrine\DBAL\Schema\AbstractSchemaManager + */ + public function getSchemaManager() + { + return $this->_connection->getSchemaManager(); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php new file mode 100644 index 00000000..fc22744a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php @@ -0,0 +1,74 @@ +. +*/ + +namespace Doctrine\DBAL\Event\Listeners; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +/** + * MySQL Session Init Event Subscriber which allows to set the Client Encoding of the Connection + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @deprecated Use "charset" option to PDO MySQL Connection instead. + */ +class MysqlSessionInit implements EventSubscriber +{ + /** + * @var string + */ + private $_charset; + + /** + * @var string + */ + private $_collation; + + /** + * Configure Charset and Collation options of MySQL Client for each Connection + * + * @param string $charset + * @param string $collation + */ + public function __construct($charset = 'utf8', $collation = false) + { + $this->_charset = $charset; + $this->_collation = $collation; + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) + { + $collation = ($this->_collation) ? " COLLATE ".$this->_collation : ""; + $args->getConnection()->executeUpdate("SET NAMES ".$this->_charset . $collation); + } + + public function getSubscribedEvents() + { + return array(Events::postConnect); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php new file mode 100644 index 00000000..83554032 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php @@ -0,0 +1,80 @@ +. +*/ + +namespace Doctrine\DBAL\Event\Listeners; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +/** + * Should be used when Oracle Server default enviroment does not match the Doctrine requirements. + * + * The following enviroment variables are required for the Doctrine default date format: + * + * NLS_TIME_FORMAT="HH24:MI:SS" + * NLS_DATE_FORMAT="YYYY-MM-DD HH24:MI:SS" + * NLS_TIMESTAMP_FORMAT="YYYY-MM-DD HH24:MI:SS" + * NLS_TIMESTAMP_TZ_FORMAT="YYYY-MM-DD HH24:MI:SS TZH:TZM" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +class OracleSessionInit implements EventSubscriber +{ + protected $_defaultSessionVars = array( + 'NLS_TIME_FORMAT' => "HH24:MI:SS", + 'NLS_DATE_FORMAT' => "YYYY-MM-DD HH24:MI:SS", + 'NLS_TIMESTAMP_FORMAT' => "YYYY-MM-DD HH24:MI:SS", + 'NLS_TIMESTAMP_TZ_FORMAT' => "YYYY-MM-DD HH24:MI:SS TZH:TZM", + 'NLS_NUMERIC_CHARACTERS' => ".,", + ); + + /** + * @param array $oracleSessionVars + */ + public function __construct(array $oracleSessionVars = array()) + { + $this->_defaultSessionVars = array_merge($this->_defaultSessionVars, $oracleSessionVars); + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) + { + if (count($this->_defaultSessionVars)) { + array_change_key_case($this->_defaultSessionVars, \CASE_UPPER); + $vars = array(); + foreach ($this->_defaultSessionVars as $option => $value) { + $vars[] = $option." = '".$value."'"; + } + $sql = "ALTER SESSION SET ".implode(" ", $vars); + $args->getConnection()->executeUpdate($sql); + } + } + + public function getSubscribedEvents() + { + return array(Events::postConnect); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/SQLSessionInit.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/SQLSessionInit.php new file mode 100644 index 00000000..8dfde625 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/Listeners/SQLSessionInit.php @@ -0,0 +1,63 @@ +. +*/ + +namespace Doctrine\DBAL\Event\Listeners; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +/** + * Session init listener for executing a single SQL statement right after a connection is opened. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Benjamin Eberlei + */ +class SQLSessionInit implements EventSubscriber +{ + /** + * @var string + */ + protected $sql; + + /** + * @param string $sql + */ + public function __construct($sql) + { + $this->sql = $sql; + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) + { + $conn = $args->getConnection(); + $conn->exec($this->sql); + } + + public function getSubscribedEvents() + { + return array(Events::postConnect); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableAddColumnEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableAddColumnEventArgs.php new file mode 100644 index 00000000..0200ce77 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableAddColumnEventArgs.php @@ -0,0 +1,114 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Event Arguments used when SQL queries for adding table columns are generated inside Doctrine\DBAL\Platform\*Platform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaAlterTableAddColumnEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Column + */ + private $_column = null; + + /** + * @var \Doctrine\DBAL\Schema\TableDiff + */ + private $_tableDiff = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(Column $column, TableDiff $tableDiff, AbstractPlatform $platform) + { + $this->_column = $column; + $this->_tableDiff = $tableDiff; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\Column + */ + public function getColumn() + { + return $this->_column; + } + + /** + * @return \Doctrine\DBAL\Schema\TableDiff + */ + public function getTableDiff() + { + return $this->_tableDiff; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaAlterTableAddColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableChangeColumnEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableChangeColumnEventArgs.php new file mode 100644 index 00000000..bd59d7d7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableChangeColumnEventArgs.php @@ -0,0 +1,114 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\ColumnDiff, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Event Arguments used when SQL queries for changing table columns are generated inside Doctrine\DBAL\Platform\*Platform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaAlterTableChangeColumnEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\ColumnDiff + */ + private $_columnDiff = null; + + /** + * @var \Doctrine\DBAL\Schema\TableDiff + */ + private $_tableDiff = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\ColumnDiff $columnDiff + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(ColumnDiff $columnDiff, TableDiff $tableDiff, AbstractPlatform $platform) + { + $this->_columnDiff = $columnDiff; + $this->_tableDiff = $tableDiff; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\ColumnDiff + */ + public function getColumnDiff() + { + return $this->_columnDiff; + } + + /** + * @return \Doctrine\DBAL\Schema\TableDiff + */ + public function getTableDiff() + { + return $this->_tableDiff; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaAlterTableChangeColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableEventArgs.php new file mode 100644 index 00000000..9f853388 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableEventArgs.php @@ -0,0 +1,99 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Event Arguments used when SQL queries for creating tables are generated inside Doctrine\DBAL\Platform\*Platform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaAlterTableEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\TableDiff + */ + private $_tableDiff = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(TableDiff $tableDiff, AbstractPlatform $platform) + { + $this->_tableDiff = $tableDiff; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\TableDiff + */ + public function getTableDiff() + { + return $this->_tableDiff; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaAlterTableEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRemoveColumnEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRemoveColumnEventArgs.php new file mode 100644 index 00000000..4b981f89 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRemoveColumnEventArgs.php @@ -0,0 +1,114 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Event Arguments used when SQL queries for removing table columns are generated inside Doctrine\DBAL\Platform\*Platform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaAlterTableRemoveColumnEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Column + */ + private $_column = null; + + /** + * @var \Doctrine\DBAL\Schema\TableDiff + */ + private $_tableDiff = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(Column $column, TableDiff $tableDiff, AbstractPlatform $platform) + { + $this->_column = $column; + $this->_tableDiff = $tableDiff; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\Column + */ + public function getColumn() + { + return $this->_column; + } + + /** + * @return \Doctrine\DBAL\Schema\TableDiff + */ + public function getTableDiff() + { + return $this->_tableDiff; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaAlterTableRemoveColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRenameColumnEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRenameColumnEventArgs.php new file mode 100644 index 00000000..90e6a385 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaAlterTableRenameColumnEventArgs.php @@ -0,0 +1,129 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Event Arguments used when SQL queries for renaming table columns are generated inside Doctrine\DBAL\Platform\*Platform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaAlterTableRenameColumnEventArgs extends SchemaEventArgs +{ + /** + * @var string + */ + private $_oldColumnName = null; + + /** + * @var \Doctrine\DBAL\Schema\Column + */ + private $_column = null; + + /** + * @var \Doctrine\DBAL\Schema\TableDiff + */ + private $_tableDiff = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param string $oldColumnName + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct($oldColumnName, Column $column, TableDiff $tableDiff, AbstractPlatform $platform) + { + $this->_oldColumnName = $oldColumnName; + $this->_column = $column; + $this->_tableDiff = $tableDiff; + $this->_platform = $platform; + } + + /** + * @return string + */ + public function getOldColumnName() + { + return $this->_oldColumnName; + } + + /** + * @return \Doctrine\DBAL\Schema\Column + */ + public function getColumn() + { + return $this->_column; + } + + /** + * @return \Doctrine\DBAL\Schema\TableDiff + */ + public function getTableDiff() + { + return $this->_tableDiff; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaAlterTableRenameColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaColumnDefinitionEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaColumnDefinitionEventArgs.php new file mode 100644 index 00000000..fecb0152 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaColumnDefinitionEventArgs.php @@ -0,0 +1,137 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Connection, + Doctrine\DBAL\Schema\Column; + +/** + * Event Arguments used when the portable column definition is generated inside Doctrine\DBAL\Schema\AbstractSchemaManager. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaColumnDefinitionEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Column + */ + private $_column = null; + + /** + * Raw column data as fetched from the database + * + * @var array + */ + private $_tableColumn = null; + + /** + * @var string + */ + private $_table = null; + + /** + * @var string + */ + private $_database = null; + + /** + * @var \Doctrine\DBAL\Connection + */ + private $_connection = null; + + /** + * @param array $tableColumn + * @param string $table + * @param string $database + * @param \Doctrine\DBAL\Connection $conn + */ + public function __construct(array $tableColumn, $table, $database, Connection $connection) + { + $this->_tableColumn = $tableColumn; + $this->_table = $table; + $this->_database = $database; + $this->_connection = $connection; + } + + /** + * Allows to clear the column which means the column will be excluded from + * tables column list. + * + * @param null|\Doctrine\DBAL\Schema\Column $column + * @return SchemaColumnDefinitionEventArgs + */ + public function setColumn(Column $column = null) + { + $this->_column = $column; + + return $this; + } + + /** + * @return \Doctrine\DBAL\Schema\Column + */ + public function getColumn() + { + return $this->_column; + } + + /** + * @return array + */ + public function getTableColumn() + { + return $this->_tableColumn; + } + + /** + * @return string + */ + public function getTable() + { + return $this->_table; + } + + /** + * @return string + */ + public function getDatabase() + { + return $this->_database; + } + + /** + * @return \Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_connection->getDatabasePlatform(); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableColumnEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableColumnEventArgs.php new file mode 100644 index 00000000..5e7383c0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableColumnEventArgs.php @@ -0,0 +1,114 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\Table; + +/** + * Event Arguments used when SQL queries for creating table columns are generated inside Doctrine\DBAL\Platform\AbstractPlatform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaCreateTableColumnEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Column + */ + private $_column = null; + + /** + * @var \Doctrine\DBAL\Schema\Table + */ + private $_table = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\Column $column + * @param \Doctrine\DBAL\Schema\Table $table + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(Column $column, Table $table, AbstractPlatform $platform) + { + $this->_column = $column; + $this->_table = $table; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\Column + */ + public function getColumn() + { + return $this->_column; + } + + /** + * @return \Doctrine\DBAL\Schema\Table + */ + public function getTable() + { + return $this->_table; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaCreateTableColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableEventArgs.php new file mode 100644 index 00000000..3149faa7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaCreateTableEventArgs.php @@ -0,0 +1,128 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table; + +/** + * Event Arguments used when SQL queries for creating tables are generated inside Doctrine\DBAL\Platform\AbstractPlatform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaCreateTableEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Table + */ + private $_table = null; + + /** + * @var array + */ + private $_columns = null; + + /** + * @var array + */ + private $_options = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var array + */ + private $_sql = array(); + + /** + * @param \Doctrine\DBAL\Schema\Table $table + * @param array $columns + * @param array $options + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct(Table $table, array $columns, array $options, AbstractPlatform $platform) + { + $this->_table = $table; + $this->_columns = $columns; + $this->_options = $options; + $this->_platform = $platform; + } + + /** + * @return \Doctrine\DBAL\Schema\Table + */ + public function getTable() + { + return $this->_table; + } + + /** + * @return array + */ + public function getColumns() + { + return $this->_columns; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->_options; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string|array $sql + * @return \Doctrine\DBAL\Event\SchemaCreateTableEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + $this->_sql = array_merge($this->_sql, $sql); + } else { + $this->_sql[] = $sql; + } + + return $this; + } + + /** + * @return array + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaDropTableEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaDropTableEventArgs.php new file mode 100644 index 00000000..55133be2 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaDropTableEventArgs.php @@ -0,0 +1,98 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table; + +/** + * Event Arguments used when the SQL query for dropping tables are generated inside Doctrine\DBAL\Platform\AbstractPlatform. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaDropTableEventArgs extends SchemaEventArgs +{ + /** + * @var string|\Doctrine\DBAL\Schema\Table + */ + private $_table = null; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @var string + */ + private $_sql = null; + + /** + * @param string|\Doctrine\DBAL\Schema\Table $table + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function __construct($table, AbstractPlatform $platform) + { + if ( ! $table instanceof Table && !is_string($table)) { + throw new \InvalidArgumentException('SchemaCreateTableEventArgs expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + $this->_table = $table; + $this->_platform = $platform; + } + + /** + * @return string|\Doctrine\DBAL\Schema\Table + */ + public function getTable() + { + return $this->_table; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getPlatform() + { + return $this->_platform; + } + + /** + * @param string $sql + * @return \Doctrine\DBAL\Event\SchemaDropTableEventArgs + */ + public function setSql($sql) + { + $this->_sql = $sql; + + return $this; + } + + /** + * @return string + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaEventArgs.php new file mode 100644 index 00000000..a3509fb2 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaEventArgs.php @@ -0,0 +1,56 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\Common\EventArgs; + +/** + * Base class for schema related events. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaEventArgs extends EventArgs +{ + /** + * @var boolean + */ + private $_preventDefault = false; + + /** + * @return \Doctrine\DBAL\Event\SchemaEventArgs + */ + public function preventDefault() + { + $this->_preventDefault = true; + + return $this; + } + + /** + * @return boolean + */ + public function isDefaultPrevented() + { + return $this->_preventDefault; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaIndexDefinitionEventArgs.php b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaIndexDefinitionEventArgs.php new file mode 100644 index 00000000..248d43e9 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Event/SchemaIndexDefinitionEventArgs.php @@ -0,0 +1,122 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\DBAL\Connection, + Doctrine\DBAL\Schema\Index; + +/** + * Event Arguments used when the portable index definition is generated inside Doctrine\DBAL\Schema\AbstractSchemaManager. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.2 + * @author Jan Sorgalla + */ +class SchemaIndexDefinitionEventArgs extends SchemaEventArgs +{ + /** + * @var \Doctrine\DBAL\Schema\Index + */ + private $_index = null; + + /** + * Raw index data as fetched from the database + * + * @var array + */ + private $_tableIndex = null; + + /** + * @var string + */ + private $_table = null; + + /** + * @var \Doctrine\DBAL\Connection + */ + private $_connection = null; + + /** + * @param array $tableIndex + * @param string $table + * @param \Doctrine\DBAL\Connection $conn + */ + public function __construct(array $tableIndex, $table, Connection $connection) + { + $this->_tableIndex = $tableIndex; + $this->_table = $table; + $this->_connection = $connection; + } + + /** + * Allows to clear the index which means the index will be excluded from + * tables index list. + * + * @param null|\Doctrine\DBAL\Schema\Index $index + * @return SchemaIndexDefinitionEventArgs + */ + public function setIndex(Index $index = null) + { + $this->_index = $index; + + return $this; + } + + /** + * @return \Doctrine\DBAL\Schema\Index + */ + public function getIndex() + { + return $this->_index; + } + + /** + * @return array + */ + public function getTableIndex() + { + return $this->_tableIndex; + } + + /** + * @return string + */ + public function getTable() + { + return $this->_table; + } + + /** + * @return \Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_connection->getDatabasePlatform(); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Events.php b/doctrine/dbal/lib/Doctrine/DBAL/Events.php new file mode 100644 index 00000000..0869dd97 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Events.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Container for all DBAL events. + * + * This class cannot be instantiated. + * + * @author Roman Borschel + * @since 2.0 + */ +final class Events +{ + private function __construct() {} + + const postConnect = 'postConnect'; + + const onSchemaCreateTable = 'onSchemaCreateTable'; + const onSchemaCreateTableColumn = 'onSchemaCreateTableColumn'; + const onSchemaDropTable = 'onSchemaDropTable'; + const onSchemaAlterTable = 'onSchemaAlterTable'; + const onSchemaAlterTableAddColumn = 'onSchemaAlterTableAddColumn'; + const onSchemaAlterTableRemoveColumn = 'onSchemaAlterTableRemoveColumn'; + const onSchemaAlterTableChangeColumn = 'onSchemaAlterTableChangeColumn'; + const onSchemaAlterTableRenameColumn = 'onSchemaAlterTableRenameColumn'; + const onSchemaColumnDefinition = 'onSchemaColumnDefinition'; + const onSchemaIndexDefinition = 'onSchemaIndexDefinition'; +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php new file mode 100644 index 00000000..9f254029 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php @@ -0,0 +1,159 @@ +. + */ + +namespace Doctrine\DBAL\Id; + +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Connection; + +/** + * Table ID Generator for those poor languages that are missing sequences. + * + * WARNING: The Table Id Generator clones a second independent database + * connection to work correctly. This means using the generator requests that + * generate IDs will have two open database connections. This is necessary to + * be safe from transaction failures in the main connection. Make sure to only + * ever use one TableGenerator otherwise you end up with many connections. + * + * TableID Generator does not work with SQLite. + * + * The TableGenerator does not take care of creating the SQL Table itself. You + * should look at the `TableGeneratorSchemaVisitor` to do this for you. + * Otherwise the schema for a table looks like: + * + * CREATE sequences ( + * sequence_name VARCHAR(255) NOT NULL, + * sequence_value INT NOT NULL DEFAULT '1', + * sequence_increment_by INT NOT NULL DEFAULT '1', + * PRIMARY KEY (table_name) + * ); + * + * Technically this generator works as follows: + * + * 1. Use a robust transaction serialization level. + * 2. Open transaction + * 3. Acquire a read lock on the table row (SELECT .. FOR UPDATE) + * 4. Increment current value by one and write back to database + * 5. Commit transaction + * + * If you are using a sequence_increment_by value that is larger than one the + * ID Generator will keep incrementing values until it hits the incrementation + * gap before issuing another query. + * + * If no row is present for a given sequence a new one will be created with the + * default values 'value' = 1 and 'increment_by' = 1 + * + * @author Benjamin Eberlei + */ +class TableGenerator +{ + /** + * @var \Doctrine\DBAL\Connection + */ + private $conn; + + /** + * @var string + */ + private $generatorTableName; + + /** + * @var array + */ + private $sequences = array(); + + /** + * @param Connection $conn + * @param string $generatorTableName + */ + public function __construct(Connection $conn, $generatorTableName = 'sequences') + { + $params = $conn->getParams(); + if ($params['driver'] == 'pdo_sqlite') { + throw new \Doctrine\DBAL\DBALException("Cannot use TableGenerator with SQLite."); + } + $this->conn = DriverManager::getConnection($params, $conn->getConfiguration(), $conn->getEventManager()); + $this->generatorTableName = $generatorTableName; + } + + /** + * Generate the next unused value for the given sequence name + * + * @param string + * @return int + */ + public function nextValue($sequenceName) + { + if (isset($this->sequences[$sequenceName])) { + $value = $this->sequences[$sequenceName]['value']; + $this->sequences[$sequenceName]['value']++; + if ($this->sequences[$sequenceName]['value'] >= $this->sequences[$sequenceName]['max']) { + unset ($this->sequences[$sequenceName]); + } + return $value; + } + + $this->conn->beginTransaction(); + + try { + $platform = $this->conn->getDatabasePlatform(); + $sql = "SELECT sequence_value, sequence_increment_by " . + "FROM " . $platform->appendLockHint($this->generatorTableName, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) . " " . + "WHERE sequence_name = ? " . $platform->getWriteLockSQL(); + $stmt = $this->conn->executeQuery($sql, array($sequenceName)); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $row = array_change_key_case($row, CASE_LOWER); + + $value = $row['sequence_value']; + $value++; + + if ($row['sequence_increment_by'] > 1) { + $this->sequences[$sequenceName] = array( + 'value' => $value, + 'max' => $row['sequence_value'] + $row['sequence_increment_by'] + ); + } + + $sql = "UPDATE " . $this->generatorTableName . " ". + "SET sequence_value = sequence_value + sequence_increment_by " . + "WHERE sequence_name = ? AND sequence_value = ?"; + $rows = $this->conn->executeUpdate($sql, array($sequenceName, $row['sequence_value'])); + + if ($rows != 1) { + throw new \Doctrine\DBAL\DBALException("Race-condition detected while updating sequence. Aborting generation"); + } + } else { + $this->conn->insert( + $this->generatorTableName, + array('sequence_name' => $sequenceName, 'sequence_value' => 1, 'sequence_increment_by' => 1) + ); + $value = 1; + } + + $this->conn->commit(); + + } catch(\Exception $e) { + $this->conn->rollback(); + throw new \Doctrine\DBAL\DBALException("Error occured while generating ID with TableGenerator, aborted generation: " . $e->getMessage(), 0, $e); + } + + return $value; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php new file mode 100644 index 00000000..119705d3 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php @@ -0,0 +1,89 @@ +. + */ + +namespace Doctrine\DBAL\Id; + +use Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +class TableGeneratorSchemaVisitor implements \Doctrine\DBAL\Schema\Visitor\Visitor +{ + /** + * @var string + */ + private $generatorTableName; + + public function __construct($generatorTableName = 'sequences') + { + $this->generatorTableName = $generatorTableName; + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + $table = $schema->createTable($this->generatorTableName); + $table->addColumn('sequence_name', 'string'); + $table->addColumn('sequence_value', 'integer', array('default' => 1)); + $table->addColumn('sequence_increment_by', 'integer', array('default' => 1)); + } + + /** + * @param Table $table + */ + public function acceptTable(Table $table) + { + } + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column) + { + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/LockMode.php b/doctrine/dbal/lib/Doctrine/DBAL/LockMode.php new file mode 100644 index 00000000..52d87d23 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/LockMode.php @@ -0,0 +1,42 @@ +. +*/ + +namespace Doctrine\DBAL; + +/** + * Contains all DBAL LockModes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class LockMode +{ + const NONE = 0; + const OPTIMISTIC = 1; + const PESSIMISTIC_READ = 2; + const PESSIMISTIC_WRITE = 4; + + final private function __construct() { } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php new file mode 100644 index 00000000..cb4b7c1b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php @@ -0,0 +1,68 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * Includes executed SQLs in a Debug Stack + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DebugStack implements SQLLogger +{ + /** @var array $queries Executed SQL queries. */ + public $queries = array(); + + /** @var boolean $enabled If Debug Stack is enabled (log queries) or not. */ + public $enabled = true; + + public $start = null; + + public $currentQuery = 0; + + /** + * {@inheritdoc} + */ + public function startQuery($sql, array $params = null, array $types = null) + { + if ($this->enabled) { + $this->start = microtime(true); + $this->queries[++$this->currentQuery] = array('sql' => $sql, 'params' => $params, 'types' => $types, 'executionMS' => 0); + } + } + + /** + * {@inheritdoc} + */ + public function stopQuery() + { + if ($this->enabled) { + $this->queries[$this->currentQuery]['executionMS'] = microtime(true) - $this->start; + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php new file mode 100644 index 00000000..e6969aaf --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php @@ -0,0 +1,61 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * A SQL logger that logs to the standard output using echo/var_dump. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EchoSQLLogger implements SQLLogger +{ + /** + * {@inheritdoc} + */ + public function startQuery($sql, array $params = null, array $types = null) + { + echo $sql . PHP_EOL; + + if ($params) { + var_dump($params); + } + + if ($types) { + var_dump($types); + } + } + + /** + * {@inheritdoc} + */ + public function stopQuery() + { + + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php new file mode 100644 index 00000000..76f02859 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php @@ -0,0 +1,63 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * Chains multiple SQLLogger + * + * + * @link www.doctrine-project.org + * @since 2.2 + * @author Christophe Coevoet + */ +class LoggerChain implements SQLLogger +{ + private $loggers = array(); + + /** + * Adds a logger in the chain + * + * @param SQLLogger $logger + */ + public function addLogger(SQLLogger $logger) + { + $this->loggers[] = $logger; + } + + /** + * {@inheritdoc} + */ + public function startQuery($sql, array $params = null, array $types = null) + { + foreach ($this->loggers as $logger) { + $logger->startQuery($sql, $params, $types); + } + } + + /** + * {@inheritdoc} + */ + public function stopQuery() + { + foreach ($this->loggers as $logger) { + $logger->stopQuery(); + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php new file mode 100644 index 00000000..e7ab2882 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * Interface for SQL loggers. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface SQLLogger +{ + /** + * Logs a SQL statement somewhere. + * + * @param string $sql The SQL to be executed. + * @param array $params The SQL parameters. + * @param array $types The SQL parameter types. + * @return void + */ + public function startQuery($sql, array $params = null, array $types = null); + + /** + * Mark the last started query as stopped. This can be used for timing of queries. + * + * @return void + */ + public function stopQuery(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php new file mode 100644 index 00000000..e53b5f64 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -0,0 +1,2857 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Connection, + Doctrine\DBAL\Types, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ColumnDiff, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Events, + Doctrine\Common\EventManager, + Doctrine\DBAL\Event\SchemaCreateTableEventArgs, + Doctrine\DBAL\Event\SchemaCreateTableColumnEventArgs, + Doctrine\DBAL\Event\SchemaDropTableEventArgs, + Doctrine\DBAL\Event\SchemaAlterTableEventArgs, + Doctrine\DBAL\Event\SchemaAlterTableAddColumnEventArgs, + Doctrine\DBAL\Event\SchemaAlterTableRemoveColumnEventArgs, + Doctrine\DBAL\Event\SchemaAlterTableChangeColumnEventArgs, + Doctrine\DBAL\Event\SchemaAlterTableRenameColumnEventArgs; + +/** + * Base class for all DatabasePlatforms. The DatabasePlatforms are the central + * point of abstraction of platform-specific behaviors, features and SQL dialects. + * They are a passive source of information. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @todo Remove any unnecessary methods. + */ +abstract class AbstractPlatform +{ + /** + * @var integer + */ + const CREATE_INDEXES = 1; + + /** + * @var integer + */ + const CREATE_FOREIGNKEYS = 2; + + /** + * @var integer + */ + const TRIM_UNSPECIFIED = 0; + + /** + * @var integer + */ + const TRIM_LEADING = 1; + + /** + * @var integer + */ + const TRIM_TRAILING = 2; + + /** + * @var integer + */ + const TRIM_BOTH = 3; + + /** + * @var array + */ + protected $doctrineTypeMapping = null; + + /** + * Contains a list of all columns that should generate parseable column comments for type-detection + * in reverse engineering scenarios. + * + * @var array + */ + protected $doctrineTypeComments = null; + + /** + * @var Doctrine\Common\EventManager + */ + protected $_eventManager; + + /** + * Holds the KeywordList instance for the current platform. + * + * @var \Doctrine\DBAL\Platforms\Keywords\KeywordList + */ + protected $_keywords; + + /** + * Constructor. + */ + public function __construct() {} + + /** + * Sets the EventManager used by the Platform. + * + * @param \Doctrine\Common\EventManager + */ + public function setEventManager(EventManager $eventManager) + { + $this->_eventManager = $eventManager; + } + + /** + * Gets the EventManager used by the Platform. + * + * @return \Doctrine\Common\EventManager + */ + public function getEventManager() + { + return $this->_eventManager; + } + + /** + * Gets the SQL snippet that declares a boolean column. + * + * @param array $columnDef + * + * @return string + */ + abstract public function getBooleanTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares a 4 byte integer column. + * + * @param array $columnDef + * + * @return string + */ + abstract public function getIntegerTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares an 8 byte integer column. + * + * @param array $columnDef + * + * @return string + */ + abstract public function getBigIntTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares a 2 byte integer column. + * + * @param array $columnDef + * + * @return string + */ + abstract public function getSmallIntTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares common properties of an integer column. + * + * @param array $columnDef + * @return string + */ + abstract protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef); + + /** + * Lazy load Doctrine Type Mappings + * + * @return void + */ + abstract protected function initializeDoctrineTypeMappings(); + + /** + * Initialize Doctrine Type Mappings with the platform defaults + * and with all additional type mappings. + */ + private function initializeAllDoctrineTypeMappings() + { + $this->initializeDoctrineTypeMappings(); + + foreach (Type::getTypesMap() as $typeName => $className) { + foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) { + $this->doctrineTypeMapping[$dbType] = $typeName; + } + } + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column type. + * + * @param array $field + * + * @return string + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( !isset($field['length'])) { + $field['length'] = $this->getVarcharDefaultLength(); + } + + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + if ($field['length'] > $this->getVarcharMaxLength()) { + return $this->getClobTypeDeclarationSQL($field); + } + + return $this->getVarcharTypeDeclarationSQLSnippet($field['length'], $fixed); + } + + /** + * Get the SQL Snippet to create a GUID/UUID field. + * + * By default this maps directly to a VARCHAR and only maps to more + * special datatypes when the underlying databases support this datatype. + * + * @param array $field + * + * @return string + */ + public function getGuidTypeDeclarationSQL(array $field) + { + return $this->getVarcharTypeDeclarationSQL($field); + } + + /** + * @param integer $length + * @param boolean $fixed + * + * @return string + * + * @throws \Doctrine\DBAL\DBALException + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + throw DBALException::notSupported('VARCHARs not supported by Platform.'); + } + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * + * @param array $field + * + * @return string + */ + abstract public function getClobTypeDeclarationSQL(array $field); + + /** + * Gets the SQL Snippet used to declare a BLOB column type. + * + * @param array $field + * + * @return string + */ + abstract public function getBlobTypeDeclarationSQL(array $field); + + /** + * Gets the name of the platform. + * + * @return string + */ + abstract public function getName(); + + /** + * Register a doctrine type to be used in conjunction with a column type of this platform. + * + * @param string $dbType + * @param string $doctrineType + * + * @throws \Doctrine\DBAL\DBALException if the type is not found + */ + public function registerDoctrineTypeMapping($dbType, $doctrineType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + if (!Types\Type::hasType($doctrineType)) { + throw DBALException::typeNotFound($doctrineType); + } + + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $doctrineType; + } + + /** + * Get the Doctrine type that is mapped for the given database column type. + * + * @param string $dbType + * + * @return string + */ + public function getDoctrineTypeMapping($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + + if (!isset($this->doctrineTypeMapping[$dbType])) { + throw new \Doctrine\DBAL\DBALException("Unknown database type ".$dbType." requested, " . get_class($this) . " may not support it."); + } + + return $this->doctrineTypeMapping[$dbType]; + } + + /** + * Check if a database type is currently supported by this platform. + * + * @param string $dbType + * + * @return boolean + */ + public function hasDoctrineTypeMappingFor($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + return isset($this->doctrineTypeMapping[$dbType]); + } + + /** + * Initialize the Doctrine Type comments instance variable for in_array() checks. + * + * @return void + */ + protected function initializeCommentedDoctrineTypes() + { + $this->doctrineTypeComments = array(); + + foreach (Type::getTypesMap() as $typeName => $className) { + $type = Type::getType($typeName); + + if ($type->requiresSQLCommentHint($this)) { + $this->doctrineTypeComments[] = $typeName; + } + } + } + + /** + * Is it necessary for the platform to add a parsable type comment to allow reverse engineering the given type? + * + * @param Type $doctrineType + * + * @return boolean + */ + public function isCommentedDoctrineType(Type $doctrineType) + { + if ($this->doctrineTypeComments === null) { + $this->initializeCommentedDoctrineTypes(); + } + + return in_array($doctrineType->getName(), $this->doctrineTypeComments); + } + + /** + * Mark this type as to be commented in ALTER TABLE and CREATE TABLE statements. + * + * @param string|Type $doctrineType + * + * @return void + */ + public function markDoctrineTypeCommented($doctrineType) + { + if ($this->doctrineTypeComments === null) { + $this->initializeCommentedDoctrineTypes(); + } + + $this->doctrineTypeComments[] = $doctrineType instanceof Type ? $doctrineType->getName() : $doctrineType; + } + + /** + * Get the comment to append to a column comment that helps parsing this type in reverse engineering. + * + * @param Type $doctrineType + * @return string + */ + public function getDoctrineTypeComment(Type $doctrineType) + { + return '(DC2Type:' . $doctrineType->getName() . ')'; + } + + /** + * Return the comment of a passed column modified by potential doctrine type comment hints. + * + * @param Column $column + * @return string + */ + protected function getColumnComment(Column $column) + { + $comment = $column->getComment(); + + if ($this->isCommentedDoctrineType($column->getType())) { + $comment .= $this->getDoctrineTypeComment($column->getType()); + } + + return $comment; + } + + /** + * Gets the character used for identifier quoting. + * + * @return string + */ + public function getIdentifierQuoteCharacter() + { + return '"'; + } + + /** + * Gets the string portion that starts an SQL comment. + * + * @return string + */ + public function getSqlCommentStartString() + { + return "--"; + } + + /** + * Gets the string portion that ends an SQL comment. + * + * @return string + */ + public function getSqlCommentEndString() + { + return "\n"; + } + + /** + * Gets the maximum length of a varchar field. + * + * @return integer + */ + public function getVarcharMaxLength() + { + return 4000; + } + + /** + * Gets the default length of a varchar field. + * + * @return integer + */ + public function getVarcharDefaultLength() + { + return 255; + } + + /** + * Gets all SQL wildcard characters of the platform. + * + * @return array + */ + public function getWildcards() + { + return array('%', '_'); + } + + /** + * Returns the regular expression operator. + * + * @return string + */ + public function getRegexpExpression() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + */ + public function getGuidExpression() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns the average value of a column + * + * @param string $column the column to use + * + * @return string generated sql including an AVG aggregate function + */ + public function getAvgExpression($column) + { + return 'AVG(' . $column . ')'; + } + + /** + * Returns the number of rows (without a NULL value) of a column + * + * If a '*' is used instead of a column the number of selected rows + * is returned. + * + * @param string|integer $column the column to use + * + * @return string generated sql including a COUNT aggregate function + */ + public function getCountExpression($column) + { + return 'COUNT(' . $column . ')'; + } + + /** + * Returns the highest value of a column + * + * @param string $column the column to use + * @return string generated sql including a MAX aggregate function + */ + public function getMaxExpression($column) + { + return 'MAX(' . $column . ')'; + } + + /** + * Returns the lowest value of a column + * + * @param string $column the column to use + * @return string + */ + public function getMinExpression($column) + { + return 'MIN(' . $column . ')'; + } + + /** + * Returns the total sum of a column + * + * @param string $column the column to use + * @return string + */ + public function getSumExpression($column) + { + return 'SUM(' . $column . ')'; + } + + // scalar functions + + /** + * Returns the md5 sum of a field. + * + * Note: Not SQL92, but common functionality + * + * @param string $column + * @return string + */ + public function getMd5Expression($column) + { + return 'MD5(' . $column . ')'; + } + + /** + * Returns the length of a text field. + * + * @param string $column + * + * @return string + */ + public function getLengthExpression($column) + { + return 'LENGTH(' . $column . ')'; + } + + /** + * Returns the squared value of a column + * + * @param string $column the column to use + * + * @return string generated sql including an SQRT aggregate function + */ + public function getSqrtExpression($column) + { + return 'SQRT(' . $column . ')'; + } + + /** + * Rounds a numeric field to the number of decimals specified. + * + * @param string $column + * @param integer $decimals + * + * @return string + */ + public function getRoundExpression($column, $decimals = 0) + { + return 'ROUND(' . $column . ', ' . $decimals . ')'; + } + + /** + * Returns the remainder of the division operation + * $expression1 / $expression2. + * + * @param string $expression1 + * @param string $expression2 + * + * @return string + */ + public function getModExpression($expression1, $expression2) + { + return 'MOD(' . $expression1 . ', ' . $expression2 . ')'; + } + + /** + * Trim a string, leading/trailing/both and with a given char which defaults to space. + * + * @param string $str + * @param integer $pos + * @param string $char has to be quoted already + * + * @return string + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + $posStr = ''; + $trimChar = ($char != false) ? $char . ' FROM ' : ''; + + switch ($pos) { + case self::TRIM_LEADING: + $posStr = 'LEADING '.$trimChar; + break; + + case self::TRIM_TRAILING: + $posStr = 'TRAILING '.$trimChar; + break; + + case self::TRIM_BOTH: + $posStr = 'BOTH '.$trimChar; + break; + } + + return 'TRIM(' . $posStr . $str . ')'; + } + + /** + * rtrim + * returns the string $str with proceeding space characters removed + * + * @param string $str literal string or column name + * + * @return string + */ + public function getRtrimExpression($str) + { + return 'RTRIM(' . $str . ')'; + } + + /** + * ltrim + * returns the string $str with leading space characters removed + * + * @param string $str literal string or column name + * + * @return string + */ + public function getLtrimExpression($str) + { + return 'LTRIM(' . $str . ')'; + } + + /** + * upper + * Returns the string $str with all characters changed to + * uppercase according to the current character set mapping. + * + * @param string $str literal string or column name + * + * @return string + */ + public function getUpperExpression($str) + { + return 'UPPER(' . $str . ')'; + } + + /** + * lower + * Returns the string $str with all characters changed to + * lowercase according to the current character set mapping. + * + * @param string $str literal string or column name + * + * @return string + */ + public function getLowerExpression($str) + { + return 'LOWER(' . $str . ')'; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $str literal string + * @param string $substr literal string to find + * @param integer $startPos position to start at, beginning of string by default + * + * @return string + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns the current system date. + * + * @return string + */ + public function getNowExpression() + { + return 'NOW()'; + } + + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function + * + * @param string $value an sql string literal or column name/alias + * @param integer $from where to start the substring portion + * @param integer $length the substring portion length + * + * @return string + */ + public function getSubstringExpression($value, $from, $length = null) + { + if ($length === null) { + return 'SUBSTRING(' . $value . ' FROM ' . $from . ')'; + } + + return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $length . ')'; + } + + /** + * Returns a series of strings concatinated + * + * concat() accepts an arbitrary number of parameters. Each parameter + * must contain an expression + * + * @param string $arg1, $arg2 ... $argN strings that will be concatenated. + * + * @return string + */ + public function getConcatExpression() + { + return join(' || ' , func_get_args()); + } + + /** + * Returns the SQL for a logical not. + * + * Example: + * + * $q = new Doctrine_Query(); + * $e = $q->expr; + * $q->select('*')->from('table') + * ->where($e->eq('id', $e->not('null')); + * + * + * @param string $expression + * + * @return string a logical expression + */ + public function getNotExpression($expression) + { + return 'NOT(' . $expression . ')'; + } + + /** + * Returns the SQL to check if a value is one in a set of + * given values. + * + * in() accepts an arbitrary number of parameters. The first parameter + * must always specify the value that should be matched against. Successive + * must contain a logical expression or an array with logical expressions. + * These expressions will be matched against the first parameter. + * + * @param string $column the value that should be matched against + * @param string|array $values values that will be matched against $column + * + * @return string logical expression + */ + public function getInExpression($column, $values) + { + if ( ! is_array($values)) { + $values = array($values); + } + + // TODO: fix this code: the method does not exist + $values = $this->getIdentifiers($values); + + if (count($values) == 0) { + throw new \InvalidArgumentException('Values must not be empty.'); + } + + return $column . ' IN (' . implode(', ', $values) . ')'; + } + + /** + * Returns SQL that checks if a expression is null. + * + * @param string $expression the expression that should be compared to null + * + * @return string logical expression + */ + public function getIsNullExpression($expression) + { + return $expression . ' IS NULL'; + } + + /** + * Returns SQL that checks if a expression is not null. + * + * @param string $expression the expression that should be compared to null + * + * @return string logical expression + */ + public function getIsNotNullExpression($expression) + { + return $expression . ' IS NOT NULL'; + } + + /** + * Returns SQL that checks if an expression evaluates to a value between + * two values. + * + * The parameter $expression is checked if it is between $value1 and $value2. + * + * Note: There is a slight difference in the way BETWEEN works on some databases. + * http://www.w3schools.com/sql/sql_between.asp. If you want complete database + * independence you should avoid using between(). + * + * @param string $expression the value to compare to + * @param string $value1 the lower value to compare with + * @param string $value2 the higher value to compare with + * + * @return string logical expression + */ + public function getBetweenExpression($expression, $value1, $value2) + { + return $expression . ' BETWEEN ' .$value1 . ' AND ' . $value2; + } + + public function getAcosExpression($value) + { + return 'ACOS(' . $value . ')'; + } + + public function getSinExpression($value) + { + return 'SIN(' . $value . ')'; + } + + public function getPiExpression() + { + return 'PI()'; + } + + public function getCosExpression($value) + { + return 'COS(' . $value . ')'; + } + + /** + * Calculate the difference in days between the two passed dates. + * + * Computes diff = date1 - date2 + * + * @param string $date1 + * @param string $date2 + * + * @return string + */ + public function getDateDiffExpression($date1, $date2) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Add the number of given days to a date. + * + * @param string $date + * @param integer $days + * + * @return string + */ + public function getDateAddDaysExpression($date, $days) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Substract the number of given days to a date. + * + * @param string $date + * @param integer $days + * + * @return string + */ + public function getDateSubDaysExpression($date, $days) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Add the number of given months to a date. + * + * @param string $date + * @param integer $months + * + * @return string + */ + public function getDateAddMonthExpression($date, $months) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Substract the number of given months to a date. + * + * @param string $date + * @param integer $months + * + * @return string + */ + public function getDateSubMonthExpression($date, $months) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets SQL bit AND comparison expression + * + * @param string $value1 + * @param string $value2 + * + * @return string + */ + public function getBitAndComparisonExpression($value1, $value2) + { + return '(' . $value1 . ' & ' . $value2 . ')'; + } + + /** + * Gets SQL bit OR comparison expression + * + * @param string $value1 + * @param string $value2 + * + * @return string + */ + public function getBitOrComparisonExpression($value1, $value2) + { + return '(' . $value1 . ' | ' . $value2 . ')'; + } + + public function getForUpdateSQL() + { + return 'FOR UPDATE'; + } + + /** + * Honors that some SQL vendors such as MsSql use table hints for locking instead of the ANSI SQL FOR UPDATE specification. + * + * @param string $fromClause + * @param integer $lockMode + * + * @return string + */ + public function appendLockHint($fromClause, $lockMode) + { + return $fromClause; + } + + /** + * Get the sql snippet to append to any SELECT statement which locks rows in shared read lock. + * + * This defaults to the ASNI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database + * vendors allow to lighten this constraint up to be a real read lock. + * + * @return string + */ + public function getReadLockSQL() + { + return $this->getForUpdateSQL(); + } + + /** + * Get the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. + * + * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ASNI SQL standard. + * + * @return string + */ + public function getWriteLockSQL() + { + return $this->getForUpdateSQL(); + } + + /** + * Get the SQL snippet to drop an existing database + * + * @param string $database name of the database that should be dropped + * + * @return string + */ + public function getDropDatabaseSQL($database) + { + return 'DROP DATABASE ' . $database; + } + + /** + * Drop a Table + * + * @throws \InvalidArgumentException + * + * @param Table|string $table + * + * @return string + */ + public function getDropTableSQL($table) + { + $tableArg = $table; + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('getDropTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + if (null !== $this->_eventManager && $this->_eventManager->hasListeners(Events::onSchemaDropTable)) { + $eventArgs = new SchemaDropTableEventArgs($tableArg, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaDropTable, $eventArgs); + + if ($eventArgs->isDefaultPrevented()) { + return $eventArgs->getSql(); + } + } + + return 'DROP TABLE ' . $table; + } + + /** + * Get SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. + * + * @param Table|string $table + * + * @return string + */ + public function getDropTemporaryTableSQL($table) + { + return $this->getDropTableSQL($table); + } + + /** + * Drop index from a table + * + * @param Index|string $name + * @param string|Table $table + * + * @return string + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + $index = $index->getQuotedName($this); + } else if(!is_string($index)) { + throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + return 'DROP INDEX ' . $index; + } + + /** + * Get drop constraint sql + * + * @param \Doctrine\DBAL\Schema\Constraint $constraint + * @param string|Table $table + * + * @return string + */ + public function getDropConstraintSQL($constraint, $table) + { + if ($constraint instanceof Constraint) { + $constraint = $constraint->getQuotedName($this); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint; + } + + /** + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * + * @return string + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; + } + + /** + * Gets the SQL statement(s) to create a table with the specified name, columns and constraints + * on this platform. + * + * @param string $table The name of the table. + * @param integer $createFlags + * + * @return array The sequence of SQL statements. + */ + public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDEXES) + { + if ( ! is_int($createFlags)) { + throw new \InvalidArgumentException("Second argument of AbstractPlatform::getCreateTableSQL() has to be integer."); + } + + if (count($table->getColumns()) === 0) { + throw DBALException::noColumnsSpecifiedForTable($table->getName()); + } + + $tableName = $table->getQuotedName($this); + $options = $table->getOptions(); + $options['uniqueConstraints'] = array(); + $options['indexes'] = array(); + $options['primary'] = array(); + + if (($createFlags&self::CREATE_INDEXES) > 0) { + foreach ($table->getIndexes() as $index) { + /* @var $index Index */ + if ($index->isPrimary()) { + $platform = $this; + $options['primary'] = array_map(function ($columnName) use ($table, $platform) { + return $table->getColumn($columnName)->getQuotedName($platform); + }, $index->getColumns()); + $options['primary_index'] = $index; + } else { + $options['indexes'][$index->getName()] = $index; + } + } + } + + $columnSql = array(); + $columns = array(); + + foreach ($table->getColumns() as $column) { + /* @var \Doctrine\DBAL\Schema\Column $column */ + + if (null !== $this->_eventManager && $this->_eventManager->hasListeners(Events::onSchemaCreateTableColumn)) { + $eventArgs = new SchemaCreateTableColumnEventArgs($column, $table, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaCreateTableColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + if ($eventArgs->isDefaultPrevented()) { + continue; + } + } + + $columnData = array(); + $columnData['name'] = $column->getQuotedName($this); + $columnData['type'] = $column->getType(); + $columnData['length'] = $column->getLength(); + $columnData['notnull'] = $column->getNotNull(); + $columnData['fixed'] = $column->getFixed(); + $columnData['unique'] = false; // TODO: what do we do about this? + $columnData['version'] = $column->hasPlatformOption("version") ? $column->getPlatformOption('version') : false; + + if (strtolower($columnData['type']) == "string" && $columnData['length'] === null) { + $columnData['length'] = 255; + } + + $columnData['unsigned'] = $column->getUnsigned(); + $columnData['precision'] = $column->getPrecision(); + $columnData['scale'] = $column->getScale(); + $columnData['default'] = $column->getDefault(); + $columnData['columnDefinition'] = $column->getColumnDefinition(); + $columnData['autoincrement'] = $column->getAutoincrement(); + $columnData['comment'] = $this->getColumnComment($column); + + if (in_array($column->getName(), $options['primary'])) { + $columnData['primary'] = true; + } + + $columns[$columnData['name']] = $columnData; + } + + if (($createFlags&self::CREATE_FOREIGNKEYS) > 0) { + $options['foreignKeys'] = array(); + foreach ($table->getForeignKeys() as $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; + } + } + + if (null !== $this->_eventManager && $this->_eventManager->hasListeners(Events::onSchemaCreateTable)) { + $eventArgs = new SchemaCreateTableEventArgs($table, $columns, $options, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaCreateTable, $eventArgs); + + if ($eventArgs->isDefaultPrevented()) { + return array_merge($eventArgs->getSql(), $columnSql); + } + } + + $sql = $this->_getCreateTableSQL($tableName, $columns, $options); + if ($this->supportsCommentOnStatement()) { + foreach ($table->getColumns() as $column) { + if ($this->getColumnComment($column)) { + $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getName(), $this->getColumnComment($column)); + } + } + } + + return array_merge($sql, $columnSql); + } + + public function getCommentOnColumnSQL($tableName, $columnName, $comment) + { + return "COMMENT ON COLUMN " . $tableName . "." . $columnName . " IS '" . $comment . "'"; + } + + /** + * Gets the SQL used to create a table. + * + * @param string $tableName + * @param array $columns + * @param array $options + * + * @return array + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $columnListSql .= ', ' . $this->getIndexDeclarationSQL($index, $definition); + } + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if ( ! empty($check)) { + $query .= ', ' . $check; + } + $query .= ')'; + + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE TEMPORARY TABLE"; + } + + /** + * Gets the SQL to create a sequence on this platform. + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * + * @return string + * + * @throws DBALException + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets the SQL statement to change a sequence on this platform. + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * + * @return string + */ + public function getAlterSequenceSQL(Sequence $sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets the SQL to create a constraint on a table on this platform. + * + * @param \Doctrine\DBAL\Schema\Constraint $constraint + * @param string|Table $table + * + * @return string + */ + public function getCreateConstraintSQL(Constraint $constraint, $table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this); + + $columns = array(); + foreach ($constraint->getColumns() as $column) { + $columns[] = $column; + } + $columnList = '('. implode(', ', $columns) . ')'; + + $referencesClause = ''; + if ($constraint instanceof Index) { + if($constraint->isPrimary()) { + $query .= ' PRIMARY KEY'; + } elseif ($constraint->isUnique()) { + $query .= ' UNIQUE'; + } else { + throw new \InvalidArgumentException( + 'Can only create primary or unique constraints, no common indexes with getCreateConstraintSQL().' + ); + } + } else if ($constraint instanceof ForeignKeyConstraint) { + $query .= ' FOREIGN KEY'; + + $foreignColumns = array(); + foreach ($constraint->getForeignColumns() as $column) { + $foreignColumns[] = $column; + } + + $referencesClause = ' REFERENCES '.$constraint->getForeignTableName(). ' ('.implode(', ', $foreignColumns).')'; + } + $query .= ' '.$columnList.$referencesClause; + + return $query; + } + + /** + * Gets the SQL to create an index on a table on this platform. + * + * @param Index $index + * @param string|Table $table name of the table on which the index is to be created + * + * @return string + */ + public function getCreateIndexSQL(Index $index, $table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($columns) . ')'; + + return $query; + } + + /** + * Adds additional flags for index generation + * + * @param Index $index + * + * @return string + */ + protected function getCreateIndexSQLFlags(Index $index) + { + return $index->isUnique() ? 'UNIQUE ' : ''; + } + + /** + * Get SQL to create an unnamed primary key constraint. + * + * @param Index $index + * @param string|Table $table + * + * @return string + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index->getColumns()) . ')'; + } + + /** + * Quotes a string so that it can be safely used as a table or column name, + * even if it is a reserved word of the platform. This also detects identifier + * chains separated by dot and quotes them independently. + * + * NOTE: Just because you CAN use quoted identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str identifier name to be quoted + * + * @return string quoted identifier string + */ + public function quoteIdentifier($str) + { + if (strpos($str, ".") !== false) { + $parts = array_map(array($this, "quoteIdentifier"), explode(".", $str)); + + return implode(".", $parts); + } + + return $this->quoteSingleIdentifier($str); + } + + /** + * Quote a single identifier (no dot chain separation) + * + * @param string $str + * + * @return string + */ + public function quoteSingleIdentifier($str) + { + $c = $this->getIdentifierQuoteCharacter(); + + return $c . str_replace($c, $c.$c, $str) . $c; + } + + /** + * Create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey ForeignKey instance + * @param string|Table $table name of the table on which the foreign key is to be created + * + * @return string + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + $query = 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); + + return $query; + } + + /** + * Gets the sql statements for altering an existing table. + * + * The method returns an array of sql statements, since some platforms need several statements. + * + * @param TableDiff $diff + * + * @return array + */ + public function getAlterTableSQL(TableDiff $diff) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * @param Column $column + * @param TableDiff $diff + * @param array $columnSql + * + * @return boolean + */ + protected function onSchemaAlterTableAddColumn(Column $column, TableDiff $diff, &$columnSql) + { + if (null === $this->_eventManager) { + return false; + } + + if ( ! $this->_eventManager->hasListeners(Events::onSchemaAlterTableAddColumn)) { + return false; + } + + $eventArgs = new SchemaAlterTableAddColumnEventArgs($column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableAddColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param Column $column + * @param TableDiff $diff + * @param array $columnSql + * + * @return boolean + */ + protected function onSchemaAlterTableRemoveColumn(Column $column, TableDiff $diff, &$columnSql) + { + if (null === $this->_eventManager) { + return false; + } + + if ( ! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRemoveColumn)) { + return false; + } + + $eventArgs = new SchemaAlterTableRemoveColumnEventArgs($column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRemoveColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param ColumnDiff $columnDiff + * @param TableDiff $diff + * @param array $columnSql + * + * @return boolean + */ + protected function onSchemaAlterTableChangeColumn(ColumnDiff $columnDiff, TableDiff $diff, &$columnSql) + { + if (null === $this->_eventManager) { + return false; + } + + if ( ! $this->_eventManager->hasListeners(Events::onSchemaAlterTableChangeColumn)) { + return false; + } + + $eventArgs = new SchemaAlterTableChangeColumnEventArgs($columnDiff, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableChangeColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param string $oldColumnName + * @param Column $column + * @param TableDiff $diff + * @param array $columnSql + * + * @return boolean + */ + protected function onSchemaAlterTableRenameColumn($oldColumnName, Column $column, TableDiff $diff, &$columnSql) + { + if (null === $this->_eventManager) { + return false; + } + + if ( ! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRenameColumn)) { + return false; + } + + $eventArgs = new SchemaAlterTableRenameColumnEventArgs($oldColumnName, $column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRenameColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param TableDiff $diff + * @param array $sql + * + * @return boolean + */ + protected function onSchemaAlterTable(TableDiff $diff, &$sql) + { + if (null === $this->_eventManager) { + return false; + } + + if ( ! $this->_eventManager->hasListeners(Events::onSchemaAlterTable)) { + return false; + } + + $eventArgs = new SchemaAlterTableEventArgs($diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTable, $eventArgs); + + $sql = array_merge($sql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $tableName = $diff->name; + + $sql = array(); + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->removedForeignKeys as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName); + } + foreach ($diff->changedForeignKeys as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName); + } + } + + foreach ($diff->removedIndexes as $index) { + $sql[] = $this->getDropIndexSQL($index, $tableName); + } + foreach ($diff->changedIndexes as $index) { + $sql[] = $this->getDropIndexSQL($index, $tableName); + } + + return $sql; + } + + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $tableName = false !== $diff->newName ? $diff->newName : $diff->name; + + $sql = array(); + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->addedForeignKeys as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + foreach ($diff->changedForeignKeys as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + } + + foreach ($diff->addedIndexes as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + foreach ($diff->changedIndexes as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + + return $sql; + } + + /** + * Common code for alter table statement generation that updates the changed Index and Foreign Key definitions. + * + * @param TableDiff $diff + * + * @return array + */ + protected function _getAlterTableIndexForeignKeySQL(TableDiff $diff) + { + return array_merge($this->getPreAlterTableIndexForeignKeySQL($diff), $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + /** + * Get declaration of a number of fields in bulk + * + * @param array $fields a multidimensional associative array. + * The first dimension determines the field name, while the second + * dimension is keyed with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * + * @return string + */ + public function getColumnDeclarationListSQL(array $fields) + { + $queryFields = array(); + + foreach ($fields as $fieldName => $field) { + $queryFields[] = $this->getColumnDeclarationSQL($fieldName, $field); + } + + return implode(', ', $queryFields); + } + + /** + * Obtain DBMS specific SQL code portion needed to declare a generic type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * check + * column check constraint + * columnDefinition + * a string that defines the complete column + * + * @return string DBMS specific SQL code portion that should be used to declare the column. + */ + public function getColumnDeclarationSQL($name, array $field) + { + if (isset($field['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSQL($field); + } else { + $default = $this->getDefaultValueDeclarationSQL($field); + + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSQL($field['charset']) : ''; + + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : ''; + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclarationSQL() : ''; + + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; + + $typeDecl = $field['type']->getSqlDeclaration($field, $this); + $columnDef = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + } + + if ($this->supportsInlineColumnComments() && isset($field['comment']) && $field['comment']) { + $columnDef .= " COMMENT '" . $field['comment'] . "'"; + } + + return $name . ' ' . $columnDef; + } + + /** + * Gets the SQL snippet that declares a floating point column of arbitrary precision. + * + * @param array $columnDef + * + * @return string + */ + public function getDecimalTypeDeclarationSQL(array $columnDef) + { + $columnDef['precision'] = ( ! isset($columnDef['precision']) || empty($columnDef['precision'])) + ? 10 : $columnDef['precision']; + $columnDef['scale'] = ( ! isset($columnDef['scale']) || empty($columnDef['scale'])) + ? 0 : $columnDef['scale']; + + return 'NUMERIC(' . $columnDef['precision'] . ', ' . $columnDef['scale'] . ')'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. + * + * @param array $field field definition array + * + * @return string DBMS specific SQL code portion needed to set a default value + */ + public function getDefaultValueDeclarationSQL($field) + { + $default = empty($field['notnull']) ? ' DEFAULT NULL' : ''; + + if (isset($field['default'])) { + $default = " DEFAULT '".$field['default']."'"; + if (isset($field['type'])) { + if (in_array((string)$field['type'], array("Integer", "BigInteger", "SmallInteger"))) { + $default = " DEFAULT ".$field['default']; + } else if ((string)$field['type'] == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL()) { + $default = " DEFAULT ".$this->getCurrentTimestampSQL(); + } else if ((string) $field['type'] == 'Boolean') { + $default = " DEFAULT '" . $this->convertBooleans($field['default']) . "'"; + } + } + } + return $default; + } + + /** + * Obtain DBMS specific SQL code portion needed to set a CHECK constraint + * declaration to be used in statements like CREATE TABLE. + * + * @param array $definition check definition + * + * @return string DBMS specific SQL code portion needed to set a CHECK constraint + */ + public function getCheckDeclarationSQL(array $definition) + { + $constraints = array(); + foreach ($definition as $field => $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $field . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $field . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); + } + + /** + * Obtain DBMS specific SQL code portion needed to set a unique + * constraint declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the unique constraint + * @param Index $index index definition + * + * @return string DBMS specific SQL code portion needed + * to set a constraint + */ + public function getUniqueConstraintDeclarationSQL($name, Index $index) + { + if (count($index->getColumns()) === 0) { + throw new \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return 'CONSTRAINT ' . $name . ' UNIQUE (' + . $this->getIndexFieldDeclarationListSQL($index->getColumns()) + . ')'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the index + * @param Index $index index definition + * + * @return string DBMS specific SQL code portion needed to set an index + */ + public function getIndexDeclarationSQL($name, Index $index) + { + $type = ''; + + if ($index->isUnique()) { + $type = 'UNIQUE '; + } + + if (count($index->getColumns()) === 0) { + throw new \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return $type . 'INDEX ' . $name . ' (' + . $this->getIndexFieldDeclarationListSQL($index->getColumns()) + . ')'; + } + + /** + * getCustomTypeDeclarationSql + * Obtail SQL code portion needed to create a custom column, + * e.g. when a field has the "columnDefinition" keyword. + * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate. + * + * @param array $columnDef + * + * @return string + */ + public function getCustomTypeDeclarationSQL(array $columnDef) + { + return $columnDef['columnDefinition']; + } + + /** + * getIndexFieldDeclarationList + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param array $fields + * + * @return string + */ + public function getIndexFieldDeclarationListSQL(array $fields) + { + $ret = array(); + + foreach ($fields as $field => $definition) { + if (is_array($definition)) { + $ret[] = $field; + } else { + $ret[] = $definition; + } + } + + return implode(', ', $ret); + } + + /** + * A method to return the required SQL string that fits between CREATE ... TABLE + * to create the table as a temporary table. + * + * Should be overridden in driver classes to return the correct string for the + * specific database type. + * + * The default is to return the string "TEMPORARY" - this will result in a + * SQL error for any database that does not support temporary tables, or that + * requires a different SQL command from "CREATE TEMPORARY TABLE". + * + * @return string The string required to be placed between "CREATE" and "TABLE" + * to generate a temporary table, if possible. + */ + public function getTemporaryTableSQL() + { + return 'TEMPORARY'; + } + + /** + * Some vendors require temporary table names to be qualified specially. + * + * @param string $tableName + * + * @return string + */ + public function getTemporaryTableName($tableName) + { + return $tableName; + } + + /** + * Get sql query to show a list of database. + * + * @return string + */ + public function getShowDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey + * + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration. + */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $sql; + } + + /** + * Return the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param ForeignKeyConstraint $foreignKey foreign key definition + * + * @return string + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($this->supportsForeignKeyOnUpdate() && $foreignKey->hasOption('onUpdate')) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); + } + if ($foreignKey->hasOption('onDelete')) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + return $query; + } + + /** + * returns given referential action in uppercase if valid, otherwise throws + * an exception + * + * @throws \InvalidArgumentException if unknown referential action given + * + * @param string $action foreign key referential action + * + * @return string + */ + public function getForeignKeyReferentialActionSQL($action) + { + $upper = strtoupper($action); + switch ($upper) { + case 'CASCADE': + case 'SET NULL': + case 'NO ACTION': + case 'RESTRICT': + case 'SET DEFAULT': + return $upper; + default: + throw new \InvalidArgumentException('Invalid foreign key action: ' . $upper); + } + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param ForeignKeyConstraint $foreignKey + * + * @return string + */ + public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = ''; + if (strlen($foreignKey->getName())) { + $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; + } + $sql .= 'FOREIGN KEY ('; + + if (count($foreignKey->getLocalColumns()) === 0) { + throw new \InvalidArgumentException("Incomplete definition. 'local' required."); + } + if (count($foreignKey->getForeignColumns()) === 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreign' required."); + } + if (strlen($foreignKey->getForeignTableName()) === 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreignTable' required."); + } + + $sql .= implode(', ', $foreignKey->getLocalColumns()) + . ') REFERENCES ' + . $foreignKey->getQuotedForeignTableName($this) . ' (' + . implode(', ', $foreignKey->getForeignColumns()) . ')'; + + return $sql; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration. + */ + public function getUniqueFieldDeclarationSQL() + { + return 'UNIQUE'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + public function getColumnCharsetDeclarationSQL($charset) + { + return ''; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + public function getColumnCollationDeclarationSQL($collation) + { + return ''; + } + + /** + * Whether the platform prefers sequences for ID generation. + * Subclasses should override this method to return TRUE if they prefer sequences. + * + * @return boolean + */ + public function prefersSequences() + { + return false; + } + + /** + * Whether the platform prefers identity columns (eg. autoincrement) for ID generation. + * Subclasses should override this method to return TRUE if they prefer identity columns. + * + * @return boolean + */ + public function prefersIdentityColumns() + { + return false; + } + + /** + * Some platforms need the boolean values to be converted. + * + * The default conversion in this implementation converts to integers (false => 0, true => 1). + * + * @param mixed $item + * + * @return mixed + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (is_bool($value)) { + $item[$k] = (int) $value; + } + } + } else if (is_bool($item)) { + $item = (int) $item; + } + + return $item; + } + + /** + * Gets the SQL specific for the platform to get the current date. + * + * @return string + */ + public function getCurrentDateSQL() + { + return 'CURRENT_DATE'; + } + + /** + * Gets the SQL specific for the platform to get the current time. + * + * @return string + */ + public function getCurrentTimeSQL() + { + return 'CURRENT_TIME'; + } + + /** + * Gets the SQL specific for the platform to get the current timestamp + * + * @return string + */ + public function getCurrentTimestampSQL() + { + return 'CURRENT_TIMESTAMP'; + } + + /** + * Get sql for transaction isolation level Connection constant + * + * @param integer $level + * + * @return string + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case Connection::TRANSACTION_READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + case Connection::TRANSACTION_READ_COMMITTED: + return 'READ COMMITTED'; + case Connection::TRANSACTION_REPEATABLE_READ: + return 'REPEATABLE READ'; + case Connection::TRANSACTION_SERIALIZABLE: + return 'SERIALIZABLE'; + default: + throw new \InvalidArgumentException('Invalid isolation level:' . $level); + } + } + + public function getListDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListSequencesSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableConstraintsSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableColumnsSQL($table, $database = null) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTablesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListUsersSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get the SQL to list all views of a database or user. + * + * @param string $database + * + * @return string + */ + public function getListViewsSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get the list of indexes for the current database. + * + * The current database parameter is optional but will always be passed + * when using the SchemaManager API and is the database the given table is in. + * + * Attention: Some platforms only support currentDatabase when they + * are connected with that database. Cross-database information schema + * requests may be impossible. + * + * @param string $table + * @param string $currentDatabase + * + * @return string + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableForeignKeysSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateViewSQL($name, $sql) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getDropViewSQL($name) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get the SQL snippet to drop an existing sequence + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * + * @return string + */ + public function getDropSequenceSQL($sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getSequenceNextValSQL($sequenceName) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * create a new database + * + * @param string $database name of the database that should be created + * + * @return string + */ + public function getCreateDatabaseSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get sql to set the transaction isolation level + * + * @param integer $level + * + * @return string + */ + public function getSetTransactionIsolationSQL($level) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create datetime fields in + * statements like CREATE TABLE + * + * @param array $fieldDeclaration + * + * @return string + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create datetime with timezone offset fields. + * + * @param array $fieldDeclaration + * + * @return string + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return $this->getDateTimeTypeDeclarationSQL($fieldDeclaration); + } + + + /** + * Obtain DBMS specific SQL to be used to create date fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * + * @return string + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create time fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * + * @return string + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getFloatDeclarationSQL(array $fieldDeclaration) + { + return 'DOUBLE PRECISION'; + } + + /** + * Gets the default transaction isolation level of the platform. + * + * @return integer The default isolation level. + * + * @see Doctrine\DBAL\Connection\TRANSACTION_* constants. + */ + public function getDefaultTransactionIsolationLevel() + { + return Connection::TRANSACTION_READ_COMMITTED; + } + + /* supports*() methods */ + + /** + * Whether the platform supports sequences. + * + * @return boolean + */ + public function supportsSequences() + { + return false; + } + + /** + * Whether the platform supports identity columns. + * Identity columns are columns that recieve an auto-generated value from the + * database on insert of a row. + * + * @return boolean + */ + public function supportsIdentityColumns() + { + return false; + } + + /** + * Whether the platform supports indexes. + * + * @return boolean + */ + public function supportsIndexes() + { + return true; + } + + /** + * Whether the platform supports altering tables. + * + * @return boolean + */ + public function supportsAlterTable() + { + return true; + } + + /** + * Whether the platform supports transactions. + * + * @return boolean + */ + public function supportsTransactions() + { + return true; + } + + /** + * Whether the platform supports savepoints. + * + * @return boolean + */ + public function supportsSavepoints() + { + return true; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return boolean + */ + public function supportsReleaseSavepoints() + { + return $this->supportsSavepoints(); + } + + /** + * Whether the platform supports primary key constraints. + * + * @return boolean + */ + public function supportsPrimaryConstraints() + { + return true; + } + + /** + * Does the platform supports foreign key constraints? + * + * @return boolean + */ + public function supportsForeignKeyConstraints() + { + return true; + } + + /** + * Does this platform supports onUpdate in foreign key constraints? + * + * @return boolean + */ + public function supportsForeignKeyOnUpdate() + { + return ($this->supportsForeignKeyConstraints() && true); + } + + /** + * Whether the platform supports database schemas. + * + * @return boolean + */ + public function supportsSchemas() + { + return false; + } + + /** + * Can this platform emulate schemas? + * + * Platforms that either support or emulate schemas don't automatically + * filter a schema for the namespaced elements in {@link + * AbstractManager#createSchema}. + * + * @return boolean + */ + public function canEmulateSchemas() + { + return false; + } + + /** + * Some databases don't allow to create and drop databases at all or only with certain tools. + * + * @return boolean + */ + public function supportsCreateDropDatabase() + { + return true; + } + + /** + * Whether the platform supports getting the affected rows of a recent + * update/delete type query. + * + * @return boolean + */ + public function supportsGettingAffectedRows() + { + return true; + } + + /** + * Does this plaform support to add inline column comments as postfix. + * + * @return boolean + */ + public function supportsInlineColumnComments() + { + return false; + } + + /** + * Does this platform support the propriortary synatx "COMMENT ON asset" + * + * @return boolean + */ + public function supportsCommentOnStatement() + { + return false; + } + + public function getIdentityColumnNullInsertSQL() + { + return ""; + } + + /** + * Does this platform views ? + * + * @return boolean + */ + public function supportsViews() + { + return true; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime value of this platform. + * + * @return string The format string. + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime with timezone value of this platform. + * + * @return string The format string. + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored date value of this platform. + * + * @return string The format string. + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored time value of this platform. + * + * @return string The format string. + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + /** + * Modify limit query + * + * @param string $query + * @param integer $limit + * @param integer $offset + * + * @return string + */ + final public function modifyLimitQuery($query, $limit, $offset = null) + { + if ($limit !== null) { + $limit = (int)$limit; + } + + if ($offset !== null) { + $offset = (int)$offset; + + if ($offset < 0) { + throw new DBALException("LIMIT argument offset=$offset is not valid"); + } + if ($offset > 0 && ! $this->supportsLimitOffset()) { + throw new DBALException(sprintf("Platform %s does not support offset values in limit queries.", $this->getName())); + } + } + + return $this->doModifyLimitQuery($query, $limit, $offset); + } + + /** + * Adds an driver-specific LIMIT clause to the query + * + * @param string $query + * @param integer $limit + * @param integer $offset + * + * @return string + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + if ($limit !== null) { + $query .= ' LIMIT ' . $limit; + } + + if ($offset !== null) { + $query .= ' OFFSET ' . $offset; + } + + return $query; + } + + /** + * Does the database platform support offsets in modify limit clauses? + * + * @return boolean + */ + public function supportsLimitOffset() + { + return true; + } + + /** + * Gets the character casing of a column in an SQL result set of this platform. + * + * @param string $column The column name for which to get the correct character casing. + * + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSQLResultCasing($column) + { + return $column; + } + + /** + * Makes any fixes to a name of a schema element (table, sequence, ...) that are required + * by restrictions of the platform, like a maximum length. + * + * @param string $schemaElementName + * + * @return string + */ + public function fixSchemaElementName($schemaElementName) + { + return $schemaElementName; + } + + /** + * Maximum length of any given databse identifier, like tables or column names. + * + * @return integer + */ + public function getMaxIdentifierLength() + { + return 63; + } + + /** + * Get the insert sql for an empty insert statement + * + * @param string $tableName + * @param string $identifierColumnName + * + * @return string $sql + */ + public function getEmptyIdentityInsertSQL($tableName, $identifierColumnName) + { + return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (null)'; + } + + /** + * Generate a Truncate Table SQL statement for a given table. + * + * Cascade is not supported on many platforms but would optionally cascade the truncate by + * following the foreign keys. + * + * @param string $tableName + * @param boolean $cascade + * + * @return string + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE '.$tableName; + } + + /** + * This is for test reasons, many vendors have special requirements for dummy statements. + * + * @return string + */ + public function getDummySelectSQL() + { + return 'SELECT 1'; + } + + /** + * Generate SQL to create a new savepoint + * + * @param string $savepoint + * + * @return string + */ + public function createSavePoint($savepoint) + { + return 'SAVEPOINT ' . $savepoint; + } + + /** + * Generate SQL to release a savepoint + * + * @param string $savepoint + * + * @return string + */ + public function releaseSavePoint($savepoint) + { + return 'RELEASE SAVEPOINT ' . $savepoint; + } + + /** + * Generate SQL to rollback a savepoint + * + * @param string $savepoint + * + * @return string + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TO SAVEPOINT ' . $savepoint; + } + + /** + * Return the keyword list instance of this platform. + * + * Throws exception if no keyword list is specified. + * + * @throws DBALException + * + * @return \Doctrine\DBAL\Platforms\Keywords\KeywordList + */ + final public function getReservedKeywordsList() + { + // Check for an existing instantiation of the keywords class. + if ($this->_keywords) { + return $this->_keywords; + } + + $class = $this->getReservedKeywordsClass(); + $keywords = new $class; + if ( ! $keywords instanceof \Doctrine\DBAL\Platforms\Keywords\KeywordList) { + throw DBALException::notSupported(__METHOD__); + } + + // Store the instance so it doesn't need to be generated on every request. + $this->_keywords = $keywords; + + return $keywords; + } + + /** + * The class name of the reserved keywords list. + * + * @return string + */ + protected function getReservedKeywordsClass() + { + throw DBALException::notSupported(__METHOD__); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DB2Platform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DB2Platform.php new file mode 100644 index 00000000..114cd7da --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DB2Platform.php @@ -0,0 +1,545 @@ +. +*/ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\TableDiff; + +class DB2Platform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * {@inheritDoc} + */ + public function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'smallint' => 'smallint', + 'bigint' => 'bigint', + 'integer' => 'integer', + 'time' => 'time', + 'date' => 'date', + 'varchar' => 'string', + 'character' => 'string', + 'clob' => 'text', + 'decimal' => 'decimal', + 'double' => 'float', + 'real' => 'float', + 'timestamp' => 'datetime', + ); + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + // todo clob(n) with $field['length']; + return 'CLOB(1M)'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'db2'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $columnDef) + { + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $columnDef) + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $columnDef) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $columnDef) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if ( ! empty($columnDef['autoincrement'])) { + $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; + } + + return $autoinc; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) { + return "TIMESTAMP(0) WITH DEFAULT"; + } + + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + public function getListDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListSequencesSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableConstraintsSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * This code fragment is originally from the Zend_Db_Adapter_Db2 class. + * + * @license New BSD License + * @param string $table + * @param string $database + * @return string + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT DISTINCT c.tabschema, c.tabname, c.colname, c.colno, + c.typename, c.default, c.nulls, c.length, c.scale, + c.identity, tc.type AS tabconsttype, k.colseq + FROM syscat.columns c + LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc + ON (k.tabschema = tc.tabschema + AND k.tabname = tc.tabname + AND tc.type = 'P')) + ON (c.tabschema = k.tabschema + AND c.tabname = k.tabname + AND c.colname = k.colname) + WHERE UPPER(c.tabname) = UPPER('" . $table . "') ORDER BY c.colno"; + } + + public function getListTablesSQL() + { + return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = 'T'"; + } + + public function getListUsersSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return "SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS"; + } + + /** + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + return "SELECT NAME, COLNAMES, UNIQUERULE FROM SYSIBM.SYSINDEXES WHERE TBNAME = UPPER('" . $table . "')"; + } + + public function getListTableForeignKeysSQL($table) + { + return "SELECT TBNAME, RELNAME, REFTBNAME, DELETERULE, UPDATERULE, FKCOLNAMES, PKCOLNAMES ". + "FROM SYSIBM.SYSRELS WHERE TBNAME = UPPER('".$table."')"; + } + + public function getCreateViewSQL($name, $sql) + { + return "CREATE VIEW ".$name." AS ".$sql; + } + + public function getDropViewSQL($name) + { + return "DROP VIEW ".$name; + } + + /** + * {@inheritDoc} + */ + public function getDropSequenceSQL($sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getSequenceNextValSQL($sequenceName) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($database) + { + return "CREATE DATABASE ".$database; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($database) + { + return "DROP DATABASE ".$database.";"; + } + + /** + * {@inheritDoc} + */ + public function supportsCreateDropDatabase() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getCurrentDateSQL() + { + return 'VALUES CURRENT DATE'; + } + + /** + * {@inheritDoc} + */ + public function getCurrentTimeSQL() + { + return 'VALUES CURRENT TIME'; + } + + /** + * {@inheritDoc} + */ + public function getCurrentTimestampSQL() + { + return "VALUES CURRENT TIMESTAMP"; + } + + /** + * {@inheritDoc} + */ + public function getIndexDeclarationSQL($name, Index $index) + { + return $this->getUniqueConstraintDeclarationSQL($name, $index); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $indexes = array(); + if (isset($options['indexes'])) { + $indexes = $options['indexes']; + } + $options['indexes'] = array(); + + $sqls = parent::_getCreateTableSQL($tableName, $columns, $options); + + foreach ($indexes as $definition) { + $sqls[] = $this->getCreateIndexSQL($definition, $tableName); + } + return $sqls; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + $columnSql = array(); + + $queryParts = array(); + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $queryParts[] = 'ALTER ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'RENAME ' . $oldColumnName . ' TO ' . $column->getQuotedName($this); + } + + $tableSql = array(); + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . implode(" ", $queryParts); + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + if ($diff->newName !== false) { + $sql[] = 'RENAME TABLE TO ' . $diff->newName; + } + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getDefaultValueDeclarationSQL($field) + { + if (isset($field['notnull']) && $field['notnull'] && !isset($field['default'])) { + if (in_array((string)$field['type'], array("Integer", "BigInteger", "SmallInteger"))) { + $field['default'] = 0; + } else if((string)$field['type'] == "DateTime") { + $field['default'] = "00-00-00 00:00:00"; + } else if ((string)$field['type'] == "Date") { + $field['default'] = "00-00-00"; + } else if((string)$field['type'] == "Time") { + $field['default'] = "00:00:00"; + } else { + $field['default'] = ''; + } + } + + unset($field['default']); // @todo this needs fixing + if (isset($field['version']) && $field['version']) { + if ((string)$field['type'] != "DateTime") { + $field['default'] = "1"; + } + } + + return parent::getDefaultValueDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($tableName, $identifierColumnName) + { + return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)'; + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "DECLARE GLOBAL TEMPORARY TABLE"; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + return "SESSION." . $tableName; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset = null) + { + if ($limit === null && $offset === null) { + return $query; + } + + $limit = (int)$limit; + $offset = (int)(($offset)?:0); + + // Todo OVER() needs ORDER BY data! + $sql = 'SELECT db22.* FROM (SELECT ROW_NUMBER() OVER() AS DC_ROWNUM, db21.* '. + 'FROM (' . $query . ') db21) db22 WHERE db22.DC_ROWNUM BETWEEN ' . ($offset+1) .' AND ' . ($offset+$limit); + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')'; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($value, $from, $length = null) + { + if ($length === null) { + return 'SUBSTR(' . $value . ', ' . $from . ')'; + } + + return 'SUBSTR(' . $value . ', ' . $from . ', ' . $length . ')'; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * DB2 returns all column names in SQL result sets in uppercase. + */ + public function getSQLResultCasing($column) + { + return strtoupper($column); + } + + public function getForUpdateSQL() + { + return ' WITH RR USE AND KEEP UPDATE LOCKS'; + } + + /** + * {@inheritDoc} + */ + public function getDummySelectSQL() + { + return 'SELECT 1 FROM sysibm.sysdummy1'; + } + + /** + * {@inheritDoc} + * + * DB2 supports savepoints, but they work semantically different than on other vendor platforms. + * + * TODO: We have to investigate how to get DB2 up and running with savepoints. + */ + public function supportsSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\DB2Keywords'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DrizzlePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DrizzlePlatform.php new file mode 100644 index 00000000..47bb364e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/DrizzlePlatform.php @@ -0,0 +1,495 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\Table; + +/** + * Drizzle platform + * + * @author Kim Hemsø Rasmussen + */ +class DrizzlePlatform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getName() + { + return 'drizzle'; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierQuoteCharacter() + { + return '`'; + } + + + /** + * {@inheritDoc} + */ public function getConcatExpression() + { + $args = func_get_args(); + + return 'CONCAT(' . join(', ', (array) $args) . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return 'DATE_ADD(' . $date . ', INTERVAL ' . $days . ' DAY)'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return 'DATE_SUB(' . $date . ', INTERVAL ' . $days . ' DAY)'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return 'DATE_ADD(' . $date . ', INTERVAL ' . $months . ' MONTH)'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return 'DATE_SUB(' . $date . ', INTERVAL ' . $months . ' MONTH)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if ( ! empty($columnDef['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + return $autoinc; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'boolean' => 'boolean', + 'varchar' => 'string', + 'integer' => 'integer', + 'blob' => 'text', + 'decimal' => 'decimal', + 'datetime' => 'datetime', + 'date' => 'date', + 'time' => 'time', + 'text' => 'text', + 'timestamp' => 'datetime', + 'double' => 'float', + 'bigint' => 'bigint', + ); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'TEXT'; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'BLOB'; + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + public function getListDatabasesSQL() + { + return "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE CATALOG_NAME='LOCAL'"; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\DrizzleKeywords'; + } + + public function getListTablesSQL() + { + return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE' AND TABLE_SCHEMA=DATABASE()"; + } + + public function getListTableColumnsSQL($table, $database = null) + { + if ($database) { + $database = "'" . $database . "'"; + } else { + $database = 'DATABASE()'; + } + + return "SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, IS_NULLABLE, IS_AUTO_INCREMENT, CHARACTER_MAXIMUM_LENGTH, COLUMN_DEFAULT," . + " NUMERIC_PRECISION, NUMERIC_SCALE" . + " FROM DATA_DICTIONARY.COLUMNS" . + " WHERE TABLE_SCHEMA=" . $database . " AND TABLE_NAME = '" . $table . "'"; + } + + public function getListTableForeignKeysSQL($table, $database = null) + { + if ($database) { + $database = "'" . $database . "'"; + } else { + $database = 'DATABASE()'; + } + + return "SELECT CONSTRAINT_NAME, CONSTRAINT_COLUMNS, REFERENCED_TABLE_NAME, REFERENCED_TABLE_COLUMNS, UPDATE_RULE, DELETE_RULE" . + " FROM DATA_DICTIONARY.FOREIGN_KEYS" . + " WHERE CONSTRAINT_SCHEMA=" . $database . " AND CONSTRAINT_TABLE='" . $table . "'"; + } + + /** + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + if ($database) { + $database = "'" . $database . "'"; + } else { + $database = 'DATABASE()'; + } + + return "SELECT INDEX_NAME AS 'key_name', COLUMN_NAME AS 'column_name', IS_USED_IN_PRIMARY AS 'primary', IS_UNIQUE=0 AS 'non_unique'" . + " FROM DATA_DICTIONARY.INDEX_PARTS" . + " WHERE TABLE_SCHEMA=" . $database . " AND TABLE_NAME='" . $table . "'"; + } + + /** + * {@inheritDoc} + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsInlineColumnComments() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsViews() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table=null) + { + if ($index instanceof Index) { + $indexName = $index->getQuotedName($this); + } else if (is_string($index)) { + $indexName = $index; + } else { + throw new \InvalidArgumentException('DrizzlePlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('DrizzlePlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + if ($index instanceof Index && $index->isPrimary()) { + // drizzle primary keys are always named "PRIMARY", + // so we cannot use them in statements because of them being keyword. + return $this->getDropPrimaryKeySQL($table); + } + + return 'DROP INDEX ' . $indexName . ' ON ' . $table; + } + + /** + * @param Index $index + * @param Table $table + * + * @return string + */ + protected function getDropPrimaryKeySQL($table) + { + return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) { + return 'TIMESTAMP'; + } + + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $columnSql = array(); + $queryParts = array(); + + if ($diff->newName !== false) { + $queryParts[] = 'RENAME TO ' . $diff->newName; + } + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . $oldColumnName . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + $sql = array(); + $tableSql = array(); + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . implode(", ", $queryParts); + } + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff) + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getDropTemporaryTableSQL($table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('getDropTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + return 'DROP TEMPORARY TABLE ' . $table; + } + + /** + * {@inheritDoc} + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (is_bool($value) || is_numeric($item)) { + $item[$key] = ($value) ? 'true' : 'false'; + } + } + } else if (is_bool($item) || is_numeric($item)) { + $item = ($item) ? 'true' : 'false'; + } + + return $item; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')'; + } + + /** + * {@inheritDoc} + */ + public function getGuidExpression() + { + return 'UUID()'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php new file mode 100644 index 00000000..53ab5e57 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php @@ -0,0 +1,437 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * DB2 Keywords + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +class DB2Keywords extends KeywordList +{ + public function getName() + { + return 'DB2'; + } + + protected function getKeywords() + { + return array( + 'ACTIVATE', + 'ADD', + 'AFTER', + 'ALIAS', + 'ALL', + 'ALLOCATE', + 'DOCUMENT', + 'DOUBLE', + 'DROP', + 'DSSIZE', + 'DYNAMIC', + 'EACH', + 'LOCK', + 'LOCKMAX', + 'LOCKSIZE', + 'LONG', + 'LOOP', + 'MAINTAINED', + 'ROUND_CEILING', + 'ROUND_DOWN', + 'ROUND_FLOOR', + 'ROUND_HALF_DOWN', + 'ROUND_HALF_EVEN', + 'ROUND_HALF_UP', + 'ALLOW', + 'ALTER', + 'AND', + 'ANY', + 'AS', + 'ASENSITIVE', + 'ASSOCIATE', + 'ASUTIME', + 'AT', + 'ATTRIBUTES', + 'AUDIT', + 'AUTHORIZATION', + 'AUX', + 'AUXILIARY', + 'BEFORE', + 'BEGIN', + 'BETWEEN', + 'BINARY', + 'BUFFERPOOL', + 'BY', + 'CACHE', + 'CALL', + 'CALLED', + 'CAPTURE', + 'CARDINALITY', + 'CASCADED', + 'CASE', + 'CAST', + 'CCSID', + 'CHAR', + 'CHARACTER', + 'CHECK', + 'CLONE', + 'CLOSE', + 'CLUSTER', + 'COLLECTION', + 'COLLID', + 'COLUMN', + 'COMMENT', + 'COMMIT', + 'CONCAT', + 'CONDITION', + 'CONNECT', + 'CONNECTION', + 'CONSTRAINT', + 'CONTAINS', + 'CONTINUE', + 'COUNT', + 'COUNT_BIG', + 'CREATE', + 'CROSS', + 'CURRENT', + 'CURRENT_DATE', + 'CURRENT_LC_CTYPE', + 'CURRENT_PATH', + 'CURRENT_SCHEMA', + 'CURRENT_SERVER', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_TIMEZONE', + 'CURRENT_USER', + 'CURSOR', + 'CYCLE', + 'DATA', + 'DATABASE', + 'DATAPARTITIONNAME', + 'DATAPARTITIONNUM', + 'EDITPROC', + 'ELSE', + 'ELSEIF', + 'ENABLE', + 'ENCODING', + 'ENCRYPTION', + 'END', + 'END-EXEC', + 'ENDING', + 'ERASE', + 'ESCAPE', + 'EVERY', + 'EXCEPT', + 'EXCEPTION', + 'EXCLUDING', + 'EXCLUSIVE', + 'EXECUTE', + 'EXISTS', + 'EXIT', + 'EXPLAIN', + 'EXTERNAL', + 'EXTRACT', + 'FENCED', + 'FETCH', + 'FIELDPROC', + 'FILE', + 'FINAL', + 'FOR', + 'FOREIGN', + 'FREE', + 'FROM', + 'FULL', + 'FUNCTION', + 'GENERAL', + 'GENERATED', + 'GET', + 'GLOBAL', + 'GO', + 'GOTO', + 'GRANT', + 'GRAPHIC', + 'GROUP', + 'HANDLER', + 'HASH', + 'HASHED_VALUE', + 'HAVING', + 'HINT', + 'HOLD', + 'HOUR', + 'HOURS', + 'IDENTITY', + 'IF', + 'IMMEDIATE', + 'IN', + 'INCLUDING', + 'INCLUSIVE', + 'INCREMENT', + 'INDEX', + 'INDICATOR', + 'INF', + 'INFINITY', + 'INHERIT', + 'INNER', + 'INOUT', + 'INSENSITIVE', + 'INSERT', + 'INTEGRITY', + 'MATERIALIZED', + 'MAXVALUE', + 'MICROSECOND', + 'MICROSECONDS', + 'MINUTE', + 'MINUTES', + 'MINVALUE', + 'MODE', + 'MODIFIES', + 'MONTH', + 'MONTHS', + 'NAN', + 'NEW', + 'NEW_TABLE', + 'NEXTVAL', + 'NO', + 'NOCACHE', + 'NOCYCLE', + 'NODENAME', + 'NODENUMBER', + 'NOMAXVALUE', + 'NOMINVALUE', + 'NONE', + 'NOORDER', + 'NORMALIZED', + 'NOT', + 'NULL', + 'NULLS', + 'NUMPARTS', + 'OBID', + 'OF', + 'OLD', + 'OLD_TABLE', + 'ON', + 'OPEN', + 'OPTIMIZATION', + 'OPTIMIZE', + 'OPTION', + 'OR', + 'ORDER', + 'OUT', + 'OUTER', + 'OVER', + 'OVERRIDING', + 'PACKAGE', + 'PADDED', + 'PAGESIZE', + 'PARAMETER', + 'PART', + 'PARTITION', + 'PARTITIONED', + 'PARTITIONING', + 'PARTITIONS', + 'PASSWORD', + 'PATH', + 'PIECESIZE', + 'PLAN', + 'POSITION', + 'PRECISION', + 'PREPARE', + 'PREVVAL', + 'PRIMARY', + 'PRIQTY', + 'PRIVILEGES', + 'PROCEDURE', + 'PROGRAM', + 'PSID', + 'ROUND_UP', + 'ROUTINE', + 'ROW', + 'ROW_NUMBER', + 'ROWNUMBER', + 'ROWS', + 'ROWSET', + 'RRN', + 'RUN', + 'SAVEPOINT', + 'SCHEMA', + 'SCRATCHPAD', + 'SCROLL', + 'SEARCH', + 'SECOND', + 'SECONDS', + 'SECQTY', + 'SECURITY', + 'SELECT', + 'SENSITIVE', + 'SEQUENCE', + 'SESSION', + 'SESSION_USER', + 'SET', + 'SIGNAL', + 'SIMPLE', + 'SNAN', + 'SOME', + 'SOURCE', + 'SPECIFIC', + 'SQL', + 'SQLID', + 'STACKED', + 'STANDARD', + 'START', + 'STARTING', + 'STATEMENT', + 'STATIC', + 'STATMENT', + 'STAY', + 'STOGROUP', + 'STORES', + 'STYLE', + 'SUBSTRING', + 'SUMMARY', + 'SYNONYM', + 'SYSFUN', + 'SYSIBM', + 'SYSPROC', + 'SYSTEM', + 'SYSTEM_USER', + 'TABLE', + 'TABLESPACE', + 'THEN', + 'TIME', + 'TIMESTAMP', + 'TO', + 'TRANSACTION', + 'TRIGGER', + 'TRIM', + 'TRUNCATE', + 'TYPE', + 'UNDO', + 'UNION', + 'UNIQUE', + 'UNTIL', + 'UPDATE', + 'DATE', + 'DAY', + 'DAYS', + 'DB2GENERAL', + 'DB2GENRL', + 'DB2SQL', + 'DBINFO', + 'DBPARTITIONNAME', + 'DBPARTITIONNUM', + 'DEALLOCATE', + 'DECLARE', + 'DEFAULT', + 'DEFAULTS', + 'DEFINITION', + 'DELETE', + 'DENSE_RANK', + 'DENSERANK', + 'DESCRIBE', + 'DESCRIPTOR', + 'DETERMINISTIC', + 'DIAGNOSTICS', + 'DISABLE', + 'DISALLOW', + 'DISCONNECT', + 'DISTINCT', + 'DO', + 'INTERSECT', + 'PUBLIC', + 'USAGE', + 'INTO', + 'QUERY', + 'USER', + 'IS', + 'QUERYNO', + 'USING', + 'ISOBID', + 'RANGE', + 'VALIDPROC', + 'ISOLATION', + 'RANK', + 'VALUE', + 'ITERATE', + 'READ', + 'VALUES', + 'JAR', + 'READS', + 'VARIABLE', + 'JAVA', + 'RECOVERY', + 'VARIANT', + 'JOIN', + 'REFERENCES', + 'VCAT', + 'KEEP', + 'REFERENCING', + 'VERSION', + 'KEY', + 'REFRESH', + 'VIEW', + 'LABEL', + 'RELEASE', + 'VOLATILE', + 'LANGUAGE', + 'RENAME', + 'VOLUMES', + 'LATERAL', + 'REPEAT', + 'WHEN', + 'LC_CTYPE', + 'RESET', + 'WHENEVER', + 'LEAVE', + 'RESIGNAL', + 'WHERE', + 'LEFT', + 'RESTART', + 'WHILE', + 'LIKE', + 'RESTRICT', + 'WITH', + 'LINKTYPE', + 'RESULT', + 'WITHOUT', + 'LOCAL', + 'RESULT_SET_LOCATOR WLM', + 'LOCALDATE', + 'RETURN', + 'WRITE', + 'LOCALE', + 'RETURNS', + 'XMLELEMENT', + 'LOCALTIME', + 'REVOKE', + 'XMLEXISTS', + 'LOCALTIMESTAMP RIGHT', + 'XMLNAMESPACES', + 'LOCATOR', + 'ROLE', + 'YEAR', + 'LOCATORS', + 'ROLLBACK', + 'YEARS', + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php new file mode 100644 index 00000000..391ef41e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php @@ -0,0 +1,340 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * Drizzle Keywordlist + * + * @author Kim Hemsø Rasmussen + */ +class DrizzleKeywords extends KeywordList +{ + public function getName() + { + return 'drizzle'; + } + + protected function getKeywords() + { + return array( + 'ABS', + 'ALL', + 'ALLOCATE', + 'ALTER', + 'AND', + 'ANY', + 'ARE', + 'ARRAY', + 'AS', + 'ASENSITIVE', + 'ASYMMETRIC', + 'AT', + 'ATOMIC', + 'AUTHORIZATION', + 'AVG', + 'BEGIN', + 'BETWEEN', + 'BIGINT', + 'BINARY', + 'BLOB', + 'BOOLEAN', + 'BOTH', + 'BY', + 'CALL', + 'CALLED', + 'CARDINALITY', + 'CASCADED', + 'CASE', + 'CAST', + 'CEIL', + 'CEILING', + 'CHAR', + 'CHARACTER', + 'CHARACTER_LENGTH', + 'CHAR_LENGTH', + 'CHECK', + 'CLOB', + 'CLOSE', + 'COALESCE', + 'COLLATE', + 'COLLECT', + 'COLUMN', + 'COMMIT', + 'CONDITION', + 'CONNECT', + 'CONSTRAINT', + 'CONVERT', + 'CORR', + 'CORRESPONDING', + 'COUNT', + 'COVAR_POP', + 'COVAR_SAMP', + 'CREATE', + 'CROSS', + 'CUBE', + 'CUME_DIST', + 'CURRENT', + 'CURRENT_DATE', + 'CURRENT_DEFAULT_TRANSFORM_GROUP', + 'CURRENT_PATH', + 'CURRENT_ROLE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_TRANSFORM_GROUP_FOR_TYPE', + 'CURRENT_USER', + 'CURSOR', + 'CYCLE', + 'DATE', + 'DAY', + 'DEALLOCATE', + 'DEC', + 'DECIMAL', + 'DECLARE', + 'DEFAULT', + 'DELETE', + 'DENSE_RANK', + 'DEREF', + 'DESCRIBE', + 'DETERMINISTIC', + 'DISCONNECT', + 'DISTINCT', + 'DOUBLE', + 'DROP', + 'DYNAMIC', + 'EACH', + 'ELEMENT', + 'ELSE', + 'END', + 'ESCAPE', + 'EVERY', + 'EXCEPT', + 'EXEC', + 'EXECUTE', + 'EXISTS', + 'EXP', + 'EXTERNAL', + 'EXTRACT', + 'FALSE', + 'FETCH', + 'FILTER', + 'FLOAT', + 'FLOOR', + 'FOR', + 'FOREIGN', + 'FREE', + 'FROM', + 'FULL', + 'FUNCTION', + 'FUSION', + 'GET', + 'GLOBAL', + 'GRANT', + 'GROUP', + 'GROUPING', + 'HAVING', + 'HOLD', + 'HOUR', + 'IDENTITY', + 'IN', + 'INDICATOR', + 'INNER', + 'INOUT', + 'INSENSITIVE', + 'INSERT', + 'INT', + 'INTEGER', + 'INTERSECT', + 'INTERSECTION', + 'INTERVAL', + 'INTO', + 'IS', + 'JOIN', + 'LANGUAGE', + 'LARGE', + 'LATERAL', + 'LEADING', + 'LEFT', + 'LIKE', + 'LN', + 'LOCAL', + 'LOCALTIME', + 'LOCALTIMESTAMP', + 'LOWER', + 'MATCH', + 'MAX', + 'MEMBER', + 'MERGE', + 'METHOD', + 'MIN', + 'MINUTE', + 'MOD', + 'MODIFIES', + 'MODULE', + 'MONTH', + 'MULTISET', + 'NATIONAL', + 'NATURAL', + 'NCHAR', + 'NCLOB', + 'NEW', + 'NO', + 'NONE', + 'NORMALIZE', + 'NOT', + 'NULL_SYM', + 'NULLIF', + 'NUMERIC', + 'OCTET_LENGTH', + 'OF', + 'OLD', + 'ON', + 'ONLY', + 'OPEN', + 'OR', + 'ORDER', + 'OUT', + 'OUTER', + 'OVER', + 'OVERLAPS', + 'OVERLAY', + 'PARAMETER', + 'PARTITION', + 'PERCENTILE_CONT', + 'PERCENTILE_DISC', + 'PERCENT_RANK', + 'POSITION', + 'POWER', + 'PRECISION', + 'PREPARE', + 'PRIMARY', + 'PROCEDURE', + 'RANGE', + 'RANK', + 'READS', + 'REAL', + 'RECURSIVE', + 'REF', + 'REFERENCES', + 'REFERENCING', + 'REGR_AVGX', + 'REGR_AVGY', + 'REGR_COUNT', + 'REGR_INTERCEPT', + 'REGR_R2', + 'REGR_SLOPE', + 'REGR_SXX', + 'REGR_SXY', + 'REGR_SYY', + 'RELEASE', + 'RESULT', + 'RETURN', + 'RETURNS', + 'REVOKE', + 'RIGHT', + 'ROLLBACK', + 'ROLLUP', + 'ROW', + 'ROWS', + 'ROW_NUMBER', + 'SAVEPOINT', + 'SCOPE', + 'SCROLL', + 'SEARCH', + 'SECOND', + 'SELECT', + 'SENSITIVE', + 'SESSION_USER', + 'SET', + 'SIMILAR', + 'SMALLINT', + 'SOME', + 'SPECIFIC', + 'SPECIFICTYPE', + 'SQL', + 'SQLEXCEPTION', + 'SQLSTATE', + 'SQLWARNING', + 'SQRT', + 'START', + 'STATIC', + 'STDDEV_POP', + 'STDDEV_SAMP', + 'SUBMULTISET', + 'SUBSTRING', + 'SUM', + 'SYMMETRIC', + 'SYSTEM', + 'SYSTEM_USER', + 'TABLE', + 'TABLESAMPLE', + 'THEN', + 'TIME', + 'TIMESTAMP', + 'TIMEZONE_HOUR', + 'TIMEZONE_MINUTE', + 'TO', + 'TRAILING', + 'TRANSLATE', + 'TRANSLATION', + 'TREAT', + 'TRIGGER', + 'TRIM', + 'TRUE', + 'UESCAPE', + 'UNION', + 'UNIQUE', + 'UNKNOWN', + 'UNNEST', + 'UPDATE', + 'UPPER', + 'USER', + 'USING', + 'VALUE', + 'VALUES', + 'VARCHAR', + 'VARYING', + 'VAR_POP', + 'VAR_SAMP', + 'WHEN', + 'WHENEVER', + 'WHERE', + 'WIDTH_BUCKET', + 'WINDOW', + 'WITH', + 'WITHIN', + 'WITHOUT', + 'XML', + 'XMLAGG', + 'XMLATTRIBUTES', + 'XMLBINARY', + 'XMLCOMMENT', + 'XMLCONCAT', + 'XMLELEMENT', + 'XMLFOREST', + 'XMLNAMESPACES', + 'XMLPARSE', + 'XMLPI', + 'XMLROOT', + 'XMLSERIALIZE', + 'YEAR', + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.php new file mode 100644 index 00000000..f30bb368 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/KeywordList.php @@ -0,0 +1,63 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * Abstract interface for a SQL reserved keyword dictionary. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +abstract class KeywordList +{ + private $keywords = null; + + /** + * Check if the given word is a keyword of this dialect/vendor platform. + * + * @param string $word + * @return bool + */ + public function isKeyword($word) + { + if ($this->keywords === null) { + $this->initializeKeywords(); + } + + return isset($this->keywords[strtoupper($word)]); + } + + protected function initializeKeywords() + { + $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); + } + + abstract protected function getKeywords(); + + /** + * Name of this keyword list. + * + * @return string + */ + abstract public function getName(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MsSQLKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MsSQLKeywords.php new file mode 100644 index 00000000..8adac119 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MsSQLKeywords.php @@ -0,0 +1,243 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * MsSQL Keywordlist + * + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author David Coallier + */ +class MsSQLKeywords extends KeywordList +{ + public function getName() + { + return 'MsSQL'; + } + + protected function getKeywords() + { + return array( + 'ADD', + 'CURRENT_TIMESTAMP', + 'GROUP', + 'OPENQUERY', + 'SERIALIZABLE', + 'ALL', + 'CURRENT_USER', + 'HAVING', + 'OPENROWSET', + 'SESSION_USER', + 'ALTER', + 'CURSOR', + 'HOLDLOCK', + 'OPTION', + 'SET', + 'AND', + 'DATABASE', + 'IDENTITY', + 'OR', + 'SETUSER', + 'ANY', + 'DBCC', + 'IDENTITYCOL', + 'ORDER', + 'SHUTDOWN', + 'AS', + 'DEALLOCATE', + 'IDENTITY_INSERT', + 'OUTER', + 'SOME', + 'ASC', + 'DECLARE', + 'IF', + 'OVER', + 'STATISTICS', + 'AUTHORIZATION', + 'DEFAULT', + 'IN', + 'PERCENT', + 'SUM', + 'AVG', + 'DELETE', + 'INDEX', + 'PERM', + 'SYSTEM_USER', + 'BACKUP', + 'DENY', + 'INNER', + 'PERMANENT', + 'TABLE', + 'BEGIN', + 'DESC', + 'INSERT', + 'PIPE', + 'TAPE', + 'BETWEEN', + 'DISK', + 'INTERSECT', + 'PLAN', + 'TEMP', + 'BREAK', + 'DISTINCT', + 'INTO', + 'PRECISION', + 'TEMPORARY', + 'BROWSE', + 'DISTRIBUTED', + 'IS', + 'PREPARE', + 'TEXTSIZE', + 'BULK', + 'DOUBLE', + 'ISOLATION', + 'PRIMARY', + 'THEN', + 'BY', + 'DROP', + 'JOIN', + 'PRINT', + 'TO', + 'CASCADE', + 'DUMMY', + 'KEY', + 'PRIVILEGES', + 'TOP', + 'CASE', + 'DUMP', + 'KILL', + 'PROC', + 'TRAN', + 'CHECK', + 'ELSE', + 'LEFT', + 'PROCEDURE', + 'TRANSACTION', + 'CHECKPOINT', + 'END', + 'LEVEL', + 'PROCESSEXIT', + 'TRIGGER', + 'CLOSE', + 'ERRLVL', + 'LIKE', + 'PUBLIC', + 'TRUNCATE', + 'CLUSTERED', + 'ERROREXIT', + 'LINENO', + 'RAISERROR', + 'TSEQUAL', + 'COALESCE', + 'ESCAPE', + 'LOAD', + 'READ', + 'UNCOMMITTED', + 'COLUMN', + 'EXCEPT', + 'MAX', + 'READTEXT', + 'UNION', + 'COMMIT', + 'EXEC', + 'MIN', + 'RECONFIGURE', + 'UNIQUE', + 'COMMITTED', + 'EXECUTE', + 'MIRROREXIT', + 'REFERENCES', + 'UPDATE', + 'COMPUTE', + 'EXISTS', + 'NATIONAL', + 'REPEATABLE', + 'UPDATETEXT', + 'CONFIRM', + 'EXIT', + 'NOCHECK', + 'REPLICATION', + 'USE', + 'CONSTRAINT', + 'FETCH', + 'NONCLUSTERED', + 'RESTORE', + 'USER', + 'CONTAINS', + 'FILE', + 'NOT', + 'RESTRICT', + 'VALUES', + 'CONTAINSTABLE', + 'FILLFACTOR', + 'NULL', + 'RETURN', + 'VARYING', + 'CONTINUE', + 'FLOPPY', + 'NULLIF', + 'REVOKE', + 'VIEW', + 'CONTROLROW', + 'FOR', + 'OF', + 'RIGHT', + 'WAITFOR', + 'CONVERT', + 'FOREIGN', + 'OFF', + 'ROLLBACK', + 'WHEN', + 'COUNT', + 'FREETEXT', + 'OFFSETS', + 'ROWCOUNT', + 'WHERE', + 'CREATE', + 'FREETEXTTABLE', + 'ON', + 'ROWGUIDCOL', + 'WHILE', + 'CROSS', + 'FROM', + 'ONCE', + 'RULE', + 'WITH', + 'CURRENT', + 'FULL', + 'ONLY', + 'SAVE', + 'WORK', + 'CURRENT_DATE', + 'GOTO', + 'OPEN', + 'SCHEMA', + 'WRITETEXT', + 'CURRENT_TIME', + 'GRANT', + 'OPENDATASOURCE', + 'SELECT', + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php new file mode 100644 index 00000000..71704f6b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php @@ -0,0 +1,268 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * MySQL Keywordlist + * + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author David Coallier + */ +class MySQLKeywords extends KeywordList +{ + public function getName() + { + return 'MySQL'; + } + + protected function getKeywords() + { + return array( + 'ADD', + 'ALL', + 'ALTER', + 'ANALYZE', + 'AND', + 'AS', + 'ASC', + 'ASENSITIVE', + 'BEFORE', + 'BETWEEN', + 'BIGINT', + 'BINARY', + 'BLOB', + 'BOTH', + 'BY', + 'CALL', + 'CASCADE', + 'CASE', + 'CHANGE', + 'CHAR', + 'CHARACTER', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'CONDITION', + 'CONNECTION', + 'CONSTRAINT', + 'CONTINUE', + 'CONVERT', + 'CREATE', + 'CROSS', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_USER', + 'CURSOR', + 'DATABASE', + 'DATABASES', + 'DAY_HOUR', + 'DAY_MICROSECOND', + 'DAY_MINUTE', + 'DAY_SECOND', + 'DEC', + 'DECIMAL', + 'DECLARE', + 'DEFAULT', + 'DELAYED', + 'DELETE', + 'DESC', + 'DESCRIBE', + 'DETERMINISTIC', + 'DISTINCT', + 'DISTINCTROW', + 'DIV', + 'DOUBLE', + 'DROP', + 'DUAL', + 'EACH', + 'ELSE', + 'ELSEIF', + 'ENCLOSED', + 'ESCAPED', + 'EXISTS', + 'EXIT', + 'EXPLAIN', + 'FALSE', + 'FETCH', + 'FLOAT', + 'FLOAT4', + 'FLOAT8', + 'FOR', + 'FORCE', + 'FOREIGN', + 'FROM', + 'FULLTEXT', + 'GOTO', + 'GRANT', + 'GROUP', + 'HAVING', + 'HIGH_PRIORITY', + 'HOUR_MICROSECOND', + 'HOUR_MINUTE', + 'HOUR_SECOND', + 'IF', + 'IGNORE', + 'IN', + 'INDEX', + 'INFILE', + 'INNER', + 'INOUT', + 'INSENSITIVE', + 'INSERT', + 'INT', + 'INT1', + 'INT2', + 'INT3', + 'INT4', + 'INT8', + 'INTEGER', + 'INTERVAL', + 'INTO', + 'IS', + 'ITERATE', + 'JOIN', + 'KEY', + 'KEYS', + 'KILL', + 'LABEL', + 'LEADING', + 'LEAVE', + 'LEFT', + 'LIKE', + 'LIMIT', + 'LINES', + 'LOAD', + 'LOCALTIME', + 'LOCALTIMESTAMP', + 'LOCK', + 'LONG', + 'LONGBLOB', + 'LONGTEXT', + 'LOOP', + 'LOW_PRIORITY', + 'MATCH', + 'MEDIUMBLOB', + 'MEDIUMINT', + 'MEDIUMTEXT', + 'MIDDLEINT', + 'MINUTE_MICROSECOND', + 'MINUTE_SECOND', + 'MOD', + 'MODIFIES', + 'NATURAL', + 'NOT', + 'NO_WRITE_TO_BINLOG', + 'NULL', + 'NUMERIC', + 'ON', + 'OPTIMIZE', + 'OPTION', + 'OPTIONALLY', + 'OR', + 'ORDER', + 'OUT', + 'OUTER', + 'OUTFILE', + 'PRECISION', + 'PRIMARY', + 'PROCEDURE', + 'PURGE', + 'RAID0', + 'READ', + 'READS', + 'REAL', + 'REFERENCES', + 'REGEXP', + 'RELEASE', + 'RENAME', + 'REPEAT', + 'REPLACE', + 'REQUIRE', + 'RESTRICT', + 'RETURN', + 'REVOKE', + 'RIGHT', + 'RLIKE', + 'SCHEMA', + 'SCHEMAS', + 'SECOND_MICROSECOND', + 'SELECT', + 'SENSITIVE', + 'SEPARATOR', + 'SET', + 'SHOW', + 'SMALLINT', + 'SONAME', + 'SPATIAL', + 'SPECIFIC', + 'SQL', + 'SQLEXCEPTION', + 'SQLSTATE', + 'SQLWARNING', + 'SQL_BIG_RESULT', + 'SQL_CALC_FOUND_ROWS', + 'SQL_SMALL_RESULT', + 'SSL', + 'STARTING', + 'STRAIGHT_JOIN', + 'TABLE', + 'TERMINATED', + 'THEN', + 'TINYBLOB', + 'TINYINT', + 'TINYTEXT', + 'TO', + 'TRAILING', + 'TRIGGER', + 'TRUE', + 'UNDO', + 'UNION', + 'UNIQUE', + 'UNLOCK', + 'UNSIGNED', + 'UPDATE', + 'USAGE', + 'USE', + 'USING', + 'UTC_DATE', + 'UTC_TIME', + 'UTC_TIMESTAMP', + 'VALUES', + 'VARBINARY', + 'VARCHAR', + 'VARCHARACTER', + 'VARYING', + 'WHEN', + 'WHERE', + 'WHILE', + 'WITH', + 'WRITE', + 'X509', + 'XOR', + 'YEAR_MONTH', + 'ZEROFILL', + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php new file mode 100644 index 00000000..525286e8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php @@ -0,0 +1,156 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * Oracle Keywordlist + * + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author David Coallier + */ +class OracleKeywords extends KeywordList +{ + public function getName() + { + return 'Oracle'; + } + + protected function getKeywords() + { + return array( + 'ACCESS', + 'ELSE', + 'MODIFY', + 'START', + 'ADD', + 'EXCLUSIVE', + 'NOAUDIT', + 'SELECT', + 'ALL', + 'EXISTS', + 'NOCOMPRESS', + 'SESSION', + 'ALTER', + 'FILE', + 'NOT', + 'SET', + 'AND', + 'FLOAT', + 'NOTFOUND ', + 'SHARE', + 'ANY', + 'FOR', + 'NOWAIT', + 'SIZE', + 'ARRAYLEN', + 'FROM', + 'NULL', + 'SMALLINT', + 'AS', + 'GRANT', + 'NUMBER', + 'SQLBUF', + 'ASC', + 'GROUP', + 'OF', + 'SUCCESSFUL', + 'AUDIT', + 'HAVING', + 'OFFLINE ', + 'SYNONYM', + 'BETWEEN', + 'IDENTIFIED', + 'ON', + 'SYSDATE', + 'BY', + 'IMMEDIATE', + 'ONLINE', + 'TABLE', + 'CHAR', + 'IN', + 'OPTION', + 'THEN', + 'CHECK', + 'INCREMENT', + 'OR', + 'TO', + 'CLUSTER', + 'INDEX', + 'ORDER', + 'TRIGGER', + 'COLUMN', + 'INITIAL', + 'PCTFREE', + 'UID', + 'COMMENT', + 'INSERT', + 'PRIOR', + 'UNION', + 'COMPRESS', + 'INTEGER', + 'PRIVILEGES', + 'UNIQUE', + 'CONNECT', + 'INTERSECT', + 'PUBLIC', + 'UPDATE', + 'CREATE', + 'INTO', + 'RAW', + 'USER', + 'CURRENT', + 'IS', + 'RENAME', + 'VALIDATE', + 'DATE', + 'LEVEL', + 'RESOURCE', + 'VALUES', + 'DECIMAL', + 'LIKE', + 'REVOKE', + 'VARCHAR', + 'DEFAULT', + 'LOCK', + 'ROW', + 'VARCHAR2', + 'DELETE', + 'LONG', + 'ROWID', + 'VIEW', + 'DESC', + 'MAXEXTENTS', + 'ROWLABEL', + 'WHENEVER', + 'DISTINCT', + 'MINUS', + 'ROWNUM', + 'WHERE', + 'DROP', + 'MODE', + 'ROWS', + 'WITH', + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQLKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQLKeywords.php new file mode 100644 index 00000000..7950f6a7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/PostgreSQLKeywords.php @@ -0,0 +1,131 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * PostgreSQL Keywordlist + * + * @license BSD http://www.opensource.org/licenses/bsd-license.php + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author Marcelo Santos Araujo + */ +class PostgreSQLKeywords extends KeywordList +{ + public function getName() + { + return 'PostgreSQL'; + } + + protected function getKeywords() + { + return array( + 'ALL', + 'ANALYSE', + 'ANALYZE', + 'AND', + 'ANY', + 'AS', + 'ASC', + 'AUTHORIZATION', + 'BETWEEN', + 'BINARY', + 'BOTH', + 'CASE', + 'CAST', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'CONSTRAINT', + 'CREATE', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'CURRENT_USER', + 'DEFAULT', + 'DEFERRABLE', + 'DESC', + 'DISTINCT', + 'DO', + 'ELSE', + 'END', + 'EXCEPT', + 'FALSE', + 'FOR', + 'FOREIGN', + 'FREEZE', + 'FROM', + 'FULL', + 'GRANT', + 'GROUP', + 'HAVING', + 'ILIKE', + 'IN', + 'INITIALLY', + 'INNER', + 'INTERSECT', + 'INTO', + 'IS', + 'ISNULL', + 'JOIN', + 'LEADING', + 'LEFT', + 'LIKE', + 'LIMIT', + 'LOCALTIME', + 'LOCALTIMESTAMP', + 'NATURAL', + 'NEW', + 'NOT', + 'NOTNULL', + 'NULL', + 'OFF', + 'OFFSET', + 'OLD', + 'ON', + 'ONLY', + 'OR', + 'ORDER', + 'OUTER', + 'OVERLAPS', + 'PLACING', + 'PRIMARY', + 'REFERENCES', + 'SELECT', + 'SESSION_USER', + 'SIMILAR', + 'SOME', + 'TABLE', + 'THEN', + 'TO', + 'TRAILING', + 'TRUE', + 'UNION', + 'UNIQUE', + 'USER', + 'USING', + 'VERBOSE', + 'WHEN', + 'WHERE' + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/ReservedKeywordsValidator.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/ReservedKeywordsValidator.php new file mode 100644 index 00000000..a61922bf --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/ReservedKeywordsValidator.php @@ -0,0 +1,116 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +use Doctrine\DBAL\Schema\Visitor\Visitor; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Sequence; +use Doctrine\DBAL\Schema\Index; + +class ReservedKeywordsValidator implements Visitor +{ + /** + * @var KeywordList[] + */ + private $keywordLists = array(); + + /** + * @var array + */ + private $violations = array(); + + public function __construct(array $keywordLists) + { + $this->keywordLists = $keywordLists; + } + + public function getViolations() + { + return $this->violations; + } + + /** + * @param string $word + * @return array + */ + private function isReservedWord($word) + { + if ($word[0] == "`") { + $word = str_replace('`', '', $word); + } + + $keywordLists = array(); + foreach ($this->keywordLists as $keywordList) { + if ($keywordList->isKeyword($word)) { + $keywordLists[] = $keywordList->getName(); + } + } + return $keywordLists; + } + + private function addViolation($asset, $violatedPlatforms) + { + if ( ! $violatedPlatforms) { + return; + } + + $this->violations[] = $asset . ' keyword violations: ' . implode(', ', $violatedPlatforms); + } + + public function acceptColumn(Table $table, Column $column) + { + $this->addViolation( + 'Table ' . $table->getName() . ' column ' . $column->getName(), + $this->isReservedWord($column->getName()) + ); + } + + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + + } + + public function acceptIndex(Table $table, Index $index) + { + + } + + public function acceptSchema(Schema $schema) + { + + } + + public function acceptSequence(Sequence $sequence) + { + + } + + public function acceptTable(Table $table) + { + $this->addViolation( + 'Table ' . $table->getName(), + $this->isReservedWord($table->getName()) + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLiteKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLiteKeywords.php new file mode 100644 index 00000000..d45b9944 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/SQLiteKeywords.php @@ -0,0 +1,164 @@ +. + */ + + +namespace Doctrine\DBAL\Platforms\Keywords; + +/** + * SQLite Keywords + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +class SQLiteKeywords extends KeywordList +{ + public function getName() + { + return 'SQLite'; + } + + protected function getKeywords() + { + return array( + 'ABORT', + 'ACTION', + 'ADD', + 'AFTER', + 'ALL', + 'ALTER', + 'ANALYZE', + 'AND', + 'AS', + 'ASC', + 'ATTACH', + 'AUTOINCREMENT', + 'BEFORE', + 'BEGIN', + 'BETWEEN', + 'BY', + 'CASCADE', + 'CASE', + 'CAST', + 'CHECK', + 'COLLATE', + 'COLUMN', + 'COMMIT', + 'CONFLICT', + 'CONSTRAINT', + 'CREATE', + 'CROSS', + 'CURRENT_DATE', + 'CURRENT_TIME', + 'CURRENT_TIMESTAMP', + 'DATABASE', + 'DEFAULT', + 'DEFERRABLE', + 'DEFERRED', + 'DELETE', + 'DESC', + 'DETACH', + 'DISTINCT', + 'DROP', + 'EACH', + 'ELSE', + 'END', + 'ESCAPE', + 'EXCEPT', + 'EXCLUSIVE', + 'EXISTS', + 'EXPLAIN', + 'FAIL', + 'FOR', + 'FOREIGN', + 'FROM', + 'FULL', + 'GLOB', + 'GROUP', + 'HAVING', + 'IF', + 'IGNORE', + 'IMMEDIATE', + 'IN', + 'INDEX', + 'INDEXED', + 'INITIALLY', + 'INNER', + 'INSERT', + 'INSTEAD', + 'INTERSECT', + 'INTO', + 'IS', + 'ISNULL', + 'JOIN', + 'KEY', + 'LEFT', + 'LIKE', + 'LIMIT', + 'MATCH', + 'NATURAL', + 'NO', + 'NOT', + 'NOTNULL', + 'NULL', + 'OF', + 'OFFSET', + 'ON', + 'OR', + 'ORDER', + 'OUTER', + 'PLAN', + 'PRAGMA', + 'PRIMARY', + 'QUERY', + 'RAISE', + 'REFERENCES', + 'REGEXP', + 'REINDEX', + 'RELEASE', + 'RENAME', + 'REPLACE', + 'RESTRICT', + 'RIGHT', + 'ROLLBACK', + 'ROW', + 'SAVEPOINT', + 'SELECT', + 'SET', + 'TABLE', + 'TEMP', + 'TEMPORARY', + 'THEN', + 'TO', + 'TRANSACTION', + 'TRIGGER', + 'UNION', + 'UNIQUE', + 'UPDATE', + 'USING', + 'VACUUM', + 'VALUES', + 'VIEW', + 'VIRTUAL', + 'WHEN', + 'WHERE' + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php new file mode 100644 index 00000000..2cf573f0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -0,0 +1,722 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\Table; + +/** + * The MySqlPlatform provides the behavior, features and SQL dialect of the + * MySQL database platform. This platform represents a MySQL 5.0 or greater platform that + * uses the InnoDB storage engine. + * + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei + * @todo Rename: MySQLPlatform + */ +class MySqlPlatform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getIdentifierQuoteCharacter() + { + return '`'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * {@inheritDoc} + */ + public function getGuidExpression() + { + return 'UUID()'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + $args = func_get_args(); + return 'CONCAT(' . join(', ', (array) $args) . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return 'DATE_ADD(' . $date . ', INTERVAL ' . $days . ' DAY)'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return 'DATE_SUB(' . $date . ', INTERVAL ' . $days . ' DAY)'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return 'DATE_ADD(' . $date . ', INTERVAL ' . $months . ' MONTH)'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return 'DATE_SUB(' . $date . ', INTERVAL ' . $months . ' MONTH)'; + } + + public function getListDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + public function getListTableConstraintsSQL($table) + { + return 'SHOW INDEX FROM ' . $table; + } + + /** + * {@inheritDoc} + * + * Two approaches to listing the table indexes. The information_schema is + * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table". + * + * @param string $table + * @param string $currentDatabase + * @return string + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + if ($currentDatabase) { + return "SELECT TABLE_NAME AS `Table`, NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, ". + "SEQ_IN_INDEX AS Seq_in_index, COLUMN_NAME AS Column_Name, COLLATION AS Collation, ". + "CARDINALITY AS Cardinality, SUB_PART AS Sub_Part, PACKED AS Packed, " . + "NULLABLE AS `Null`, INDEX_TYPE AS Index_Type, COMMENT AS Comment " . + "FROM information_schema.STATISTICS WHERE TABLE_NAME = '" . $table . "' AND TABLE_SCHEMA = '" . $currentDatabase . "'"; + } + + return 'SHOW INDEX FROM ' . $table; + } + + public function getListViewsSQL($database) + { + return "SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = '".$database."'"; + } + + public function getListTableForeignKeysSQL($table, $database = null) + { + $sql = "SELECT DISTINCT k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, ". + "k.`REFERENCED_COLUMN_NAME` /*!50116 , c.update_rule, c.delete_rule */ ". + "FROM information_schema.key_column_usage k /*!50116 ". + "INNER JOIN information_schema.referential_constraints c ON ". + " c.constraint_name = k.constraint_name AND ". + " c.table_name = '$table' */ WHERE k.table_name = '$table'"; + + if ($database) { + $sql .= " AND k.table_schema = '$database' /*!50116 AND c.constraint_schema = '$database' */"; + } + + $sql .= " AND k.`REFERENCED_COLUMN_NAME` is not NULL"; + + return $sql; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + if ( ! empty($field['length']) && is_numeric($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYTEXT'; + } + + if ($length <= 65532) { + return 'TEXT'; + } + + if ($length <= 16777215) { + return 'MEDIUMTEXT'; + } + } + + return 'LONGTEXT'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) { + return 'TIMESTAMP'; + } + + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'TINYINT(1)'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + public function getCollationFieldDeclaration($collation) + { + return 'COLLATE ' . $collation; + } + + /** + * {@inheritDoc} + * + * MySql prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * MySql supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsInlineColumnComments() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getShowDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + public function getListTablesSQL() + { + return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"; + } + + public function getListTableColumnsSQL($table, $database = null) + { + if ($database) { + return "SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ". + "COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, " . + "CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS CollactionName ". + "FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '" . $database . "' AND TABLE_NAME = '" . $table . "'"; + } + + return 'DESCRIBE ' . $table; + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $index => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition); + } + } + + // add all indexes + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE '; + if (!empty($options['temporary'])) { + $query .= 'TEMPORARY '; + } + $query .= 'TABLE ' . $tableName . ' (' . $queryFields . ') '; + + if (isset($options['comment'])) { + $comment = trim($options['comment'], " '"); + + $query .= sprintf("COMMENT = '%s' ", str_replace("'", "''", $comment)); + } + + if ( ! isset($options['charset'])) { + $options['charset'] = 'utf8'; + } + + if ( ! isset($options['collate'])) { + $options['collate'] = 'utf8_unicode_ci'; + } + + $query .= 'DEFAULT CHARACTER SET ' . $options['charset']; + $query .= ' COLLATE ' . $options['collate']; + + if ( ! isset($options['engine'])) { + $options['engine'] = 'InnoDB'; + } + $query .= ' ENGINE = ' . $options['engine']; + + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $columnSql = array(); + $queryParts = array(); + if ($diff->newName !== false) { + $queryParts[] = 'RENAME TO ' . $diff->newName; + } + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $columnArray = $column->toArray(); + $columnArray['comment'] = $this->getColumnComment($column); + $queryParts[] = 'CHANGE ' . $oldColumnName . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); + } + + $sql = array(); + $tableSql = array(); + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . implode(", ", $queryParts); + } + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff) + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $sql = array(); + $table = $diff->name; + + foreach ($diff->removedIndexes as $remKey => $remIndex) { + + foreach ($diff->addedIndexes as $addKey => $addIndex) { + if ($remIndex->getColumns() == $addIndex->getColumns()) { + + $columns = $addIndex->getColumns(); + $type = ''; + if ($addIndex->isUnique()) { + $type = 'UNIQUE '; + } + + $query = 'ALTER TABLE ' . $table . ' DROP INDEX ' . $remIndex->getName() . ', '; + $query .= 'ADD ' . $type . 'INDEX ' . $addIndex->getName(); + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($columns) . ')'; + + $sql[] = $query; + + unset($diff->removedIndexes[$remKey]); + unset($diff->addedIndexes[$addKey]); + + break; + } + } + } + + $sql = array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } else if ($index->hasFlag('fulltext')) { + $type .= 'FULLTEXT '; + } + + return $type; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if ( ! empty($columnDef['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + $unsigned = (isset($columnDef['unsigned']) && $columnDef['unsigned']) ? ' UNSIGNED' : ''; + + return $unsigned . $autoinc; + } + + /** + * {@inheritDoc} + */ + public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table=null) + { + if ($index instanceof Index) { + $indexName = $index->getQuotedName($this); + } else if(is_string($index)) { + $indexName = $index; + } else { + throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + if ($index instanceof Index && $index->isPrimary()) { + // mysql primary keys are always named "PRIMARY", + // so we cannot use them in statements because of them being keyword. + return $this->getDropPrimaryKeySQL($table); + } + + return 'DROP INDEX ' . $indexName . ' ON ' . $table; + } + + /** + * @param string $table + * + * @return string + */ + protected function getDropPrimaryKeySQL($table) + { + return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'mysql'; + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'LOCK IN SHARE MODE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'tinyint' => 'boolean', + 'smallint' => 'smallint', + 'mediumint' => 'integer', + 'int' => 'integer', + 'integer' => 'integer', + 'bigint' => 'bigint', + 'tinytext' => 'text', + 'mediumtext' => 'text', + 'longtext' => 'text', + 'text' => 'text', + 'varchar' => 'string', + 'string' => 'string', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'time' => 'time', + 'float' => 'float', + 'double' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'numeric' => 'decimal', + 'year' => 'date', + 'longblob' => 'blob', + 'blob' => 'blob', + 'mediumblob' => 'blob', + 'tinyblob' => 'blob', + 'binary' => 'blob', + 'varbinary' => 'blob', + 'set' => 'simple_array', + ); + } + + /** + * {@inheritDoc} + */ + public function getVarcharMaxLength() + { + return 65535; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\MySQLKeywords'; + } + + /** + * {@inheritDoc} + * + * MySQL commits a transaction implicitly when DROP TABLE is executed, however not + * if DROP TEMPORARY TABLE is executed. + */ + public function getDropTemporaryTableSQL($table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('getDropTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + return 'DROP TEMPORARY TABLE ' . $table; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'LONGBLOB'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php new file mode 100644 index 00000000..cd5c7744 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php @@ -0,0 +1,822 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\Sequence; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\DBALException; + +/** + * OraclePlatform. + * + * @since 2.0 + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + */ +class OraclePlatform extends AbstractPlatform +{ + /** + * Assertion for Oracle identifiers + * + * @link http://docs.oracle.com/cd/B19306_01/server.102/b14200/sql_elements008.htm + * + * @param string + * + * @throws DBALException + */ + static public function assertValidIdentifier($identifier) + { + if ( ! preg_match('(^(([a-zA-Z]{1}[a-zA-Z0-9_$#]{0,})|("[^"]+"))$)', $identifier)) { + throw new DBALException("Invalid Oracle identifier"); + } + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($value, $position, $length = null) + { + if ($length !== null) { + return "SUBSTR($value, $position, $length)"; + } + + return "SUBSTR($value, $position)"; + } + + /** + * {@inheritDoc} + */ + public function getNowExpression($type = 'timestamp') + { + switch ($type) { + case 'date': + case 'time': + case 'timestamp': + default: + return 'TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')'; + } + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'INSTR('.$str.', '.$substr.')'; + } + + return 'INSTR('.$str.', '.$substr.', '.$startPos.')'; + } + + /** + * {@inheritDoc} + */ + public function getGuidExpression() + { + return 'SYS_GUID()'; + } + + /** + * {@inheritDoc} + * + * Note: Since Oracle timestamp differences are calculated down to the microsecond we have to truncate + * them to the difference in days. This is obviously a restriction of the original functionality, but we + * need to make this a portable function. + */ + public function getDateDiffExpression($date1, $date2) + { + return "TRUNC(TO_NUMBER(SUBSTR((" . $date1 . "-" . $date2 . "), 1, INSTR(" . $date1 . "-" . $date2 .", ' '))))"; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return '(' . $date . '+' . $days . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return '(' . $date . '-' . $days . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return "ADD_MONTHS(" . $date . ", " . $months . ")"; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return "ADD_MONTHS(" . $date . ", -" . $months . ")"; + } + + /** + * {@inheritDoc} + */ + public function getBitAndComparisonExpression($value1, $value2) + { + return 'BITAND('.$value1 . ', ' . $value2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getBitOrComparisonExpression($value1, $value2) + { + return '(' . $value1 . '-' . + $this->getBitAndComparisonExpression($value1, $value2) + . '+' . $value2 . ')'; + } + + /** + * {@inheritDoc} + * + * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. + * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection + * in {@see listSequences()} + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + /** + * {@inheritDoc} + */ + public function getAlterSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + /** + * {@inheritDoc} + */ + public function getSequenceNextValSQL($sequenceName) + { + return 'SELECT ' . $sequenceName . '.nextval FROM DUAL'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case \Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + case \Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED: + return 'READ COMMITTED'; + case \Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ: + case \Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE: + return 'SERIALIZABLE'; + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'NUMBER(1)'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'NUMBER(10)'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'NUMBER(20)'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'NUMBER(5)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(2000)') + : ($length ? 'VARCHAR2(' . $length . ')' : 'VARCHAR2(4000)'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'CLOB'; + } + + public function getListDatabasesSQL() + { + return 'SELECT username FROM all_users'; + } + + public function getListSequencesSQL($database) + { + return "SELECT sequence_name, min_value, increment_by FROM sys.all_sequences ". + "WHERE SEQUENCE_OWNER = '".strtoupper($database)."'"; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($table, array $columns, array $options = array()) + { + $indexes = isset($options['indexes']) ? $options['indexes'] : array(); + $options['indexes'] = array(); + $sql = parent::_getCreateTableSQL($table, $columns, $options); + + foreach ($columns as $name => $column) { + if (isset($column['sequence'])) { + $sql[] = $this->getCreateSequenceSQL($column['sequence'], 1); + } + + if (isset($column['autoincrement']) && $column['autoincrement'] || + (isset($column['autoinc']) && $column['autoinc'])) { + $sql = array_merge($sql, $this->getCreateAutoincrementSql($name, $table)); + } + } + + if (isset($indexes) && ! empty($indexes)) { + foreach ($indexes as $index) { + $sql[] = $this->getCreateIndexSQL($index, $table); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + * + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaOracleReader.html + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + $table = strtoupper($table); + + return "SELECT uind.index_name AS name, " . + " uind.index_type AS type, " . + " decode( uind.uniqueness, 'NONUNIQUE', 0, 'UNIQUE', 1 ) AS is_unique, " . + " uind_col.column_name AS column_name, " . + " uind_col.column_position AS column_pos, " . + " (SELECT ucon.constraint_type FROM user_constraints ucon WHERE ucon.constraint_name = uind.index_name) AS is_primary ". + "FROM user_indexes uind, user_ind_columns uind_col " . + "WHERE uind.index_name = uind_col.index_name AND uind_col.table_name = '$table' ORDER BY uind_col.column_position ASC"; + } + + public function getListTablesSQL() + { + return 'SELECT * FROM sys.user_tables'; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return 'SELECT view_name, text FROM sys.user_views'; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + public function getCreateAutoincrementSql($name, $table, $start = 1) + { + $table = strtoupper($table); + $sql = array(); + + $indexName = $table . '_AI_PK'; + + $idx = new Index($indexName, array($name), true, true); + + $sql[] = 'DECLARE + constraints_Count NUMBER; +BEGIN + SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count FROM USER_CONSTRAINTS WHERE TABLE_NAME = \''.$table.'\' AND CONSTRAINT_TYPE = \'P\'; + IF constraints_Count = 0 OR constraints_Count = \'\' THEN + EXECUTE IMMEDIATE \''.$this->getCreateConstraintSQL($idx, $table).'\'; + END IF; +END;'; + + $sequenceName = $table . '_SEQ'; + $sequence = new Sequence($sequenceName, $start); + $sql[] = $this->getCreateSequenceSQL($sequence); + + $triggerName = $table . '_AI_PK'; + $sql[] = 'CREATE TRIGGER ' . $triggerName . ' + BEFORE INSERT + ON ' . $table . ' + FOR EACH ROW +DECLARE + last_Sequence NUMBER; + last_InsertID NUMBER; +BEGIN + SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $name . ' FROM DUAL; + IF (:NEW.' . $name . ' IS NULL OR :NEW.'.$name.' = 0) THEN + SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $name . ' FROM DUAL; + ELSE + SELECT NVL(Last_Number, 0) INTO last_Sequence + FROM User_Sequences + WHERE Sequence_Name = \'' . $sequenceName . '\'; + SELECT :NEW.' . $name . ' INTO last_InsertID FROM DUAL; + WHILE (last_InsertID > last_Sequence) LOOP + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END LOOP; + END IF; +END;'; + + return $sql; + } + + public function getDropAutoincrementSql($table) + { + $table = strtoupper($table); + $trigger = $table . '_AI_PK'; + + $sql[] = 'DROP TRIGGER ' . $trigger; + $sql[] = $this->getDropSequenceSQL($table.'_SEQ'); + + $indexName = $table . '_AI_PK'; + $sql[] = $this->getDropConstraintSQL($indexName, $table); + + return $sql; + } + + public function getListTableForeignKeysSQL($table) + { + $table = strtoupper($table); + + return "SELECT alc.constraint_name, + alc.DELETE_RULE, + alc.search_condition, + cols.column_name \"local_column\", + cols.position, + r_alc.table_name \"references_table\", + r_cols.column_name \"foreign_column\" + FROM user_cons_columns cols +LEFT JOIN user_constraints alc + ON alc.constraint_name = cols.constraint_name +LEFT JOIN user_constraints r_alc + ON alc.r_constraint_name = r_alc.constraint_name +LEFT JOIN user_cons_columns r_cols + ON r_alc.constraint_name = r_cols.constraint_name + AND cols.position = r_cols.position + WHERE alc.constraint_name = cols.constraint_name + AND alc.constraint_type = 'R' + AND alc.table_name = '".$table."'"; + } + + public function getListTableConstraintsSQL($table) + { + $table = strtoupper($table); + return 'SELECT * FROM user_constraints WHERE table_name = \'' . $table . '\''; + } + + public function getListTableColumnsSQL($table, $database = null) + { + $table = strtoupper($table); + + $tabColumnsTableName = "user_tab_columns"; + $ownerCondition = ''; + + if (null !== $database){ + $database = strtoupper($database); + $tabColumnsTableName = "all_tab_columns"; + $ownerCondition = "AND c.owner = '".$database."'"; + } + + return "SELECT c.*, d.comments FROM $tabColumnsTableName c ". + "INNER JOIN user_col_comments d ON d.TABLE_NAME = c.TABLE_NAME AND d.COLUMN_NAME = c.COLUMN_NAME ". + "WHERE c.table_name = '" . $table . "' ".$ownerCondition." ORDER BY c.column_name"; + } + + /** + * {@inheritDoc} + */ + public function getDropSequenceSQL($sequence) + { + if ($sequence instanceof Sequence) { + $sequence = $sequence->getQuotedName($this); + } + + return 'DROP SEQUENCE ' . $sequence; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($database) + { + return 'DROP USER ' . $database . ' CASCADE'; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + $commentsSQL = array(); + $columnSql = array(); + + $fields = array(); + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + if ($comment = $this->getColumnComment($column)) { + $commentsSQL[] = $this->getCommentOnColumnSQL($diff->name, $column->getName(), $comment); + } + } + + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ADD (' . implode(', ', $fields) . ')'; + } + + $fields = array(); + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $column = $columnDiff->column; + $fields[] = $column->getQuotedName($this). ' ' . $this->getColumnDeclarationSQL('', $column->toArray()); + if ($columnDiff->hasChanged('comment') && $comment = $this->getColumnComment($column)) { + $commentsSQL[] = $this->getCommentOnColumnSQL($diff->name, $column->getName(), $comment); + } + } + + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' MODIFY (' . implode(', ', $fields) . ')'; + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME COLUMN ' . $oldColumnName .' TO ' . $column->getQuotedName($this); + } + + $fields = array(); + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $fields[] = $column->getQuotedName($this); + } + + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' DROP (' . implode(', ', $fields).')'; + } + + $tableSql = array(); + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if ($diff->newName !== false) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME TO ' . $diff->newName; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff), $commentsSQL); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function prefersSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'oracle'; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset = null) + { + $limit = (int) $limit; + $offset = (int) $offset; + + if (preg_match('/^\s*SELECT/i', $query)) { + if (!preg_match('/\sFROM\s/i', $query)) { + $query .= " FROM dual"; + } + if ($limit > 0) { + $max = $offset + $limit; + $column = '*'; + if ($offset > 0) { + $min = $offset + 1; + $query = 'SELECT * FROM (SELECT a.' . $column . ', rownum AS doctrine_rownum FROM (' . + $query . + ') a WHERE rownum <= ' . $max . ') WHERE doctrine_rownum >= ' . $min; + } else { + $query = 'SELECT a.' . $column . ' FROM (' . $query . ') a WHERE ROWNUM <= ' . $max; + } + } + } + + return $query; + } + + /** + * {@inheritDoc} + * + * Oracle returns all column names in SQL result sets in uppercase. + */ + public function getSQLResultCasing($column) + { + return strtoupper($column); + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE GLOBAL TEMPORARY TABLE"; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sP'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d 00:00:00'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return '1900-01-01 H:i:s'; + } + + /** + * {@inheritDoc} + */ + public function fixSchemaElementName($schemaElementName) + { + if (strlen($schemaElementName) > 30) { + // Trim it + return substr($schemaElementName, 0, 30); + } + + return $schemaElementName; + } + + /** + * {@inheritDoc} + */ + public function getMaxIdentifierLength() + { + return 30; + } + + /** + * {@inheritDoc} + */ + public function supportsSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsForeignKeyOnUpdate() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE TABLE '.$tableName; + } + + /** + * {@inheritDoc} + */ + public function getDummySelectSQL() + { + return 'SELECT 1 FROM DUAL'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'integer' => 'integer', + 'number' => 'integer', + 'pls_integer' => 'boolean', + 'binary_integer' => 'boolean', + 'varchar' => 'string', + 'varchar2' => 'string', + 'nvarchar2' => 'string', + 'char' => 'string', + 'nchar' => 'string', + 'date' => 'datetime', + 'timestamp' => 'datetime', + 'timestamptz' => 'datetimetz', + 'float' => 'float', + 'long' => 'string', + 'clob' => 'text', + 'nclob' => 'text', + 'raw' => 'text', + 'long raw' => 'text', + 'rowid' => 'string', + 'urowid' => 'string', + 'blob' => 'blob', + ); + } + + /** + * {@inheritDoc} + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\OracleKeywords'; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'BLOB'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php new file mode 100644 index 00000000..36bbcb72 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -0,0 +1,767 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Table; + +/** + * PostgreSqlPlatform. + * + * @since 2.0 + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @todo Rename: PostgreSQLPlatform + */ +class PostgreSqlPlatform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getSubstringExpression($value, $from, $length = null) + { + if ($length === null) { + return 'SUBSTR(' . $value . ', ' . $from . ')'; + } + + return 'SUBSTR(' . $value . ', ' . $from . ', ' . $length . ')'; + } + + /** + * {@inheritDoc} + */ + public function getNowExpression() + { + return 'LOCALTIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'SIMILAR TO'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos !== false) { + $str = $this->getSubstringExpression($str, $startPos); + + return 'CASE WHEN (POSITION('.$substr.' IN '.$str.') = 0) THEN 0 ELSE (POSITION('.$substr.' IN '.$str.') + '.($startPos-1).') END'; + } + + return 'POSITION('.$substr.' IN '.$str.')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return "(" . $date ." + (" . $days . " || ' day')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return "(" . $date ." - (" . $days . " || ' day')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return "(" . $date ." + (" . $months . " || ' month')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return "(" . $date ." - (" . $months . " || ' month')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function supportsSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsSchemas() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function prefersSequences() + { + return true; + } + + public function getListDatabasesSQL() + { + return 'SELECT datname FROM pg_database'; + } + + public function getListSequencesSQL($database) + { + return "SELECT + c.relname, n.nspname AS schemaname + FROM + pg_class c, pg_namespace n + WHERE relkind = 'S' AND n.oid = c.relnamespace AND + (n.nspname NOT LIKE 'pg_%' AND n.nspname != 'information_schema')"; + } + + public function getListTablesSQL() + { + return "SELECT tablename AS table_name, schemaname AS schema_name + FROM pg_tables WHERE schemaname NOT LIKE 'pg_%' AND schemaname != 'information_schema' AND tablename != 'geometry_columns' AND tablename != 'spatial_ref_sys'"; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return 'SELECT viewname, definition FROM pg_views'; + } + + public function getListTableForeignKeysSQL($table, $database = null) + { + return "SELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid = + ( + SELECT c.oid + FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n + WHERE " .$this->getTableWhereClause($table) ." AND n.oid = c.relnamespace + ) + AND r.contype = 'f'"; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + public function getListTableConstraintsSQL($table) + { + return "SELECT + relname + FROM + pg_class + WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname = '$table' + AND pg_class.oid = pg_index.indrelid + AND (indisunique = 't' OR indisprimary = 't') + )"; + } + + /** + * {@inheritDoc} + * + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + return "SELECT relname, pg_index.indisunique, pg_index.indisprimary, + pg_index.indkey, pg_index.indrelid + FROM pg_class, pg_index + WHERE oid IN ( + SELECT indexrelid + FROM pg_index si, pg_class sc, pg_namespace sn + WHERE " . $this->getTableWhereClause($table, 'sc', 'sn')." AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid + ) AND pg_index.indexrelid = oid"; + } + + /** + * @param string $table + * @param string $classAlias + * @param string $namespaceAlias + * + * @return string + */ + private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n') + { + $whereClause = $namespaceAlias.".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND "; + if (strpos($table, ".") !== false) { + list($schema, $table) = explode(".", $table); + $schema = "'" . $schema . "'"; + } else { + $schema = "ANY(string_to_array((select setting from pg_catalog.pg_settings where name = 'search_path'),','))"; + } + $whereClause .= "$classAlias.relname = '" . $table . "' AND $namespaceAlias.nspname = $schema"; + + return $whereClause; + } + + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT + a.attnum, + a.attname AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM pg_catalog.pg_type t2 + WHERE t2.typtype = 'd' AND t2.typname = format_type(a.atttypid, a.atttypmod)) AS domain_complete_type, + a.attnotnull AS isnotnull, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_attrdef.adsrc + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n + WHERE ".$this->getTableWhereClause($table, 'c', 'n') ." + AND a.attnum > 0 + AND a.attrelid = c.oid + AND a.atttypid = t.oid + AND n.oid = c.relnamespace + ORDER BY a.attnum"; + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey) + { + $query = ''; + + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { + $query .= ' DEFERRABLE'; + } else { + $query .= ' NOT DEFERRABLE'; + } + + if ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false) { + $query .= ' INITIALLY DEFERRED'; + } else { + $query .= ' INITIALLY IMMEDIATE'; + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + $commentsSQL = array(); + $columnSql = array(); + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + if ($comment = $this->getColumnComment($column)) { + $commentsSQL[] = $this->getCommentOnColumnSQL($diff->name, $column->getName(), $comment); + } + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $query = 'DROP ' . $column->getQuotedName($this); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + foreach ($diff->changedColumns as $columnDiff) { + /** @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */ + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $oldColumnName = $columnDiff->oldColumnName; + $column = $columnDiff->column; + + if ($columnDiff->hasChanged('type')) { + $type = $column->getType(); + + // here was a server version check before, but DBAL API does not support this anymore. + $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSqlDeclaration($column->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + if ($columnDiff->hasChanged('default')) { + $query = 'ALTER ' . $oldColumnName . ' SET ' . $this->getDefaultValueDeclarationSQL($column->toArray()); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + if ($columnDiff->hasChanged('notnull')) { + $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotNull() ? 'SET' : 'DROP') . ' NOT NULL'; + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + if ($columnDiff->hasChanged('autoincrement')) { + if ($column->getAutoincrement()) { + // add autoincrement + $seqName = $diff->name . '_' . $oldColumnName . '_seq'; + + $sql[] = "CREATE SEQUENCE " . $seqName; + $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ") FROM " . $diff->name . "))"; + $query = "ALTER " . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')"; + $sql[] = "ALTER TABLE " . $diff->name . " " . $query; + } else { + // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have + $query = "ALTER " . $oldColumnName . " " . "DROP DEFAULT"; + $sql[] = "ALTER TABLE " . $diff->name . " " . $query; + } + } + + if ($columnDiff->hasChanged('comment') && $comment = $this->getColumnComment($column)) { + $commentsSQL[] = $this->getCommentOnColumnSQL($diff->name, $column->getName(), $comment); + } + + if ($columnDiff->hasChanged('length')) { + $query = 'ALTER ' . $column->getName() . ' TYPE ' . $column->getType()->getSqlDeclaration($column->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME COLUMN ' . $oldColumnName . ' TO ' . $column->getQuotedName($this); + } + + $tableSql = array(); + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if ($diff->newName !== false) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME TO ' . $diff->newName; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff), $commentsSQL); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getCreateSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' START ' . $sequence->getInitialValue(); + } + + /** + * {@inheritDoc} + */ + public function getAlterSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + /** + * {@inheritDoc} + */ + public function getDropSequenceSQL($sequence) + { + if ($sequence instanceof \Doctrine\DBAL\Schema\Sequence) { + $sequence = $sequence->getQuotedName($this); + } + return 'DROP SEQUENCE ' . $sequence; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')'; + + $sql[] = $query; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + } + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + * + * Postgres wants boolean values converted to the strings 'true'/'false'. + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (is_bool($value) || is_numeric($item)) { + $item[$key] = ($value) ? 'true' : 'false'; + } + } + } else { + if (is_bool($item) || is_numeric($item)) { + $item = ($item) ? 'true' : 'false'; + } + } + + return $item; + } + + public function getSequenceNextValSQL($sequenceName) + { + return "SELECT NEXTVAL('" . $sequenceName . "')"; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' + . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + if ( ! empty($field['autoincrement'])) { + return 'SERIAL'; + } + + return 'INT'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + if ( ! empty($field['autoincrement'])) { + return 'BIGSERIAL'; + } + return 'BIGINT'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $field) + { + return 'UUID'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'TEXT'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'postgresql'; + } + + /** + * {@inheritDoc} + * + * PostgreSQL returns all column names in SQL result sets in lowercase. + */ + public function getSQLResultCasing($column) + { + return strtolower($column); + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sO'; + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE '.$tableName.' '.(($cascade)?'CASCADE':''); + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'FOR SHARE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'smallint' => 'smallint', + 'int2' => 'smallint', + 'serial' => 'integer', + 'serial4' => 'integer', + 'int' => 'integer', + 'int4' => 'integer', + 'integer' => 'integer', + 'bigserial' => 'bigint', + 'serial8' => 'bigint', + 'bigint' => 'bigint', + 'int8' => 'bigint', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'text' => 'text', + 'varchar' => 'string', + 'interval' => 'string', + '_varchar' => 'string', + 'char' => 'string', + 'bpchar' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'timestamptz' => 'datetimetz', + 'time' => 'time', + 'timetz' => 'time', + 'float' => 'float', + 'float4' => 'float', + 'float8' => 'float', + 'double' => 'float', + 'double precision' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'money' => 'decimal', + 'numeric' => 'decimal', + 'year' => 'date', + 'uuid' => 'guid', + 'bytea' => 'blob', + ); + } + + /** + * {@inheritDoc} + */ + public function getVarcharMaxLength() + { + return 65535; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\PostgreSQLKeywords'; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'BYTEA'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php new file mode 100644 index 00000000..71829cd0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\Table; + +/** + * Platform to ensure compatibility of Doctrine with SQL Azure + * + * On top of SQL Server 2008 the following functionality is added: + * + * - Create tables with the FEDERATED ON syntax. + */ +class SQLAzurePlatform extends SQLServer2008Platform +{ + /** + * {@inheritDoc} + */ + public function getCreateTableSQL(Table $table, $createFlags=self::CREATE_INDEXES) + { + $sql = parent::getCreateTableSQL($table, $createFlags); + + if ($table->hasOption('azure.federatedOnColumnName')) { + $distributionName = $table->getOption('azure.federatedOnDistributionName'); + $columnName = $table->getOption('azure.federatedOnColumnName'); + $stmt = ' FEDERATED ON (' . $distributionName . ' = ' . $columnName . ')'; + + $sql[0] = $sql[0] . $stmt; + } + + return $sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php new file mode 100644 index 00000000..9923ef9d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +/** + * Platform to ensure compatibility of Doctrine with SQLServer2005 version and + * higher. + * + * Differences to SQL Server 2008 are: + * + * - DATETIME2 datatype does not exist, only DATETIME which has a precision of + * 3. This is not supported by PHP DateTime, so we are emulating it by + * setting .000 manually. + * - Starting with SQLServer2005 VARCHAR(MAX), VARBINARY(MAX) and + * NVARCHAR(max) replace the old TEXT, NTEXT and IMAGE types. See + * {@link http://www.sql-server-helper.com/faq/sql-server-2005-varchar-max-p01.aspx} + * for more information. + */ +class SQLServer2005Platform extends SQLServerPlatform +{ + /** + * {@inheritDoc} + */ + public function supportsLimitOffset() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'VARCHAR(MAX)'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php new file mode 100644 index 00000000..909ab84f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2008Platform.php @@ -0,0 +1,100 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +/** + * Platform to ensure compatibility of Doctrine with SQLServer2008 version. + * + * Differences to SQL Server 2005 and before are that a new DATETIME2 type was + * introduced that has a higher precision. + */ +class SQLServer2008Platform extends SQLServer2005Platform +{ + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + // 3 - microseconds precision length + // http://msdn.microsoft.com/en-us/library/ms187819.aspx + return 'DATETIME2(6)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s.u'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s.u P'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + /** + * {@inheritDoc} + * + * Adding Datetime2 Type + */ + protected function initializeDoctrineTypeMappings() + { + parent::initializeDoctrineTypeMappings(); + $this->doctrineTypeMapping['datetime2'] = 'datetime'; + $this->doctrineTypeMapping['date'] = 'date'; + $this->doctrineTypeMapping['time'] = 'time'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php new file mode 100644 index 00000000..acac55ba --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php @@ -0,0 +1,927 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\Table; + +/** + * The SQLServerPlatform provides the behavior, features and SQL dialect of the + * Microsoft SQL Server database platform. + * + * @since 2.0 + * @author Roman Borschel + * @author Jonathan H. Wage + * @author Benjamin Eberlei + */ +class SQLServerPlatform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return 'DATEADD(day, ' . $days . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return 'DATEADD(day, -1 * ' . $days . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return 'DATEADD(month, ' . $months . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return 'DATEADD(month, -1 * ' . $months . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + * + * MsSql prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * MsSql supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + /** + * {@inheritDoc} + */ + public function supportsCreateDropDatabase() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + $index = $index->getQuotedName($this); + } else if (!is_string($index)) { + throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + if (!isset($table)) { + return 'DROP INDEX ' . $index; + } + + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + + return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index') + ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . " + ELSE + DROP INDEX " . $index . " ON " . $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + // @todo does other code breaks because of this? + // force primary keys to be not null + foreach ($columns as &$column) { + if (isset($column['primary']) && $column['primary']) { + $column['notnull'] = true; + } + } + + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + + if (isset($options['primary']) && !empty($options['primary'])) { + $flags = ''; + if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { + $flags = ' NONCLUSTERED'; + } + $columnListSql .= ', PRIMARY KEY' . $flags . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if (!empty($check)) { + $query .= ', ' . $check; + } + $query .= ')'; + + $sql[] = $query; + + if (isset($options['indexes']) && !empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + } + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + $flags = ''; + if ($index->hasFlag('nonclustered')) { + $flags = ' NONCLUSTERED'; + } + return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY' . $flags . ' (' . $this->getIndexFieldDeclarationListSQL($index->getColumns()) . ')'; + } + + /** + * {@inheritDoc} + */ + public function getUniqueConstraintDeclarationSQL($name, Index $index) + { + $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index); + + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + + return $constraint; + } + + /** + * {@inheritDoc} + */ + public function getCreateIndexSQL(Index $index, $table) + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique() && !$index->isPrimary()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } + + if ($index->hasFlag('clustered')) { + $type .= 'CLUSTERED '; + } else if ($index->hasFlag('nonclustered')) { + $type .= 'NONCLUSTERED '; + } + + return $type; + } + + /** + * Extend unique key constraint with required filters + * + * @param string $sql + * @param Index $index + * + * @return string + */ + private function _appendUniqueConstraintDefinition($sql, Index $index) + { + $fields = array(); + foreach ($index->getColumns() as $field => $definition) { + if (!is_array($definition)) { + $field = $definition; + } + + $fields[] = $field . ' IS NOT NULL'; + } + + return $sql . ' WHERE ' . implode(' AND ', $fields); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $queryParts = array(); + $sql = array(); + $columnSql = array(); + + foreach ($diff->addedColumns as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->removedColumns as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + /* @var $columnDiff \Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $queryParts[] = 'ALTER COLUMN ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $sql[] = "sp_RENAME '". $diff->name. ".". $oldColumnName . "' , '".$column->getQuotedName($this)."', 'COLUMN'"; + $queryParts[] = 'ALTER COLUMN ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + $tableSql = array(); + + if ($this->onSchemaAlterTable($diff, $tableSql)) { + return array_merge($tableSql, $columnSql); + } + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + if ($diff->newName !== false) { + $sql[] = "sp_RENAME '" . $diff->name . "', '" . $diff->newName . "'"; + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** + * {@inheritDoc} + */ + public function getShowDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + /** + * {@inheritDoc} + */ + public function getListTablesSQL() + { + // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + return "SELECT name FROM sysobjects WHERE type = 'U' AND name != 'sysdiagrams' ORDER BY name"; + } + + /** + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "exec sp_columns @table_name = '" . $table . "'"; + } + + /** + * {@inheritDoc} + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return "SELECT f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id + WHERE OBJECT_NAME (f.parent_object_id) = '" . $table . "'"; + } + + /** + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + return "exec sp_helpindex '" . $table . "'"; + } + + /** + * {@inheritDoc} + */ + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name"; + } + + /** + * {@inheritDoc} + */ + public function getDropViewSQL($name) + { + return 'DROP VIEW ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * {@inheritDoc} + */ + public function getGuidExpression() + { + return 'UUID()'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'CHARINDEX(' . $substr . ', ' . $str . ')'; + } + + return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getModExpression($expression1, $expression2) + { + return $expression1 . ' % ' . $expression2; + } + + /** + * {@inheritDoc} + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + if ( ! $char) { + switch ($pos) { + case self::TRIM_LEADING: + $trimFn = 'LTRIM'; + break; + + case self::TRIM_TRAILING: + $trimFn = 'RTRIM'; + break; + + default: + return 'LTRIM(RTRIM(' . $str . '))'; + } + + return $trimFn . '(' . $str . ')'; + } + + /** Original query used to get those expressions + declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x'; + declare @pat varchar(10) = '%[^' + @trim_char + ']%'; + select @c as string + , @trim_char as trim_char + , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading + , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing + , reverse(stuff(reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null)), 1, patindex(@pat, reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null))) - 1, null)) as trim_both; + */ + $pattern = "'%[^' + $char + ']%'"; + + if ($pos == self::TRIM_LEADING) { + return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; + } + + if ($pos == self::TRIM_TRAILING) { + return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; + } + + return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null))) - 1, null))'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + $args = func_get_args(); + + return '(' . implode(' + ', $args) . ')'; + } + + public function getListDatabasesSQL() + { + return 'SELECT * FROM SYS.DATABASES'; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($value, $from, $length = null) + { + if (!is_null($length)) { + return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $length . ')'; + } + + return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'LEN(' . $column . ')'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $field) + { + return 'UNIQUEIDENTIFIER'; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NVARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'TEXT'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return (!empty($columnDef['autoincrement'])) ? ' IDENTITY' : ''; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BIT'; + } + + /** + * {@inheritDoc} + * + * @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html + */ + protected function doModifyLimitQuery($query, $limit, $offset = null) + { + if ($limit > 0) { + if ($offset == 0) { + $query = preg_replace('/^(SELECT\s(DISTINCT\s)?)/i', '\1TOP ' . $limit . ' ', $query); + } else { + $orderby = stristr($query, 'ORDER BY'); + + if ( ! $orderby) { + $over = 'ORDER BY (SELECT 0)'; + } else { + $over = preg_replace('/\"[^,]*\".\"([^,]*)\"/i', '"inner_tbl"."$1"', $orderby); + } + + // Remove ORDER BY clause from $query + $query = preg_replace('/\s+ORDER BY(.*)/', '', $query); + $query = preg_replace('/^SELECT\s/', '', $query); + + $start = $offset + 1; + $end = $offset + $limit; + + $query = "SELECT * FROM (SELECT ROW_NUMBER() OVER ($over) AS doctrine_rownum, $query) AS doctrine_tbl WHERE doctrine_rownum BETWEEN $start AND $end"; + } + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function supportsLimitOffset() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (is_bool($value) || is_numeric($item)) { + $item[$key] = ($value) ? 1 : 0; + } + } + } else if (is_bool($item) || is_numeric($item)) { + $item = ($item) ? 1 : 0; + } + + return $item; + } + + /** + * {@inheritDoc} + */ + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE TABLE"; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + return '#' . $tableName; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s.000'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d H:i:s.000'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return 'Y-m-d H:i:s.000'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return $this->getDateTimeFormatString(); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'mssql'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'bigint' => 'bigint', + 'numeric' => 'decimal', + 'bit' => 'boolean', + 'smallint' => 'smallint', + 'decimal' => 'decimal', + 'smallmoney' => 'integer', + 'int' => 'integer', + 'tinyint' => 'smallint', + 'money' => 'integer', + 'float' => 'float', + 'real' => 'float', + 'double' => 'float', + 'double precision' => 'float', + 'datetimeoffset' => 'datetimetz', + 'smalldatetime' => 'datetime', + 'datetime' => 'datetime', + 'char' => 'string', + 'varchar' => 'string', + 'text' => 'text', + 'nchar' => 'string', + 'nvarchar' => 'string', + 'ntext' => 'text', + 'binary' => 'text', + 'varbinary' => 'blob', + 'image' => 'text', + 'uniqueidentifier' => 'guid', + ); + } + + /** + * {@inheritDoc} + */ + public function createSavePoint($savepoint) + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + /** + * {@inheritDoc} + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * {@inheritDoc} + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** + * {@inheritDoc} + */ + public function appendLockHint($fromClause, $lockMode) + { + // @todo coorect + if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) { + return $fromClause . ' WITH (tablockx)'; + } + + if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) { + return $fromClause . ' WITH (tablockx)'; + } + + return $fromClause; + } + + /** + * {@inheritDoc} + */ + public function getForUpdateSQL() + { + return ' '; + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\MsSQLKeywords'; + } + + /** + * {@inheritDoc} + */ + public function quoteSingleIdentifier($str) + { + return "[" . str_replace("]", "][", $str) . "]"; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE TABLE '.$tableName; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'VARBINARY(MAX)'; + } + + /** + * {@inheritDoc} + */ + public function getDefaultValueDeclarationSQL($field) + { + if ( ! isset($field['default'])) { + return empty($field['notnull']) ? ' NULL' : ''; + } + + if ( ! isset($field['type'])) { + return " DEFAULT '" . $field['default'] . "'"; + } + + if (in_array((string) $field['type'], array('Integer', 'BigInteger', 'SmallInteger'))) { + return " DEFAULT " . $field['default']; + } + + if ((string) $field['type'] == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL()) { + return " DEFAULT " . $this->getCurrentTimestampSQL(); + } + + if ((string) $field['type'] == 'Boolean') { + return " DEFAULT '" . $this->convertBooleans($field['default']) . "'"; + } + + return " DEFAULT '" . $field['default'] . "'"; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php new file mode 100644 index 00000000..7ed52017 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -0,0 +1,529 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException; + +/** + * The SqlitePlatform class describes the specifics and dialects of the SQLite + * database platform. + * + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei + * @todo Rename: SQLitePlatform + */ +class SqlitePlatform extends AbstractPlatform +{ + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * {@inheritDoc} + */ + public function getNowExpression($type = 'timestamp') + { + switch ($type) { + case 'time': + return 'time(\'now\')'; + case 'date': + return 'date(\'now\')'; + case 'timestamp': + default: + return 'datetime(\'now\')'; + } + } + + /** + * {@inheritDoc} + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + $trimChar = ($char != false) ? (', ' . $char) : ''; + + switch ($pos) { + case self::TRIM_LEADING: + $trimFn = 'LTRIM'; + break; + + case self::TRIM_TRAILING: + $trimFn = 'RTRIM'; + break; + + default: + $trimFn = 'TRIM'; + } + + return $trimFn . '(' . $str . $trimChar . ')'; + } + + /** + * {@inheritDoc} + * + * SQLite only supports the 2 parameter variant of this function + */ + public function getSubstringExpression($value, $position, $length = null) + { + if ($length !== null) { + return 'SUBSTR(' . $value . ', ' . $position . ', ' . $length . ')'; + } + + return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE('.$str.', '.$substr.')'; + } + + return 'LOCATE('.$str.', '.$substr.', '.$startPos.')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'ROUND(JULIANDAY('.$date1 . ')-JULIANDAY('.$date2.'))'; + } + + /** + * {@inheritDoc} + */ + public function getDateAddDaysExpression($date, $days) + { + return "DATE(" . $date . ",'+". $days . " day')"; + } + + /** + * {@inheritDoc} + */ + public function getDateSubDaysExpression($date, $days) + { + return "DATE(" . $date . ",'-". $days . " day')"; + } + + /** + * {@inheritDoc} + */ + public function getDateAddMonthExpression($date, $months) + { + return "DATE(" . $date . ",'+". $months . " month')"; + } + + /** + * {@inheritDoc} + */ + public function getDateSubMonthExpression($date, $months) + { + return "DATE(" . $date . ",'-". $months . " month')"; + } + + /** + * {@inheritDoc} + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case \Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED: + return 0; + case \Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED: + case \Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ: + case \Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE: + return 1; + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getTinyIntTypeDeclarationSql(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getMediumIntTypeDeclarationSql(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return 'INTEGER'; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = array()) + { + $name = str_replace(".", "__", $name); + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; + } + + $query[] = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')'; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index => $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + + if (isset($options['unique']) && ! empty($options['unique'])) { + foreach ($options['unique'] as $index => $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + + return $query; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT'); + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'CLOB'; + } + + public function getListTableConstraintsSQL($table) + { + $table = str_replace(".", "__", $table); + + return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = '$table' AND sql NOT NULL ORDER BY name"; + } + + public function getListTableColumnsSQL($table, $currentDatabase = null) + { + $table = str_replace(".", "__", $table); + + return "PRAGMA table_info($table)"; + } + + /** + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $currentDatabase = null) + { + $table = str_replace(".", "__", $table); + + return "PRAGMA index_list($table)"; + } + + public function getListTablesSQL() + { + return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type = 'table' ORDER BY name"; + } + + /** + * {@inheritDoc} + */ + public function getListViewsSQL($database) + { + return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + /** + * {@inheritDoc} + * + * SQLite does support foreign key constraints, but only in CREATE TABLE statements... + * This really limits their usefulness and requires SQLite specific handling, so + * we simply say that SQLite does NOT support foreign keys for now... + */ + public function supportsForeignKeyConstraints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsAlterTable() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'sqlite'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableName = str_replace(".", "__", $tableName); + return 'DELETE FROM '.$tableName; + } + + /** + * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction() + * + * @param int|float $value + * + * @return float + */ + static public function udfSqrt($value) + { + return sqrt($value); + } + + /** + * User-defined function for Sqlite that implements MOD(a, b) + * + * @param integer $a + * @param integer $b + * + * @return integer + */ + static public function udfMod($a, $b) + { + return ($a % $b); + } + + /** + * @param string $str + * @param string $substr + * @param integer $offset + * + * @return integer + */ + static public function udfLocate($str, $substr, $offset = 0) + { + $pos = strpos($str, $substr, $offset); + if ($pos !== false) { + return $pos+1; + } + + return 0; + } + + public function getForUpdateSql() + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'boolean' => 'boolean', + 'tinyint' => 'boolean', + 'smallint' => 'smallint', + 'mediumint' => 'integer', + 'int' => 'integer', + 'integer' => 'integer', + 'serial' => 'integer', + 'bigint' => 'bigint', + 'bigserial' => 'bigint', + 'clob' => 'text', + 'tinytext' => 'text', + 'mediumtext' => 'text', + 'longtext' => 'text', + 'text' => 'text', + 'varchar' => 'string', + 'longvarchar' => 'string', + 'varchar2' => 'string', + 'nvarchar' => 'string', + 'image' => 'string', + 'ntext' => 'string', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'time' => 'time', + 'float' => 'float', + 'double' => 'float', + 'double precision' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'numeric' => 'decimal', + 'blob' => 'blob', + ); + } + + /** + * {@inheritDoc} + */ + protected function getReservedKeywordsClass() + { + return 'Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords'; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $field) + { + return 'BLOB'; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + $tableName = str_replace(".", "__", $tableName); + + return $tableName; + } + + /** + * {@inheritDoc} + * + * Sqlite Platform emulates schema by underscoring each dot and generating tables + * into the default database. + * + * This hack is implemented to be able to use SQLite as testdriver when + * using schema supporting databases. + */ + public function canEmulateSchemas() + { + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Portability/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Portability/Connection.php new file mode 100644 index 00000000..410fa825 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Portability/Connection.php @@ -0,0 +1,119 @@ +. + */ + + +namespace Doctrine\DBAL\Portability; + +use Doctrine\Common\EventManager; +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Cache\QueryCacheProfile; + +class Connection extends \Doctrine\DBAL\Connection +{ + const PORTABILITY_ALL = 255; + const PORTABILITY_NONE = 0; + const PORTABILITY_RTRIM = 1; + const PORTABILITY_EMPTY_TO_NULL = 4; + const PORTABILITY_FIX_CASE = 8; + + const PORTABILITY_ORACLE = 9; + const PORTABILITY_POSTGRESQL = 13; + const PORTABILITY_SQLITE = 13; + const PORTABILITY_OTHERVENDORS = 12; + const PORTABILITY_DRIZZLE = 13; + const PORTABILITY_SQLSRV = 13; + + /** + * @var int + */ + private $portability = self::PORTABILITY_NONE; + + /** + * @var int + */ + private $case; + + public function connect() + { + $ret = parent::connect(); + if ($ret) { + $params = $this->getParams(); + if (isset($params['portability'])) { + if ($this->_platform->getName() === "oracle") { + $params['portability'] = $params['portability'] & self::PORTABILITY_ORACLE; + } else if ($this->_platform->getName() === "postgresql") { + $params['portability'] = $params['portability'] & self::PORTABILITY_POSTGRESQL; + } else if ($this->_platform->getName() === "sqlite") { + $params['portability'] = $params['portability'] & self::PORTABILITY_SQLITE; + } else if ($this->_platform->getName() === "drizzle") { + $params['portability'] = self::PORTABILITY_DRIZZLE; + } else if ($this->_platform->getName() === 'sqlsrv') { + $params['portability'] = $params['portabililty'] & self::PORTABILITY_SQLSRV; + } else { + $params['portability'] = $params['portability'] & self::PORTABILITY_OTHERVENDORS; + } + $this->portability = $params['portability']; + } + if (isset($params['fetch_case']) && $this->portability & self::PORTABILITY_FIX_CASE) { + if ($this->_conn instanceof \Doctrine\DBAL\Driver\PDOConnection) { + // make use of c-level support for case handling + $this->_conn->setAttribute(\PDO::ATTR_CASE, $params['fetch_case']); + } else { + $this->case = ($params['fetch_case'] == \PDO::CASE_LOWER) ? CASE_LOWER : CASE_UPPER; + } + } + } + return $ret; + } + + public function getPortability() + { + return $this->portability; + } + + public function getFetchCase() + { + return $this->case; + } + + public function executeQuery($query, array $params = array(), $types = array(), QueryCacheProfile $qcp = null) + { + return new Statement(parent::executeQuery($query, $params, $types, $qcp), $this); + } + + /** + * Prepares an SQL statement. + * + * @param string $statement The SQL statement to prepare. + * @return \Doctrine\DBAL\Driver\Statement The prepared statement. + */ + public function prepare($statement) + { + return new Statement(parent::prepare($statement), $this); + } + + public function query() + { + $this->connect(); + + $stmt = call_user_func_array(array($this->_conn, 'query'), func_get_args()); + return new Statement($stmt, $this); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Portability/Statement.php b/doctrine/dbal/lib/Doctrine/DBAL/Portability/Statement.php new file mode 100644 index 00000000..98076bda --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Portability/Statement.php @@ -0,0 +1,195 @@ +. + */ + +namespace Doctrine\DBAL\Portability; + +use PDO; + +/** + * Portability Wrapper for a Statement + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +class Statement implements \IteratorAggregate, \Doctrine\DBAL\Driver\Statement +{ + + /** + * @var int + */ + private $portability; + + /** + * @var \Doctrine\DBAL\Driver\Statement + */ + private $stmt; + + /** + * @var int + */ + private $case; + + /** + * @var int + */ + private $defaultFetchMode = PDO::FETCH_BOTH; + + /** + * Wraps Statement and applies portability measures + * + * @param \Doctrine\DBAL\Driver\Statement $stmt + * @param \Doctrine\DBAL\Connection $conn + */ + public function __construct($stmt, Connection $conn) + { + $this->stmt = $stmt; + $this->portability = $conn->getPortability(); + $this->case = $conn->getFetchCase(); + } + + public function bindParam($column, &$variable, $type = null,$length = null) + { + return $this->stmt->bindParam($column, $variable, $type); + } + + public function bindValue($param, $value, $type = null) + { + return $this->stmt->bindValue($param, $value, $type); + } + + public function closeCursor() + { + return $this->stmt->closeCursor(); + } + + public function columnCount() + { + return $this->stmt->columnCount(); + } + + public function errorCode() + { + return $this->stmt->errorCode(); + } + + public function errorInfo() + { + return $this->stmt->errorInfo(); + } + + public function execute($params = null) + { + return $this->stmt->execute($params); + } + + public function setFetchMode($fetchMode, $arg1 = null, $arg2 = null) + { + $this->defaultFetchMode = $fetchMode; + $this->stmt->setFetchMode($fetchMode, $arg1, $arg2); + } + + public function getIterator() + { + $data = $this->fetchAll(); + return new \ArrayIterator($data); + } + + public function fetch($fetchMode = null) + { + $fetchMode = $fetchMode ?: $this->defaultFetchMode; + + $row = $this->stmt->fetch($fetchMode); + + $row = $this->fixRow($row, + $this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL|Connection::PORTABILITY_RTRIM), + !is_null($this->case) && ($fetchMode == PDO::FETCH_ASSOC || $fetchMode == PDO::FETCH_BOTH) && ($this->portability & Connection::PORTABILITY_FIX_CASE) + ); + + return $row; + } + + public function fetchAll($fetchMode = null, $columnIndex = 0) + { + $fetchMode = $fetchMode ?: $this->defaultFetchMode; + + if ($columnIndex != 0) { + $rows = $this->stmt->fetchAll($fetchMode, $columnIndex); + } else { + $rows = $this->stmt->fetchAll($fetchMode); + } + + $iterateRow = $this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL|Connection::PORTABILITY_RTRIM); + $fixCase = !is_null($this->case) && ($fetchMode == PDO::FETCH_ASSOC || $fetchMode == PDO::FETCH_BOTH) && ($this->portability & Connection::PORTABILITY_FIX_CASE); + if ( ! $iterateRow && !$fixCase) { + return $rows; + } + + foreach ($rows as $num => $row) { + $rows[$num] = $this->fixRow($row, $iterateRow, $fixCase); + } + + return $rows; + } + + protected function fixRow($row, $iterateRow, $fixCase) + { + if ( ! $row) { + return $row; + } + + if ($fixCase) { + $row = array_change_key_case($row, $this->case); + } + + if ($iterateRow) { + foreach ($row as $k => $v) { + if (($this->portability & Connection::PORTABILITY_EMPTY_TO_NULL) && $v === '') { + $row[$k] = null; + } else if (($this->portability & Connection::PORTABILITY_RTRIM) && is_string($v)) { + $row[$k] = rtrim($v); + } + } + } + return $row; + } + + public function fetchColumn($columnIndex = 0) + { + $value = $this->stmt->fetchColumn($columnIndex); + + if ($this->portability & (Connection::PORTABILITY_EMPTY_TO_NULL|Connection::PORTABILITY_RTRIM)) { + if (($this->portability & Connection::PORTABILITY_EMPTY_TO_NULL) && $value === '') { + $value = null; + } else if (($this->portability & Connection::PORTABILITY_RTRIM) && is_string($value)) { + $value = rtrim($value); + } + } + + return $value; + } + + public function rowCount() + { + return $this->stmt->rowCount(); + } + +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/CompositeExpression.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/CompositeExpression.php new file mode 100644 index 00000000..5d55b22c --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/CompositeExpression.php @@ -0,0 +1,130 @@ +. + */ + +namespace Doctrine\DBAL\Query\Expression; + +/** + * Composite expression is responsible to build a group of similar expression. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.1 + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class CompositeExpression implements \Countable +{ + /** + * Constant that represents an AND composite expression + */ + const TYPE_AND = 'AND'; + + /** + * Constant that represents an OR composite expression + */ + const TYPE_OR = 'OR'; + + /** + * @var string Holds the instance type of composite expression + */ + private $type; + + /** + * @var array Each expression part of the composite expression + */ + private $parts = array(); + + /** + * Constructor. + * + * @param string $type Instance type of composite expression + * @param array $parts Composition of expressions to be joined on composite expression + */ + public function __construct($type, array $parts = array()) + { + $this->type = $type; + + $this->addMultiple($parts); + } + + /** + * Adds multiple parts to composite expression. + * + * @param array $parts + * + * @return CompositeExpression + */ + public function addMultiple(array $parts = array()) + { + foreach ((array) $parts as $part) { + $this->add($part); + } + + return $this; + } + + /** + * Adds an expression to composite expression. + * + * @param mixed $part + * @return CompositeExpression + */ + public function add($part) + { + if ( ! empty($part) || ($part instanceof self && $part->count() > 0)) { + $this->parts[] = $part; + } + + return $this; + } + + /** + * Retrieves the amount of expressions on composite expression. + * + * @return integer + */ + public function count() + { + return count($this->parts); + } + + /** + * Retrieve the string representation of this composite expression. + * + * @return string + */ + public function __toString() + { + if (count($this->parts) === 1) { + return (string) $this->parts[0]; + } + + return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')'; + } + + /** + * Return type of this composite expression (AND/OR) + * + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php new file mode 100644 index 00000000..4f502328 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/Expression/ExpressionBuilder.php @@ -0,0 +1,264 @@ +. + */ + +namespace Doctrine\DBAL\Query\Expression; + +use Doctrine\DBAL\Connection; + +/** + * ExpressionBuilder class is responsible to dynamically create SQL query parts. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.1 + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class ExpressionBuilder +{ + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + + /** + * @var Doctrine\DBAL\Connection DBAL Connection + */ + private $connection = null; + + /** + * Initializes a new ExpressionBuilder. + * + * @param \Doctrine\DBAL\Connection $connection DBAL Connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Creates a conjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) AND (u.role = ?) + * $expr->andX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * @return CompositeExpression + */ + public function andX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + /** + * Creates a disjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?) OR (u.role = ?) + * $qb->where($qb->expr()->orX('u.type = ?', 'u.role = ?')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * @return CompositeExpression + */ + public function orX($x = null) + { + return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); + } + + /** + * Creates a comparison expression. + * + * @param mixed $x Left expression + * @param string $operator One of the ExpressionBuilder::* constants. + * @param mixed $y Right expression + * @return string + */ + public function comparison($x, $operator, $y) + { + return $x . ' ' . $operator . ' ' . $y; + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function eq($x, $y) + { + return $this->comparison($x, self::EQ, $y); + } + + /** + * Creates a non equality comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <> . Example: + * + * [php] + * // u.id <> 1 + * $q->where($q->expr()->neq('u.id', '1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function neq($x, $y) + { + return $this->comparison($x, self::NEQ, $y); + } + + /** + * Creates a lower-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a < . Example: + * + * [php] + * // u.id < ? + * $q->where($q->expr()->lt('u.id', '?')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function lt($x, $y) + { + return $this->comparison($x, self::LT, $y); + } + + /** + * Creates a lower-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <= . Example: + * + * [php] + * // u.id <= ? + * $q->where($q->expr()->lte('u.id', '?')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function lte($x, $y) + { + return $this->comparison($x, self::LTE, $y); + } + + /** + * Creates a greater-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a > . Example: + * + * [php] + * // u.id > ? + * $q->where($q->expr()->gt('u.id', '?')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function gt($x, $y) + { + return $this->comparison($x, self::GT, $y); + } + + /** + * Creates a greater-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a >= . Example: + * + * [php] + * // u.id >= ? + * $q->where($q->expr()->gte('u.id', '?')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return string + */ + public function gte($x, $y) + { + return $this->comparison($x, self::GTE, $y); + } + + /** + * Creates an IS NULL expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IS NULL + * + * @return string + */ + public function isNull($x) + { + return $x . ' IS NULL'; + } + + /** + * Creates an IS NOT NULL expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IS NOT NULL + * + * @return string + */ + public function isNotNull($x) + { + return $x . ' IS NOT NULL'; + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by LIKE() comparison. + * @param mixed $y Argument to be used in LIKE() comparison. + * + * @return string + */ + public function like($x, $y) + { + return $this->comparison($x, 'LIKE', $y); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input Parameter to be quoted. + * @param string $type Type of the parameter. + * + * @return string + */ + public function literal($input, $type = null) + { + return $this->connection->quote($input, $type); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php new file mode 100644 index 00000000..e0c5af14 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php @@ -0,0 +1,1087 @@ +. + */ + +namespace Doctrine\DBAL\Query; + +use Doctrine\DBAL\Query\Expression\CompositeExpression, + Doctrine\DBAL\Connection; + +/** + * QueryBuilder class is responsible to dynamically create SQL queries. + * + * Important: Verify that every feature you use will work with your database vendor. + * SQL Query Builder does not attempt to validate the generated SQL at all. + * + * The query builder does no validation whatsoever if certain features even work with the + * underlying database vendor. Limit queries and joins are NOT applied to UPDATE and DELETE statements + * even if some vendors such as MySQL support it. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.1 + * @author Guilherme Blanco + * @author Benjamin Eberlei + */ +class QueryBuilder +{ + /* The query types. */ + const SELECT = 0; + const DELETE = 1; + const UPDATE = 2; + + /** The builder states. */ + const STATE_DIRTY = 0; + const STATE_CLEAN = 1; + + /** + * @var Doctrine\DBAL\Connection DBAL Connection + */ + private $connection = null; + + /** + * @var array The array of SQL parts collected. + */ + private $sqlParts = array( + 'select' => array(), + 'from' => array(), + 'join' => array(), + 'set' => array(), + 'where' => null, + 'groupBy' => array(), + 'having' => null, + 'orderBy' => array() + ); + + /** + * @var string The complete SQL string for this query. + */ + private $sql; + + /** + * @var array The query parameters. + */ + private $params = array(); + + /** + * @var array The parameter type map of this query. + */ + private $paramTypes = array(); + + /** + * @var integer The type of query this is. Can be select, update or delete. + */ + private $type = self::SELECT; + + /** + * @var integer The state of the query object. Can be dirty or clean. + */ + private $state = self::STATE_CLEAN; + + /** + * @var integer The index of the first result to retrieve. + */ + private $firstResult = null; + + /** + * @var integer The maximum number of results to retrieve. + */ + private $maxResults = null; + + /** + * The counter of bound parameters used with {@see bindValue) + * + * @var int + */ + private $boundCounter = 0; + + /** + * Initializes a new QueryBuilder. + * + * @param \Doctrine\DBAL\Connection $connection DBAL Connection + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + * + * @return \Doctrine\DBAL\Query\Expression\ExpressionBuilder + */ + public function expr() + { + return $this->connection->getExpressionBuilder(); + } + + /** + * Get the type of the currently built query. + * + * @return integer + */ + public function getType() + { + return $this->type; + } + + /** + * Get the associated DBAL Connection for this query builder. + * + * @return \Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->connection; + } + + /** + * Get the state of this query builder instance. + * + * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. + */ + public function getState() + { + return $this->state; + } + + /** + * Execute this query using the bound parameters and their types. + * + * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} + * for insert, update and delete statements. + * + * @return mixed + */ + public function execute() + { + if ($this->type == self::SELECT) { + return $this->connection->executeQuery($this->getSQL(), $this->params, $this->paramTypes); + } else { + return $this->connection->executeUpdate($this->getSQL(), $this->params, $this->paramTypes); + } + } + + /** + * Get the complete SQL string formed by the current specifications of this QueryBuilder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * echo $qb->getSQL(); // SELECT u FROM User u + * + * + * @return string The sql query string. + */ + public function getSQL() + { + if ($this->sql !== null && $this->state === self::STATE_CLEAN) { + return $this->sql; + } + + $sql = ''; + + switch ($this->type) { + case self::DELETE: + $sql = $this->getSQLForDelete(); + break; + + case self::UPDATE: + $sql = $this->getSQLForUpdate(); + break; + + case self::SELECT: + default: + $sql = $this->getSQLForSelect(); + break; + } + + $this->state = self::STATE_CLEAN; + $this->sql = $sql; + + return $sql; + } + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id') + * ->setParameter(':user_id', 1); + * + * + * @param string|integer $key The parameter position or name. + * @param mixed $value The parameter value. + * @param string|null $type PDO::PARAM_* + * @return QueryBuilder This QueryBuilder instance. + */ + public function setParameter($key, $value, $type = null) + { + if ($type !== null) { + $this->paramTypes[$key] = $type; + } + + $this->params[$key] = $value; + + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(array( + * ':user_id1' => 1, + * ':user_id2' => 2 + * )); + * + * + * @param array $params The query parameters to set. + * @param array $types The query parameters types to set. + * @return QueryBuilder This QueryBuilder instance. + */ + public function setParameters(array $params, array $types = array()) + { + $this->paramTypes = $types; + $this->params = $params; + + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed. + * + * @return array The currently defined query parameters. + */ + public function getParameters() + { + return $this->params; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param mixed $key The key (index or name) of the bound parameter. + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) + { + return isset($this->params[$key]) ? $this->params[$key] : null; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param integer $firstResult The first result to return. + * @return \Doctrine\DBAL\Query\QueryBuilder This QueryBuilder instance. + */ + public function setFirstResult($firstResult) + { + $this->state = self::STATE_DIRTY; + $this->firstResult = $firstResult; + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return integer The position of the first result. + */ + public function getFirstResult() + { + return $this->firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param integer $maxResults The maximum number of results to retrieve. + * @return \Doctrine\DBAL\Query\QueryBuilder This QueryBuilder instance. + */ + public function setMaxResults($maxResults) + { + $this->state = self::STATE_DIRTY; + $this->maxResults = $maxResults; + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return integer Maximum number of results. + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Either appends to or replaces a single, generic query part. + * + * The available parts are: 'select', 'from', 'set', 'where', + * 'groupBy', 'having' and 'orderBy'. + * + * @param string $sqlPartName + * @param string $sqlPart + * @param boolean $append + * @return \Doctrine\DBAL\Query\QueryBuilder This QueryBuilder instance. + */ + public function add($sqlPartName, $sqlPart, $append = false) + { + $isArray = is_array($sqlPart); + $isMultiple = is_array($this->sqlParts[$sqlPartName]); + + if ($isMultiple && !$isArray) { + $sqlPart = array($sqlPart); + } + + $this->state = self::STATE_DIRTY; + + if ($append) { + if ($sqlPartName == "orderBy" || $sqlPartName == "groupBy" || $sqlPartName == "select" || $sqlPartName == "set") { + foreach ($sqlPart as $part) { + $this->sqlParts[$sqlPartName][] = $part; + } + } else if ($isArray && is_array($sqlPart[key($sqlPart)])) { + $key = key($sqlPart); + $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key]; + } else if ($isMultiple) { + $this->sqlParts[$sqlPartName][] = $sqlPart; + } else { + $this->sqlParts[$sqlPartName] = $sqlPart; + } + + return $this; + } + + $this->sqlParts[$sqlPartName] = $sqlPart; + + return $this; + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id', 'p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); + * + * + * @param mixed $select The selection expressions. + * @return QueryBuilder This QueryBuilder instance. + */ + public function select($select = null) + { + $this->type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects, false); + } + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->addSelect('p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); + * + * + * @param mixed $select The selection expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addSelect($select = null) + { + $this->type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects, true); + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain table. + * + * + * $qb = $conn->createQueryBuilder() + * ->delete('users', 'u') + * ->where('u.id = :user_id'); + * ->setParameter(':user_id', 1); + * + * + * @param string $delete The table whose rows are subject to the deletion. + * @param string $alias The table alias used in the constructed query. + * @return QueryBuilder This QueryBuilder instance. + */ + public function delete($delete = null, $alias = null) + { + $this->type = self::DELETE; + + if ( ! $delete) { + return $this; + } + + return $this->add('from', array( + 'table' => $delete, + 'alias' => $alias + )); + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain table + * + * + * $qb = $conn->createQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $update The table whose rows are subject to the update. + * @param string $alias The table alias used in the constructed query. + * @return QueryBuilder This QueryBuilder instance. + */ + public function update($update = null, $alias = null) + { + $this->type = self::UPDATE; + + if ( ! $update) { + return $this; + } + + return $this->add('from', array( + 'table' => $update, + 'alias' => $alias + )); + } + + /** + * Create and add a query root corresponding to the table identified by the + * given alias, forming a cartesian product with any existing query roots. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->from('users', 'u') + * + * + * @param string $from The table + * @param string $alias The alias of the table + * @return QueryBuilder This QueryBuilder instance. + */ + public function from($from, $alias) + { + return $this->add('from', array( + 'table' => $from, + 'alias' => $alias + ), true); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause + * @param string $join The table name to join + * @param string $alias The alias of the join table + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function join($fromAlias, $join, $alias, $condition = null) + { + return $this->innerJoin($fromAlias, $join, $alias, $condition); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause + * @param string $join The table name to join + * @param string $alias The alias of the join table + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function innerJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', array( + $fromAlias => array( + 'joinType' => 'inner', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition + ) + ), true); + } + + /** + * Creates and adds a left join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause + * @param string $join The table name to join + * @param string $alias The alias of the join table + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function leftJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', array( + $fromAlias => array( + 'joinType' => 'left', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition + ) + ), true); + } + + /** + * Creates and adds a right join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause + * @param string $join The table name to join + * @param string $alias The alias of the join table + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function rightJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', array( + $fromAlias => array( + 'joinType' => 'right', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition + ) + ), true); + } + + /** + * Sets a new value for a column in a bulk update query. + * + * + * $qb = $conn->createQueryBuilder() + * ->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $key The column to set. + * @param string $value The value, expression, placeholder, etc. + * @return QueryBuilder This QueryBuilder instance. + */ + public function set($key, $value) + { + return $this->add('set', $key .' = ' . $value, true); + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programatically build and/or expressions + * $qb = $conn->createQueryBuilder(); + * + * $or = $qb->expr()->orx(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('users', 'u') + * ->set('u.password', md5('password')) + * ->where($or); + * + * + * @param mixed $predicates The restriction predicates. + * @return QueryBuilder This QueryBuilder instance. + */ + public function where($predicates) + { + if ( ! (func_num_args() == 1 && $predicates instanceof CompositeExpression) ) { + $predicates = new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @param mixed $where The query restrictions. + * @return QueryBuilder This QueryBuilder instance. + * @see where() + */ + public function andWhere($where) + { + $where = $this->getQueryPart('where'); + $args = func_get_args(); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new CompositeExpression(CompositeExpression::TYPE_AND, $args); + } + + return $this->add('where', $where, true); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @param mixed $where The WHERE statement + * @return QueryBuilder $qb + * @see where() + */ + public function orWhere($where) + { + $where = $this->getQueryPart('where'); + $args = func_get_args(); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new CompositeExpression(CompositeExpression::TYPE_OR, $args); + } + + return $this->add('where', $where, true); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.id'); + * + * + * @param mixed $groupBy The grouping expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function groupBy($groupBy) + { + if (empty($groupBy)) { + return $this; + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, false); + } + + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.lastLogin'); + * ->addGroupBy('u.createdAt') + * + * + * @param mixed $groupBy The grouping expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addGroupBy($groupBy) + { + if (empty($groupBy)) { + return $this; + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, true); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * @return QueryBuilder This QueryBuilder instance. + */ + public function having($having) + { + if ( ! (func_num_args() == 1 && $having instanceof CompositeExpression)) { + $having = new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * @return QueryBuilder This QueryBuilder instance. + */ + public function andHaving($having) + { + $having = $this->getQueryPart('having'); + $args = func_get_args(); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new CompositeExpression(CompositeExpression::TYPE_AND, $args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * @return QueryBuilder This QueryBuilder instance. + */ + public function orHaving($having) + { + $having = $this->getQueryPart('having'); + $args = func_get_args(); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new CompositeExpression(CompositeExpression::TYPE_OR, $args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * @return QueryBuilder This QueryBuilder instance. + */ + public function orderBy($sort, $order = null) + { + return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), false); + } + + /** + * Adds an ordering to the query results. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addOrderBy($sort, $order = null) + { + return $this->add('orderBy', $sort . ' ' . (! $order ? 'ASC' : $order), true); + } + + /** + * Get a query part by its name. + * + * @param string $queryPartName + * @return mixed $queryPart + */ + public function getQueryPart($queryPartName) + { + return $this->sqlParts[$queryPartName]; + } + + /** + * Get all query parts. + * + * @return array $sqlParts + */ + public function getQueryParts() + { + return $this->sqlParts; + } + + /** + * Reset SQL parts + * + * @param array $queryPartNames + * @return QueryBuilder + */ + public function resetQueryParts($queryPartNames = null) + { + if (is_null($queryPartNames)) { + $queryPartNames = array_keys($this->sqlParts); + } + + foreach ($queryPartNames as $queryPartName) { + $this->resetQueryPart($queryPartName); + } + + return $this; + } + + /** + * Reset single SQL part + * + * @param string $queryPartName + * @return QueryBuilder + */ + public function resetQueryPart($queryPartName) + { + $this->sqlParts[$queryPartName] = is_array($this->sqlParts[$queryPartName]) + ? array() : null; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + private function getSQLForSelect() + { + $query = 'SELECT ' . implode(', ', $this->sqlParts['select']) . ' FROM '; + + $fromClauses = array(); + + // Loop through all FROM clauses + foreach ($this->sqlParts['from'] as $from) { + $fromClause = $from['table'] . ' ' . $from['alias']; + + if (isset($this->sqlParts['join'][$from['alias']])) { + foreach ($this->sqlParts['join'][$from['alias']] as $join) { + $fromClause .= ' ' . strtoupper($join['joinType']) + . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'] + . ' ON ' . ((string) $join['joinCondition']); + } + } + + $fromClauses[$from['alias']] = $fromClause; + } + + // loop through all JOIN clasues for validation purpose + foreach ($this->sqlParts['join'] as $fromAlias => $joins) { + if ( ! isset($fromClauses[$fromAlias]) ) { + throw QueryException::unknownFromAlias($fromAlias, array_keys($fromClauses)); + } + } + + $query .= implode(', ', $fromClauses) + . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '') + . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '') + . ($this->sqlParts['having'] !== null ? ' HAVING ' . ((string) $this->sqlParts['having']) : '') + . ($this->sqlParts['orderBy'] ? ' ORDER BY ' . implode(', ', $this->sqlParts['orderBy']) : ''); + + return ($this->maxResults === null && $this->firstResult == null) + ? $query + : $this->connection->getDatabasePlatform()->modifyLimitQuery($query, $this->maxResults, $this->firstResult); + } + + /** + * Converts this instance into an UPDATE string in SQL. + * + * @return string + */ + private function getSQLForUpdate() + { + $table = $this->sqlParts['from']['table'] . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + $query = 'UPDATE ' . $table + . ' SET ' . implode(", ", $this->sqlParts['set']) + . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + + return $query; + } + + /** + * Converts this instance into a DELETE string in SQL. + * + * @return string + */ + private function getSQLForDelete() + { + $table = $this->sqlParts['from']['table'] . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + $query = 'DELETE FROM ' . $table . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + + return $query; + } + + /** + * Gets a string representation of this QueryBuilder which corresponds to + * the final SQL query being constructed. + * + * @return string The string representation of this QueryBuilder. + */ + public function __toString() + { + return $this->getSQL(); + } + + /** + * Create a new named parameter and bind the value $value to it. + * + * This method provides a shortcut for PDOStatement::bindValue + * when using prepared statements. + * + * The parameter $value specifies the value that you want to bind. If + * $placeholder is not provided bindValue() will automatically create a + * placeholder for you. An automatic placeholder will be of the name + * ':dcValue1', ':dcValue2' etc. + * + * For more information see {@link http://php.net/pdostatement-bindparam} + * + * Example: + * + * $value = 2; + * $q->eq( 'id', $q->bindValue( $value ) ); + * $stmt = $q->executeQuery(); // executed with 'id = 2' + * + * + * @license New BSD License + * @link http://www.zetacomponents.org + * @param mixed $value + * @param mixed $type + * @param string $placeHolder the name to bind with. The string must start with a colon ':'. + * @return string the placeholder name used. + */ + public function createNamedParameter( $value, $type = \PDO::PARAM_STR, $placeHolder = null ) + { + if ( $placeHolder === null ) { + $this->boundCounter++; + $placeHolder = ":dcValue" . $this->boundCounter; + } + $this->setParameter(substr($placeHolder, 1), $value, $type); + + return $placeHolder; + } + + /** + * Create a new positional parameter and bind the given value to it. + * + * Attention: If you are using positional parameters with the query builder you have + * to be very careful to bind all parameters in the order they appear in the SQL + * statement , otherwise they get bound in the wrong order which can lead to serious + * bugs in your code. + * + * Example: + * + * $qb = $conn->createQueryBuilder(); + * $qb->select('u.*') + * ->from('users', 'u') + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', PDO::PARAM_STR)) + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', PDO::PARAM_STR)) + * + * + * @param mixed $value + * @param mixed $type + * @return string + */ + public function createPositionalParameter($value, $type = \PDO::PARAM_STR) + { + $this->boundCounter++; + $this->setParameter($this->boundCounter, $value, $type); + return "?"; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php new file mode 100644 index 00000000..addc7456 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php @@ -0,0 +1,40 @@ +. + */ + +namespace Doctrine\DBAL\Query; + +use Doctrine\DBAL\DBALException; + +/** + * Driver interface. + * Interface that all DBAL drivers must implement. + * + * @since 2.1.4 + */ +class QueryException extends DBALException +{ + static public function unknownFromAlias($alias, $registeredAliases) + { + return new self("The given alias '" . $alias . "' is not part of " . + "any FROM clause table. The currently registered FROM-clause " . + "aliases are: " . implode(", ", $registeredAliases) . ". Join clauses " . + "are bound to from clauses to provide support for mixing of multiple " . + "from and join clauses."); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/README.markdown b/doctrine/dbal/lib/Doctrine/DBAL/README.markdown new file mode 100644 index 00000000..e69de29b diff --git a/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php new file mode 100644 index 00000000..a4cd1009 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php @@ -0,0 +1,183 @@ +. + */ + + +namespace Doctrine\DBAL; + +use Doctrine\DBAL\Connection; + +/** + * Utility class that parses sql statements with regard to types and parameters. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + */ +class SQLParserUtils +{ + /** + * Get an array of the placeholders in an sql statements as keys and their positions in the query string. + * + * Returns an integer => integer pair (indexed from zero) for a positional statement + * and a string => int[] pair for a named statement. + * + * @param string $statement + * @param bool $isPositional + * @return array + */ + static public function getPlaceholderPositions($statement, $isPositional = true) + { + $match = ($isPositional) ? '?' : ':'; + if (strpos($statement, $match) === false) { + return array(); + } + + $count = 0; + $inLiteral = false; // a valid query never starts with quotes + $stmtLen = strlen($statement); + $paramMap = array(); + for ($i = 0; $i < $stmtLen; $i++) { + if ($statement[$i] == $match && !$inLiteral) { + // real positional parameter detected + if ($isPositional) { + $paramMap[$count] = $i; + } else { + $name = ""; + // TODO: Something faster/better to match this than regex? + for ($j = $i + 1; ($j < $stmtLen && preg_match('(([a-zA-Z0-9_]{1}))', $statement[$j])); $j++) { + $name .= $statement[$j]; + } + $paramMap[$i] = $name; // named parameters can be duplicated! + $i = $j; + } + ++$count; + } else if ($statement[$i] == "'" || $statement[$i] == '"') { + $inLiteral = ! $inLiteral; // switch state! + } + } + + return $paramMap; + } + + /** + * For a positional query this method can rewrite the sql statement with regard to array parameters. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters to bind to the query. + * @param array $types The types the previous parameters are in. + * + * @return array + */ + static public function expandListParameters($query, $params, $types) + { + $isPositional = is_int(key($params)); + $arrayPositions = array(); + $bindIndex = -1; + + foreach ($types as $name => $type) { + ++$bindIndex; + + if ($type !== Connection::PARAM_INT_ARRAY && $type !== Connection::PARAM_STR_ARRAY) { + continue; + } + + if ($isPositional) { + $name = $bindIndex; + } + + $arrayPositions[$name] = false; + } + + if (( ! $arrayPositions && $isPositional) || (count($params) != count($types))) { + return array($query, $params, $types); + } + + $paramPos = self::getPlaceholderPositions($query, $isPositional); + + if ($isPositional) { + $paramOffset = 0; + $queryOffset = 0; + + foreach ($paramPos as $needle => $needlePos) { + if ( ! isset($arrayPositions[$needle])) { + continue; + } + + $needle += $paramOffset; + $needlePos += $queryOffset; + $count = count($params[$needle]); + + $params = array_merge( + array_slice($params, 0, $needle), + $params[$needle], + array_slice($params, $needle + 1) + ); + + $types = array_merge( + array_slice($types, 0, $needle), + array_fill(0, $count, $types[$needle] - Connection::ARRAY_PARAM_OFFSET), // array needles are at PDO::PARAM_* + 100 + array_slice($types, $needle + 1) + ); + + $expandStr = implode(", ", array_fill(0, $count, "?")); + $query = substr($query, 0, $needlePos) . $expandStr . substr($query, $needlePos + 1); + + $paramOffset += ($count - 1); // Grows larger by number of parameters minus the replaced needle. + $queryOffset += (strlen($expandStr) - 1); + } + + return array($query, $params, $types); + } + + + $queryOffset = 0; + $typesOrd = array(); + $paramsOrd = array(); + + foreach ($paramPos as $pos => $paramName) { + $paramLen = strlen($paramName) + 1; + $value = $params[$paramName]; + + if ( ! isset($arrayPositions[$paramName])) { + $pos += $queryOffset; + $queryOffset -= ($paramLen - 1); + $paramsOrd[] = $value; + $typesOrd[] = $types[$paramName]; + $query = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen)); + + continue; + } + + $count = count($value); + $expandStr = $count > 0 ? implode(', ', array_fill(0, $count, '?')) : '?'; + + foreach ($value as $val) { + $paramsOrd[] = $val; + $typesOrd[] = $types[$paramName] - Connection::ARRAY_PARAM_OFFSET; + } + + $pos += $queryOffset; + $queryOffset += (strlen($expandStr) - $paramLen); + $query = substr($query, 0, $pos) . $expandStr . substr($query, ($pos + $paramLen)); + } + + return array($query, $paramsOrd, $typesOrd); + } +} \ No newline at end of file diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.php new file mode 100644 index 00000000..17a9c0ff --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractAsset.php @@ -0,0 +1,214 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * The abstract asset allows to reset the name of all assets without publishing this to the public userland. + * + * This encapsulation hack is necessary to keep a consistent state of the database schema. Say we have a list of tables + * array($tableName => Table($tableName)); if you want to rename the table, you have to make sure + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +abstract class AbstractAsset +{ + /** + * @var string + */ + protected $_name; + + /** + * Namespace of the asset. If none isset the default namespace is assumed. + * + * @var string + */ + protected $_namespace; + + /** + * @var bool + */ + protected $_quoted = false; + + /** + * Set name of this asset + * + * @param string $name + */ + protected function _setName($name) + { + if ($this->isIdentifierQuoted($name)) { + $this->_quoted = true; + $name = $this->trimQuotes($name); + } + if (strpos($name, ".") !== false) { + $parts = explode(".", $name); + $this->_namespace = $parts[0]; + $name = $parts[1]; + } + $this->_name = $name; + } + + /** + * Is this asset in the default namespace? + * + * @param string $defaultNamespaceName + * @return bool + */ + public function isInDefaultNamespace($defaultNamespaceName) + { + return $this->_namespace == $defaultNamespaceName || $this->_namespace === null; + } + + /** + * Get namespace name of this asset. + * + * If NULL is returned this means the default namespace is used. + * + * @return string + */ + public function getNamespaceName() + { + return $this->_namespace; + } + + /** + * The shortest name is stripped of the default namespace. All other + * namespaced elements are returned as full-qualified names. + * + * @param string + * @return string + */ + public function getShortestName($defaultNamespaceName) + { + $shortestName = $this->getName(); + if ($this->_namespace == $defaultNamespaceName) { + $shortestName = $this->_name; + } + return strtolower($shortestName); + } + + /** + * The normalized name is full-qualified and lowerspaced. Lowerspacing is + * actually wrong, but we have to do it to keep our sanity. If you are + * using database objects that only differentiate in the casing (FOO vs + * Foo) then you will NOT be able to use Doctrine Schema abstraction. + * + * Every non-namespaced element is prefixed with the default namespace + * name which is passed as argument to this method. + * + * @return string + */ + public function getFullQualifiedName($defaultNamespaceName) + { + $name = $this->getName(); + if ( ! $this->_namespace) { + $name = $defaultNamespaceName . "." . $name; + } + return strtolower($name); + } + + /** + * Check if this asset's name is quoted + * + * @return bool + */ + public function isQuoted() + { + return $this->_quoted; + } + + /** + * Check if this identifier is quoted. + * + * @param string $identifier + * @return bool + */ + protected function isIdentifierQuoted($identifier) + { + return (isset($identifier[0]) && ($identifier[0] == '`' || $identifier[0] == '"')); + } + + /** + * Trim quotes from the identifier. + * + * @param string $identifier + * @return string + */ + protected function trimQuotes($identifier) + { + return str_replace(array('`', '"'), '', $identifier); + } + + /** + * Return name of this schema asset. + * + * @return string + */ + public function getName() + { + if ($this->_namespace) { + return $this->_namespace . "." . $this->_name; + } + return $this->_name; + } + + /** + * Get the quoted representation of this asset but only if it was defined with one. Otherwise + * return the plain unquoted value as inserted. + * + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedName(AbstractPlatform $platform) + { + $keywords = $platform->getReservedKeywordsList(); + $parts = explode(".", $this->getName()); + foreach ($parts as $k => $v) { + $parts[$k] = ($this->_quoted || $keywords->isKeyword($v)) ? $platform->quoteIdentifier($v) : $v; + } + + return implode(".", $parts); + } + + /** + * Generate an identifier from a list of column names obeying a certain string length. + * + * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars, + * however building idents automatically for foreign keys, composite keys or such can easily create + * very long names. + * + * @param array $columnNames + * @param string $prefix + * @param int $maxSize + * @return string + */ + protected function _generateIdentifierName($columnNames, $prefix='', $maxSize=30) + { + $hash = implode("", array_map(function($column) { + return dechex(crc32($column)); + }, $columnNames)); + return substr(strtoupper($prefix . "_" . $hash), 0, $maxSize); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php new file mode 100644 index 00000000..425426e0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php @@ -0,0 +1,896 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Events; +use Doctrine\DBAL\Event\SchemaColumnDefinitionEventArgs; +use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs; +use Doctrine\DBAL\Types; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Base class for schema managers. Schema managers are used to inspect and/or + * modify the database schema/structure. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Roman Borschel + * @author Jonathan H. Wage + * @author Benjamin Eberlei + * @since 2.0 + */ +abstract class AbstractSchemaManager +{ + /** + * Holds instance of the Doctrine connection for this schema manager + * + * @var \Doctrine\DBAL\Connection + */ + protected $_conn; + + /** + * Holds instance of the database platform used for this schema manager + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + /** + * Constructor. Accepts the Connection instance to manage the schema for + * + * @param \Doctrine\DBAL\Connection $conn + */ + public function __construct(\Doctrine\DBAL\Connection $conn) + { + $this->_conn = $conn; + $this->_platform = $this->_conn->getDatabasePlatform(); + } + + /** + * Return associated platform. + * + * @return \Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_platform; + } + + /** + * Try any method on the schema manager. Normally a method throws an + * exception when your DBMS doesn't support it or if an error occurs. + * This method allows you to try and method on your SchemaManager + * instance and will return false if it does not work or is not supported. + * + * + * $result = $sm->tryMethod('dropView', 'view_name'); + * + * + * @return mixed + */ + public function tryMethod() + { + $args = func_get_args(); + $method = $args[0]; + unset($args[0]); + $args = array_values($args); + + try { + return call_user_func_array(array($this, $method), $args); + } catch (\Exception $e) { + return false; + } + } + + /** + * List the available databases for this connection + * + * @return array $databases + */ + public function listDatabases() + { + $sql = $this->_platform->getListDatabasesSQL(); + + $databases = $this->_conn->fetchAll($sql); + + return $this->_getPortableDatabasesList($databases); + } + + /** + * List the available sequences for this connection + * + * @return Sequence[] + */ + public function listSequences($database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListSequencesSQL($database); + + $sequences = $this->_conn->fetchAll($sql); + + return $this->filterAssetNames($this->_getPortableSequencesList($sequences)); + } + + /** + * List the columns for a given table. + * + * In contrast to other libraries and to the old version of Doctrine, + * this column definition does try to contain the 'primary' field for + * the reason that it is not portable accross different RDBMS. Use + * {@see listTableIndexes($tableName)} to retrieve the primary key + * of a table. We're a RDBMS specifies more details these are held + * in the platformDetails array. + * + * @param string $table The name of the table. + * @param string $database + * @return Column[] + */ + public function listTableColumns($table, $database = null) + { + if ( ! $database) { + $database = $this->_conn->getDatabase(); + } + + $sql = $this->_platform->getListTableColumnsSQL($table, $database); + + $tableColumns = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableColumnList($table, $database, $tableColumns); + } + + /** + * List the indexes for a given table returning an array of Index instances. + * + * Keys of the portable indexes list are all lower-cased. + * + * @param string $table The name of the table + * @return Index[] $tableIndexes + */ + public function listTableIndexes($table) + { + $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase()); + + $tableIndexes = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableIndexesList($tableIndexes, $table); + } + + /** + * Return true if all the given tables exist. + * + * @param array $tableNames + * @return bool + */ + public function tablesExist($tableNames) + { + $tableNames = array_map('strtolower', (array)$tableNames); + return count($tableNames) == count(\array_intersect($tableNames, array_map('strtolower', $this->listTableNames()))); + } + + /** + * Return a list of all tables in the current database + * + * @return array + */ + public function listTableNames() + { + $sql = $this->_platform->getListTablesSQL(); + + $tables = $this->_conn->fetchAll($sql); + $tableNames = $this->_getPortableTablesList($tables); + return $this->filterAssetNames($tableNames); + } + + /** + * Filter asset names if they are configured to return only a subset of all + * the found elements. + * + * @param array $assetNames + * @return array + */ + protected function filterAssetNames($assetNames) + { + $filterExpr = $this->getFilterSchemaAssetsExpression(); + if ( ! $filterExpr) { + return $assetNames; + } + return array_values ( + array_filter($assetNames, function ($assetName) use ($filterExpr) { + $assetName = ($assetName instanceof AbstractAsset) ? $assetName->getName() : $assetName; + return preg_match($filterExpr, $assetName); + }) + ); + } + + protected function getFilterSchemaAssetsExpression() + { + return $this->_conn->getConfiguration()->getFilterSchemaAssetsExpression(); + } + + /** + * List the tables for this connection + * + * @return Table[] + */ + public function listTables() + { + $tableNames = $this->listTableNames(); + + $tables = array(); + foreach ($tableNames as $tableName) { + $tables[] = $this->listTableDetails($tableName); + } + + return $tables; + } + + /** + * @param string $tableName + * @return Table + */ + public function listTableDetails($tableName) + { + $columns = $this->listTableColumns($tableName); + $foreignKeys = array(); + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->listTableForeignKeys($tableName); + } + $indexes = $this->listTableIndexes($tableName); + + return new Table($tableName, $columns, $indexes, $foreignKeys, false, array()); + } + + /** + * List the views this connection has + * + * @return View[] + */ + public function listViews() + { + $database = $this->_conn->getDatabase(); + $sql = $this->_platform->getListViewsSQL($database); + $views = $this->_conn->fetchAll($sql); + + return $this->_getPortableViewsList($views); + } + + /** + * List the foreign keys for the given table + * + * @param string $table The name of the table + * @return ForeignKeyConstraint[] + */ + public function listTableForeignKeys($table, $database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); + $tableForeignKeys = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableForeignKeysList($tableForeignKeys); + } + + /* drop*() Methods */ + + /** + * Drops a database. + * + * NOTE: You can not drop the database this SchemaManager is currently connected to. + * + * @param string $database The name of the database to drop + */ + public function dropDatabase($database) + { + $this->_execSql($this->_platform->getDropDatabaseSQL($database)); + } + + /** + * Drop the given table + * + * @param string $table The name of the table to drop + */ + public function dropTable($table) + { + $this->_execSql($this->_platform->getDropTableSQL($table)); + } + + /** + * Drop the index from the given table + * + * @param Index|string $index The name of the index + * @param string|Table $table The name of the table + */ + public function dropIndex($index, $table) + { + if($index instanceof Index) { + $index = $index->getQuotedName($this->_platform); + } + + $this->_execSql($this->_platform->getDropIndexSQL($index, $table)); + } + + /** + * Drop the constraint from the given table + * + * @param Constraint $constraint + * @param string $table The name of the table + */ + public function dropConstraint(Constraint $constraint, $table) + { + $this->_execSql($this->_platform->getDropConstraintSQL($constraint, $table)); + } + + /** + * Drops a foreign key from a table. + * + * @param ForeignKeyConstraint|string $table The name of the table with the foreign key. + * @param Table|string $name The name of the foreign key. + * @return boolean $result + */ + public function dropForeignKey($foreignKey, $table) + { + $this->_execSql($this->_platform->getDropForeignKeySQL($foreignKey, $table)); + } + + /** + * Drops a sequence with a given name. + * + * @param string $name The name of the sequence to drop. + */ + public function dropSequence($name) + { + $this->_execSql($this->_platform->getDropSequenceSQL($name)); + } + + /** + * Drop a view + * + * @param string $name The name of the view + * @return boolean $result + */ + public function dropView($name) + { + $this->_execSql($this->_platform->getDropViewSQL($name)); + } + + /* create*() Methods */ + + /** + * Creates a new database. + * + * @param string $database The name of the database to create. + */ + public function createDatabase($database) + { + $this->_execSql($this->_platform->getCreateDatabaseSQL($database)); + } + + /** + * Create a new table. + * + * @param Table $table + * @param int $createFlags + */ + public function createTable(Table $table) + { + $createFlags = AbstractPlatform::CREATE_INDEXES|AbstractPlatform::CREATE_FOREIGNKEYS; + $this->_execSql($this->_platform->getCreateTableSQL($table, $createFlags)); + } + + /** + * Create a new sequence + * + * @param Sequence $sequence + * @throws \Doctrine\DBAL\ConnectionException if something fails at database level + */ + public function createSequence($sequence) + { + $this->_execSql($this->_platform->getCreateSequenceSQL($sequence)); + } + + /** + * Create a constraint on a table + * + * @param Constraint $constraint + * @param string|Table $table + */ + public function createConstraint(Constraint $constraint, $table) + { + $this->_execSql($this->_platform->getCreateConstraintSQL($constraint, $table)); + } + + /** + * Create a new index on a table + * + * @param Index $index + * @param string $table name of the table on which the index is to be created + */ + public function createIndex(Index $index, $table) + { + $this->_execSql($this->_platform->getCreateIndexSQL($index, $table)); + } + + /** + * Create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey ForeignKey instance + * @param string|Table $table name of the table on which the foreign key is to be created + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $this->_execSql($this->_platform->getCreateForeignKeySQL($foreignKey, $table)); + } + + /** + * Create a new view + * + * @param View $view + */ + public function createView(View $view) + { + $this->_execSql($this->_platform->getCreateViewSQL($view->getQuotedName($this->_platform), $view->getSql())); + } + + /* dropAndCreate*() Methods */ + + /** + * Drop and create a constraint + * + * @param Constraint $constraint + * @param string $table + * @see dropConstraint() + * @see createConstraint() + */ + public function dropAndCreateConstraint(Constraint $constraint, $table) + { + $this->tryMethod('dropConstraint', $constraint, $table); + $this->createConstraint($constraint, $table); + } + + /** + * Drop and create a new index on a table + * + * @param string|Table $table name of the table on which the index is to be created + * @param Index $index + */ + public function dropAndCreateIndex(Index $index, $table) + { + $this->tryMethod('dropIndex', $index->getQuotedName($this->_platform), $table); + $this->createIndex($index, $table); + } + + /** + * Drop and create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey associative array that defines properties of the foreign key to be created. + * @param string|Table $table name of the table on which the foreign key is to be created + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $this->tryMethod('dropForeignKey', $foreignKey, $table); + $this->createForeignKey($foreignKey, $table); + } + + /** + * Drop and create a new sequence + * + * @param Sequence $sequence + * @throws \Doctrine\DBAL\ConnectionException if something fails at database level + */ + public function dropAndCreateSequence(Sequence $sequence) + { + $this->tryMethod('dropSequence', $sequence->getQuotedName($this->_platform)); + $this->createSequence($sequence); + } + + /** + * Drop and create a new table. + * + * @param Table $table + */ + public function dropAndCreateTable(Table $table) + { + $this->tryMethod('dropTable', $table->getQuotedName($this->_platform)); + $this->createTable($table); + } + + /** + * Drop and creates a new database. + * + * @param string $database The name of the database to create. + */ + public function dropAndCreateDatabase($database) + { + $this->tryMethod('dropDatabase', $database); + $this->createDatabase($database); + } + + /** + * Drop and create a new view + * + * @param View $view + */ + public function dropAndCreateView(View $view) + { + $this->tryMethod('dropView', $view->getQuotedName($this->_platform)); + $this->createView($view); + } + + /* alterTable() Methods */ + + /** + * Alter an existing tables schema + * + * @param TableDiff $tableDiff + */ + public function alterTable(TableDiff $tableDiff) + { + $queries = $this->_platform->getAlterTableSQL($tableDiff); + if (is_array($queries) && count($queries)) { + foreach ($queries as $ddlQuery) { + $this->_execSql($ddlQuery); + } + } + } + + /** + * Rename a given table to another name + * + * @param string $name The current name of the table + * @param string $newName The new name of the table + */ + public function renameTable($name, $newName) + { + $tableDiff = new TableDiff($name); + $tableDiff->newName = $newName; + $this->alterTable($tableDiff); + } + + /** + * Methods for filtering return values of list*() methods to convert + * the native DBMS data definition to a portable Doctrine definition + */ + + protected function _getPortableDatabasesList($databases) + { + $list = array(); + foreach ($databases as $value) { + if ($value = $this->_getPortableDatabaseDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database; + } + + protected function _getPortableFunctionsList($functions) + { + $list = array(); + foreach ($functions as $value) { + if ($value = $this->_getPortableFunctionDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableFunctionDefinition($function) + { + return $function; + } + + protected function _getPortableTriggersList($triggers) + { + $list = array(); + foreach ($triggers as $value) { + if ($value = $this->_getPortableTriggerDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTriggerDefinition($trigger) + { + return $trigger; + } + + protected function _getPortableSequencesList($sequences) + { + $list = array(); + foreach ($sequences as $value) { + if ($value = $this->_getPortableSequenceDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + /** + * @param array $sequence + * @return Sequence + */ + protected function _getPortableSequenceDefinition($sequence) + { + throw DBALException::notSupported('Sequences'); + } + + /** + * Independent of the database the keys of the column list result are lowercased. + * + * The name of the created column instance however is kept in its case. + * + * @param string $table The name of the table. + * @param string $database + * @param array $tableColumns + * @return array + */ + protected function _getPortableTableColumnList($table, $database, $tableColumns) + { + $eventManager = $this->_platform->getEventManager(); + + $list = array(); + foreach ($tableColumns as $tableColumn) { + $column = null; + $defaultPrevented = false; + + if (null !== $eventManager && $eventManager->hasListeners(Events::onSchemaColumnDefinition)) { + $eventArgs = new SchemaColumnDefinitionEventArgs($tableColumn, $table, $database, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaColumnDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $column = $eventArgs->getColumn(); + } + + if ( ! $defaultPrevented) { + $column = $this->_getPortableTableColumnDefinition($tableColumn); + } + + if ($column) { + $name = strtolower($column->getQuotedName($this->_platform)); + $list[$name] = $column; + } + } + return $list; + } + + /** + * Get Table Column Definition + * + * @param array $tableColumn + * @return Column + */ + abstract protected function _getPortableTableColumnDefinition($tableColumn); + + /** + * Aggregate and group the index results according to the required data result. + * + * @param array $tableIndexRows + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexRows, $tableName=null) + { + $result = array(); + foreach($tableIndexRows as $tableIndex) { + $indexName = $keyName = $tableIndex['key_name']; + if($tableIndex['primary']) { + $keyName = 'primary'; + } + $keyName = strtolower($keyName); + + if(!isset($result[$keyName])) { + $result[$keyName] = array( + 'name' => $indexName, + 'columns' => array($tableIndex['column_name']), + 'unique' => $tableIndex['non_unique'] ? false : true, + 'primary' => $tableIndex['primary'], + 'flags' => isset($tableIndex['flags']) ? $tableIndex['flags'] : array(), + ); + } else { + $result[$keyName]['columns'][] = $tableIndex['column_name']; + } + } + + $eventManager = $this->_platform->getEventManager(); + + $indexes = array(); + foreach($result as $indexKey => $data) { + $index = null; + $defaultPrevented = false; + + if (null !== $eventManager && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) { + $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $index = $eventArgs->getIndex(); + } + + if ( ! $defaultPrevented) { + $index = new Index($data['name'], $data['columns'], $data['unique'], $data['primary'], $data['flags']); + } + + if ($index) { + $indexes[$indexKey] = $index; + } + } + + return $indexes; + } + + protected function _getPortableTablesList($tables) + { + $list = array(); + foreach ($tables as $value) { + if ($value = $this->_getPortableTableDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTableDefinition($table) + { + return $table; + } + + protected function _getPortableUsersList($users) + { + $list = array(); + foreach ($users as $value) { + if ($value = $this->_getPortableUserDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableUserDefinition($user) + { + return $user; + } + + protected function _getPortableViewsList($views) + { + $list = array(); + foreach ($views as $value) { + if ($view = $this->_getPortableViewDefinition($value)) { + $viewName = strtolower($view->getQuotedName($this->_platform)); + $list[$viewName] = $view; + } + } + return $list; + } + + protected function _getPortableViewDefinition($view) + { + return false; + } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $value) { + if ($value = $this->_getPortableTableForeignKeyDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return $tableForeignKey; + } + + protected function _execSql($sql) + { + foreach ((array) $sql as $query) { + $this->_conn->executeUpdate($query); + } + } + + /** + * Create a schema instance for the current database. + * + * @return Schema + */ + public function createSchema() + { + $sequences = array(); + if($this->_platform->supportsSequences()) { + $sequences = $this->listSequences(); + } + $tables = $this->listTables(); + + return new Schema($tables, $sequences, $this->createSchemaConfig()); + } + + /** + * Create the configuration for this schema. + * + * @return SchemaConfig + */ + public function createSchemaConfig() + { + $schemaConfig = new SchemaConfig(); + $schemaConfig->setMaxIdentifierLength($this->_platform->getMaxIdentifierLength()); + + $searchPaths = $this->getSchemaSearchPaths(); + if (isset($searchPaths[0])) { + $schemaConfig->setName($searchPaths[0]); + } + + $params = $this->_conn->getParams(); + if (isset($params['defaultTableOptions'])) { + $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']); + } + + return $schemaConfig; + } + + /** + * The search path for namespaces in the currently connected database. + * + * The first entry is usually the default namespace in the Schema. All + * further namespaces contain tables/sequences which can also be addressed + * with a short, not full-qualified name. + * + * For databases that don't support subschema/namespaces this method + * returns the name of the currently connected database. + * + * @return array + */ + public function getSchemaSearchPaths() + { + return array($this->_conn->getDatabase()); + } + + /** + * Given a table comment this method tries to extract a typehint for Doctrine Type, or returns + * the type given as default. + * + * @param string $comment + * @param string $currentType + * @return string + */ + public function extractDoctrineTypeFromComment($comment, $currentType) + { + if (preg_match("(\(DC2Type:([a-zA-Z0-9_]+)\))", $comment, $match)) { + $currentType = $match[1]; + } + return $currentType; + } + + public function removeDoctrineTypeFromComment($comment, $type) + { + return str_replace('(DC2Type:'.$type.')', '', $comment); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php new file mode 100644 index 00000000..47ce78f9 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php @@ -0,0 +1,423 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use \Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Object representation of a database column + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Column extends AbstractAsset +{ + /** + * @var \Doctrine\DBAL\Types\Type + */ + protected $_type; + + /** + * @var int + */ + protected $_length = null; + + /** + * @var int + */ + protected $_precision = 10; + + /** + * @var int + */ + protected $_scale = 0; + + /** + * @var bool + */ + protected $_unsigned = false; + + /** + * @var bool + */ + protected $_fixed = false; + + /** + * @var bool + */ + protected $_notnull = true; + + /** + * @var string + */ + protected $_default = null; + + /** + * @var bool + */ + protected $_autoincrement = false; + + /** + * @var array + */ + protected $_platformOptions = array(); + + /** + * @var string + */ + protected $_columnDefinition = null; + + /** + * @var string + */ + protected $_comment = null; + + /** + * @var array + */ + protected $_customSchemaOptions = array(); + + /** + * Create a new Column + * + * @param string $columnName + * @param \Doctrine\DBAL\Types\Type $type + * @param int $length + * @param bool $notNull + * @param mixed $default + * @param bool $unsigned + * @param bool $fixed + * @param int $precision + * @param int $scale + * @param array $platformOptions + */ + public function __construct($columnName, Type $type, array $options=array()) + { + $this->_setName($columnName); + $this->setType($type); + $this->setOptions($options); + } + + /** + * @param array $options + * @return Column + */ + public function setOptions(array $options) + { + foreach ($options as $name => $value) { + $method = "set".$name; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * @param Type $type + * @return Column + */ + public function setType(Type $type) + { + $this->_type = $type; + return $this; + } + + /** + * @param int $length + * @return Column + */ + public function setLength($length) + { + if($length !== null) { + $this->_length = (int)$length; + } else { + $this->_length = null; + } + return $this; + } + + /** + * @param int $precision + * @return Column + */ + public function setPrecision($precision) + { + if (!is_numeric($precision)) { + $precision = 10; // defaults to 10 when no valid precision is given. + } + + $this->_precision = (int)$precision; + return $this; + } + + /** + * @param int $scale + * @return Column + */ + public function setScale($scale) + { + if (!is_numeric($scale)) { + $scale = 0; + } + + $this->_scale = (int)$scale; + return $this; + } + + /** + * + * @param bool $unsigned + * @return Column + */ + public function setUnsigned($unsigned) + { + $this->_unsigned = (bool)$unsigned; + return $this; + } + + /** + * + * @param bool $fixed + * @return Column + */ + public function setFixed($fixed) + { + $this->_fixed = (bool)$fixed; + return $this; + } + + /** + * @param bool $notnull + * @return Column + */ + public function setNotnull($notnull) + { + $this->_notnull = (bool)$notnull; + return $this; + } + + /** + * + * @param mixed $default + * @return Column + */ + public function setDefault($default) + { + $this->_default = $default; + return $this; + } + + /** + * + * @param array $platformOptions + * @return Column + */ + public function setPlatformOptions(array $platformOptions) + { + $this->_platformOptions = $platformOptions; + return $this; + } + + /** + * + * @param string $name + * @param mixed $value + * @return Column + */ + public function setPlatformOption($name, $value) + { + $this->_platformOptions[$name] = $value; + return $this; + } + + /** + * + * @param string + * @return Column + */ + public function setColumnDefinition($value) + { + $this->_columnDefinition = $value; + return $this; + } + + public function getType() + { + return $this->_type; + } + + public function getLength() + { + return $this->_length; + } + + public function getPrecision() + { + return $this->_precision; + } + + public function getScale() + { + return $this->_scale; + } + + public function getUnsigned() + { + return $this->_unsigned; + } + + public function getFixed() + { + return $this->_fixed; + } + + public function getNotnull() + { + return $this->_notnull; + } + + public function getDefault() + { + return $this->_default; + } + + public function getPlatformOptions() + { + return $this->_platformOptions; + } + + public function hasPlatformOption($name) + { + return isset($this->_platformOptions[$name]); + } + + public function getPlatformOption($name) + { + return $this->_platformOptions[$name]; + } + + public function getColumnDefinition() + { + return $this->_columnDefinition; + } + + public function getAutoincrement() + { + return $this->_autoincrement; + } + + public function setAutoincrement($flag) + { + $this->_autoincrement = $flag; + return $this; + } + + public function setComment($comment) + { + $this->_comment = $comment; + return $this; + } + + public function getComment() + { + return $this->_comment; + } + + /** + * @param string $name + * @param mixed $value + * @return Column + */ + public function setCustomSchemaOption($name, $value) + { + $this->_customSchemaOptions[$name] = $value; + return $this; + } + + /** + * @param string $name + * @return boolean + */ + public function hasCustomSchemaOption($name) + { + return isset($this->_customSchemaOptions[$name]); + } + + /** + * @param string $name + * @return mixed + */ + public function getCustomSchemaOption($name) + { + return $this->_customSchemaOptions[$name]; + } + + /** + * @param array $customSchemaOptions + * @return Column + */ + public function setCustomSchemaOptions(array $customSchemaOptions) + { + $this->_customSchemaOptions = $customSchemaOptions; + return $this; + } + + /** + * @return array + */ + public function getCustomSchemaOptions() + { + return $this->_customSchemaOptions; + } + + /** + * @param Visitor $visitor + */ + public function visit(\Doctrine\DBAL\Schema\Visitor $visitor) + { + $visitor->accept($this); + } + + /** + * @return array + */ + public function toArray() + { + return array_merge(array( + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, + 'columnDefinition' => $this->_columnDefinition, + 'comment' => $this->_comment, + ), $this->_platformOptions, $this->_customSchemaOptions); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php new file mode 100644 index 00000000..e8cfa1de --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Represent the change of a column + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ColumnDiff +{ + public $oldColumnName; + + /** + * @var Column + */ + public $column; + + /** + * @var array + */ + public $changedProperties = array(); + + public function __construct($oldColumnName, Column $column, array $changedProperties = array()) + { + $this->oldColumnName = $oldColumnName; + $this->column = $column; + $this->changedProperties = $changedProperties; + } + + public function hasChanged($propertyName) + { + return in_array($propertyName, $this->changedProperties); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php new file mode 100644 index 00000000..f73dd51e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php @@ -0,0 +1,427 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Compare to Schemas and return an instance of SchemaDiff + * + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Comparator +{ + /** + * @param Schema $fromSchema + * @param Schema $toSchema + * @return SchemaDiff + */ + static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) + { + $c = new self(); + return $c->compare($fromSchema, $toSchema); + } + + /** + * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. + * + * The returned differences are returned in such a way that they contain the + * operations to change the schema stored in $fromSchema to the schema that is + * stored in $toSchema. + * + * @param Schema $fromSchema + * @param Schema $toSchema + * + * @return SchemaDiff + */ + public function compare(Schema $fromSchema, Schema $toSchema) + { + $diff = new SchemaDiff(); + + $foreignKeysToTable = array(); + + foreach ( $toSchema->getTables() as $table ) { + $tableName = $table->getShortestName($toSchema->getName()); + if ( ! $fromSchema->hasTable($tableName)) { + $diff->newTables[$tableName] = $toSchema->getTable($tableName); + } else { + $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName)); + if ($tableDifferences !== false) { + $diff->changedTables[$tableName] = $tableDifferences; + } + } + } + + /* Check if there are tables removed */ + foreach ($fromSchema->getTables() as $table) { + $tableName = $table->getShortestName($fromSchema->getName()); + + $table = $fromSchema->getTable($tableName); + if ( ! $toSchema->hasTable($tableName) ) { + $diff->removedTables[$tableName] = $table; + } + + // also remember all foreign keys that point to a specific table + foreach ($table->getForeignKeys() as $foreignKey) { + $foreignTable = strtolower($foreignKey->getForeignTableName()); + if (!isset($foreignKeysToTable[$foreignTable])) { + $foreignKeysToTable[$foreignTable] = array(); + } + $foreignKeysToTable[$foreignTable][] = $foreignKey; + } + } + + foreach ($diff->removedTables as $tableName => $table) { + if (isset($foreignKeysToTable[$tableName])) { + $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]); + + // deleting duplicated foreign keys present on both on the orphanedForeignKey + // and the removedForeignKeys from changedTables + foreach ($foreignKeysToTable[$tableName] as $foreignKey) { + // strtolower the table name to make if compatible with getShortestName + $localTableName = strtolower($foreignKey->getLocalTableName()); + if (isset($diff->changedTables[$localTableName])) { + foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) { + unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]); + } + } + } + } + } + + foreach ($toSchema->getSequences() as $sequence) { + $sequenceName = $sequence->getShortestName($toSchema->getName()); + if ( ! $fromSchema->hasSequence($sequenceName)) { + $diff->newSequences[] = $sequence; + } else { + if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { + $diff->changedSequences[] = $toSchema->getSequence($sequenceName); + } + } + } + + foreach ($fromSchema->getSequences() as $sequence) { + if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) { + continue; + } + + $sequenceName = $sequence->getShortestName($fromSchema->getName()); + + if ( ! $toSchema->hasSequence($sequenceName)) { + $diff->removedSequences[] = $sequence; + } + } + + return $diff; + } + + private function isAutoIncrementSequenceInSchema($schema, $sequence) + { + foreach ($schema->getTables() as $table) { + if ($sequence->isAutoIncrementsFor($table)) { + return true; + } + } + + return false; + } + + /** + * + * @param Sequence $sequence1 + * @param Sequence $sequence2 + */ + public function diffSequence(Sequence $sequence1, Sequence $sequence2) + { + if($sequence1->getAllocationSize() != $sequence2->getAllocationSize()) { + return true; + } + + if($sequence1->getInitialValue() != $sequence2->getInitialValue()) { + return true; + } + + return false; + } + + /** + * Returns the difference between the tables $table1 and $table2. + * + * If there are no differences this method returns the boolean false. + * + * @param Table $table1 + * @param Table $table2 + * + * @return bool|TableDiff + */ + public function diffTable(Table $table1, Table $table2) + { + $changes = 0; + $tableDifferences = new TableDiff($table1->getName()); + + $table1Columns = $table1->getColumns(); + $table2Columns = $table2->getColumns(); + + /* See if all the fields in table 1 exist in table 2 */ + foreach ( $table2Columns as $columnName => $column ) { + if ( !$table1->hasColumn($columnName) ) { + $tableDifferences->addedColumns[$columnName] = $column; + $changes++; + } + } + /* See if there are any removed fields in table 2 */ + foreach ( $table1Columns as $columnName => $column ) { + if ( !$table2->hasColumn($columnName) ) { + $tableDifferences->removedColumns[$columnName] = $column; + $changes++; + } + } + + foreach ( $table1Columns as $columnName => $column ) { + if ( $table2->hasColumn($columnName) ) { + $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); + if (count($changedProperties) ) { + $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); + $tableDifferences->changedColumns[$column->getName()] = $columnDiff; + $changes++; + } + } + } + + $this->detectColumnRenamings($tableDifferences); + + $table1Indexes = $table1->getIndexes(); + $table2Indexes = $table2->getIndexes(); + + foreach ($table2Indexes as $index2Name => $index2Definition) { + foreach ($table1Indexes as $index1Name => $index1Definition) { + if ($this->diffIndex($index1Definition, $index2Definition) === false) { + unset($table1Indexes[$index1Name]); + unset($table2Indexes[$index2Name]); + } else { + if ($index1Name == $index2Name) { + $tableDifferences->changedIndexes[$index2Name] = $table2Indexes[$index2Name]; + unset($table1Indexes[$index1Name]); + unset($table2Indexes[$index2Name]); + $changes++; + } + } + } + } + + foreach ($table1Indexes as $index1Name => $index1Definition) { + $tableDifferences->removedIndexes[$index1Name] = $index1Definition; + $changes++; + } + + foreach ($table2Indexes as $index2Name => $index2Definition) { + $tableDifferences->addedIndexes[$index2Name] = $index2Definition; + $changes++; + } + + $fromFkeys = $table1->getForeignKeys(); + $toFkeys = $table2->getForeignKeys(); + + foreach ($fromFkeys as $key1 => $constraint1) { + foreach ($toFkeys as $key2 => $constraint2) { + if($this->diffForeignKey($constraint1, $constraint2) === false) { + unset($fromFkeys[$key1]); + unset($toFkeys[$key2]); + } else { + if (strtolower($constraint1->getName()) == strtolower($constraint2->getName())) { + $tableDifferences->changedForeignKeys[] = $constraint2; + $changes++; + unset($fromFkeys[$key1]); + unset($toFkeys[$key2]); + } + } + } + } + + foreach ($fromFkeys as $key1 => $constraint1) { + $tableDifferences->removedForeignKeys[] = $constraint1; + $changes++; + } + + foreach ($toFkeys as $key2 => $constraint2) { + $tableDifferences->addedForeignKeys[] = $constraint2; + $changes++; + } + + return $changes ? $tableDifferences : false; + } + + /** + * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguities between different possibilities should not lead to renaming at all. + * + * @param TableDiff $tableDifferences + */ + private function detectColumnRenamings(TableDiff $tableDifferences) + { + $renameCandidates = array(); + foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) { + foreach ($tableDifferences->removedColumns as $removedColumnName => $removedColumn) { + if (count($this->diffColumn($addedColumn, $removedColumn)) == 0) { + $renameCandidates[$addedColumn->getName()][] = array($removedColumn, $addedColumn, $addedColumnName); + } + } + } + + foreach ($renameCandidates as $candidateColumns) { + if (count($candidateColumns) == 1) { + list($removedColumn, $addedColumn) = $candidateColumns[0]; + $removedColumnName = strtolower($removedColumn->getName()); + $addedColumnName = strtolower($addedColumn->getName()); + + $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; + unset($tableDifferences->addedColumns[$addedColumnName]); + unset($tableDifferences->removedColumns[$removedColumnName]); + } + } + } + + /** + * @param ForeignKeyConstraint $key1 + * @param ForeignKeyConstraint $key2 + * @return bool + */ + public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) + { + if (array_map('strtolower', $key1->getLocalColumns()) != array_map('strtolower', $key2->getLocalColumns())) { + return true; + } + + if (array_map('strtolower', $key1->getForeignColumns()) != array_map('strtolower', $key2->getForeignColumns())) { + return true; + } + + if ($key1->onUpdate() != $key2->onUpdate()) { + return true; + } + + if ($key1->onDelete() != $key2->onDelete()) { + return true; + } + + return false; + } + + /** + * Returns the difference between the fields $field1 and $field2. + * + * If there are differences this method returns $field2, otherwise the + * boolean false. + * + * @param Column $column1 + * @param Column $column2 + * + * @return array + */ + public function diffColumn(Column $column1, Column $column2) + { + $changedProperties = array(); + if ( $column1->getType() != $column2->getType() ) { + $changedProperties[] = 'type'; + } + + if ($column1->getNotnull() != $column2->getNotnull()) { + $changedProperties[] = 'notnull'; + } + + if ($column1->getDefault() != $column2->getDefault()) { + $changedProperties[] = 'default'; + } + + if ($column1->getUnsigned() != $column2->getUnsigned()) { + $changedProperties[] = 'unsigned'; + } + + if ($column1->getType() instanceof \Doctrine\DBAL\Types\StringType) { + // check if value of length is set at all, default value assumed otherwise. + $length1 = $column1->getLength() ?: 255; + $length2 = $column2->getLength() ?: 255; + if ($length1 != $length2) { + $changedProperties[] = 'length'; + } + + if ($column1->getFixed() != $column2->getFixed()) { + $changedProperties[] = 'fixed'; + } + } + + if ($column1->getType() instanceof \Doctrine\DBAL\Types\DecimalType) { + if (($column1->getPrecision()?:10) != ($column2->getPrecision()?:10)) { + $changedProperties[] = 'precision'; + } + if ($column1->getScale() != $column2->getScale()) { + $changedProperties[] = 'scale'; + } + } + + if ($column1->getAutoincrement() != $column2->getAutoincrement()) { + $changedProperties[] = 'autoincrement'; + } + + // only allow to delete comment if its set to '' not to null. + if ($column1->getComment() !== null && $column1->getComment() != $column2->getComment()) { + $changedProperties[] = 'comment'; + } + + $options1 = $column1->getCustomSchemaOptions(); + $options2 = $column2->getCustomSchemaOptions(); + + $commonKeys = array_keys(array_intersect_key($options1, $options2)); + + foreach ($commonKeys as $key) { + if ($options1[$key] !== $options2[$key]) { + $changedProperties[] = $key; + } + } + + $diffKeys = array_keys(array_diff_key($options1, $options2) + array_diff_key($options2, $options1)); + + $changedProperties = array_merge($changedProperties, $diffKeys); + + return $changedProperties; + } + + /** + * Finds the difference between the indexes $index1 and $index2. + * + * Compares $index1 with $index2 and returns $index2 if there are any + * differences or false in case there are no differences. + * + * @param Index $index1 + * @param Index $index2 + * @return bool + */ + public function diffIndex(Index $index1, Index $index2) + { + if ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1)) { + return false; + } + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php new file mode 100644 index 00000000..f80410f2 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Constraint.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Marker interface for contraints + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +interface Constraint +{ + public function getName(); + + public function getQuotedName(AbstractPlatform $platform); + + public function getColumns(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/DB2SchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DB2SchemaManager.php new file mode 100644 index 00000000..e11c64cb --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DB2SchemaManager.php @@ -0,0 +1,214 @@ +. +*/ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs; +use Doctrine\DBAL\Events; + +/** + * IBM Db2 Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class DB2SchemaManager extends AbstractSchemaManager +{ + /** + * Return a list of all tables in the current database + * + * Apparently creator is the schema not the user who created it: + * {@link http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=/com.ibm.db29.doc.sqlref/db2z_sysibmsystablestable.htm} + * + * @return array + */ + public function listTableNames() + { + $sql = $this->_platform->getListTablesSQL(); + $sql .= " AND CREATOR = UPPER('".$this->_conn->getUsername()."')"; + + $tables = $this->_conn->fetchAll($sql); + + return $this->_getPortableTablesList($tables); + } + + + /** + * Get Table Column Definition + * + * @param array $tableColumn + * @return Column + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, \CASE_LOWER); + + $length = null; + $fixed = null; + $unsigned = false; + $scale = false; + $precision = false; + + $type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']); + + switch (strtolower($tableColumn['typename'])) { + case 'varchar': + $length = $tableColumn['length']; + $fixed = false; + break; + case 'character': + $length = $tableColumn['length']; + $fixed = true; + break; + case 'clob': + $length = $tableColumn['length']; + break; + case 'decimal': + case 'double': + case 'real': + $scale = $tableColumn['scale']; + $precision = $tableColumn['length']; + break; + } + + $options = array( + 'length' => $length, + 'unsigned' => (bool)$unsigned, + 'fixed' => (bool)$fixed, + 'default' => ($tableColumn['default'] == "NULL") ? null : $tableColumn['default'], + 'notnull' => (bool) ($tableColumn['nulls'] == 'N'), + 'scale' => null, + 'precision' => null, + 'platformOptions' => array(), + ); + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['colname'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableTablesList($tables) + { + $tableNames = array(); + foreach ($tables as $tableRow) { + $tableRow = array_change_key_case($tableRow, \CASE_LOWER); + $tableNames[] = $tableRow['name']; + } + return $tableNames; + } + + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $eventManager = $this->_platform->getEventManager(); + + $indexes = array(); + foreach($tableIndexes as $indexKey => $data) { + $data = array_change_key_case($data, \CASE_LOWER); + $unique = ($data['uniquerule'] == "D") ? false : true; + $primary = ($data['uniquerule'] == "P"); + + $indexName = strtolower($data['name']); + if ($primary) { + $keyName = 'primary'; + } else { + $keyName = $indexName; + } + + $data = array( + 'name' => $indexName, + 'columns' => explode("+", ltrim($data['colnames'], '+')), + 'unique' => $unique, + 'primary' => $primary + ); + + $index = null; + $defaultPrevented = false; + + if (null !== $eventManager && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) { + $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $index = $eventArgs->getIndex(); + } + + if ( ! $defaultPrevented) { + $index = new Index($data['name'], $data['columns'], $data['unique'], $data['primary']); + } + + if ($index) { + $indexes[$indexKey] = $index; + } + } + + return $indexes; + } + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); + + $tableForeignKey['deleterule'] = $this->_getPortableForeignKeyRuleDef($tableForeignKey['deleterule']); + $tableForeignKey['updaterule'] = $this->_getPortableForeignKeyRuleDef($tableForeignKey['updaterule']); + + return new ForeignKeyConstraint( + array_map('trim', (array)$tableForeignKey['fkcolnames']), + $tableForeignKey['reftbname'], + array_map('trim', (array)$tableForeignKey['pkcolnames']), + $tableForeignKey['relname'], + array( + 'onUpdate' => $tableForeignKey['updaterule'], + 'onDelete' => $tableForeignKey['deleterule'], + ) + ); + } + + protected function _getPortableForeignKeyRuleDef($def) + { + if ($def == "C") { + return "CASCADE"; + } else if ($def == "N") { + return "SET NULL"; + } + return null; + } + + protected function _getPortableViewDefinition($view) + { + $view = array_change_key_case($view, \CASE_LOWER); + // sadly this still segfaults on PDO_IBM, see http://pecl.php.net/bugs/bug.php?id=17199 + //$view['text'] = (is_resource($view['text']) ? stream_get_contents($view['text']) : $view['text']); + if (!is_resource($view['text'])) { + $pos = strpos($view['text'], ' AS '); + $sql = substr($view['text'], $pos+4); + } else { + $sql = ''; + } + + return new View($view['name'], $sql); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php new file mode 100644 index 00000000..6278a736 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Schema manager for the Drizzle RDBMS. + * + * @author Kim Hemsø Rasmussen + */ +class DrizzleSchemaManager extends AbstractSchemaManager +{ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableName = $tableColumn['COLUMN_NAME']; + $dbType = strtolower($tableColumn['DATA_TYPE']); + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['COLUMN_COMMENT'], $type); + $tableColumn['COLUMN_COMMENT'] = $this->removeDoctrineTypeFromComment($tableColumn['COLUMN_COMMENT'], $type); + + $options = array( + 'notnull' => !(bool)$tableColumn['IS_NULLABLE'], + 'length' => (int)$tableColumn['CHARACTER_MAXIMUM_LENGTH'], + 'default' => empty($tableColumn['COLUMN_DEFAULT']) ? null : $tableColumn['COLUMN_DEFAULT'], + 'autoincrement' => (bool)$tableColumn['IS_AUTO_INCREMENT'], + 'scale' => (int)$tableColumn['NUMERIC_SCALE'], + 'precision' => (int)$tableColumn['NUMERIC_PRECISION'], + 'comment' => (isset($tableColumn['COLUMN_COMMENT']) ? $tableColumn['COLUMN_COMMENT'] : null), + ); + + return new Column($tableName, \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database['SCHEMA_NAME']; + } + + protected function _getPortableTableDefinition($table) + { + return $table['TABLE_NAME']; + } + + public function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $columns = array(); + foreach (explode(',', $tableForeignKey['CONSTRAINT_COLUMNS']) as $value) { + $columns[] = trim($value, ' `'); + } + + $ref_columns = array(); + foreach (explode(',', $tableForeignKey['REFERENCED_TABLE_COLUMNS']) as $value) { + $ref_columns[] = trim($value, ' `'); + } + + return new ForeignKeyConstraint( + $columns, + $tableForeignKey['REFERENCED_TABLE_NAME'], + $ref_columns, + $tableForeignKey['CONSTRAINT_NAME'], + array( + 'onUpdate' => $tableForeignKey['UPDATE_RULE'], + 'onDelete' => $tableForeignKey['DELETE_RULE'], + ) + ); + } + + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + $indexes = array(); + foreach ($tableIndexes as $k) { + $k['primary'] = (boolean)$k['primary']; + $indexes[] = $k; + } + + return parent::_getPortableTableIndexesList($indexes, $tableName); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php new file mode 100644 index 00000000..481d6930 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php @@ -0,0 +1,193 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; +use Doctrine\DBAL\Platforms\AbstractPlatform; + +class ForeignKeyConstraint extends AbstractAsset implements Constraint +{ + /** + * @var Table + */ + protected $_localTable; + + /** + * @var array + */ + protected $_localColumnNames; + + /** + * @var string + */ + protected $_foreignTableName; + + /** + * @var array + */ + protected $_foreignColumnNames; + + /** + * @var string + */ + protected $_cascade = ''; + + /** + * @var array + */ + protected $_options; + + /** + * + * @param array $localColumnNames + * @param string $foreignTableName + * @param array $foreignColumnNames + * @param string $cascade + * @param string|null $name + */ + public function __construct(array $localColumnNames, $foreignTableName, array $foreignColumnNames, $name=null, array $options=array()) + { + $this->_setName($name); + $this->_localColumnNames = $localColumnNames; + $this->_foreignTableName = $foreignTableName; + $this->_foreignColumnNames = $foreignColumnNames; + $this->_options = $options; + } + + /** + * @return string + */ + public function getLocalTableName() + { + return $this->_localTable->getName(); + } + + /** + * @param Table $table + */ + public function setLocalTable(Table $table) + { + $this->_localTable = $table; + } + + /** + * @return array + */ + public function getLocalColumns() + { + return $this->_localColumnNames; + } + + public function getColumns() + { + return $this->_localColumnNames; + } + + /** + * @return string + */ + public function getForeignTableName() + { + return $this->_foreignTableName; + } + + /** + * Get the quoted representation of this asset but only if it was defined with one. Otherwise + * return the plain unquoted value as inserted. + * + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedForeignTableName(AbstractPlatform $platform) + { + $keywords = $platform->getReservedKeywordsList(); + $parts = explode(".", $this->getForeignTableName()); + foreach ($parts AS $k => $v) { + $parts[$k] = ($this->_quoted || $keywords->isKeyword($v)) ? $platform->quoteIdentifier($v) : $v; + } + + return implode(".", $parts); + } + + /** + * @return array + */ + public function getForeignColumns() + { + return $this->_foreignColumnNames; + } + + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + public function getOption($name) + { + return $this->_options[$name]; + } + + /** + * Gets the options associated with this constraint + * + * @return array + */ + public function getOptions() + { + return $this->_options; + } + + /** + * Foreign Key onUpdate status + * + * @return string|null + */ + public function onUpdate() + { + return $this->_onEvent('onUpdate'); + } + + /** + * Foreign Key onDelete status + * + * @return string|null + */ + public function onDelete() + { + return $this->_onEvent('onDelete'); + } + + /** + * @param string $event + * @return string|null + */ + private function _onEvent($event) + { + if (isset($this->_options[$event])) { + $onEvent = strtoupper($this->_options[$event]); + if (!in_array($onEvent, array('NO ACTION', 'RESTRICT'))) { + return $onEvent; + } + } + return false; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php new file mode 100644 index 00000000..625e4039 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php @@ -0,0 +1,241 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; + +class Index extends AbstractAsset implements Constraint +{ + /** + * @var array + */ + protected $_columns; + + /** + * @var bool + */ + protected $_isUnique = false; + + /** + * @var bool + */ + protected $_isPrimary = false; + + /** + * Platform specific flags for indexes. + * + * @var array + */ + protected $_flags = array(); + + /** + * @param string $indexName + * @param array $column + * @param bool $isUnique + * @param bool $isPrimary + */ + public function __construct($indexName, array $columns, $isUnique = false, $isPrimary = false, array $flags = array()) + { + $isUnique = ($isPrimary)?true:$isUnique; + + $this->_setName($indexName); + $this->_isUnique = $isUnique; + $this->_isPrimary = $isPrimary; + + foreach ($columns as $column) { + $this->_addColumn($column); + } + foreach ($flags as $flag) { + $this->addFlag($flag); + } + } + + /** + * @param string $column + */ + protected function _addColumn($column) + { + if(is_string($column)) { + $this->_columns[] = $column; + } else { + throw new \InvalidArgumentException("Expecting a string as Index Column"); + } + } + + /** + * @return array + */ + public function getColumns() + { + return $this->_columns; + } + + /** + * @return array + */ + public function getUnquotedColumns() + { + return array_map(array($this, 'trimQuotes'), $this->getColumns()); + } + + /** + * Is the index neither unique nor primary key? + * + * @return bool + */ + public function isSimpleIndex() + { + return !$this->_isPrimary && !$this->_isUnique; + } + + /** + * @return bool + */ + public function isUnique() + { + return $this->_isUnique; + } + + /** + * @return bool + */ + public function isPrimary() + { + return $this->_isPrimary; + } + + /** + * @param string $columnName + * @param int $pos + * @return bool + */ + public function hasColumnAtPosition($columnName, $pos = 0) + { + $columnName = $this->trimQuotes(strtolower($columnName)); + $indexColumns = array_map('strtolower', $this->getUnquotedColumns()); + return array_search($columnName, $indexColumns) === $pos; + } + + /** + * Check if this index exactly spans the given column names in the correct order. + * + * @param array $columnNames + * @return boolean + */ + public function spansColumns(array $columnNames) + { + $sameColumns = true; + for ($i = 0; $i < count($this->_columns); $i++) { + if (!isset($columnNames[$i]) || $this->trimQuotes(strtolower($this->_columns[$i])) != $this->trimQuotes(strtolower($columnNames[$i]))) { + $sameColumns = false; + } + } + return $sameColumns; + } + + /** + * Check if the other index already fullfills all the indexing and constraint needs of the current one. + * + * @param Index $other + * @return bool + */ + public function isFullfilledBy(Index $other) + { + // allow the other index to be equally large only. It being larger is an option + // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) + if (count($other->getColumns()) != count($this->getColumns())) { + return false; + } + + // Check if columns are the same, and even in the same order + $sameColumns = $this->spansColumns($other->getColumns()); + + if ($sameColumns) { + if ( ! $this->isUnique() && !$this->isPrimary()) { + // this is a special case: If the current key is neither primary or unique, any uniqe or + // primary key will always have the same effect for the index and there cannot be any constraint + // overlaps. This means a primary or unique index can always fullfill the requirements of just an + // index that has no constraints. + return true; + } else if ($other->isPrimary() != $this->isPrimary()) { + return false; + } else if ($other->isUnique() != $this->isUnique()) { + return false; + } + return true; + } + return false; + } + + /** + * Detect if the other index is a non-unique, non primary index that can be overwritten by this one. + * + * @param Index $other + * @return bool + */ + public function overrules(Index $other) + { + if ($other->isPrimary()) { + return false; + } else if ($this->isSimpleIndex() && $other->isUnique()) { + return false; + } + + if ($this->spansColumns($other->getColumns()) && ($this->isPrimary() || $this->isUnique())) { + return true; + } + return false; + } + + /** + * Add Flag for an index that translates to platform specific handling. + * + * @example $index->addFlag('CLUSTERED') + * @param string $flag + * @return Index + */ + public function addFlag($flag) + { + $this->flags[strtolower($flag)] = true; + return $this; + } + + /** + * Does this index have a specific flag? + * + * @param string $flag + * @return bool + */ + public function hasFlag($flag) + { + return isset($this->flags[strtolower($flag)]); + } + + /** + * Remove a flag + * + * @param string $flag + * @return void + */ + public function removeFlag($flag) + { + unset($this->flags[strtolower($flag)]); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php new file mode 100644 index 00000000..a9a59645 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php @@ -0,0 +1,208 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Schema manager for the MySql RDBMS. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Roman Borschel + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +class MySqlSchemaManager extends AbstractSchemaManager +{ + protected function _getPortableViewDefinition($view) + { + return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); + } + + protected function _getPortableTableDefinition($table) + { + return array_shift($table); + } + + protected function _getPortableUserDefinition($user) + { + return array( + 'user' => $user['User'], + 'password' => $user['Password'], + ); + } + + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + foreach($tableIndexes as $k => $v) { + $v = array_change_key_case($v, CASE_LOWER); + if($v['key_name'] == 'PRIMARY') { + $v['primary'] = true; + } else { + $v['primary'] = false; + } + if (strpos($v['index_type'], 'FULLTEXT') !== false) { + $v['flags'] = array('FULLTEXT'); + } + $tableIndexes[$k] = $v; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + protected function _getPortableSequenceDefinition($sequence) + { + return end($sequence); + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database['Database']; + } + + /** + * Gets a portable column definition. + * + * The database type is mapped to a corresponding Doctrine mapping type. + * + * @param $tableColumn + * @return array + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['type']); + $dbType = strtok($dbType, '(), '); + if (isset($tableColumn['length'])) { + $length = $tableColumn['length']; + $decimal = ''; + } else { + $length = strtok('(), '); + $decimal = strtok('(), ') ? strtok('(), '):null; + } + $type = array(); + $fixed = null; + + if ( ! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $scale = null; + $precision = null; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + + switch ($dbType) { + case 'char': + $fixed = true; + break; + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + if(preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['type'], $match)) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + break; + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + case 'bigint': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + case 'blob': + case 'year': + $length = null; + break; + } + + $length = ((int) $length == 0) ? null : (int) $length; + + $options = array( + 'length' => $length, + 'unsigned' => (bool) (strpos($tableColumn['type'], 'unsigned') !== false), + 'fixed' => (bool) $fixed, + 'default' => isset($tableColumn['default']) ? $tableColumn['default'] : null, + 'notnull' => (bool) ($tableColumn['null'] != 'YES'), + 'scale' => null, + 'precision' => null, + 'autoincrement' => (bool) (strpos($tableColumn['extra'], 'auto_increment') !== false), + 'comment' => (isset($tableColumn['comment'])) ? $tableColumn['comment'] : null + ); + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['field'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $key => $value) { + $value = array_change_key_case($value, CASE_LOWER); + if (!isset($list[$value['constraint_name']])) { + if (!isset($value['delete_rule']) || $value['delete_rule'] == "RESTRICT") { + $value['delete_rule'] = null; + } + if (!isset($value['update_rule']) || $value['update_rule'] == "RESTRICT") { + $value['update_rule'] = null; + } + + $list[$value['constraint_name']] = array( + 'name' => $value['constraint_name'], + 'local' => array(), + 'foreign' => array(), + 'foreignTable' => $value['referenced_table_name'], + 'onDelete' => $value['delete_rule'], + 'onUpdate' => $value['update_rule'], + ); + } + $list[$value['constraint_name']]['local'][] = $value['column_name']; + $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; + } + + $result = array(); + foreach($list as $constraint) { + $result[] = new ForeignKeyConstraint( + array_values($constraint['local']), $constraint['foreignTable'], + array_values($constraint['foreign']), $constraint['name'], + array( + 'onDelete' => $constraint['onDelete'], + 'onUpdate' => $constraint['onUpdate'], + ) + ); + } + + return $result; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/OracleSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/OracleSchemaManager.php new file mode 100644 index 00000000..2a880d9b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/OracleSchemaManager.php @@ -0,0 +1,286 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Oracle Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +class OracleSchemaManager extends AbstractSchemaManager +{ + protected function _getPortableViewDefinition($view) + { + $view = \array_change_key_case($view, CASE_LOWER); + + return new View($view['view_name'], $view['text']); + } + + protected function _getPortableUserDefinition($user) + { + $user = \array_change_key_case($user, CASE_LOWER); + + return array( + 'user' => $user['username'], + ); + } + + protected function _getPortableTableDefinition($table) + { + $table = \array_change_key_case($table, CASE_LOWER); + + return $table['table_name']; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $indexBuffer = array(); + foreach ( $tableIndexes as $tableIndex ) { + $tableIndex = \array_change_key_case($tableIndex, CASE_LOWER); + + $keyName = strtolower($tableIndex['name']); + + if ( strtolower($tableIndex['is_primary']) == "p" ) { + $keyName = 'primary'; + $buffer['primary'] = true; + $buffer['non_unique'] = false; + } else { + $buffer['primary'] = false; + $buffer['non_unique'] = ( $tableIndex['is_unique'] == 0 ) ? true : false; + } + $buffer['key_name'] = $keyName; + $buffer['column_name'] = $tableIndex['column_name']; + $indexBuffer[] = $buffer; + } + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = \array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['data_type']); + if(strpos($dbType, "timestamp(") === 0) { + if (strpos($dbType, "WITH TIME ZONE")) { + $dbType = "timestamptz"; + } else { + $dbType = "timestamp"; + } + } + + $type = array(); + $length = $unsigned = $fixed = null; + if ( ! empty($tableColumn['data_length'])) { + $length = $tableColumn['data_length']; + } + + if ( ! isset($tableColumn['column_name'])) { + $tableColumn['column_name'] = ''; + } + + if (stripos($tableColumn['data_default'], 'NULL') !== null) { + $tableColumn['data_default'] = null; + } + + $precision = null; + $scale = null; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type); + $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type); + + switch ($dbType) { + case 'number': + if ($tableColumn['data_precision'] == 20 && $tableColumn['data_scale'] == 0) { + $precision = 20; + $scale = 0; + $type = 'bigint'; + } elseif ($tableColumn['data_precision'] == 5 && $tableColumn['data_scale'] == 0) { + $type = 'smallint'; + $precision = 5; + $scale = 0; + } elseif ($tableColumn['data_precision'] == 1 && $tableColumn['data_scale'] == 0) { + $precision = 1; + $scale = 0; + $type = 'boolean'; + } elseif ($tableColumn['data_scale'] > 0) { + $precision = $tableColumn['data_precision']; + $scale = $tableColumn['data_scale']; + $type = 'decimal'; + } + $length = null; + break; + case 'pls_integer': + case 'binary_integer': + $length = null; + break; + case 'varchar': + case 'varchar2': + case 'nvarchar2': + $length = $tableColumn['char_length']; + $fixed = false; + break; + case 'char': + case 'nchar': + $length = $tableColumn['char_length']; + $fixed = true; + break; + case 'date': + case 'timestamp': + $length = null; + break; + case 'float': + $precision = $tableColumn['data_precision']; + $scale = $tableColumn['data_scale']; + $length = null; + break; + case 'clob': + case 'nclob': + $length = null; + break; + case 'blob': + case 'raw': + case 'long raw': + case 'bfile': + $length = null; + break; + case 'rowid': + case 'urowid': + default: + $length = null; + } + + $options = array( + 'notnull' => (bool) ($tableColumn['nullable'] === 'N'), + 'fixed' => (bool) $fixed, + 'unsigned' => (bool) $unsigned, + 'default' => $tableColumn['data_default'], + 'length' => $length, + 'precision' => $precision, + 'scale' => $scale, + 'comment' => (isset($tableColumn['comments'])) ? $tableColumn['comments'] : null, + 'platformDetails' => array(), + ); + + return new Column($tableColumn['column_name'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $value) { + $value = \array_change_key_case($value, CASE_LOWER); + if (!isset($list[$value['constraint_name']])) { + if ($value['delete_rule'] == "NO ACTION") { + $value['delete_rule'] = null; + } + + $list[$value['constraint_name']] = array( + 'name' => $value['constraint_name'], + 'local' => array(), + 'foreign' => array(), + 'foreignTable' => $value['references_table'], + 'onDelete' => $value['delete_rule'], + ); + } + $list[$value['constraint_name']]['local'][$value['position']] = $value['local_column']; + $list[$value['constraint_name']]['foreign'][$value['position']] = $value['foreign_column']; + } + + $result = array(); + foreach($list as $constraint) { + $result[] = new ForeignKeyConstraint( + array_values($constraint['local']), $constraint['foreignTable'], + array_values($constraint['foreign']), $constraint['name'], + array('onDelete' => $constraint['onDelete']) + ); + } + + return $result; + } + + protected function _getPortableSequenceDefinition($sequence) + { + $sequence = \array_change_key_case($sequence, CASE_LOWER); + return new Sequence($sequence['sequence_name'], $sequence['increment_by'], $sequence['min_value']); + } + + protected function _getPortableFunctionDefinition($function) + { + $function = \array_change_key_case($function, CASE_LOWER); + return $function['name']; + } + + protected function _getPortableDatabaseDefinition($database) + { + $database = \array_change_key_case($database, CASE_LOWER); + return $database['username']; + } + + public function createDatabase($database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + + $params = $this->_conn->getParams(); + $username = $database; + $password = $params['password']; + + $query = 'CREATE USER ' . $username . ' IDENTIFIED BY ' . $password; + $result = $this->_conn->executeUpdate($query); + + $query = 'GRANT CREATE SESSION, CREATE TABLE, UNLIMITED TABLESPACE, CREATE SEQUENCE, CREATE TRIGGER TO ' . $username; + $result = $this->_conn->executeUpdate($query); + + return true; + } + + public function dropAutoincrement($table) + { + $sql = $this->_platform->getDropAutoincrementSql($table); + foreach ($sql as $query) { + $this->_conn->executeUpdate($query); + } + + return true; + } + + public function dropTable($name) + { + $this->dropAutoincrement($name); + + return parent::dropTable($name); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php new file mode 100644 index 00000000..800fee27 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php @@ -0,0 +1,359 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * PostgreSQL Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @since 2.0 + */ +class PostgreSqlSchemaManager extends AbstractSchemaManager +{ + /** + * @var array + */ + private $existingSchemaPaths; + + /** + * Get all the existing schema names. + * + * @return array + */ + public function getSchemaNames() + { + $rows = $this->_conn->fetchAll("SELECT nspname as schema_name FROM pg_namespace WHERE nspname !~ '^pg_.*' and nspname != 'information_schema'"); + return array_map(function($v) { return $v['schema_name']; }, $rows); + } + + /** + * Return an array of schema search paths + * + * This is a PostgreSQL only function. + * + * @return array + */ + public function getSchemaSearchPaths() + { + $params = $this->_conn->getParams(); + $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path')); + if (isset($params['user'])) { + $schema = str_replace('"$user"', $params['user'], $schema); + } + return $schema; + } + + /** + * Get names of all existing schemas in the current users search path. + * + * This is a PostgreSQL only function. + * + * @return array + */ + public function getExistingSchemaSearchPaths() + { + if ($this->existingSchemaPaths === null) { + $this->determineExistingSchemaSearchPaths(); + } + return $this->existingSchemaPaths; + } + + /** + * Use this to set or reset the order of the existing schemas in the current search path of the user + * + * This is a PostgreSQL only function. + * + * @return void + */ + public function determineExistingSchemaSearchPaths() + { + $names = $this->getSchemaNames(); + $paths = $this->getSchemaSearchPaths(); + + $this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) { + return in_array($v, $names); + }); + } + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $onUpdate = null; + $onDelete = null; + + if (preg_match('(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) { + $onUpdate = $match[1]; + } + if (preg_match('(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', $tableForeignKey['condef'], $match)) { + $onDelete = $match[1]; + } + + if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) { + // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get + // the idea to trim them here. + $localColumns = array_map('trim', explode(",", $values[1])); + $foreignColumns = array_map('trim', explode(",", $values[3])); + $foreignTable = $values[2]; + } + + return new ForeignKeyConstraint( + $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'], + array('onUpdate' => $onUpdate, 'onDelete' => $onDelete) + ); + } + + public function dropDatabase($database) + { + $params = $this->_conn->getParams(); + $params["dbname"] = "postgres"; + $tmpPlatform = $this->_platform; + $tmpConn = $this->_conn; + + $this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params); + $this->_platform = $this->_conn->getDatabasePlatform(); + + parent::dropDatabase($database); + + $this->_platform = $tmpPlatform; + $this->_conn = $tmpConn; + } + + public function createDatabase($database) + { + $params = $this->_conn->getParams(); + $params["dbname"] = "postgres"; + $tmpPlatform = $this->_platform; + $tmpConn = $this->_conn; + + $this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params); + $this->_platform = $this->_conn->getDatabasePlatform(); + + parent::createDatabase($database); + + $this->_platform = $tmpPlatform; + $this->_conn = $tmpConn; + } + + protected function _getPortableTriggerDefinition($trigger) + { + return $trigger['trigger_name']; + } + + protected function _getPortableViewDefinition($view) + { + return new View($view['viewname'], $view['definition']); + } + + protected function _getPortableUserDefinition($user) + { + return array( + 'user' => $user['usename'], + 'password' => $user['passwd'] + ); + } + + protected function _getPortableTableDefinition($table) + { + $schemas = $this->getExistingSchemaSearchPaths(); + $firstSchema = array_shift($schemas); + + if ($table['schema_name'] == $firstSchema) { + return $table['table_name']; + } else { + return $table['schema_name'] . "." . $table['table_name']; + } + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $buffer = array(); + foreach ($tableIndexes as $row) { + $colNumbers = explode(' ', $row['indkey']); + $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )'; + $columnNameSql = "SELECT attnum, attname FROM pg_attribute + WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;"; + + $stmt = $this->_conn->executeQuery($columnNameSql); + $indexColumns = $stmt->fetchAll(); + + // required for getting the order of the columns right. + foreach ($colNumbers as $colNum) { + foreach ($indexColumns as $colRow) { + if ($colNum == $colRow['attnum']) { + $buffer[] = array( + 'key_name' => $row['relname'], + 'column_name' => trim($colRow['attname']), + 'non_unique' => !$row['indisunique'], + 'primary' => $row['indisprimary'] + ); + } + } + } + } + return parent::_getPortableTableIndexesList($buffer, $tableName); + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database['datname']; + } + + protected function _getPortableSequenceDefinition($sequence) + { + if ($sequence['schemaname'] != 'public') { + $sequenceName = $sequence['schemaname'] . "." . $sequence['relname']; + } else { + $sequenceName = $sequence['relname']; + } + + $data = $this->_conn->fetchAll('SELECT min_value, increment_by FROM ' . $sequenceName); + return new Sequence($sequenceName, $data[0]['increment_by'], $data[0]['min_value']); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + if (strtolower($tableColumn['type']) === 'varchar') { + // get length from varchar definition + $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']); + $tableColumn['length'] = $length; + } + + $matches = array(); + + $autoincrement = false; + if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) { + $tableColumn['sequence'] = $matches[1]; + $tableColumn['default'] = null; + $autoincrement = true; + } + + if (stripos($tableColumn['default'], 'NULL') === 0) { + $tableColumn['default'] = null; + } + + $length = (isset($tableColumn['length'])) ? $tableColumn['length'] : null; + if ($length == '-1' && isset($tableColumn['atttypmod'])) { + $length = $tableColumn['atttypmod'] - 4; + } + if ((int) $length <= 0) { + $length = null; + } + $fixed = null; + + if (!isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + + $dbType = strtolower($tableColumn['type']); + if (strlen($tableColumn['domain_type']) && !$this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) { + $dbType = strtolower($tableColumn['domain_type']); + $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + } + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + + switch ($dbType) { + case 'smallint': + case 'int2': + $length = null; + break; + case 'int': + case 'int4': + case 'integer': + $length = null; + break; + case 'bigint': + case 'int8': + $length = null; + break; + case 'bool': + case 'boolean': + $length = null; + break; + case 'text': + $fixed = false; + break; + case 'varchar': + case 'interval': + case '_varchar': + $fixed = false; + break; + case 'char': + case 'bpchar': + $fixed = true; + break; + case 'float': + case 'float4': + case 'float8': + case 'double': + case 'double precision': + case 'real': + case 'decimal': + case 'money': + case 'numeric': + if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + break; + case 'year': + $length = null; + break; + } + + if ($tableColumn['default'] && preg_match("('([^']+)'::)", $tableColumn['default'], $match)) { + $tableColumn['default'] = $match[1]; + } + + $options = array( + 'length' => $length, + 'notnull' => (bool) $tableColumn['isnotnull'], + 'default' => $tableColumn['default'], + 'primary' => (bool) ($tableColumn['pri'] == 't'), + 'precision' => $precision, + 'scale' => $scale, + 'fixed' => $fixed, + 'unsigned' => false, + 'autoincrement' => $autoincrement, + 'comment' => $tableColumn['comment'], + ); + + return new Column($tableColumn['field'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php new file mode 100644 index 00000000..174dc11f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php @@ -0,0 +1,263 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Events; +use Doctrine\DBAL\Event\SchemaIndexDefinitionEventArgs; +use Doctrine\DBAL\Driver\SQLSrv\SQLSrvException; + +/** + * SQL Server Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Juozas Kaziukenas + * @since 2.0 + */ +class SQLServerSchemaManager extends AbstractSchemaManager +{ + /** + * @override + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $dbType = strtolower($tableColumn['TYPE_NAME']); + + $autoincrement = false; + if (stripos($dbType, 'identity')) { + $dbType = trim(str_ireplace('identity', '', $dbType)); + $autoincrement = true; + } + + $type = array(); + $unsigned = $fixed = null; + + if (!isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $default = $tableColumn['COLUMN_DEF']; + + while ($default != ($default2 = preg_replace("/^\((.*)\)$/", '$1', $default))) { + $default = trim($default2, "'"); + } + + $length = (int) $tableColumn['LENGTH']; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + switch ($type) { + case 'char': + if ($tableColumn['LENGTH'] == '1') { + $type = 'boolean'; + if (preg_match('/^(is|has)/', $tableColumn['name'])) { + $type = array_reverse($type); + } + } + $fixed = true; + break; + case 'text': + $fixed = false; + break; + } + switch ($dbType) { + case 'nchar': + case 'nvarchar': + case 'ntext': + // Unicode data requires 2 bytes per character + $length = $length / 2; + break; + } + + $options = array( + 'length' => ($length == 0 || !in_array($type, array('text', 'string'))) ? null : $length, + 'unsigned' => (bool) $unsigned, + 'fixed' => (bool) $fixed, + 'default' => $default !== 'NULL' ? $default : null, + 'notnull' => (bool) ($tableColumn['IS_NULLABLE'] != 'YES'), + 'scale' => $tableColumn['SCALE'], + 'precision' => $tableColumn['PRECISION'], + 'autoincrement' => $autoincrement, + ); + + return new Column($tableColumn['COLUMN_NAME'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + /** + * @override + */ + protected function _getPortableTableIndexesList($tableIndexRows, $tableName=null) + { + // TODO: Remove code duplication with AbstractSchemaManager; + $result = array(); + foreach ($tableIndexRows as $tableIndex) { + $indexName = $keyName = $tableIndex['index_name']; + if (strpos($tableIndex['index_description'], 'primary key') !== false) { + $keyName = 'primary'; + } + $keyName = strtolower($keyName); + + $flags = array(); + if (strpos($tableIndex['index_description'], 'clustered') !== false) { + $flags[] = 'clustered'; + } else if (strpos($tableIndex['index_description'], 'nonclustered') !== false) { + $flags[] = 'nonclustered'; + } + + $result[$keyName] = array( + 'name' => $indexName, + 'columns' => explode(', ', $tableIndex['index_keys']), + 'unique' => strpos($tableIndex['index_description'], 'unique') !== false, + 'primary' => strpos($tableIndex['index_description'], 'primary key') !== false, + 'flags' => $flags, + ); + } + + $eventManager = $this->_platform->getEventManager(); + + $indexes = array(); + foreach ($result as $indexKey => $data) { + $index = null; + $defaultPrevented = false; + + if (null !== $eventManager && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) { + $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $index = $eventArgs->getIndex(); + } + + if ( ! $defaultPrevented) { + $index = new Index($data['name'], $data['columns'], $data['unique'], $data['primary']); + } + + if ($index) { + $indexes[$indexKey] = $index; + } + } + + return $indexes; + } + + /** + * @override + */ + public function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return new ForeignKeyConstraint( + (array) $tableForeignKey['ColumnName'], + $tableForeignKey['ReferenceTableName'], + (array) $tableForeignKey['ReferenceColumnName'], + $tableForeignKey['ForeignKey'], + array( + 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), + 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), + ) + ); + } + + /** + * @override + */ + protected function _getPortableTableDefinition($table) + { + return $table['name']; + } + + /** + * @override + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database['name']; + } + + /** + * @override + */ + protected function _getPortableViewDefinition($view) + { + // @todo + return new View($view['name'], null); + } + + /** + * List the indexes for a given table returning an array of Index instances. + * + * Keys of the portable indexes list are all lower-cased. + * + * @param string $table The name of the table + * @return Index[] $tableIndexes + */ + public function listTableIndexes($table) + { + $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase()); + + try { + $tableIndexes = $this->_conn->fetchAll($sql); + } catch(\PDOException $e) { + if ($e->getCode() == "IMSSP") { + return array(); + } else { + throw $e; + } + } catch(SQLSrvException $e) { + if (strpos($e->getMessage(), 'SQLSTATE [01000, 15472]') === 0) { + return array(); + } else { + throw $e; + } + } + + return $this->_getPortableTableIndexesList($tableIndexes, $table); + } + + /** + * @override + */ + public function alterTable(TableDiff $tableDiff) + { + if(count($tableDiff->removedColumns) > 0) { + foreach($tableDiff->removedColumns as $col){ + $columnConstraintSql = $this->getColumnConstraintSQL($tableDiff->name, $col->getName()); + foreach ($this->_conn->fetchAll($columnConstraintSql) as $constraint) { + $this->_conn->exec("ALTER TABLE $tableDiff->name DROP CONSTRAINT " . $constraint['Name']); + } + } + } + + return parent::alterTable($tableDiff); + } + + /** + * This function retrieves the constraints for a given column. + */ + private function getColumnConstraintSQL($table, $column) + { + return "SELECT SysObjects.[Name] + FROM SysObjects INNER JOIN (SELECT [Name],[ID] FROM SysObjects WHERE XType = 'U') AS Tab + ON Tab.[ID] = Sysobjects.[Parent_Obj] + INNER JOIN sys.default_constraints DefCons ON DefCons.[object_id] = Sysobjects.[ID] + INNER JOIN SysColumns Col ON Col.[ColID] = DefCons.[parent_column_id] AND Col.[ID] = Tab.[ID] + WHERE Col.[Name] = " . $this->_conn->quote($column) ." AND Tab.[Name] = " . $this->_conn->quote($table) . " + ORDER BY Col.[Name]"; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Schema.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Schema.php new file mode 100644 index 00000000..5a6ff751 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Schema.php @@ -0,0 +1,373 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector; +use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector; +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Object representation of a database schema + * + * Different vendors have very inconsistent naming with regard to the concept + * of a "schema". Doctrine understands a schema as the entity that conceptually + * wraps a set of database objects such as tables, sequences, indexes and + * foreign keys that belong to each other into a namespace. A Doctrine Schema + * has nothing to do with the "SCHEMA" defined as in PostgreSQL, it is more + * related to the concept of "DATABASE" that exists in MySQL and PostgreSQL. + * + * Every asset in the doctrine schema has a name. A name consists of either a + * namespace.local name pair or just a local unqualified name. + * + * The abstraction layer that covers a PostgreSQL schema is the namespace of an + * database object (asset). A schema can have a name, which will be used as + * default namespace for the unqualified database objects that are created in + * the schema. + * + * In the case of MySQL where cross-database queries are allowed this leads to + * databases being "misinterpreted" as namespaces. This is intentional, however + * the CREATE/DROP SQL visitors will just filter this queries and do not + * execute them. Only the queries for the currently connected database are + * executed. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class Schema extends AbstractAsset +{ + /** + * @var array + */ + protected $_tables = array(); + + /** + * @var array + */ + protected $_sequences = array(); + + /** + * @var SchemaConfig + */ + protected $_schemaConfig = false; + + /** + * @param array $tables + * @param array $sequences + * @param array $views + * @param array $triggers + * @param SchemaConfig $schemaConfig + */ + public function __construct(array $tables=array(), array $sequences=array(), SchemaConfig $schemaConfig=null) + { + if ($schemaConfig == null) { + $schemaConfig = new SchemaConfig(); + } + $this->_schemaConfig = $schemaConfig; + $this->_setName($schemaConfig->getName() ?: 'public'); + + foreach ($tables as $table) { + $this->_addTable($table); + } + + foreach ($sequences as $sequence) { + $this->_addSequence($sequence); + } + } + + /** + * @return bool + */ + public function hasExplicitForeignKeyIndexes() + { + return $this->_schemaConfig->hasExplicitForeignKeyIndexes(); + } + + /** + * @param Table $table + */ + protected function _addTable(Table $table) + { + $tableName = $table->getFullQualifiedName($this->getName()); + if(isset($this->_tables[$tableName])) { + throw SchemaException::tableAlreadyExists($tableName); + } + + $this->_tables[$tableName] = $table; + $table->setSchemaConfig($this->_schemaConfig); + } + + /** + * @param Sequence $sequence + */ + protected function _addSequence(Sequence $sequence) + { + $seqName = $sequence->getFullQualifiedName($this->getName()); + if (isset($this->_sequences[$seqName])) { + throw SchemaException::sequenceAlreadyExists($seqName); + } + $this->_sequences[$seqName] = $sequence; + } + + /** + * Get all tables of this schema. + * + * @return array + */ + public function getTables() + { + return $this->_tables; + } + + /** + * @param string $tableName + * @return Table + */ + public function getTable($tableName) + { + $tableName = $this->getFullQualifiedAssetName($tableName); + if (!isset($this->_tables[$tableName])) { + throw SchemaException::tableDoesNotExist($tableName); + } + + return $this->_tables[$tableName]; + } + + /** + * @return string + */ + private function getFullQualifiedAssetName($name) + { + if ($this->isIdentifierQuoted($name)) { + $name = $this->trimQuotes($name); + } + if (strpos($name, ".") === false) { + $name = $this->getName() . "." . $name; + } + return strtolower($name); + } + + /** + * Does this schema have a table with the given name? + * + * @param string $tableName + * @return Schema + */ + public function hasTable($tableName) + { + $tableName = $this->getFullQualifiedAssetName($tableName); + return isset($this->_tables[$tableName]); + } + + /** + * Get all table names, prefixed with a schema name, even the default one + * if present. + * + * @return array + */ + public function getTableNames() + { + return array_keys($this->_tables); + } + + public function hasSequence($sequenceName) + { + $sequenceName = $this->getFullQualifiedAssetName($sequenceName); + return isset($this->_sequences[$sequenceName]); + } + + /** + * @throws SchemaException + * @param string $sequenceName + * @return \Doctrine\DBAL\Schema\Sequence + */ + public function getSequence($sequenceName) + { + $sequenceName = $this->getFullQualifiedAssetName($sequenceName); + if(!$this->hasSequence($sequenceName)) { + throw SchemaException::sequenceDoesNotExist($sequenceName); + } + return $this->_sequences[$sequenceName]; + } + + /** + * @return \Doctrine\DBAL\Schema\Sequence[] + */ + public function getSequences() + { + return $this->_sequences; + } + + /** + * Create a new table + * + * @param string $tableName + * @return Table + */ + public function createTable($tableName) + { + $table = new Table($tableName); + $this->_addTable($table); + + foreach ($this->_schemaConfig->getDefaultTableOptions() as $name => $value) { + $table->addOption($name, $value); + } + + return $table; + } + + /** + * Rename a table + * + * @param string $oldTableName + * @param string $newTableName + * @return Schema + */ + public function renameTable($oldTableName, $newTableName) + { + $table = $this->getTable($oldTableName); + $table->_setName($newTableName); + + $this->dropTable($oldTableName); + $this->_addTable($table); + return $this; + } + + /** + * Drop a table from the schema. + * + * @param string $tableName + * @return Schema + */ + public function dropTable($tableName) + { + $tableName = $this->getFullQualifiedAssetName($tableName); + $table = $this->getTable($tableName); + unset($this->_tables[$tableName]); + return $this; + } + + /** + * Create a new sequence + * + * @param string $sequenceName + * @param int $allocationSize + * @param int $initialValue + * @return Sequence + */ + public function createSequence($sequenceName, $allocationSize=1, $initialValue=1) + { + $seq = new Sequence($sequenceName, $allocationSize, $initialValue); + $this->_addSequence($seq); + return $seq; + } + + /** + * @param string $sequenceName + * @return Schema + */ + public function dropSequence($sequenceName) + { + $sequenceName = $this->getFullQualifiedAssetName($sequenceName); + unset($this->_sequences[$sequenceName]); + return $this; + } + + /** + * Return an array of necessary sql queries to create the schema on the given platform. + * + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + * @return array + */ + public function toSql(\Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $sqlCollector = new CreateSchemaSqlCollector($platform); + $this->visit($sqlCollector); + + return $sqlCollector->getQueries(); + } + + /** + * Return an array of necessary sql queries to drop the schema on the given platform. + * + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + * @return array + */ + public function toDropSql(\Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $dropSqlCollector = new DropSchemaSqlCollector($platform); + $this->visit($dropSqlCollector); + + return $dropSqlCollector->getQueries(); + } + + /** + * @param Schema $toSchema + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function getMigrateToSql(Schema $toSchema, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $comparator = new Comparator(); + $schemaDiff = $comparator->compare($this, $toSchema); + return $schemaDiff->toSql($platform); + } + + /** + * @param Schema $fromSchema + * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + */ + public function getMigrateFromSql(Schema $fromSchema, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $comparator = new Comparator(); + $schemaDiff = $comparator->compare($fromSchema, $this); + return $schemaDiff->toSql($platform); + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptSchema($this); + + foreach ($this->_tables as $table) { + $table->visit($visitor); + } + foreach ($this->_sequences as $sequence) { + $sequence->visit($visitor); + } + } + + /** + * Cloning a Schema triggers a deep clone of all related assets. + * + * @return void + */ + public function __clone() + { + foreach ($this->_tables as $k => $table) { + $this->_tables[$k] = clone $table; + } + foreach ($this->_sequences as $k => $sequence) { + $this->_sequences[$k] = clone $sequence; + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php new file mode 100644 index 00000000..4b054d45 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php @@ -0,0 +1,119 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Configuration for a Schema + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class SchemaConfig +{ + /** + * @var bool + */ + protected $hasExplicitForeignKeyIndexes = false; + + /** + * @var int + */ + protected $maxIdentifierLength = 63; + + /** + * @var string + */ + protected $name; + + /** + * @var array + */ + protected $defaultTableOptions = array(); + + /** + * @return bool + */ + public function hasExplicitForeignKeyIndexes() + { + return $this->hasExplicitForeignKeyIndexes; + } + + /** + * @param bool $flag + */ + public function setExplicitForeignKeyIndexes($flag) + { + $this->hasExplicitForeignKeyIndexes = (bool)$flag; + } + + /** + * @param int $length + */ + public function setMaxIdentifierLength($length) + { + $this->maxIdentifierLength = (int)$length; + } + + /** + * @return int + */ + public function getMaxIdentifierLength() + { + return $this->maxIdentifierLength; + } + + /** + * Get default namespace of schema objects. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * set default namespace name of schema objects. + * + * @param string $name the value to set. + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Get the default options that are passed to Table instances created with + * Schema#createTable(). + * + * @return array + */ + public function getDefaultTableOptions() + { + return $this->defaultTableOptions; + } + + public function setDefaultTableOptions(array $defaultTableOptions) + { + $this->defaultTableOptions = $defaultTableOptions; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php new file mode 100644 index 00000000..5cb5bc6a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php @@ -0,0 +1,176 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use \Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Schema Diff + * + * + * @link www.doctrine-project.org + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class SchemaDiff +{ + /** + * All added tables + * + * @var array(string=>ezcDbSchemaTable) + */ + public $newTables = array(); + + /** + * All changed tables + * + * @var array(string=>ezcDbSchemaTableDiff) + */ + public $changedTables = array(); + + /** + * All removed tables + * + * @var array(string=>Table) + */ + public $removedTables = array(); + + /** + * @var array + */ + public $newSequences = array(); + + /** + * @var array + */ + public $changedSequences = array(); + + /** + * @var array + */ + public $removedSequences = array(); + + /** + * @var array + */ + public $orphanedForeignKeys = array(); + + /** + * Constructs an SchemaDiff object. + * + * @param array(string=>Table) $newTables + * @param array(string=>TableDiff) $changedTables + * @param array(string=>bool) $removedTables + */ + public function __construct($newTables = array(), $changedTables = array(), $removedTables = array()) + { + $this->newTables = $newTables; + $this->changedTables = $changedTables; + $this->removedTables = $removedTables; + } + + /** + * The to save sql mode ensures that the following things don't happen: + * + * 1. Tables are deleted + * 2. Sequences are deleted + * 3. Foreign Keys which reference tables that would otherwise be deleted. + * + * This way it is ensured that assets are deleted which might not be relevant to the metadata schema at all. + * + * @param AbstractPlatform $platform + * @return array + */ + public function toSaveSql(AbstractPlatform $platform) + { + return $this->_toSql($platform, true); + } + + /** + * @param AbstractPlatform $platform + * @return array + */ + public function toSql(AbstractPlatform $platform) + { + return $this->_toSql($platform, false); + } + + /** + * @param AbstractPlatform $platform + * @param bool $saveMode + * @return array + */ + protected function _toSql(AbstractPlatform $platform, $saveMode = false) + { + $sql = array(); + + if ($platform->supportsForeignKeyConstraints() && $saveMode == false) { + foreach ($this->orphanedForeignKeys as $orphanedForeignKey) { + $sql[] = $platform->getDropForeignKeySQL($orphanedForeignKey, $orphanedForeignKey->getLocalTableName()); + } + } + + if ($platform->supportsSequences() == true) { + foreach ($this->changedSequences as $sequence) { + $sql[] = $platform->getAlterSequenceSQL($sequence); + } + + if ($saveMode === false) { + foreach ($this->removedSequences as $sequence) { + $sql[] = $platform->getDropSequenceSQL($sequence); + } + } + + foreach ($this->newSequences as $sequence) { + $sql[] = $platform->getCreateSequenceSQL($sequence); + } + } + + $foreignKeySql = array(); + foreach ($this->newTables as $table) { + $sql = array_merge( + $sql, + $platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES) + ); + + if ($platform->supportsForeignKeyConstraints()) { + foreach ($table->getForeignKeys() as $foreignKey) { + $foreignKeySql[] = $platform->getCreateForeignKeySQL($foreignKey, $table); + } + } + } + $sql = array_merge($sql, $foreignKeySql); + + if ($saveMode === false) { + foreach ($this->removedTables as $table) { + $sql[] = $platform->getDropTableSQL($table); + } + } + + foreach ($this->changedTables as $tableDiff) { + $sql = array_merge($sql, $platform->getAlterTableSQL($tableDiff)); + } + + return $sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaException.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaException.php new file mode 100644 index 00000000..a8cb93d7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaException.php @@ -0,0 +1,126 @@ +getName()." requires a named foreign key, ". + "but the given foreign key from (".implode(", ", $foreignKey->getColumns()).") onto foreign table ". + "'".$foreignKey->getForeignTableName()."' (".implode(", ", $foreignKey->getForeignColumns()).") is currently ". + "unnamed." + ); + } + + static public function alterTableChangeNotSupported($changeName) { + return new self ("Alter table change not supported, given '$changeName'"); + } +} \ No newline at end of file diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php new file mode 100644 index 00000000..b4fb39c0 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php @@ -0,0 +1,120 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Sequence Structure + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Sequence extends AbstractAsset +{ + /** + * @var int + */ + protected $_allocationSize = 1; + + /** + * @var int + */ + protected $_initialValue = 1; + + /** + * + * @param string $name + * @param int $allocationSize + * @param int $initialValue + */ + public function __construct($name, $allocationSize=1, $initialValue=1) + { + $this->_setName($name); + $this->_allocationSize = (is_numeric($allocationSize))?$allocationSize:1; + $this->_initialValue = (is_numeric($initialValue))?$initialValue:1; + } + + public function getAllocationSize() + { + return $this->_allocationSize; + } + + public function getInitialValue() + { + return $this->_initialValue; + } + + public function setAllocationSize($allocationSize) + { + $this->_allocationSize = (is_numeric($allocationSize))?$allocationSize:1; + } + + public function setInitialValue($initialValue) + { + $this->_initialValue = (is_numeric($initialValue))?$initialValue:1; + } + + /** + * Check if this sequence is an autoincrement sequence for a given table. + * + * This is used inside the comparator to not report sequences as missing, + * when the "from" schema implicitly creates the sequences. + * + * @param Table $table + * + * @return bool + */ + public function isAutoIncrementsFor(Table $table) + { + if ( ! $table->hasPrimaryKey()) { + return false; + } + + $pkColumns = $table->getPrimaryKey()->getColumns(); + + if (count($pkColumns) != 1) { + return false; + } + + $column = $table->getColumn($pkColumns[0]); + + if ( ! $column->getAutoincrement()) { + return false; + } + + $sequenceName = $this->getShortestName($table->getNamespaceName()); + $tableName = $table->getShortestName($table->getNamespaceName()); + $tableSequenceName = sprintf('%s_%s_seq', $tableName, $pkColumns[0]); + + return $tableSequenceName === $sequenceName; + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptSequence($this); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php new file mode 100644 index 00000000..41a941d5 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -0,0 +1,190 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * SqliteSchemaManager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Jonathan H. Wage + * @version $Revision$ + * @since 2.0 + */ +class SqliteSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritdoc} + * + * @override + */ + public function dropDatabase($database) + { + if (file_exists($database)) { + unlink($database); + } + } + + /** + * {@inheritdoc} + * + * @override + */ + public function createDatabase($database) + { + $params = $this->_conn->getParams(); + $driver = $params['driver']; + $options = array( + 'driver' => $driver, + 'path' => $database + ); + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $conn->connect(); + $conn->close(); + } + + protected function _getPortableTableDefinition($table) + { + return $table['name']; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $indexBuffer = array(); + + // fetch primary + $stmt = $this->_conn->executeQuery( "PRAGMA TABLE_INFO ('$tableName')" ); + $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC); + foreach($indexArray as $indexColumnRow) { + if($indexColumnRow['pk'] == "1") { + $indexBuffer[] = array( + 'key_name' => 'primary', + 'primary' => true, + 'non_unique' => false, + 'column_name' => $indexColumnRow['name'] + ); + } + } + + // fetch regular indexes + foreach($tableIndexes as $tableIndex) { + // Ignore indexes with reserved names, e.g. autoindexes + if (strpos($tableIndex['name'], 'sqlite_') !== 0) { + $keyName = $tableIndex['name']; + $idx = array(); + $idx['key_name'] = $keyName; + $idx['primary'] = false; + $idx['non_unique'] = $tableIndex['unique']?false:true; + + $stmt = $this->_conn->executeQuery( "PRAGMA INDEX_INFO ( '{$keyName}' )" ); + $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + foreach ( $indexArray as $indexColumnRow ) { + $idx['column_name'] = $indexColumnRow['name']; + $indexBuffer[] = $idx; + } + } + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + protected function _getPortableTableIndexDefinition($tableIndex) + { + return array( + 'name' => $tableIndex['name'], + 'unique' => (bool) $tableIndex['unique'] + ); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $e = explode('(', $tableColumn['type']); + $tableColumn['type'] = $e[0]; + if (isset($e[1])) { + $length = trim($e[1], ')'); + $tableColumn['length'] = $length; + } + + $dbType = strtolower($tableColumn['type']); + $length = isset($tableColumn['length']) ? $tableColumn['length'] : null; + $unsigned = (boolean) isset($tableColumn['unsigned']) ? $tableColumn['unsigned'] : false; + $fixed = false; + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $default = $tableColumn['dflt_value']; + if ($default == 'NULL') { + $default = null; + } + if ($default !== null) { + // SQLite returns strings wrapped in single quotes, so we need to strip them + $default = preg_replace("/^'(.*)'$/", '\1', $default); + } + $notnull = (bool) $tableColumn['notnull']; + + if ( ! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + + switch ($dbType) { + case 'char': + $fixed = true; + break; + case 'float': + case 'double': + case 'real': + case 'decimal': + case 'numeric': + if (isset($tableColumn['length'])) { + list($precision, $scale) = array_map('trim', explode(', ', $tableColumn['length'])); + } + $length = null; + break; + } + + $options = array( + 'length' => $length, + 'unsigned' => (bool) $unsigned, + 'fixed' => $fixed, + 'notnull' => $notnull, + 'default' => $default, + 'precision' => $precision, + 'scale' => $scale, + 'autoincrement' => false, + ); + + return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableViewDefinition($view) + { + return new View($view['name'], $view['sql']); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php new file mode 100644 index 00000000..2ee743a9 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Synchronizer; + +use Doctrine\DBAL\Connection; + +/** + * Abstract schema synchronizer with methods for executing batches of SQL. + */ +abstract class AbstractSchemaSynchronizer implements SchemaSynchronizer +{ + /** + * @var Connection + */ + protected $conn; + + public function __construct(Connection $conn) + { + $this->conn = $conn; + } + + protected function processSqlSafely(array $sql) + { + foreach ($sql as $s) { + try { + $this->conn->exec($s); + } catch(\Exception $e) { + + } + } + } + + protected function processSql(array $sql) + { + foreach ($sql as $s) { + $this->conn->exec($s); + } + } + +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php new file mode 100644 index 00000000..f932f20d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Synchronizer; + +use Doctrine\DBAL\Schema\Schema; + +/** + * The synchronizer knows how to synchronize a schema with the configured + * database. + * + * @author Benjamin Eberlei + */ +interface SchemaSynchronizer +{ + /** + * Get the SQL statements that can be executed to create the schema. + * + * @param Schema $createSchema + * @return array + */ + function getCreateSchema(Schema $createSchema); + + /** + * Get the SQL Statements to update given schema with the underlying db. + * + * @param Schema $toSchema + * @param bool $noDrops + * @return array + */ + function getUpdateSchema(Schema $toSchema, $noDrops = false); + + /** + * Get the SQL Statements to drop the given schema from underlying db. + * + * @param Schema $dropSchema + * @return array + */ + function getDropSchema(Schema $dropSchema); + + /** + * Get the SQL statements to drop all schema assets from underlying db. + * + * @return array + */ + function getDropAllSchema(); + + /** + * Create the Schema + * + * @param Schema $createSchema + * @return void + */ + function createSchema(Schema $createSchema); + + /** + * Update the Schema to new schema version. + * + * @param Schema $toSchema + * @param bool $noDrops + * @return void + */ + function updateSchema(Schema $toSchema, $noDrops = false); + + /** + * Drop the given database schema from the underlying db. + * + * @param Schema $dropSchema + * @return void + */ + function dropSchema(Schema $dropSchema); + + /** + * Drop all assets from the underyling db. + * + * @return void + */ + function dropAllSchema(); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php new file mode 100644 index 00000000..a090ecd8 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php @@ -0,0 +1,196 @@ +. + */ +namespace Doctrine\DBAL\Schema\Synchronizer; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Comparator; +use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector; + +/** + * Schema Synchronizer for Default DBAL Connection + * + * @author Benjamin Eberlei + */ +class SingleDatabaseSynchronizer extends AbstractSchemaSynchronizer +{ + /** + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $platform; + + public function __construct(Connection $conn) + { + parent::__construct($conn); + $this->platform = $conn->getDatabasePlatform(); + } + + /** + * Get the SQL statements that can be executed to create the schema. + * + * @param Schema $createSchema + * @return array + */ + public function getCreateSchema(Schema $createSchema) + { + return $createSchema->toSql($this->platform); + } + + /** + * Get the SQL Statements to update given schema with the underlying db. + * + * @param Schema $toSchema + * @param bool $noDrops + * @return array + */ + public function getUpdateSchema(Schema $toSchema, $noDrops = false) + { + $comparator = new Comparator(); + $sm = $this->conn->getSchemaManager(); + + $fromSchema = $sm->createSchema(); + $schemaDiff = $comparator->compare($fromSchema, $toSchema); + + if ($noDrops) { + return $schemaDiff->toSaveSql($this->platform); + } + + return $schemaDiff->toSql($this->platform); + } + + /** + * Get the SQL Statements to drop the given schema from underlying db. + * + * @param Schema $dropSchema + * @return array + */ + public function getDropSchema(Schema $dropSchema) + { + $visitor = new DropSchemaSqlCollector($this->platform); + $sm = $this->conn->getSchemaManager(); + + $fullSchema = $sm->createSchema(); + + foreach ($fullSchema->getTables() as $table) { + if ( $dropSchema->hasTable($table->getName())) { + $visitor->acceptTable($table); + } + + foreach ($table->getForeignKeys() as $foreignKey) { + if ( ! $dropSchema->hasTable($table->getName())) { + continue; + } + + if ( ! $dropSchema->hasTable($foreignKey->getForeignTableName())) { + continue; + } + + $visitor->acceptForeignKey($table, $foreignKey); + } + } + + if ( ! $this->platform->supportsSequences()) { + return $visitor->getQueries(); + } + + foreach ($dropSchema->getSequences() as $sequence) { + $visitor->acceptSequence($sequence); + } + + foreach ($dropSchema->getTables() as $table) { + /* @var $sequence Table */ + if ( ! $table->hasPrimaryKey()) { + continue; + } + + $columns = $table->getPrimaryKey()->getColumns(); + if (count($columns) > 1) { + continue; + } + + $checkSequence = $table->getName() . "_" . $columns[0] . "_seq"; + if ($fullSchema->hasSequence($checkSequence)) { + $visitor->acceptSequence($fullSchema->getSequence($checkSequence)); + } + } + + return $visitor->getQueries(); + } + + /** + * Get the SQL statements to drop all schema assets from underlying db. + * + * @return array + */ + public function getDropAllSchema() + { + $sm = $this->conn->getSchemaManager(); + $visitor = new DropSchemaSqlCollector($this->platform); + + /* @var $schema \Doctrine\DBAL\Schema\Schema */ + $schema = $sm->createSchema(); + $schema->visit($visitor); + + return $visitor->getQueries(); + } + + /** + * Create the Schema + * + * @param Schema $createSchema + * @return void + */ + public function createSchema(Schema $createSchema) + { + $this->processSql($this->getCreateSchema($createSchema)); + } + + /** + * Update the Schema to new schema version. + * + * @param Schema $toSchema + * @param bool $noDrops + * @return void + */ + public function updateSchema(Schema $toSchema, $noDrops = false) + { + $this->processSql($this->getUpdateSchema($toSchema, $noDrops)); + } + + /** + * Drop the given database schema from the underlying db. + * + * @param Schema $dropSchema + * @return void + */ + public function dropSchema(Schema $dropSchema) + { + $this->processSqlSafely($this->getDropSchema($dropSchema)); + } + + /** + * Drop all assets from the underyling db. + * + * @return void + */ + public function dropAllSchema() + { + $this->processSql($this->getDropAllSchema()); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php new file mode 100644 index 00000000..a7047b78 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php @@ -0,0 +1,678 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Schema\Visitor\Visitor; +use Doctrine\DBAL\DBALException; + +/** + * Object Representation of a table + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Table extends AbstractAsset +{ + /** + * @var string + */ + protected $_name = null; + + /** + * @var array + */ + protected $_columns = array(); + + /** + * @var array + */ + protected $_indexes = array(); + + /** + * @var string + */ + protected $_primaryKeyName = false; + + /** + * @var array + */ + protected $_fkConstraints = array(); + + /** + * @var array + */ + protected $_options = array(); + + /** + * @var SchemaConfig + */ + protected $_schemaConfig = null; + + /** + * + * @param string $tableName + * @param array $columns + * @param array $indexes + * @param array $fkConstraints + * @param int $idGeneratorType + * @param array $options + */ + public function __construct($tableName, array $columns=array(), array $indexes=array(), array $fkConstraints=array(), $idGeneratorType = 0, array $options=array()) + { + if (strlen($tableName) == 0) { + throw DBALException::invalidTableName($tableName); + } + + $this->_setName($tableName); + $this->_idGeneratorType = $idGeneratorType; + + foreach ($columns as $column) { + $this->_addColumn($column); + } + + foreach ($indexes as $idx) { + $this->_addIndex($idx); + } + + foreach ($fkConstraints as $constraint) { + $this->_addForeignKeyConstraint($constraint); + } + + $this->_options = $options; + } + + /** + * @param SchemaConfig $schemaConfig + */ + public function setSchemaConfig(SchemaConfig $schemaConfig) + { + $this->_schemaConfig = $schemaConfig; + } + + /** + * @return int + */ + protected function _getMaxIdentifierLength() + { + if ($this->_schemaConfig instanceof SchemaConfig) { + return $this->_schemaConfig->getMaxIdentifierLength(); + } else { + return 63; + } + } + + /** + * Set Primary Key + * + * @param array $columns + * @param string $indexName + * @return Table + */ + public function setPrimaryKey(array $columns, $indexName = false) + { + $primaryKey = $this->_createIndex($columns, $indexName ?: "primary", true, true); + + foreach ($columns as $columnName) { + $column = $this->getColumn($columnName); + $column->setNotnull(true); + } + + return $primaryKey; + } + + /** + * @param array $columnNames + * @param string $indexName + * @return Table + */ + public function addIndex(array $columnNames, $indexName = null) + { + if($indexName == null) { + $indexName = $this->_generateIdentifierName( + array_merge(array($this->getName()), $columnNames), "idx", $this->_getMaxIdentifierLength() + ); + } + + return $this->_createIndex($columnNames, $indexName, false, false); + } + + /** + * Drop an index from this table. + * + * @param string $indexName + * @return void + */ + public function dropPrimaryKey() + { + $this->dropIndex($this->_primaryKeyName); + $this->_primaryKeyName = false; + } + + /** + * Drop an index from this table. + * + * @param string $indexName + * @return void + */ + public function dropIndex($indexName) + { + $indexName = strtolower($indexName); + if ( ! $this->hasIndex($indexName)) { + throw SchemaException::indexDoesNotExist($indexName, $this->_name); + } + unset($this->_indexes[$indexName]); + } + + /** + * + * @param array $columnNames + * @param string $indexName + * @return Table + */ + public function addUniqueIndex(array $columnNames, $indexName = null) + { + if ($indexName === null) { + $indexName = $this->_generateIdentifierName( + array_merge(array($this->getName()), $columnNames), "uniq", $this->_getMaxIdentifierLength() + ); + } + + return $this->_createIndex($columnNames, $indexName, true, false); + } + + /** + * Check if an index begins in the order of the given columns. + * + * @param array $columnsNames + * @return bool + */ + public function columnsAreIndexed(array $columnsNames) + { + foreach ($this->getIndexes() as $index) { + /* @var $index Index */ + if ($index->spansColumns($columnsNames)) { + return true; + } + } + return false; + } + + /** + * + * @param array $columnNames + * @param string $indexName + * @param bool $isUnique + * @param bool $isPrimary + * @return Table + */ + private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary) + { + if (preg_match('(([^a-zA-Z0-9_]+))', $indexName)) { + throw SchemaException::indexNameInvalid($indexName); + } + + foreach ($columnNames as $columnName => $indexColOptions) { + if (is_numeric($columnName) && is_string($indexColOptions)) { + $columnName = $indexColOptions; + } + + if ( ! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + $this->_addIndex(new Index($indexName, $columnNames, $isUnique, $isPrimary)); + return $this; + } + + /** + * @param string $columnName + * @param string $columnType + * @param array $options + * @return Column + */ + public function addColumn($columnName, $typeName, array $options=array()) + { + $column = new Column($columnName, Type::getType($typeName), $options); + + $this->_addColumn($column); + return $column; + } + + /** + * Rename Column + * + * @param string $oldColumnName + * @param string $newColumnName + * @return Table + */ + public function renameColumn($oldColumnName, $newColumnName) + { + throw new DBALException("Table#renameColumn() was removed, because it drops and recreates " . + "the column instead. There is no fix available, because a schema diff cannot reliably detect if a " . + "column was renamed or one column was created and another one dropped."); + } + + /** + * Change Column Details + * + * @param string $columnName + * @param array $options + * @return Table + */ + public function changeColumn($columnName, array $options) + { + $column = $this->getColumn($columnName); + $column->setOptions($options); + return $this; + } + + /** + * Drop Column from Table + * + * @param string $columnName + * @return Table + */ + public function dropColumn($columnName) + { + $columnName = strtolower($columnName); + $column = $this->getColumn($columnName); + unset($this->_columns[$columnName]); + return $this; + } + + + /** + * Add a foreign key constraint + * + * Name is inferred from the local columns + * + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @param string $constraintName + * @return Table + */ + public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array(), $constraintName = null) + { + $constraintName = $constraintName ?: $this->_generateIdentifierName(array_merge((array)$this->getName(), $localColumnNames), "fk", $this->_getMaxIdentifierLength()); + return $this->addNamedForeignKeyConstraint($constraintName, $foreignTable, $localColumnNames, $foreignColumnNames, $options); + } + + /** + * Add a foreign key constraint + * + * Name is to be generated by the database itsself. + * + * @deprecated Use {@link addForeignKeyConstraint} + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @return Table + */ + public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array()) + { + return $this->addForeignKeyConstraint($foreignTable, $localColumnNames, $foreignColumnNames, $options); + } + + /** + * Add a foreign key constraint with a given name + * + * @deprecated Use {@link addForeignKeyConstraint} + * @param string $name + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @return Table + */ + public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array()) + { + if ($foreignTable instanceof Table) { + $foreignTableName = $foreignTable->getName(); + + foreach ($foreignColumnNames as $columnName) { + if ( ! $foreignTable->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName()); + } + } + } else { + $foreignTableName = $foreignTable; + } + + foreach ($localColumnNames as $columnName) { + if ( ! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + + $constraint = new ForeignKeyConstraint( + $localColumnNames, $foreignTableName, $foreignColumnNames, $name, $options + ); + $this->_addForeignKeyConstraint($constraint); + + return $this; + } + + /** + * @param string $name + * @param string $value + * @return Table + */ + public function addOption($name, $value) + { + $this->_options[$name] = $value; + return $this; + } + + /** + * @param Column $column + */ + protected function _addColumn(Column $column) + { + $columnName = $column->getName(); + $columnName = strtolower($columnName); + + if (isset($this->_columns[$columnName])) { + throw SchemaException::columnAlreadyExists($this->getName(), $columnName); + } + + $this->_columns[$columnName] = $column; + } + + /** + * Add index to table + * + * @param Index $indexCandidate + * @return Table + */ + protected function _addIndex(Index $indexCandidate) + { + // check for duplicates + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFullfilledBy($existingIndex)) { + return $this; + } + } + + $indexName = $indexCandidate->getName(); + $indexName = strtolower($indexName); + + if (isset($this->_indexes[$indexName]) || ($this->_primaryKeyName != false && $indexCandidate->isPrimary())) { + throw SchemaException::indexAlreadyExists($indexName, $this->_name); + } + + // remove overruled indexes + foreach ($this->_indexes as $idxKey => $existingIndex) { + if ($indexCandidate->overrules($existingIndex)) { + unset($this->_indexes[$idxKey]); + } + } + + if ($indexCandidate->isPrimary()) { + $this->_primaryKeyName = $indexName; + } + + $this->_indexes[$indexName] = $indexCandidate; + return $this; + } + + /** + * @param ForeignKeyConstraint $constraint + */ + protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) + { + $constraint->setLocalTable($this); + + if(strlen($constraint->getName())) { + $name = $constraint->getName(); + } else { + $name = $this->_generateIdentifierName( + array_merge((array)$this->getName(), $constraint->getLocalColumns()), "fk", $this->_getMaxIdentifierLength() + ); + } + $name = strtolower($name); + + $this->_fkConstraints[$name] = $constraint; + // add an explicit index on the foreign key columns. If there is already an index that fullfils this requirements drop the request. + // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes + // lead to duplicates. This creates compuation overhead in this case, however no duplicate indexes are ever added (based on columns). + $this->addIndex($constraint->getColumns()); + } + + /** + * Does Table have a foreign key constraint with the given name? + * * + * @param string $constraintName + * @return bool + */ + public function hasForeignKey($constraintName) + { + $constraintName = strtolower($constraintName); + return isset($this->_fkConstraints[$constraintName]); + } + + /** + * @param string $constraintName + * @return ForeignKeyConstraint + */ + public function getForeignKey($constraintName) + { + $constraintName = strtolower($constraintName); + if(!$this->hasForeignKey($constraintName)) { + throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name); + } + + return $this->_fkConstraints[$constraintName]; + } + + public function removeForeignKey($constraintName) + { + $constraintName = strtolower($constraintName); + if(!$this->hasForeignKey($constraintName)) { + throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name); + } + + unset($this->_fkConstraints[$constraintName]); + } + + /** + * @return Column[] + */ + public function getColumns() + { + $columns = $this->_columns; + + $pkCols = array(); + $fkCols = array(); + + if ($this->hasPrimaryKey()) { + $pkCols = $this->getPrimaryKey()->getColumns(); + } + foreach ($this->getForeignKeys() as $fk) { + /* @var $fk ForeignKeyConstraint */ + $fkCols = array_merge($fkCols, $fk->getColumns()); + } + $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns))); + + uksort($columns, function($a, $b) use($colNames) { + return (array_search($a, $colNames) >= array_search($b, $colNames)); + }); + return $columns; + } + + + /** + * Does this table have a column with the given name? + * + * @param string $columnName + * @return bool + */ + public function hasColumn($columnName) + { + $columnName = $this->trimQuotes(strtolower($columnName)); + return isset($this->_columns[$columnName]); + } + + /** + * Get a column instance + * + * @param string $columnName + * @return Column + */ + public function getColumn($columnName) + { + $columnName = strtolower($this->trimQuotes($columnName)); + if ( ! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + + return $this->_columns[$columnName]; + } + + /** + * @return Index|null + */ + public function getPrimaryKey() + { + if ( ! $this->hasPrimaryKey()) { + return null; + } + return $this->getIndex($this->_primaryKeyName); + } + + public function getPrimaryKeyColumns() + { + if ( ! $this->hasPrimaryKey()) { + throw new DBALException("Table " . $this->getName() . " has no primary key."); + } + return $this->getPrimaryKey()->getColumns(); + } + + /** + * Check if this table has a primary key. + * + * @return bool + */ + public function hasPrimaryKey() + { + return ($this->_primaryKeyName && $this->hasIndex($this->_primaryKeyName)); + } + + /** + * @param string $indexName + * @return bool + */ + public function hasIndex($indexName) + { + $indexName = strtolower($indexName); + return (isset($this->_indexes[$indexName])); + } + + /** + * @param string $indexName + * @return Index + */ + public function getIndex($indexName) + { + $indexName = strtolower($indexName); + if ( ! $this->hasIndex($indexName)) { + throw SchemaException::indexDoesNotExist($indexName, $this->_name); + } + return $this->_indexes[$indexName]; + } + + /** + * @return array + */ + public function getIndexes() + { + return $this->_indexes; + } + + /** + * Get Constraints + * + * @return array + */ + public function getForeignKeys() + { + return $this->_fkConstraints; + } + + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + public function getOption($name) + { + return $this->_options[$name]; + } + + public function getOptions() + { + return $this->_options; + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptTable($this); + + foreach ($this->getColumns() as $column) { + $visitor->acceptColumn($this, $column); + } + + foreach ($this->getIndexes() as $index) { + $visitor->acceptIndex($this, $index); + } + + foreach ($this->getForeignKeys() as $constraint) { + $visitor->acceptForeignKey($this, $constraint); + } + } + + /** + * Clone of a Table triggers a deep clone of all affected assets + */ + public function __clone() + { + foreach ($this->_columns as $k => $column) { + $this->_columns[$k] = clone $column; + } + foreach ($this->_indexes as $k => $index) { + $this->_indexes[$k] = clone $index; + } + foreach ($this->_fkConstraints as $k => $fk) { + $this->_fkConstraints[$k] = clone $fk; + $this->_fkConstraints[$k]->setLocalTable($this); + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php new file mode 100644 index 00000000..f5604683 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php @@ -0,0 +1,136 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Table Diff + * + * + * @link www.doctrine-project.org + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @since 2.0 + * @author Benjamin Eberlei + */ +class TableDiff +{ + /** + * @var string + */ + public $name = null; + + /** + * @var string + */ + public $newName = false; + + /** + * All added fields + * + * @var array(string=>Column) + */ + public $addedColumns; + + /** + * All changed fields + * + * @var array(string=>Column) + */ + public $changedColumns = array(); + + /** + * All removed fields + * + * @var array(string=>Column) + */ + public $removedColumns = array(); + + /** + * Columns that are only renamed from key to column instance name. + * + * @var array(string=>Column) + */ + public $renamedColumns = array(); + + /** + * All added indexes + * + * @var array(string=>Index) + */ + public $addedIndexes = array(); + + /** + * All changed indexes + * + * @var array(string=>Index) + */ + public $changedIndexes = array(); + + /** + * All removed indexes + * + * @var array(string=>bool) + */ + public $removedIndexes = array(); + + /** + * All added foreign key definitions + * + * @var array + */ + public $addedForeignKeys = array(); + + /** + * All changed foreign keys + * + * @var array + */ + public $changedForeignKeys = array(); + + /** + * All removed foreign keys + * + * @var array + */ + public $removedForeignKeys = array(); + + /** + * Constructs an TableDiff object. + * + * @param array(string=>Column) $addedColumns + * @param array(string=>Column) $changedColumns + * @param array(string=>bool) $removedColumns + * @param array(string=>Index) $addedIndexes + * @param array(string=>Index) $changedIndexes + * @param array(string=>bool) $removedIndexes + */ + public function __construct($tableName, $addedColumns = array(), + $changedColumns = array(), $removedColumns = array(), $addedIndexes = array(), + $changedIndexes = array(), $removedIndexes = array()) + { + $this->name = $tableName; + $this->addedColumns = $addedColumns; + $this->changedColumns = $changedColumns; + $this->removedColumns = $removedColumns; + $this->addedIndexes = $addedIndexes; + $this->changedIndexes = $changedIndexes; + $this->removedIndexes = $removedIndexes; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/View.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/View.php new file mode 100644 index 00000000..8283d07d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/View.php @@ -0,0 +1,53 @@ +. +*/ + +namespace Doctrine\DBAL\Schema; + +/** + * Representation of a Database View + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class View extends AbstractAsset +{ + /** + * @var string + */ + private $_sql; + + public function __construct($name, $sql) + { + $this->_setName($name); + $this->_sql = $sql; + } + + /** + * @return string + */ + public function getSql() + { + return $this->_sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php new file mode 100644 index 00000000..51228c18 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php @@ -0,0 +1,178 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +class CreateSchemaSqlCollector implements Visitor +{ + /** + * @var array + */ + private $_createTableQueries = array(); + + /** + * @var array + */ + private $_createSequenceQueries = array(); + + /** + * @var array + */ + private $_createFkConstraintQueries = array(); + + /** + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @param AbstractPlatform $platform + */ + public function __construct(AbstractPlatform $platform) + { + $this->_platform = $platform; + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + + } + + /** + * Generate DDL Statements to create the accepted table with all its dependencies. + * + * @param Table $table + */ + public function acceptTable(Table $table) + { + $namespace = $this->getNamespace($table); + + $this->_createTableQueries[$namespace] = array_merge( + $this->_createTableQueries[$namespace], + $this->_platform->getCreateTableSQL($table) + ); + } + + public function acceptColumn(Table $table, Column $column) + { + + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + $namespace = $this->getNamespace($localTable); + + if ($this->_platform->supportsForeignKeyConstraints()) { + $this->_createFkConstraintQueries[$namespace] = array_merge( + $this->_createFkConstraintQueries[$namespace], + (array) $this->_platform->getCreateForeignKeySQL( + $fkConstraint, $localTable + ) + ); + } + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + $namespace = $this->getNamespace($sequence); + + $this->_createSequenceQueries[$namespace] = array_merge( + $this->_createSequenceQueries[$namespace], + (array)$this->_platform->getCreateSequenceSQL($sequence) + ); + } + + private function getNamespace($asset) + { + $namespace = $asset->getNamespaceName() ?: 'default'; + if ( !isset($this->_createTableQueries[$namespace])) { + $this->_createTableQueries[$namespace] = array(); + $this->_createSequenceQueries[$namespace] = array(); + $this->_createFkConstraintQueries[$namespace] = array(); + } + + return $namespace; + } + + /** + * @return array + */ + public function resetQueries() + { + $this->_createTableQueries = array(); + $this->_createSequenceQueries = array(); + $this->_createFkConstraintQueries = array(); + } + + /** + * Get all queries collected so far. + * + * @return array + */ + public function getQueries() + { + $sql = array(); + foreach (array_keys($this->_createTableQueries) as $namespace) { + if ($this->_platform->supportsSchemas()) { + // TODO: Create Schema here + } + } + foreach ($this->_createTableQueries as $schemaSql) { + $sql = array_merge($sql, $schemaSql); + } + foreach ($this->_createSequenceQueries as $schemaSql) { + $sql = array_merge($sql, $schemaSql); + } + foreach ($this->_createFkConstraintQueries as $schemaSql) { + $sql = array_merge($sql, $schemaSql); + } + return $sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php new file mode 100644 index 00000000..6edb7989 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php @@ -0,0 +1,160 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\SchemaException, + Doctrine\DBAL\Schema\Index; + +/** + * Gather SQL statements that allow to completly drop the current schema. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + */ +class DropSchemaSqlCollector implements Visitor +{ + /** + * @var \SplObjectStorage + */ + private $constraints; + + /** + * @var \SplObjectStorage + */ + private $sequences; + + /** + * @var \SplObjectStorage + */ + private $tables; + + /** + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $platform; + + /** + * @param AbstractPlatform $platform + */ + public function __construct(AbstractPlatform $platform) + { + $this->platform = $platform; + $this->clearQueries(); + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + + } + + /** + * @param Table $table + */ + public function acceptTable(Table $table) + { + $this->tables->attach($table); + } + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column) + { + + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + if (strlen($fkConstraint->getName()) == 0) { + throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint); + } + + $this->constraints->attach($fkConstraint); + $this->constraints[$fkConstraint] = $localTable; + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + $this->sequences->attach($sequence); + } + + /** + * @return void + */ + public function clearQueries() + { + $this->constraints = new \SplObjectStorage(); + $this->sequences = new \SplObjectStorage(); + $this->tables = new \SplObjectStorage(); + } + + /** + * @return array + */ + public function getQueries() + { + $sql = array(); + foreach ($this->constraints as $fkConstraint) { + $localTable = $this->constraints[$fkConstraint]; + $sql[] = $this->platform->getDropForeignKeySQL($fkConstraint, $localTable); + } + + foreach ($this->sequences as $sequence) { + $sql[] = $this->platform->getDropSequenceSQL($sequence); + } + + foreach ($this->tables as $table) { + $sql[] = $this->platform->getDropTableSQL($table); + } + + return $sql; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Graphviz.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Graphviz.php new file mode 100644 index 00000000..8ae69a2f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Graphviz.php @@ -0,0 +1,151 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +class Graphviz implements \Doctrine\DBAL\Schema\Visitor\Visitor +{ + private $output = ''; + + public function acceptColumn(Table $table, Column $column) + { + + } + + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + $this->output .= $this->createNodeRelation( + $fkConstraint->getLocalTableName() . ":col" . current($fkConstraint->getLocalColumns()).":se", + $fkConstraint->getForeignTableName() . ":col" . current($fkConstraint->getForeignColumns()).":se", + array( + 'dir' => 'back', + 'arrowtail' => 'dot', + 'arrowhead' => 'normal', + ) + ); + } + + public function acceptIndex(Table $table, Index $index) + { + + } + + public function acceptSchema(Schema $schema) + { + $this->output = 'digraph "' . sha1( mt_rand() ) . '" {' . "\n"; + $this->output .= 'splines = true;' . "\n"; + $this->output .= 'overlap = false;' . "\n"; + $this->output .= 'outputorder=edgesfirst;'."\n"; + $this->output .= 'mindist = 0.6;' . "\n"; + $this->output .= 'sep = .2;' . "\n"; + } + + public function acceptSequence(Sequence $sequence) + { + + } + + public function acceptTable(Table $table) + { + $this->output .= $this->createNode( + $table->getName(), + array( + 'label' => $this->createTableLabel( $table ), + 'shape' => 'plaintext', + ) + ); + } + + private function createTableLabel( Table $table ) + { + // Start the table + $label = '<'; + + // The title + $label .= ''; + + // The attributes block + foreach( $table->getColumns() as $column ) { + $columnLabel = $column->getName(); + + $label .= ''; + $label .= ''; + $label .= ''; + } + + // End the table + $label .= '
' . $table->getName() . '
'; + $label .= '' . $columnLabel . ''; + $label .= '' . strtolower($column->getType()) . ''; + if ($table->hasPrimaryKey() && in_array($column->getName(), $table->getPrimaryKey()->getColumns())) { + $label .= "\xe2\x9c\xb7"; + } + $label .= '
>'; + + return $label; + } + + private function createNode( $name, $options ) + { + $node = $name . " ["; + foreach( $options as $key => $value ) + { + $node .= $key . '=' . $value . ' '; + } + $node .= "]\n"; + return $node; + } + + private function createNodeRelation( $node1, $node2, $options ) + { + $relation = $node1 . ' -> ' . $node2 . ' ['; + foreach( $options as $key => $value ) + { + $relation .= $key . '=' . $value . ' '; + } + $relation .= "]\n"; + return $relation; + } + + /** + * Write dot language output to a file. This should usually be a *.dot file. + * + * You have to convert the output into a viewable format. For example use "neato" on linux systems + * and execute: + * + * neato -Tpng -o er.png er.dot + * + * @param string $filename + * @return void + */ + public function write($filename) + { + file_put_contents($filename, $this->output . "}"); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/RemoveNamespacedAssets.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/RemoveNamespacedAssets.php new file mode 100644 index 00000000..348b2b7b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/RemoveNamespacedAssets.php @@ -0,0 +1,113 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +/** + * Remove assets from a schema that are not in the default namespace. + * + * Some databases such as MySQL support cross databases joins, but don't + * allow to call DDLs to a database from another connected database. + * Before a schema is serialized into SQL this visitor can cleanup schemas with + * non default namespaces. + * + * This visitor filters all these non-default namespaced tables and sequences + * and removes them from the SChema instance. + * + * @author Benjamin Eberlei + * @since 2.2 + */ +class RemoveNamespacedAssets implements Visitor +{ + /** + * @var Schema + */ + private $schema; + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + $this->schema = $schema; + } + + /** + * @param Table $table + */ + public function acceptTable(Table $table) + { + if ( ! $table->isInDefaultNamespace($this->schema->getName()) ) { + $this->schema->dropTable($table->getName()); + } + } + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + if ( ! $sequence->isInDefaultNamespace($this->schema->getName()) ) { + $this->schema->dropSequence($sequence->getName()); + } + } + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column) + { + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + // The table may already be deleted in a previous + // RemoveNamespacedAssets#acceptTable call. Removing Foreign keys that + // point to nowhere. + if ( ! $this->schema->hasTable($fkConstraint->getForeignTableName())) { + $localTable->removeForeignKey($fkConstraint->getName()); + return; + } + + $foreignTable = $this->schema->getTable($fkConstraint->getForeignTableName()); + if ( ! $foreignTable->isInDefaultNamespace($this->schema->getName()) ) { + $localTable->removeForeignKey($fkConstraint->getName()); + } + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php new file mode 100644 index 00000000..7840fd74 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php @@ -0,0 +1,75 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +/** + * Schema Visitor used for Validation or Generation purposes. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +interface Visitor +{ + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema); + + /** + * @param Table $table + */ + public function acceptTable(Table $table); + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column); + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint); + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index); + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php new file mode 100644 index 00000000..8ef2e100 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php @@ -0,0 +1,200 @@ +. + */ + +namespace Doctrine\DBAL\Sharding; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\DBAL\Driver; +use Doctrine\DBAL\Configuration; + +use Doctrine\Common\EventManager; + +use Doctrine\DBAL\Sharding\ShardChoser\ShardChoser; + +/** + * Sharding implementation that pools many different connections + * internally and serves data from the currently active connection. + * + * The internals of this class are: + * + * - All sharding clients are specified and given a shard-id during + * configuration. + * - By default, the global shard is selected. If no global shard is configured + * an exception is thrown on access. + * - Selecting a shard by distribution value delegates the mapping + * "distributionValue" => "client" to the ShardChooser interface. + * - An exception is thrown if trying to switch shards during an open + * transaction. + * + * Instantiation through the DriverManager looks like: + * + * @example + * + * $conn = DriverManager::getConnection(array( + * 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + * 'driver' => 'pdo_mysql', + * 'global' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), + * 'shards' => array( + * array('id' => 1, 'user' => 'slave1', 'password', 'host' => '', 'dbname' => ''), + * array('id' => 2, 'user' => 'slave2', 'password', 'host' => '', 'dbname' => ''), + * ), + * 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + * )); + * $shardManager = $conn->getShardManager(); + * $shardManager->selectGlobal(); + * $shardManager->selectShard($value); + * + * @author Benjamin Eberlei + */ +class PoolingShardConnection extends Connection +{ + /** + * @var array + */ + private $activeConnections; + + /** + * @var int + */ + private $activeShardId; + + /** + * @var array + */ + private $connections; + + /** + * @var ShardManager + */ + private $shardManager; + + public function __construct(array $params, Driver $driver, Configuration $config = null, EventManager $eventManager = null) + { + if ( !isset($params['global']) || !isset($params['shards'])) { + throw new \InvalidArgumentException("Connection Parameters require 'global' and 'shards' configurations."); + } + + if ( !isset($params['shardChoser'])) { + throw new \InvalidArgumentException("Missing Shard Choser configuration 'shardChoser'"); + } + + if (is_string($params['shardChoser'])) { + $params['shardChoser'] = new $params['shardChoser']; + } + + if ( ! ($params['shardChoser'] instanceof ShardChoser)) { + throw new \InvalidArgumentException("The 'shardChoser' configuration is not a valid instance of Doctrine\DBAL\Sharding\ShardChoser\ShardChoser"); + } + + $this->connections[0] = array_merge($params, $params['global']); + + foreach ($params['shards'] as $shard) { + if ( ! isset($shard['id'])) { + throw new \InvalidArgumentException("Missing 'id' for one configured shard. Please specificy a unique shard-id."); + } + + if ( !is_numeric($shard['id']) || $shard['id'] < 1) { + throw new \InvalidArgumentException("Shard Id has to be a non-negative number."); + } + + if (isset($this->connections[$shard['id']])) { + throw new \InvalidArgumentException("Shard " . $shard['id'] . " is duplicated in the configuration."); + } + + $this->connections[$shard['id']] = array_merge($params, $shard); + } + + parent::__construct($params, $driver, $config, $eventManager); + } + + /** + * Connect to a given shard + * + * @param mixed $shardId + * @return bool + */ + public function connect($shardId = null) + { + if ($shardId === null && $this->_conn) { + return false; + } + + if ($shardId !== null && $shardId === $this->activeShardId) { + return false; + } + + if ($this->getTransactionNestingLevel() > 0) { + throw new ShardingException("Cannot switch shard when transaction is active."); + } + + $this->activeShardId = (int)$shardId; + + if (isset($this->activeConnections[$this->activeShardId])) { + $this->_conn = $this->activeConnections[$this->activeShardId]; + return false; + } + + $this->_conn = $this->activeConnections[$this->activeShardId] = $this->connectTo($this->activeShardId); + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + $eventArgs = new Event\ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + + /** + * Connect to a specific connection + * + * @param string $shardId + * @return Driver + */ + protected function connectTo($shardId) + { + $params = $this->getParams(); + + $driverOptions = isset($params['driverOptions']) ? $params['driverOptions'] : array(); + + $connectionParams = $this->connections[$shardId]; + + $user = isset($connectionParams['user']) ? $connectionParams['user'] : null; + $password = isset($connectionParams['password']) ? $connectionParams['password'] : null; + + return $this->_driver->connect($connectionParams, $user, $password, $driverOptions); + } + + public function isConnected($shardId = null) + { + if ($shardId === null) { + return $this->_conn !== null; + } + + return isset($this->activeConnections[$shardId]); + } + + public function close() + { + $this->_conn = null; + $this->activeConnections = null; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php new file mode 100644 index 00000000..4b514b96 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php @@ -0,0 +1,97 @@ +. + */ + +namespace Doctrine\DBAL\Sharding; + +use Doctrine\DBAL\Sharding\ShardChoser\ShardChoser; + +/** + * Shard Manager for the Connection Pooling Shard Strategy + * + * @author Benjamin Eberlei + */ +class PoolingShardManager implements ShardManager +{ + private $conn; + private $choser; + private $currentDistributionValue; + + public function __construct(PoolingShardConnection $conn) + { + $params = $conn->getParams(); + $this->conn = $conn; + $this->choser = $params['shardChoser']; + } + + public function selectGlobal() + { + $this->conn->connect(0); + $this->currentDistributionValue = null; + } + + public function selectShard($distributionValue) + { + $shardId = $this->choser->pickShard($distributionValue, $this->conn); + $this->conn->connect($shardId); + $this->currentDistributionValue = $distributionValue; + } + + public function getCurrentDistributionValue() + { + return $this->currentDistributionValue; + } + + public function getShards() + { + $params = $this->conn->getParams(); + $shards = array(); + + foreach ($params['shards'] as $shard) { + $shards[] = array('id' => $shard['id']); + } + + return $shards; + } + + public function queryAll($sql, array $params, array $types) + { + $shards = $this->getShards(); + if (!$shards) { + throw new \RuntimeException("No shards found."); + } + + $result = array(); + $oldDistribution = $this->getCurrentDistributionValue(); + + foreach ($shards as $shard) { + $this->selectShard($shard['id']); + foreach ($this->conn->fetchAll($sql, $params, $types) as $row) { + $result[] = $row; + } + } + + if ($oldDistribution === null) { + $this->selectGlobal(); + } else { + $this->selectShard($oldDistribution); + } + + return $result; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php new file mode 100644 index 00000000..cbd6c71a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php @@ -0,0 +1,295 @@ +. + */ + +namespace Doctrine\DBAL\Sharding\SQLAzure; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Type; + +use Doctrine\DBAL\Schema\Synchronizer\AbstractSchemaSynchronizer; +use Doctrine\DBAL\Sharding\SingleDatabaseSynchronizer; + +/** + * SQL Azure Schema Synchronizer + * + * Will iterate over all shards when performing schema operations. This is done + * by partitioning the passed schema into subschemas for the federation and the + * global database and then applying the operations step by step using the + * {@see \Doctrine\DBAL\Sharding\SingleDatabaseSynchronizer}. + * + * @author Benjamin Eberlei + */ +class SQLAzureFederationsSynchronizer extends AbstractSchemaSynchronizer +{ + const FEDERATION_TABLE_FEDERATED = 'azure.federated'; + const FEDERATION_DISTRIBUTION_NAME = 'azure.federatedOnDistributionName'; + + + /** + * @var SQLAzureShardManager + */ + private $shardManager; + + /** + * @var SchemaSynchronizer + */ + private $synchronizer; + + public function __construct(Connection $conn, SQLAzureShardManager $shardManager, SchemaSynchronizer $sync = null) + { + parent::__construct($conn); + $this->shardManager = $shardManager; + $this->synchronizer = $sync ?: new SingleDatabaseSynchronizer($conn); + } + + /** + * Get the SQL statements that can be executed to create the schema. + * + * @param Schema $createSchema + * @return array + */ + public function getCreateSchema(Schema $createSchema) + { + $sql = array(); + + list($global, $federation) = $this->partitionSchema($createSchema); + + $globalSql = $this->synchronizer->getCreateSchema($global); + if ($globalSql) { + $sql[] = "-- Create Root Federation\n" . + "USE FEDERATION ROOT WITH RESET;"; + $sql = array_merge($sql, $globalSql); + } + + $federationSql = $this->synchronizer->getCreateSchema($federation); + + if ($federationSql) { + $defaultValue = $this->getFederationTypeDefaultValue(); + + $sql[] = $this->getCreateFederationStatement(); + $sql[] = "USE FEDERATION " . $this->shardManager->getFederationName() . " (" . $this->shardManager->getDistributionKey() . " = " . $defaultValue . ") WITH RESET, FILTERING = OFF;"; + $sql = array_merge($sql, $federationSql); + } + + return $sql; + } + + /** + * Get the SQL Statements to update given schema with the underlying db. + * + * @param Schema $toSchema + * @param bool $noDrops + * @return array + */ + public function getUpdateSchema(Schema $toSchema, $noDrops = false) + { + return $this->work($toSchema, function($synchronizer, $schema) use ($noDrops) { + return $synchronizer->getUpdateSchema($schema, $noDrops); + }); + } + + /** + * Get the SQL Statements to drop the given schema from underlying db. + * + * @param Schema $dropSchema + * @return array + */ + public function getDropSchema(Schema $dropSchema) + { + return $this->work($dropSchema, function($synchronizer, $schema) { + return $synchronizer->getDropSchema($schema); + }); + } + + /** + * Create the Schema + * + * @param Schema $createSchema + * @return void + */ + public function createSchema(Schema $createSchema) + { + $this->processSql($this->getCreateSchema($createSchema)); + } + + /** + * Update the Schema to new schema version. + * + * @param Schema $toSchema + * @return void + */ + public function updateSchema(Schema $toSchema, $noDrops = false) + { + $this->processSql($this->getUpdateSchema($toSchema, $noDrops)); + } + + /** + * Drop the given database schema from the underlying db. + * + * @param Schema $dropSchema + * @return void + */ + public function dropSchema(Schema $dropSchema) + { + $this->processSqlSafely($this->getDropSchema($dropSchema)); + } + + /** + * Get the SQL statements to drop all schema assets from underlying db. + * + * @return array + */ + public function getDropAllSchema() + { + $this->shardManager->selectGlobal(); + $globalSql = $this->synchronizer->getDropAllSchema(); + + if ($globalSql) { + $sql[] = "-- Work on Root Federation\nUSE FEDERATION ROOT WITH RESET;"; + $sql = array_merge($sql, $globalSql); + } + + $shards = $this->shardManager->getShards(); + foreach ($shards as $shard) { + $this->shardManager->selectShard($shard['rangeLow']); + + $federationSql = $this->synchronizer->getDropAllSchema(); + if ($federationSql) { + $sql[] = "-- Work on Federation ID " . $shard['id'] . "\n" . + "USE FEDERATION " . $this->shardManager->getFederationName() . " (" . $this->shardManager->getDistributionKey() . " = " . $shard['rangeLow'].") WITH RESET, FILTERING = OFF;"; + $sql = array_merge($sql, $federationSql); + } + } + + $sql[] = "USE FEDERATION ROOT WITH RESET;"; + $sql[] = "DROP FEDERATION " . $this->shardManager->getFederationName(); + + return $sql; + } + + /** + * Drop all assets from the underyling db. + * + * @return void + */ + public function dropAllSchema() + { + $this->processSqlSafely($this->getDropAllSchema()); + } + + private function partitionSchema(Schema $schema) + { + return array( + $this->extractSchemaFederation($schema, false), + $this->extractSchemaFederation($schema, true), + ); + } + + private function extractSchemaFederation(Schema $schema, $isFederation) + { + $partionedSchema = clone $schema; + + foreach ($partionedSchema->getTables() as $table) { + if ($isFederation) { + $table->addOption(self::FEDERATION_DISTRIBUTION_NAME, $this->shardManager->getDistributionKey()); + } + + if ( $table->hasOption(self::FEDERATION_TABLE_FEDERATED) !== $isFederation) { + $partionedSchema->dropTable($table->getName()); + } else { + foreach ($table->getForeignKeys() as $fk) { + $foreignTable = $schema->getTable($fk->getForeignTableName()); + if ($foreignTable->hasOption(self::FEDERATION_TABLE_FEDERATED) !== $isFederation) { + throw new \RuntimeException("Cannot have foreign key between global/federation."); + } + } + } + } + + return $partionedSchema; + } + + /** + * Work on the Global/Federation based on currently existing shards and + * perform the given operation on the underyling schema synchronizer given + * the different partioned schema instances. + * + * @param Schema $schema + * @param Closure $operation + * @return array + */ + private function work(Schema $schema, \Closure $operation) + { + list($global, $federation) = $this->partitionSchema($schema); + $sql = array(); + + $this->shardManager->selectGlobal(); + $globalSql = $operation($this->synchronizer, $global); + + if ($globalSql) { + $sql[] = "-- Work on Root Federation\nUSE FEDERATION ROOT WITH RESET;"; + $sql = array_merge($sql, $globalSql); + } + + $shards = $this->shardManager->getShards(); + + foreach ($shards as $shard) { + $this->shardManager->selectShard($shard['rangeLow']); + + $federationSql = $operation($this->synchronizer, $federation); + if ($federationSql) { + $sql[] = "-- Work on Federation ID " . $shard['id'] . "\n" . + "USE FEDERATION " . $this->shardManager->getFederationName() . " (" . $this->shardManager->getDistributionKey() . " = " . $shard['rangeLow'].") WITH RESET, FILTERING = OFF;"; + $sql = array_merge($sql, $federationSql); + } + } + + return $sql; + } + + private function getFederationTypeDefaultValue() + { + $federationType = Type::getType($this->shardManager->getDistributionType()); + + switch ($federationType->getName()) { + case Type::GUID: + $defaultValue = '00000000-0000-0000-0000-000000000000'; + break; + case Type::INTEGER: + case Type::SMALLINT: + case Type::BIGINT: + $defaultValue = '0'; + break; + default: + $defaultValue = ''; + break; + } + return $defaultValue; + } + + private function getCreateFederationStatement() + { + $federationType = Type::getType($this->shardManager->getDistributionType()); + $federationTypeSql = $federationType->getSqlDeclaration(array(), $this->conn->getDatabasePlatform()); + + return "--Create Federation\n" . + "CREATE FEDERATION " . $this->shardManager->getFederationName() . " (" . $this->shardManager->getDistributionKey() . " " . $federationTypeSql ." RANGE)"; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php new file mode 100644 index 00000000..3d76ef9d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php @@ -0,0 +1,237 @@ +. + */ + +namespace Doctrine\DBAL\Sharding\SQLAzure; + +use Doctrine\DBAL\Sharding\ShardManager; +use Doctrine\DBAL\Sharding\ShardingException; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Types\Type; + +/** + * Sharding using the SQL Azure Federations support. + * + * @author Benjamin Eberlei + */ +class SQLAzureShardManager implements ShardManager +{ + /** + * @var string + */ + private $federationName; + + /** + * @var bool + */ + private $filteringEnabled; + + /** + * @var string + */ + private $distributionKey; + + /** + * @var string + */ + private $distributionType; + + /** + * @var Connection + */ + private $conn; + + /** + * @var string + */ + private $currentDistributionValue; + + /** + * @param Connection $conn + */ + public function __construct(Connection $conn) + { + $this->conn = $conn; + $params = $conn->getParams(); + + if ( ! isset($params['sharding']['federationName'])) { + throw ShardingException::missingDefaultFederationName(); + } + + if ( ! isset($params['sharding']['distributionKey'])) { + throw ShardingException::missingDefaultDistributionKey(); + } + + if ( ! isset($params['sharding']['distributionType'])) { + throw ShardingException::missingDistributionType(); + } + + $this->federationName = $params['sharding']['federationName']; + $this->distributionKey = $params['sharding']['distributionKey']; + $this->distributionType = $params['sharding']['distributionType']; + $this->filteringEnabled = (isset($params['sharding']['filteringEnabled'])) ? (bool)$params['sharding']['filteringEnabled'] : false; + } + + /** + * Get name of the federation + * + * @return string + */ + public function getFederationName() + { + return $this->federationName; + } + + /** + * Get the distribution key + * + * @return string + */ + public function getDistributionKey() + { + return $this->distributionKey; + } + + /** + * Get the Doctrine Type name used for the distribution + * + * @return string + */ + public function getDistributionType() + { + return $this->distributionType; + } + + /** + * Enabled/Disable filtering on the fly. + * + * @param bool $flag + * @return void + */ + public function setFilteringEnabled($flag) + { + $this->filteringEnabled = (bool)$flag; + } + + /** + * {@inheritDoc} + */ + public function selectGlobal() + { + if ($this->conn->isTransactionActive()) { + throw ShardingException::activeTransaction(); + } + + $sql = "USE FEDERATION ROOT WITH RESET"; + $this->conn->exec($sql); + $this->currentDistributionValue = null; + } + + /** + * {@inheritDoc} + */ + public function selectShard($distributionValue) + { + if ($this->conn->isTransactionActive()) { + throw ShardingException::activeTransaction(); + } + + if ($distributionValue === null || is_bool($distributionValue) || !is_scalar($distributionValue)) { + throw ShardingException::noShardDistributionValue(); + } + + $platform = $this->conn->getDatabasePlatform(); + $sql = sprintf( + "USE FEDERATION %s (%s = %s) WITH RESET, FILTERING = %s;", + $platform->quoteIdentifier($this->federationName), + $platform->quoteIdentifier($this->distributionKey), + $this->conn->quote($distributionValue), + ($this->filteringEnabled ? 'ON' : 'OFF') + ); + + $this->conn->exec($sql); + $this->currentDistributionValue = $distributionValue; + } + + /** + * {@inheritDoc} + */ + public function getCurrentDistributionValue() + { + return $this->currentDistributionValue; + } + + /** + * {@inheritDoc} + */ + public function getShards() + { + $sql = "SELECT member_id as id, + distribution_name as distribution_key, + CAST(range_low AS CHAR) AS rangeLow, + CAST(range_high AS CHAR) AS rangeHigh + FROM sys.federation_member_distributions d + INNER JOIN sys.federations f ON f.federation_id = d.federation_id + WHERE f.name = " . $this->conn->quote($this->federationName); + return $this->conn->fetchAll($sql); + } + + /** + * {@inheritDoc} + */ + public function queryAll($sql, array $params = array(), array $types = array()) + { + $shards = $this->getShards(); + if (!$shards) { + throw new \RuntimeException("No shards found for " . $this->federationName); + } + + $result = array(); + $oldDistribution = $this->getCurrentDistributionValue(); + + foreach ($shards as $shard) { + $this->selectShard($shard['rangeLow']); + foreach ($this->conn->fetchAll($sql, $params, $types) as $row) { + $result[] = $row; + } + } + + if ($oldDistribution === null) { + $this->selectGlobal(); + } else { + $this->selectShard($oldDistribution); + } + + return $result; + } + + /** + * Split Federation at a given distribution value. + * + * @param mixed $splitDistributionValue + */ + public function splitFederation($splitDistributionValue) + { + $type = Type::getType($this->distributionType); + + $sql = "ALTER FEDERATION " . $this->getFederationName() . " " . + "SPLIT AT (" . $this->getDistributionKey() . " = " . + $this->conn->quote($splitDistributionValue, $type->getBindingType()) . ")"; + $this->conn->exec($sql); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php new file mode 100644 index 00000000..5770be28 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php @@ -0,0 +1,160 @@ +. + */ + +namespace Doctrine\DBAL\Sharding\SQLAzure\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +/** + * Converts a single tenant schema into a multi-tenant schema for SQL Azure + * Federations under the following assumptions: + * + * - Every table is part of the multi-tenant application, only explicitly + * excluded tables are non-federated. The behavior of the tables being in + * global or federated database is undefined. It depends on you selecting a + * federation before DDL statements or not. + * - Every Primary key of a federated table is extended by another column + * 'tenant_id' with a default value of the SQLAzure function + * `federation_filtering_value('tenant_id')`. + * - You always have to work with `filtering=On` when using federations with this + * multi-tenant approach. + * - Primary keys are either using globally unique ids (GUID, Table Generator) + * or you explicitly add the tenent_id in every UPDATE or DELETE statement + * (otherwise they will affect the same-id rows from other tenents as well). + * SQLAzure throws errors when you try to create IDENTIY columns on federated + * tables. + * + * @author Benjamin Eberlei + */ +class MultiTenantVisitor implements Visitor +{ + /** + * @var array + */ + private $excludedTables = array(); + + /** + * @var string + */ + private $tenantColumnName; + + /** + * @var string + */ + private $tenantColumnType = 'integer'; + + /** + * Name of the federation distribution, defaulting to the tenantColumnName + * if not specified. + * + * @var string + */ + private $distributionName; + + public function __construct(array $excludedTables = array(), $tenantColumnName = 'tenant_id', $distributionName = null) + { + $this->excludedTables = $excludedTables; + $this->tenantColumnName = $tenantColumnName; + $this->distributionName = $distributionName ?: $tenantColumnName; + } + + /** + * @param Table $table + */ + public function acceptTable(Table $table) + { + if (in_array($table->getName(), $this->excludedTables)) { + return; + } + + $table->addColumn($this->tenantColumnName, $this->tenantColumnType, array( + 'default' => "federation_filtering_value('". $this->distributionName ."')", + )); + + $clusteredIndex = $this->getClusteredIndex($table); + + $indexColumns = $clusteredIndex->getColumns(); + $indexColumns[] = $this->tenantColumnName; + + if ($clusteredIndex->isPrimary()) { + $table->dropPrimaryKey(); + $table->setPrimaryKey($indexColumns); + } else { + $table->dropIndex($clusteredIndex->getName()); + $table->addIndex($indexColumns, $clusteredIndex->getName()); + $table->getIndex($clusteredIndex->getName())->addFlag('clustered'); + } + } + + private function getClusteredIndex($table) + { + foreach ($table->getIndexes() as $index) { + if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) { + return $index; + } else if ($index->hasFlag('clustered')) { + return $index; + } + } + throw new \RuntimeException("No clustered index found on table " . $table->getName()); + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + } + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column) + { + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php new file mode 100644 index 00000000..9714ee54 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php @@ -0,0 +1,36 @@ +. + */ + +namespace Doctrine\DBAL\Sharding\ShardChoser; + +use Doctrine\DBAL\Sharding\PoolingShardConnection; + +/** + * The MultiTenant Shard choser assumes that the distribution value directly + * maps to the shard id. + * + * @author Benjamin Eberlei + */ +class MultiTenantShardChoser implements ShardChoser +{ + public function pickShard($distributionValue, PoolingShardConnection $conn) + { + return $distributionValue; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php new file mode 100644 index 00000000..924daa5d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php @@ -0,0 +1,40 @@ +. + */ + +namespace Doctrine\DBAL\Sharding\ShardChoser; + +use Doctrine\DBAL\Sharding\PoolingShardConnection; + +/** + * Given a distribution value this shard-choser strategy will pick the shard to + * connect to for retrieving rows with the distribution value. + * + * @author Benjamin Eberlei + */ +interface ShardChoser +{ + /** + * Pick a shard for the given distribution value + * + * @param string $distributionValue + * @param PoolingShardConnection $conn + * @return int + */ + function pickShard($distributionValue, PoolingShardConnection $conn); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php new file mode 100644 index 00000000..1d10e0e1 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php @@ -0,0 +1,94 @@ +. + */ + +namespace Doctrine\DBAL\Sharding; + +use Doctrine\DBAL\Connection; + +/** + * Sharding Manager gives access to APIs to implementing sharding on top of + * Doctrine\DBAL\Connection instances. + * + * For simplicity and developer ease-of-use (and understanding) the sharding + * API only covers single shard queries, no fan-out support. It is primarily + * suited for multi-tenant applications. + * + * The assumption about sharding here + * is that a distribution value can be found that gives access to all the + * necessary data for all use-cases. Switching between shards should be done with + * caution, especially if lazy loading is implemented. Any query is always + * executed against the last shard that was selected. If a query is created for + * a shard Y but then a shard X is selected when its actually excecuted you + * will hit the wrong shard. + * + * @author Benjamin Eberlei + */ +interface ShardManager +{ + /** + * Select global database with global data. + * + * This is the default database that is connected when no shard is + * selected. + * + * @return void + */ + function selectGlobal(); + + /** + * SELECT queries after this statement will be issued against the selected + * shard. + * + * @throws ShardingException If no value is passed as shard identifier. + * @param mixed $distributionValue + * @param array $options + * @return void + */ + function selectShard($distributionValue); + + /** + * Get the distribution value currently used for sharding. + * + * @return string + */ + function getCurrentDistributionValue(); + + /** + * Get information about the amount of shards and other details. + * + * Format is implementation specific, each shard is one element and has a + * 'name' attribute at least. + * + * @return array + */ + function getShards(); + + /** + * Query all shards in undefined order and return the results appended to + * each other. Restore the previous distribution value after execution. + * + * Using {@link Connection::fetchAll} to retrieve rows internally. + * + * @param string $sql + * @param array $params + * @param array $types + * @return array + */ + function queryAll($sql, array $params, array $types); +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php new file mode 100644 index 00000000..2dc1d21a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\DBAL\Sharding; + +use Doctrine\DBAL\DBALException; + +/** + * Sharding related Exceptions + * + * @since 2.3 + */ +class ShardingException extends DBALException +{ + static public function notImplemented() + { + return new self("This functionality is not implemented with this sharding provider.", 1331557937); + } + + static public function missingDefaultFederationName() + { + return new self("SQLAzure requires a federation name to be set during sharding configuration.", 1332141280); + } + + static public function missingDefaultDistributionKey() + { + return new self("SQLAzure requires a distribution key to be set during sharding configuration.", 1332141329); + } + + static public function activeTransaction() + { + return new self("Cannot switch shard during an active transaction.", 1332141766); + } + + static public function noShardDistributionValue() + { + return new self("You have to specify a string or integer as shard distribution value.", 1332142103); + } + + static public function missingDistributionType() + { + return new self("You have to specify a sharding distribution type such as 'integer', 'string', 'guid'."); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Statement.php b/doctrine/dbal/lib/Doctrine/DBAL/Statement.php new file mode 100644 index 00000000..1bf9d742 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Statement.php @@ -0,0 +1,264 @@ +. + */ + +namespace Doctrine\DBAL; + +use PDO, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Driver\Statement as DriverStatement; + +/** + * A thin wrapper around a Doctrine\DBAL\Driver\Statement that adds support + * for logging, DBAL mapping types, etc. + * + * @author Roman Borschel + * @since 2.0 + */ +class Statement implements \IteratorAggregate, DriverStatement +{ + /** + * @var string The SQL statement. + */ + protected $sql; + /** + * @var array The bound parameters. + */ + protected $params = array(); + /** + * @var array The parameter types + */ + protected $types = array(); + /** + * @var \Doctrine\DBAL\Driver\Statement The underlying driver statement. + */ + protected $stmt; + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform The underlying database platform. + */ + protected $platform; + /** + * @var \Doctrine\DBAL\Connection The connection this statement is bound to and executed on. + */ + protected $conn; + + /** + * Creates a new Statement for the given SQL and Connection. + * + * @param string $sql The SQL of the statement. + * @param \Doctrine\DBAL\Connection The connection on which the statement should be executed. + */ + public function __construct($sql, Connection $conn) + { + $this->sql = $sql; + $this->stmt = $conn->getWrappedConnection()->prepare($sql); + $this->conn = $conn; + $this->platform = $conn->getDatabasePlatform(); + } + + /** + * Binds a parameter value to the statement. + * + * The value can optionally be bound with a PDO binding type or a DBAL mapping type. + * If bound with a DBAL mapping type, the binding type is derived from the mapping + * type and the value undergoes the conversion routines of the mapping type before + * being bound. + * + * @param string $name The name or position of the parameter. + * @param mixed $value The value of the parameter. + * @param mixed $type Either a PDO binding type or a DBAL mapping type name or instance. + * @return boolean TRUE on success, FALSE on failure. + */ + public function bindValue($name, $value, $type = null) + { + $this->params[$name] = $value; + $this->types[$name] = $type; + if ($type !== null) { + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->platform); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type; // PDO::PARAM_* constants + } + return $this->stmt->bindValue($name, $value, $bindingType); + } else { + return $this->stmt->bindValue($name, $value); + } + } + + /** + * Binds a parameter to a value by reference. + * + * Binding a parameter by reference does not support DBAL mapping types. + * + * @param string $name The name or position of the parameter. + * @param mixed $var The reference to the variable to bind + * @param integer $type The PDO binding type. + * @return boolean TRUE on success, FALSE on failure. + */ + public function bindParam($name, &$var, $type = PDO::PARAM_STR, $length = null) + { + return $this->stmt->bindParam($name, $var, $type, $length ); + } + + /** + * Executes the statement with the currently bound parameters. + * + * @param array $params + * @return boolean TRUE on success, FALSE on failure. + */ + public function execute($params = null) + { + $logger = $this->conn->getConfiguration()->getSQLLogger(); + if ($logger) { + $logger->startQuery($this->sql, $this->params, $this->types); + } + + try { + $stmt = $this->stmt->execute($params); + } catch (\Exception $ex) { + throw DBALException::driverExceptionDuringQuery($ex, $this->sql, $this->conn->resolveParams($this->params, $this->types)); + } + + if ($logger) { + $logger->stopQuery(); + } + $this->params = array(); + $this->types = array(); + return $stmt; + } + + /** + * Closes the cursor, freeing the database resources used by this statement. + * + * @return boolean TRUE on success, FALSE on failure. + */ + public function closeCursor() + { + return $this->stmt->closeCursor(); + } + + /** + * Returns the number of columns in the result set. + * + * @return integer + */ + public function columnCount() + { + return $this->stmt->columnCount(); + } + + /** + * Fetches the SQLSTATE associated with the last operation on the statement. + * + * @return string + */ + public function errorCode() + { + return $this->stmt->errorCode(); + } + + /** + * Fetches extended error information associated with the last operation on the statement. + * + * @return array + */ + public function errorInfo() + { + return $this->stmt->errorInfo(); + } + + public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) + { + if ($arg2 === null) { + return $this->stmt->setFetchMode($fetchMode); + } else if ($arg3 === null) { + return $this->stmt->setFetchMode($fetchMode, $arg2); + } + + return $this->stmt->setFetchMode($fetchMode, $arg2, $arg3); + } + + public function getIterator() + { + return $this->stmt; + } + + /** + * Fetches the next row from a result set. + * + * @param integer $fetchMode + * @return mixed The return value of this function on success depends on the fetch type. + * In all cases, FALSE is returned on failure. + */ + public function fetch($fetchMode = null) + { + return $this->stmt->fetch($fetchMode); + } + + /** + * Returns an array containing all of the result set rows. + * + * @param integer $fetchMode + * @param mixed $fetchArgument + * @return array An array containing all of the remaining rows in the result set. + */ + public function fetchAll($fetchMode = null, $fetchArgument = 0) + { + if ($fetchArgument !== 0) { + return $this->stmt->fetchAll($fetchMode, $fetchArgument); + } + return $this->stmt->fetchAll($fetchMode); + } + + /** + * Returns a single column from the next row of a result set. + * + * @param integer $columnIndex + * @return mixed A single column from the next row of a result set or FALSE if there are no more rows. + */ + public function fetchColumn($columnIndex = 0) + { + return $this->stmt->fetchColumn($columnIndex); + } + + /** + * Returns the number of rows affected by the last execution of this statement. + * + * @return integer The number of affected rows. + */ + public function rowCount() + { + return $this->stmt->rowCount(); + } + + /** + * Gets the wrapped driver statement. + * + * @return \Doctrine\DBAL\Driver\Statement + */ + public function getWrappedStatement() + { + return $this->stmt; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php new file mode 100644 index 00000000..c2795f2e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php @@ -0,0 +1,124 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console; + +/** + * Task for executing arbitrary SQL that can come from a file or directly from + * the command line. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ImportCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('dbal:import') + ->setDescription('Import SQL file(s) directly to Database.') + ->setDefinition(array( + new InputArgument( + 'file', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'File path(s) of SQL to be executed.' + ) + )) + ->setHelp(<<getHelper('db')->getConnection(); + + if (($fileNames = $input->getArgument('file')) !== null) { + foreach ((array) $fileNames as $fileName) { + $fileName = realpath($fileName); + + if ( ! file_exists($fileName)) { + throw new \InvalidArgumentException( + sprintf("SQL file '%s' does not exist.", $fileName) + ); + } else if ( ! is_readable($fileName)) { + throw new \InvalidArgumentException( + sprintf("SQL file '%s' does not have read permissions.", $fileName) + ); + } + + $output->write(sprintf("Processing file '%s'... ", $fileName)); + $sql = file_get_contents($fileName); + + if ($conn instanceof \Doctrine\DBAL\Driver\PDOConnection) { + // PDO Drivers + try { + $lines = 0; + + $stmt = $conn->prepare($sql); + $stmt->execute(); + + do { + // Required due to "MySQL has gone away!" issue + $stmt->fetch(); + $stmt->closeCursor(); + + $lines++; + } while ($stmt->nextRowset()); + + $output->write(sprintf('%d statements executed!', $lines) . PHP_EOL); + } catch (\PDOException $e) { + $output->write('error!' . PHP_EOL); + + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } else { + // Non-PDO Drivers (ie. OCI8 driver) + $stmt = $conn->prepare($sql); + $rs = $stmt->execute(); + + if ($rs) { + $output->writeln('OK!' . PHP_EOL); + } else { + $error = $stmt->errorInfo(); + + $output->write('error!' . PHP_EOL); + + throw new \RuntimeException($error[2], $error[0]); + } + + $stmt->closeCursor(); + } + } + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ReservedWordsCommand.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ReservedWordsCommand.php new file mode 100644 index 00000000..7b1bb1c4 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ReservedWordsCommand.php @@ -0,0 +1,133 @@ +. + */ + + +namespace Doctrine\DBAL\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console\Command\Command, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface; +use Doctrine\DBAL\Platforms\Keywords\ReservedKeywordsValidator; + +class ReservedWordsCommand extends Command +{ + private $keywordListClasses = array( + 'mysql' => 'Doctrine\DBAL\Platforms\Keywords\MySQLKeywords', + 'mssql' => 'Doctrine\DBAL\Platforms\Keywords\MsSQLKeywords', + 'sqlite' => 'Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords', + 'pgsql' => 'Doctrine\DBAL\Platforms\Keywords\PostgreSQLKeywords', + 'oracle' => 'Doctrine\DBAL\Platforms\Keywords\OracleKeywords', + 'db2' => 'Doctrine\DBAL\Platforms\Keywords\DB2Keywords', + ); + + /** + * If you want to add or replace a keywords list use this command + * + * @param string $name + * @param string $class + */ + public function setKeywordListClass($name, $class) + { + $this->keywordListClasses[$name] = $class; + } + + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('dbal:reserved-words') + ->setDescription('Checks if the current database contains identifiers that are reserved.') + ->setDefinition(array( + new InputOption( + 'list', 'l', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Keyword-List name.' + ) + )) + ->setHelp(<<%command.full_name% + +If you want to check against specific dialects you can +pass them to the command: + + %command.full_name% mysql pgsql + +The following keyword lists are currently shipped with Doctrine: + + * mysql + * pgsql + * sqlite + * oracle + * mssql + * db2 (Not checked by default) +EOT + ); + } + + /** + * @see Console\Command\Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /* @var $conn \Doctrine\DBAL\Connection */ + $conn = $this->getHelper('db')->getConnection(); + + $keywordLists = (array)$input->getOption('list'); + if ( ! $keywordLists) { + $keywordLists = array('mysql', 'pgsql', 'sqlite', 'oracle', 'mssql'); + } + + $keywords = array(); + foreach ($keywordLists as $keywordList) { + if (!isset($this->keywordListClasses[$keywordList])) { + throw new \InvalidArgumentException( + "There exists no keyword list with name '" . $keywordList . "'. ". + "Known lists: " . implode(", ", array_keys($this->keywordListClasses)) + ); + } + $class = $this->keywordListClasses[$keywordList]; + $keywords[] = new $class; + } + + $output->write('Checking keyword violations for ' . implode(", ", $keywordLists) . "...", true); + + /* @var $schema \Doctrine\DBAL\Schema\Schema */ + $schema = $conn->getSchemaManager()->createSchema(); + $visitor = new ReservedKeywordsValidator($keywords); + $schema->visit($visitor); + + $violations = $visitor->getViolations(); + if (count($violations) == 0) { + $output->write("No reserved keywords violations have been found!", true); + } else { + $output->write('There are ' . count($violations) . ' reserved keyword violations in your database schema:', true); + foreach ($violations as $violation) { + $output->write(' - ' . $violation, true); + } + } + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php new file mode 100644 index 00000000..ca84b275 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php @@ -0,0 +1,87 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Task for executing arbitrary SQL that can come from a file or directly from + * the command line. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class RunSqlCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('dbal:run-sql') + ->setDescription('Executes arbitrary SQL directly from the command line.') + ->setDefinition(array( + new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'), + new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set.', 7) + )) + ->setHelp(<<getHelper('db')->getConnection(); + + if (($sql = $input->getArgument('sql')) === null) { + throw new \RuntimeException("Argument 'SQL' is required in order to execute this command correctly."); + } + + $depth = $input->getOption('depth'); + + if ( ! is_numeric($depth)) { + throw new \LogicException("Option 'depth' must contains an integer value"); + } + + if (stripos($sql, 'select') === 0) { + $resultSet = $conn->fetchAll($sql); + } else { + $resultSet = $conn->executeUpdate($sql); + } + + ob_start(); + \Doctrine\Common\Util\Debug::dump($resultSet, (int) $depth); + $message = ob_get_clean(); + + $output->write($message); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php new file mode 100644 index 00000000..d3fa431d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php @@ -0,0 +1,74 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Helper; + +use Symfony\Component\Console\Helper\Helper, + Doctrine\DBAL\Connection; + +/** + * Doctrine CLI Connection Helper. + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConnectionHelper extends Helper +{ + /** + * Doctrine Database Connection + * @var Connection + */ + protected $_connection; + + /** + * Constructor + * + * @param Connection $connection Doctrine Database Connection + */ + public function __construct(Connection $connection) + { + $this->_connection = $connection; + } + + /** + * Retrieves Doctrine Database Connection + * + * @return Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @see Helper + */ + public function getName() + { + return 'connection'; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.php new file mode 100644 index 00000000..447f9ee9 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/ArrayType.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a PHP array to a clob SQL type. + * + * @since 2.0 + */ +class ArrayType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return serialize($value); + } + + public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + $val = unserialize($value); + if ($val === false && $value != 'b:0;') { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } + + public function getName() + { + return Type::TARRAY; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php new file mode 100644 index 00000000..f4603812 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php @@ -0,0 +1,56 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a database BIGINT to a PHP string. + * + * @author robo + * @since 2.0 + */ +class BigIntType extends Type +{ + public function getName() + { + return Type::BIGINT; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration); + } + + public function getBindingType() + { + return \PDO::PARAM_STR; + } + + /** + * {@inheritdoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (string) $value; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php new file mode 100644 index 00000000..32c93a1d --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php @@ -0,0 +1,67 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL BLOB to a PHP resource stream + * + * @since 2.2 + */ +class BlobType extends Type +{ + /** @override */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getBlobTypeDeclarationSQL($fieldDeclaration); + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if (null === $value) { + return null; + } + if (is_string($value)) { + $value = fopen('data://text/plain;base64,' . base64_encode($value), 'r'); + } else if ( ! is_resource($value)) { + throw ConversionException::conversionFailed($value, self::BLOB); + } + return $value; + } + + public function getName() + { + return Type::BLOB; + } + + public function getBindingType() + { + return \PDO::PARAM_LOB; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php new file mode 100644 index 00000000..f1a968d7 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/BooleanType.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL boolean to a PHP boolean. + * + * @since 2.0 + */ +class BooleanType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getBooleanTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $platform->convertBooleans($value); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (bool) $value; + } + + public function getName() + { + return Type::BOOLEAN; + } + + public function getBindingType() + { + return \PDO::PARAM_BOOL; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php new file mode 100644 index 00000000..3a19d1a4 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/ConversionException.php @@ -0,0 +1,65 @@ +. + */ + + +/** + * Conversion Exception is thrown when the database to PHP conversion fails + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +namespace Doctrine\DBAL\Types; + +class ConversionException extends \Doctrine\DBAL\DBALException +{ + /** + * Thrown when a Database to Doctrine Type Conversion fails. + * + * @param string $value + * @param string $toType + * @return ConversionException + */ + static public function conversionFailed($value, $toType) + { + $value = (strlen($value) > 32) ? substr($value, 0, 20) . "..." : $value; + return new self('Could not convert database value "' . $value . '" to Doctrine Type ' . $toType); + } + + /** + * Thrown when a Database to Doctrine Type Conversion fails and we can make a statement + * about the expected format. + * + * @param string $value + * @param string $toType + * @return ConversionException + */ + static public function conversionFailedFormat($value, $toType, $expectedFormat) + { + $value = (strlen($value) > 32) ? substr($value, 0, 20) . "..." : $value; + return new self( + 'Could not convert database value "' . $value . '" to Doctrine Type ' . + $toType . '. Expected format: ' . $expectedFormat + ); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php new file mode 100644 index 00000000..06de729b --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeType.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DATETIME/TIMESTAMP to a PHP DateTime object. + * + * @since 2.0 + */ +class DateTimeType extends Type +{ + public function getName() + { + return Type::DATETIME; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTimeTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateTimeFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateTime) { + return $value; + } + + $val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value); + if ( ! $val) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeFormatString()); + } + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.php new file mode 100644 index 00000000..e0a786c6 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateTimeTzType.php @@ -0,0 +1,79 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * DateTime type saving additional timezone information. + * + * Caution: Databases are not necessarily experts at storing timezone related + * data of dates. First, of all the supported vendors only PostgreSQL and Oracle + * support storing Timezone data. But those two don't save the actual timezone + * attached to a DateTime instance (for example "Europe/Berlin" or "America/Montreal") + * but the current offset of them related to UTC. That means depending on daylight saving times + * or not you may get different offsets. + * + * This datatype makes only sense to use, if your application works with an offset, not + * with an actual timezone that uses transitions. Otherwise your DateTime instance + * attached with a timezone such as Europe/Berlin gets saved into the database with + * the offset and re-created from persistence with only the offset, not the original timezone + * attached. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DateTimeTzType extends Type +{ + public function getName() + { + return Type::DATETIMETZ; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTimeTzTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateTimeTzFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateTime) { + return $value; + } + + $val = \DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value); + if ( ! $val) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateTimeTzFormatString()); + } + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php new file mode 100644 index 00000000..a3f70181 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/DateType.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DATE to a PHP Date object. + * + * @since 2.0 + */ +class DateType extends Type +{ + public function getName() + { + return Type::DATE; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateTime) { + return $value; + } + + $val = \DateTime::createFromFormat('!'.$platform->getDateFormatString(), $value); + if ( ! $val) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getDateFormatString()); + } + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php new file mode 100644 index 00000000..fd61b53f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/DecimalType.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DECIMAL to a PHP double. + * + * @since 2.0 + */ +class DecimalType extends Type +{ + public function getName() + { + return Type::DECIMAL; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDecimalTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : $value; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.php new file mode 100644 index 00000000..d02ca0c5 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/FloatType.php @@ -0,0 +1,54 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +class FloatType extends Type +{ + public function getName() + { + return Type::FLOAT; + } + + /** + * @param array $fieldDeclaration + * @param AbstractPlatform $platform + * @return string + */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getFloatDeclarationSQL($fieldDeclaration); + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (double) $value; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php new file mode 100644 index 00000000..af340a86 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Represents a GUID/UUID datatype (both are actually synomys) in the database. + * + * @author Benjamin Eberlei + * @since 2.3 + */ +class GuidType extends StringType +{ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getGuidTypeDeclarationSQL($fieldDeclaration); + } + + public function getName() + { + return Type::GUID; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.php new file mode 100644 index 00000000..bac9b3f5 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/IntegerType.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL INT to a PHP integer. + * + * @author Roman Borschel + * @since 2.0 + */ +class IntegerType extends Type +{ + public function getName() + { + return Type::INTEGER; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getIntegerTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (int) $value; + } + + public function getBindingType() + { + return \PDO::PARAM_INT; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php new file mode 100755 index 00000000..ca005578 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/JsonArrayType.php @@ -0,0 +1,66 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Array Type which can be used to generate json arrays. + * + * @since 2.3 + * @author Johannes M. Schmitt + */ +class JsonArrayType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (null === $value) { + return null; + } + + return json_encode($value); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return array(); + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + + return json_decode($value, true); + } + + public function getName() + { + return Type::JSON_ARRAY; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php new file mode 100644 index 00000000..9510d29c --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/ObjectType.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a PHP object to a clob SQL type. + * + * @since 2.0 + */ +class ObjectType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return serialize($value); + } + + public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + $val = unserialize($value); + if ($val === false && $value !== 'b:0;') { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } + + public function getName() + { + return Type::OBJECT; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.php new file mode 100755 index 00000000..719d7f1e --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/SimpleArrayType.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Array Type which can be used for simple values. + * + * Only use this type if you are sure that your values cannot contain a ",". + * + * @since 2.3 + * @author Johannes M. Schmitt + */ +class SimpleArrayType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (!$value) { + return null; + } + + return implode(',', $value); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return array(); + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + + return explode(',', $value); + } + + public function getName() + { + return Type::SIMPLE_ARRAY; + } + + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return true; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php new file mode 100644 index 00000000..97e9aaff --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/SmallIntType.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a database SMALLINT to a PHP integer. + * + * @author robo + */ +class SmallIntType extends Type +{ + public function getName() + { + return Type::SMALLINT; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getSmallIntTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (int) $value; + } + + public function getBindingType() + { + return \PDO::PARAM_INT; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php new file mode 100644 index 00000000..48c76d6a --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/StringType.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL VARCHAR to a PHP string. + * + * @since 2.0 + */ +class StringType extends Type +{ + /** @override */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration); + } + + /** @override */ + public function getDefaultLength(AbstractPlatform $platform) + { + return $platform->getVarcharDefaultLength(); + } + + /** @override */ + public function getName() + { + return Type::STRING; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php new file mode 100644 index 00000000..98ecbe62 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/TextType.php @@ -0,0 +1,56 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL CLOB to a PHP string. + * + * @since 2.0 + */ +class TextType extends Type +{ + /** @override */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (is_resource($value)) ? stream_get_contents($value) : $value; + } + + public function getName() + { + return Type::TEXT; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php new file mode 100644 index 00000000..8653750f --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/TimeType.php @@ -0,0 +1,68 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL TIME to a PHP DateTime object. + * + * @since 2.0 + */ +class TimeType extends Type +{ + public function getName() + { + return Type::TIME; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getTimeTypeDeclarationSQL($fieldDeclaration); + } + + /** + * {@inheritdoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getTimeFormatString()) : null; + } + + /** + * {@inheritdoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateTime) { + return $value; + } + + $val = \DateTime::createFromFormat($platform->getTimeFormatString(), $value); + if ( ! $val) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), $platform->getTimeFormatString()); + } + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php new file mode 100644 index 00000000..d137c605 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php @@ -0,0 +1,305 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\DBALException; + +/** + * The base class for so-called Doctrine mapping types. + * + * A Type object is obtained by calling the static {@link getType()} method. + * + * @author Roman Borschel + * @author Benjamin Eberlei + * @since 2.0 + */ +abstract class Type +{ + const TARRAY = 'array'; + const SIMPLE_ARRAY = 'simple_array'; + const JSON_ARRAY = 'json_array'; + const BIGINT = 'bigint'; + const BOOLEAN = 'boolean'; + const DATETIME = 'datetime'; + const DATETIMETZ = 'datetimetz'; + const DATE = 'date'; + const TIME = 'time'; + const DECIMAL = 'decimal'; + const INTEGER = 'integer'; + const OBJECT = 'object'; + const SMALLINT = 'smallint'; + const STRING = 'string'; + const TEXT = 'text'; + const BLOB = 'blob'; + const FLOAT = 'float'; + const GUID = 'guid'; + + /** Map of already instantiated type objects. One instance per type (flyweight). */ + private static $_typeObjects = array(); + + /** The map of supported doctrine mapping types. */ + private static $_typesMap = array( + self::TARRAY => 'Doctrine\DBAL\Types\ArrayType', + self::SIMPLE_ARRAY => 'Doctrine\DBAL\Types\SimpleArrayType', + self::JSON_ARRAY => 'Doctrine\DBAL\Types\JsonArrayType', + self::OBJECT => 'Doctrine\DBAL\Types\ObjectType', + self::BOOLEAN => 'Doctrine\DBAL\Types\BooleanType', + self::INTEGER => 'Doctrine\DBAL\Types\IntegerType', + self::SMALLINT => 'Doctrine\DBAL\Types\SmallIntType', + self::BIGINT => 'Doctrine\DBAL\Types\BigIntType', + self::STRING => 'Doctrine\DBAL\Types\StringType', + self::TEXT => 'Doctrine\DBAL\Types\TextType', + self::DATETIME => 'Doctrine\DBAL\Types\DateTimeType', + self::DATETIMETZ => 'Doctrine\DBAL\Types\DateTimeTzType', + self::DATE => 'Doctrine\DBAL\Types\DateType', + self::TIME => 'Doctrine\DBAL\Types\TimeType', + self::DECIMAL => 'Doctrine\DBAL\Types\DecimalType', + self::FLOAT => 'Doctrine\DBAL\Types\FloatType', + self::BLOB => 'Doctrine\DBAL\Types\BlobType', + self::GUID => 'Doctrine\DBAL\Types\GuidType', + ); + + /* Prevent instantiation and force use of the factory method. */ + final private function __construct() {} + + /** + * Converts a value from its PHP representation to its database representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The database representation of the value. + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Gets the default length of this type. + * + * @todo Needed? + */ + public function getDefaultLength(AbstractPlatform $platform) + { + return null; + } + + /** + * Gets the SQL declaration snippet for a field of this type. + * + * @param array $fieldDeclaration The field declaration. + * @param AbstractPlatform $platform The currently used database platform. + */ + abstract public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform); + + /** + * Gets the name of this type. + * + * @return string + * @todo Needed? + */ + abstract public function getName(); + + /** + * Factory method to create type instances. + * Type instances are implemented as flyweights. + * + * @static + * @throws DBALException + * @param string $name The name of the type (as returned by getName()). + * @return \Doctrine\DBAL\Types\Type + */ + public static function getType($name) + { + if ( ! isset(self::$_typeObjects[$name])) { + if ( ! isset(self::$_typesMap[$name])) { + throw DBALException::unknownColumnType($name); + } + self::$_typeObjects[$name] = new self::$_typesMap[$name](); + } + + return self::$_typeObjects[$name]; + } + + /** + * Adds a custom type to the type map. + * + * @static + * @param string $name Name of the type. This should correspond to what getName() returns. + * @param string $className The class name of the custom type. + * @throws DBALException + */ + public static function addType($name, $className) + { + if (isset(self::$_typesMap[$name])) { + throw DBALException::typeExists($name); + } + + self::$_typesMap[$name] = $className; + } + + /** + * Checks if exists support for a type. + * + * @static + * @param string $name Name of the type + * @return boolean TRUE if type is supported; FALSE otherwise + */ + public static function hasType($name) + { + return isset(self::$_typesMap[$name]); + } + + /** + * Overrides an already defined type to use a different implementation. + * + * @static + * @param string $name + * @param string $className + * @throws DBALException + */ + public static function overrideType($name, $className) + { + if ( ! isset(self::$_typesMap[$name])) { + throw DBALException::typeNotFound($name); + } + + if (isset(self::$_typeObjects[$name])) { + unset(self::$_typeObjects[$name]); + } + + self::$_typesMap[$name] = $className; + } + + /** + * Gets the (preferred) binding type for values of this type that + * can be used when binding parameters to prepared statements. + * + * This method should return one of the PDO::PARAM_* constants, that is, one of: + * + * PDO::PARAM_BOOL + * PDO::PARAM_NULL + * PDO::PARAM_INT + * PDO::PARAM_STR + * PDO::PARAM_LOB + * + * @return integer + */ + public function getBindingType() + { + return \PDO::PARAM_STR; + } + + /** + * Get the types array map which holds all registered types and the corresponding + * type class + * + * @return array $typesMap + */ + public static function getTypesMap() + { + return self::$_typesMap; + } + + public function __toString() + { + $e = explode('\\', get_class($this)); + return str_replace('Type', '', end($e)); + } + + /** + * Does working with this column require SQL conversion functions? + * + * This is a metadata function that is required for example in the ORM. + * Usage of {@link convertToDatabaseValueSQL} and + * {@link convertToPHPValueSQL} works for any type and mostly + * does nothing. This method can additionally be used for optimization purposes. + * + * @return bool + */ + public function canRequireSQLConversion() + { + return false; + } + + /** + * Modifies the SQL expression (identifier, parameter) to convert to a database value. + * + * @param string $sqlExpr + * @param AbstractPlatform $platform + * @return string + */ + public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) + { + return $sqlExpr; + } + + /** + * Modifies the SQL expression (identifier, parameter) to convert to a PHP value. + * + * @param string $sqlExpr + * @param AbstractPlatform $platform + * @return string + */ + public function convertToPHPValueSQL($sqlExpr, $platform) + { + return $sqlExpr; + } + + /** + * Get an array of database types that map to this Doctrine type. + * + * @param AbstractPlatform $platform + * @return array + */ + public function getMappedDatabaseTypes(AbstractPlatform $platform) + { + return array(); + } + + /** + * If this Doctrine Type maps to an already mapped database type, + * reverse schema engineering can't take them apart. You need to mark + * one of those types as commented, which will have Doctrine use an SQL + * comment to typehint the actual Doctrine Type. + * + * @param AbstractPlatform $platform + * @return bool + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + return false; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeType.php new file mode 100644 index 00000000..70858223 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/VarDateTimeType.php @@ -0,0 +1,60 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Variable DateTime Type using date_create() instead of DateTime::createFromFormat() + * + * This type has performance implications as it runs twice as long as the regular + * {@see DateTimeType}, however in certain PostgreSQL configurations with + * TIMESTAMP(n) columns where n > 0 it is necessary to use this type. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class VarDateTimeType extends DateTimeType +{ + /** + * @throws ConversionException + * @param string $value + * @param AbstractPlatform $platform + * @return \DateTime + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateTime) { + return $value; + } + + $val = date_create($value); + if ( ! $val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Version.php b/doctrine/dbal/lib/Doctrine/DBAL/Version.php new file mode 100644 index 00000000..343c7616 --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/Version.php @@ -0,0 +1,55 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Class to store and retrieve the version of Doctrine + * + * + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Version +{ + /** + * Current Doctrine Version + */ + const VERSION = '2.3.1'; + + /** + * Compares a Doctrine version with the current one. + * + * @param string $version Doctrine version to compare. + * @return int Returns -1 if older, 0 if it is the same, 1 if version + * passed as argument is newer. + */ + public static function compare($version) + { + $currentVersion = str_replace(' ', '', strtolower(self::VERSION)); + $version = str_replace(' ', '', $version); + + return version_compare($version, $currentVersion); + } +} diff --git a/doctrine/dbal/phpunit.xml.dist b/doctrine/dbal/phpunit.xml.dist new file mode 100644 index 00000000..2290d75a --- /dev/null +++ b/doctrine/dbal/phpunit.xml.dist @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + ./tests/Doctrine/Tests/DBAL + + + \ No newline at end of file diff --git a/doctrine/dbal/run-all.sh b/doctrine/dbal/run-all.sh new file mode 100755 index 00000000..d0140c84 --- /dev/null +++ b/doctrine/dbal/run-all.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# This script is a small convenience wrapper for running the doctrine testsuite against a large bunch of databases. +# Just create the phpunit.xmls as described in the array below and configure the specific files section +# to connect to that database. Just omit a file if you dont have that database and the tests will be skipped. + +configs[1]="mysql.phpunit.xml" +configs[2]='postgres.phpunit.xml' +configs[3]='sqlite.phpunit.xml' +configs[4]='oracle.phpunit.xml' +configs[5]='db2.phpunit.xml' +configs[6]='pdo-ibm.phpunit.xml' +configs[7]='sqlsrv.phpunit.xml' + +for i in "${configs[@]}"; do + if [ -f "$i" ]; + then + echo "RUNNING TESTS WITH CONFIG $i" + phpunit -c "$i" "$@" + fi; +done diff --git a/doctrine/dbal/tests/.gitignore b/doctrine/dbal/tests/.gitignore new file mode 100644 index 00000000..72104052 --- /dev/null +++ b/doctrine/dbal/tests/.gitignore @@ -0,0 +1,3 @@ +Doctrine/Tests/Proxies/ +Doctrine/Tests/ORM/Proxy/generated/ +Doctrine/Tests/ORM/Tools/Export/export diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/ConnectionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/ConnectionTest.php new file mode 100644 index 00000000..48fbb69f --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/ConnectionTest.php @@ -0,0 +1,177 @@ + 'pdo_mysql', + 'host' => 'localhost', + 'user' => 'root', + 'password' => 'password', + 'port' => '1234' + ); + $this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params); + } + + public function testIsConnected() + { + $this->assertFalse($this->_conn->isConnected()); + } + + public function testNoTransactionActiveByDefault() + { + $this->assertFalse($this->_conn->isTransactionActive()); + } + + public function testCommitWithNoActiveTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\DBAL\ConnectionException'); + $this->_conn->commit(); + } + + public function testRollbackWithNoActiveTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\DBAL\ConnectionException'); + $this->_conn->rollback(); + } + + public function testSetRollbackOnlyNoActiveTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\DBAL\ConnectionException'); + $this->_conn->setRollbackOnly(); + } + + public function testIsRollbackOnlyNoActiveTransaction_ThrowsException() + { + $this->setExpectedException('Doctrine\DBAL\ConnectionException'); + $this->_conn->isRollbackOnly(); + } + + public function testGetConfiguration() + { + $config = $this->_conn->getConfiguration(); + + $this->assertInstanceOf('Doctrine\DBAL\Configuration', $config); + } + + public function testGetHost() + { + $this->assertEquals('localhost', $this->_conn->getHost()); + } + + public function testGetPort() + { + $this->assertEquals('1234', $this->_conn->getPort()); + } + + public function testGetUsername() + { + $this->assertEquals('root', $this->_conn->getUsername()); + } + + public function testGetPassword() + { + $this->assertEquals('password', $this->_conn->getPassword()); + } + + public function testGetDriver() + { + $this->assertInstanceOf('Doctrine\DBAL\Driver\PDOMySql\Driver', $this->_conn->getDriver()); + } + + public function testGetEventManager() + { + $this->assertInstanceOf('Doctrine\Common\EventManager', $this->_conn->getEventManager()); + } + + public function testConnectDispatchEvent() + { + $listenerMock = $this->getMock('ConnectDispatchEventListener', array('postConnect')); + $listenerMock->expects($this->once())->method('postConnect'); + + $eventManager = new EventManager(); + $eventManager->addEventListener(array(Events::postConnect), $listenerMock); + + $driverMock = $this->getMock('Doctrine\DBAL\Driver'); + $driverMock->expects(($this->at(0))) + ->method('connect'); + $platform = new Mocks\MockPlatform(); + + $conn = new Connection(array('platform' => $platform), $driverMock, new Configuration(), $eventManager); + $conn->connect(); + } + + public function testEventManagerPassedToPlatform() + { + $this->assertInstanceOf('Doctrine\Common\EventManager', $this->_conn->getDatabasePlatform()->getEventManager()); + $this->assertSame($this->_conn->getEventManager(), $this->_conn->getDatabasePlatform()->getEventManager()); + } + + /** + * @expectedException Doctrine\DBAL\DBALException + * @dataProvider getQueryMethods + */ + public function testDriverExceptionIsWrapped($method) + { + $this->setExpectedException('Doctrine\DBAL\DBALException', "An exception occurred while executing 'MUUHAAAAHAAAA': + +SQLSTATE[HY000]: General error: 1 near \"MUUHAAAAHAAAA\""); + + $con = \Doctrine\DBAL\DriverManager::getConnection(array( + 'driver' => 'pdo_sqlite', + 'memory' => true, + )); + + $con->$method('MUUHAAAAHAAAA'); + } + + public function getQueryMethods() + { + return array( + array('exec'), + array('query'), + array('executeQuery'), + array('executeUpdate'), + array('prepare'), + ); + } + + /** + * Pretty dumb test, however we want to check that the EchoSQLLogger correctly implements the interface. + * + * @group DBAL-11 + */ + public function testEchoSQLLogger() + { + $logger = new \Doctrine\DBAL\Logging\EchoSQLLogger(); + $this->_conn->getConfiguration()->setSQLLogger($logger); + $this->assertSame($logger, $this->_conn->getConfiguration()->getSQLLogger()); + } + + /** + * Pretty dumb test, however we want to check that the DebugStack correctly implements the interface. + * + * @group DBAL-11 + */ + public function testDebugSQLStack() + { + $logger = new \Doctrine\DBAL\Logging\DebugStack(); + $this->_conn->getConfiguration()->setSQLLogger($logger); + $this->assertSame($logger, $this->_conn->getConfiguration()->getSQLLogger()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Driver/OCI8/OCI8StatementTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Driver/OCI8/OCI8StatementTest.php new file mode 100644 index 00000000..a3dfe2af --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Driver/OCI8/OCI8StatementTest.php @@ -0,0 +1,82 @@ +markTestSkipped('oci8 is not installed.'); + } + + parent::setUp(); + } + + /** + * This scenario shows that when the first parameter is not null + * it properly sets $hasZeroIndex to 1 and calls bindValue starting at 1. + * + * This also verifies that the statement will check with the connection to + * see what the current execution mode is. + * + * The expected exception is due to oci_execute failing due to no valid connection. + * + * @dataProvider executeDataProvider + * @expectedException \Doctrine\DBAL\Driver\OCI8\OCI8Exception + */ + public function testExecute(array $params) + { + $statement = $this->getMock('\Doctrine\DBAL\Driver\OCI8\OCI8Statement', + array('bindValue', 'errorInfo'), + array(), '', false); + + $statement->expects($this->at(0)) + ->method('bindValue') + ->with( + $this->equalTo(1), + $this->equalTo($params[0]) + ); + $statement->expects($this->at(1)) + ->method('bindValue') + ->with( + $this->equalTo(2), + $this->equalTo($params[1]) + ); + $statement->expects($this->at(2)) + ->method('bindValue') + ->with( + $this->equalTo(3), + $this->equalTo($params[2]) + ); + + // can't pass to constructor since we don't have a real database handle, + // but execute must check the connection for the executeMode + $conn = $this->getMock('\Doctrine\DBAL\Driver\OCI8\OCI8Connection', array('getExecuteMode'), array(), '', false); + $conn->expects($this->once()) + ->method('getExecuteMode'); + + $reflProperty = new \ReflectionProperty($statement, '_conn'); + $reflProperty->setAccessible(true); + $reflProperty->setValue($statement, $conn); + + $statement->execute($params); + } + + public static function executeDataProvider() + { + return array( + // $hasZeroIndex = isset($params[0]); == true + array( + array(0 => 'test', 1 => null, 2 => 'value') + ), + // $hasZeroIndex = isset($params[0]); == false + array( + array(0 => null, 1 => 'test', 2 => 'value') + ) + ); + } + +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/DriverManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/DriverManagerTest.php new file mode 100644 index 00000000..055b2aeb --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/DriverManagerTest.php @@ -0,0 +1,117 @@ + 'test' + ); + $test = \Doctrine\DBAL\DriverManager::getConnection($options); + } + + public function testValidPdoInstance() + { + $options = array( + 'pdo' => new \PDO('sqlite::memory:') + ); + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $this->assertEquals('sqlite', $conn->getDatabasePlatform()->getName()); + } + + /** + * @group DBAL-32 + */ + public function testPdoInstanceSetErrorMode() + { + $pdo = new \PDO('sqlite::memory:'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT); + $options = array( + 'pdo' => $pdo + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $this->assertEquals(\PDO::ERRMODE_EXCEPTION, $pdo->getAttribute(\PDO::ATTR_ERRMODE)); + } + + /** + * @expectedException \Doctrine\DBAL\DBALException + */ + public function testCheckParams() + { + $conn = \Doctrine\DBAL\DriverManager::getConnection(array()); + } + + /** + * @expectedException \Doctrine\DBAL\DBALException + */ + public function testInvalidDriver() + { + $conn = \Doctrine\DBAL\DriverManager::getConnection(array('driver' => 'invalid_driver')); + } + + public function testCustomPlatform() + { + $mockPlatform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $options = array( + 'pdo' => new \PDO('sqlite::memory:'), + 'platform' => $mockPlatform + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $this->assertSame($mockPlatform, $conn->getDatabasePlatform()); + } + + public function testCustomWrapper() + { + $wrapperClass = 'Doctrine\Tests\Mocks\ConnectionMock'; + + $options = array( + 'pdo' => new \PDO('sqlite::memory:'), + 'wrapperClass' => $wrapperClass, + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $this->assertInstanceOf($wrapperClass, $conn); + } + + public function testInvalidWrapperClass() + { + $this->setExpectedException('\Doctrine\DBAL\DBALException'); + + $options = array( + 'pdo' => new \PDO('sqlite::memory:'), + 'wrapperClass' => 'stdClass', + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + } + + public function testInvalidDriverClass() + { + $this->setExpectedException('\Doctrine\DBAL\DBALException'); + + $options = array( + 'driverClass' => 'stdClass' + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + } + + public function testValidDriverClass() + { + $options = array( + 'driverClass' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', + ); + + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $this->assertInstanceOf('Doctrine\DBAL\Driver\PDOMySql\Driver', $conn->getDriver()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/MysqlSessionInitTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/MysqlSessionInitTest.php new file mode 100644 index 00000000..1ef47dd5 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/MysqlSessionInitTest.php @@ -0,0 +1,33 @@ +getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + $connectionMock->expects($this->once()) + ->method('executeUpdate') + ->with($this->equalTo("SET NAMES foo COLLATE bar")); + + $eventArgs = new ConnectionEventArgs($connectionMock); + + + $listener = new MysqlSessionInit('foo', 'bar'); + $listener->postConnect($eventArgs); + } + + public function testGetSubscribedEvents() + { + $listener = new MysqlSessionInit(); + $this->assertEquals(array(Events::postConnect), $listener->getSubscribedEvents()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/OracleSessionInitTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/OracleSessionInitTest.php new file mode 100644 index 00000000..847ed6b1 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/OracleSessionInitTest.php @@ -0,0 +1,33 @@ +getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + $connectionMock->expects($this->once()) + ->method('executeUpdate') + ->with($this->isType('string')); + + $eventArgs = new ConnectionEventArgs($connectionMock); + + + $listener = new OracleSessionInit(); + $listener->postConnect($eventArgs); + } + + public function testGetSubscribedEvents() + { + $listener = new OracleSessionInit(); + $this->assertEquals(array(Events::postConnect), $listener->getSubscribedEvents()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/SQLSessionInitTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/SQLSessionInitTest.php new file mode 100644 index 00000000..ed2dd43d --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Events/SQLSessionInitTest.php @@ -0,0 +1,35 @@ +getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + $connectionMock->expects($this->once()) + ->method('exec') + ->with($this->equalTo("SET SEARCH_PATH TO foo, public, TIMEZONE TO 'Europe/Berlin'")); + + $eventArgs = new ConnectionEventArgs($connectionMock); + + $listener = new SQLSessionInit("SET SEARCH_PATH TO foo, public, TIMEZONE TO 'Europe/Berlin'"); + $listener->postConnect($eventArgs); + } + + public function testGetSubscribedEvents() + { + $listener = new SQLSessionInit("SET SEARCH_PATH TO foo, public, TIMEZONE TO 'Europe/Berlin'"); + $this->assertEquals(array(Events::postConnect), $listener->getSubscribedEvents()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/BlobTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/BlobTest.php new file mode 100644 index 00000000..7c565425 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/BlobTest.php @@ -0,0 +1,83 @@ +addColumn('id', 'integer'); + $table->addColumn('clobfield', 'text'); + $table->addColumn('blobfield', 'blob'); + $table->setPrimaryKey(array('id')); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + } catch(\Exception $e) { + + } + $this->_conn->exec($this->_conn->getDatabasePlatform()->getTruncateTableSQL('blob_table')); + } + + public function testInsert() + { + $ret = $this->_conn->insert('blob_table', + array('id' => 1, 'clobfield' => 'test', 'blobfield' => 'test'), + array(\PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_LOB) + ); + $this->assertEquals(1, $ret); + } + + public function testSelect() + { + $ret = $this->_conn->insert('blob_table', + array('id' => 1, 'clobfield' => 'test', 'blobfield' => 'test'), + array(\PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_LOB) + ); + + $this->assertBlobContains('test'); + } + + public function testUpdate() + { + $ret = $this->_conn->insert('blob_table', + array('id' => 1, 'clobfield' => 'test', 'blobfield' => 'test'), + array(\PDO::PARAM_INT, \PDO::PARAM_STR, \PDO::PARAM_LOB) + ); + + $this->_conn->update('blob_table', + array('blobfield' => 'test2'), + array('id' => 1), + array(\PDO::PARAM_LOB, \PDO::PARAM_INT) + ); + + $this->assertBlobContains('test2'); + } + + private function assertBlobContains($text) + { + $rows = $this->_conn->fetchAll('SELECT * FROM blob_table'); + + $this->assertEquals(1, count($rows)); + $row = array_change_key_case($rows[0], CASE_LOWER); + + $blobValue = Type::getType('blob')->convertToPHPValue($row['blobfield'], $this->_conn->getDatabasePlatform()); + + $this->assertInternalType('resource', $blobValue); + $this->assertEquals($text, stream_get_contents($blobValue)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php new file mode 100644 index 00000000..31da42f3 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php @@ -0,0 +1,219 @@ +resetSharedConn(); + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + $this->resetSharedConn(); + } + + public function testGetWrappedConnection() + { + $this->assertInstanceOf('Doctrine\DBAL\Driver\Connection', $this->_conn->getWrappedConnection()); + } + + public function testCommitWithRollbackOnlyThrowsException() + { + $this->_conn->beginTransaction(); + $this->_conn->setRollbackOnly(); + $this->setExpectedException('Doctrine\DBAL\ConnectionException'); + $this->_conn->commit(); + } + + public function testTransactionNestingBehavior() + { + try { + $this->_conn->beginTransaction(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + + try { + $this->_conn->beginTransaction(); + $this->assertEquals(2, $this->_conn->getTransactionNestingLevel()); + throw new \Exception; + $this->_conn->commit(); // never reached + } catch (\Exception $e) { + $this->_conn->rollback(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + //no rethrow + } + $this->assertTrue($this->_conn->isRollbackOnly()); + + $this->_conn->commit(); // should throw exception + $this->fail('Transaction commit after failed nested transaction should fail.'); + } catch (ConnectionException $e) { + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + $this->_conn->rollback(); + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } + } + + public function testTransactionNestingBehaviorWithSavepoints() + { + if (!$this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->_conn->setNestTransactionsWithSavepoints(true); + try { + $this->_conn->beginTransaction(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + + try { + $this->_conn->beginTransaction(); + $this->assertEquals(2, $this->_conn->getTransactionNestingLevel()); + $this->_conn->beginTransaction(); + $this->assertEquals(3, $this->_conn->getTransactionNestingLevel()); + $this->_conn->commit(); + $this->assertEquals(2, $this->_conn->getTransactionNestingLevel()); + throw new \Exception; + $this->_conn->commit(); // never reached + } catch (\Exception $e) { + $this->_conn->rollback(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + //no rethrow + } + $this->assertFalse($this->_conn->isRollbackOnly()); + try { + $this->_conn->setNestTransactionsWithSavepoints(false); + $this->fail('Should not be able to disable savepoints in usage for nested transactions inside an open transaction.'); + } catch (ConnectionException $e) { + $this->assertTrue($this->_conn->getNestTransactionsWithSavepoints()); + } + $this->_conn->commit(); // should not throw exception + } catch (ConnectionException $e) { + $this->fail('Transaction commit after failed nested transaction should not fail when using savepoints.'); + $this->_conn->rollback(); + } + } + + public function testTransactionNestingBehaviorCantBeChangedInActiveTransaction() + { + if (!$this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform to support savepoints.'); + } + + $this->_conn->beginTransaction(); + try { + $this->_conn->setNestTransactionsWithSavepoints(true); + $this->fail('An exception should have been thrown by chaning the nesting transaction behavior within an transaction.'); + } catch(ConnectionException $e) { + $this->_conn->rollBack(); + } + } + + public function testSetNestedTransactionsThroughSavepointsNotSupportedThrowsException() + { + if ($this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform not to support savepoints.'); + } + + $this->setExpectedException('Doctrine\DBAL\ConnectionException', "Savepoints are not supported by this driver."); + + $this->_conn->setNestTransactionsWithSavepoints(true); + } + + public function testCreateSavepointsNotSupportedThrowsException() + { + if ($this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform not to support savepoints.'); + } + + $this->setExpectedException('Doctrine\DBAL\ConnectionException', "Savepoints are not supported by this driver."); + + $this->_conn->createSavepoint('foo'); + } + + public function testReleaseSavepointsNotSupportedThrowsException() + { + if ($this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform not to support savepoints.'); + } + + $this->setExpectedException('Doctrine\DBAL\ConnectionException', "Savepoints are not supported by this driver."); + + $this->_conn->releaseSavepoint('foo'); + } + + public function testRollbackSavepointsNotSupportedThrowsException() + { + if ($this->_conn->getDatabasePlatform()->supportsSavepoints()) { + $this->markTestSkipped('This test requires the platform not to support savepoints.'); + } + + $this->setExpectedException('Doctrine\DBAL\ConnectionException', "Savepoints are not supported by this driver."); + + $this->_conn->rollbackSavepoint('foo'); + } + + public function testTransactionBehaviorWithRollback() + { + try { + $this->_conn->beginTransaction(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + + throw new \Exception; + + $this->_conn->commit(); // never reached + } catch (\Exception $e) { + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + $this->_conn->rollback(); + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } + } + + public function testTransactionBehaviour() + { + try { + $this->_conn->beginTransaction(); + $this->assertEquals(1, $this->_conn->getTransactionNestingLevel()); + $this->_conn->commit(); + } catch (\Exception $e) { + $this->_conn->rollback(); + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } + + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } + + public function testTransactionalWithException() + { + try { + $this->_conn->transactional(function($conn) { + $conn->executeQuery($conn->getDatabasePlatform()->getDummySelectSQL()); + throw new \RuntimeException("Ooops!"); + }); + } catch (\RuntimeException $expected) { + $this->assertEquals(0, $this->_conn->getTransactionNestingLevel()); + } + } + + public function testTransactional() + { + $this->_conn->transactional(function($conn) { + /* @var $conn Connection */ + $conn->executeQuery($conn->getDatabasePlatform()->getDummySelectSQL()); + }); + } + + /** + * Tests that the quote function accepts DBAL and PDO types. + */ + public function testQuote() + { + $this->assertEquals($this->_conn->quote("foo", Type::STRING), $this->_conn->quote("foo", \PDO::PARAM_STR)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/DataAccessTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/DataAccessTest.php new file mode 100644 index 00000000..4b57a447 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/DataAccessTest.php @@ -0,0 +1,543 @@ +addColumn('test_int', 'integer'); + $table->addColumn('test_string', 'string'); + $table->addColumn('test_datetime', 'datetime', array('notnull' => false)); + $table->setPrimaryKey(array('test_int')); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + + $this->_conn->insert('fetch_table', array('test_int' => 1, 'test_string' => 'foo', 'test_datetime' => '2010-01-01 10:10:10')); + self::$generated = true; + } + } + + public function testPrepareWithBindValue() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindValue(1, 1); + $stmt->bindValue(2, 'foo'); + $stmt->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $row = array_change_key_case($row, \CASE_LOWER); + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row); + } + + public function testPrepareWithBindParam() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindParam(1, $paramInt); + $stmt->bindParam(2, $paramStr); + $stmt->execute(); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $row = array_change_key_case($row, \CASE_LOWER); + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row); + } + + public function testPrepareWithFetchAll() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindParam(1, $paramInt); + $stmt->bindParam(2, $paramStr); + $stmt->execute(); + + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $rows[0] = array_change_key_case($rows[0], \CASE_LOWER); + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $rows[0]); + } + + /** + * @group DBAL-228 + */ + public function testPrepareWithFetchAllBoth() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindParam(1, $paramInt); + $stmt->bindParam(2, $paramStr); + $stmt->execute(); + + $rows = $stmt->fetchAll(\PDO::FETCH_BOTH); + $rows[0] = array_change_key_case($rows[0], \CASE_LOWER); + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo', 0 => 1, 1 => 'foo'), $rows[0]); + } + + public function testPrepareWithFetchColumn() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindParam(1, $paramInt); + $stmt->bindParam(2, $paramStr); + $stmt->execute(); + + $column = $stmt->fetchColumn(); + $this->assertEquals(1, $column); + } + + public function testPrepareWithIterator() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + + $stmt->bindParam(1, $paramInt); + $stmt->bindParam(2, $paramStr); + $stmt->execute(); + + $rows = array(); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + foreach ($stmt as $row) { + $rows[] = array_change_key_case($row, \CASE_LOWER); + } + + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $rows[0]); + } + + public function testPrepareWithQuoted() + { + $table = 'fetch_table'; + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM " . $this->_conn->quoteIdentifier($table) . " ". + "WHERE test_int = " . $this->_conn->quote($paramInt) . " AND test_string = " . $this->_conn->quote($paramStr); + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + } + + public function testPrepareWithExecuteParams() + { + $paramInt = 1; + $paramStr = 'foo'; + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->prepare($sql); + $this->assertInstanceOf('Doctrine\DBAL\Statement', $stmt); + $stmt->execute(array($paramInt, $paramStr)); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $this->assertTrue($row !== false); + $row = array_change_key_case($row, \CASE_LOWER); + $this->assertEquals(array('test_int' => 1, 'test_string' => 'foo'), $row); + } + + public function testFetchAll() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $data = $this->_conn->fetchAll($sql, array(1, 'foo')); + + $this->assertEquals(1, count($data)); + + $row = $data[0]; + $this->assertEquals(2, count($row)); + + $row = array_change_key_case($row, \CASE_LOWER); + $this->assertEquals(1, $row['test_int']); + $this->assertEquals('foo', $row['test_string']); + } + + public function testFetchBoth() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $row = $this->_conn->executeQuery($sql, array(1, 'foo'))->fetch(\PDO::FETCH_BOTH); + + $this->assertTrue($row !== false); + + $row = array_change_key_case($row, \CASE_LOWER); + + $this->assertEquals(1, $row['test_int']); + $this->assertEquals('foo', $row['test_string']); + $this->assertEquals(1, $row[0]); + $this->assertEquals('foo', $row[1]); + } + + public function testFetchRow() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $row = $this->_conn->fetchAssoc($sql, array(1, 'foo')); + + $this->assertTrue($row !== false); + + $row = array_change_key_case($row, \CASE_LOWER); + + $this->assertEquals(1, $row['test_int']); + $this->assertEquals('foo', $row['test_string']); + } + + public function testFetchArray() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $row = $this->_conn->fetchArray($sql, array(1, 'foo')); + + $this->assertEquals(1, $row[0]); + $this->assertEquals('foo', $row[1]); + } + + public function testFetchColumn() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $testInt = $this->_conn->fetchColumn($sql, array(1, 'foo'), 0); + + $this->assertEquals(1, $testInt); + + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $testString = $this->_conn->fetchColumn($sql, array(1, 'foo'), 1); + + $this->assertEquals('foo', $testString); + } + + /** + * @group DDC-697 + */ + public function testExecuteQueryBindDateTimeType() + { + $sql = 'SELECT count(*) AS c FROM fetch_table WHERE test_datetime = ?'; + $stmt = $this->_conn->executeQuery($sql, + array(1 => new \DateTime('2010-01-01 10:10:10')), + array(1 => Type::DATETIME) + ); + + $this->assertEquals(1, $stmt->fetchColumn()); + } + + /** + * @group DDC-697 + */ + public function testExecuteUpdateBindDateTimeType() + { + $datetime = new \DateTime('2010-02-02 20:20:20'); + + $sql = 'INSERT INTO fetch_table (test_int, test_string, test_datetime) VALUES (?, ?, ?)'; + $affectedRows = $this->_conn->executeUpdate($sql, + array(1 => 50, 2 => 'foo', 3 => $datetime), + array(1 => PDO::PARAM_INT, 2 => PDO::PARAM_STR, 3 => Type::DATETIME) + ); + + $this->assertEquals(1, $affectedRows); + $this->assertEquals(1, $this->_conn->executeQuery( + 'SELECT count(*) AS c FROM fetch_table WHERE test_datetime = ?', + array(1 => $datetime), + array(1 => Type::DATETIME) + )->fetchColumn()); + } + + /** + * @group DDC-697 + */ + public function testPrepareQueryBindValueDateTimeType() + { + $sql = 'SELECT count(*) AS c FROM fetch_table WHERE test_datetime = ?'; + $stmt = $this->_conn->prepare($sql); + $stmt->bindValue(1, new \DateTime('2010-01-01 10:10:10'), Type::DATETIME); + $stmt->execute(); + + $this->assertEquals(1, $stmt->fetchColumn()); + } + + /** + * @group DBAL-78 + */ + public function testNativeArrayListSupport() + { + for ($i = 100; $i < 110; $i++) { + $this->_conn->insert('fetch_table', array('test_int' => $i, 'test_string' => 'foo' . $i, 'test_datetime' => '2010-01-01 10:10:10')); + } + + $stmt = $this->_conn->executeQuery('SELECT test_int FROM fetch_table WHERE test_int IN (?)', + array(array(100, 101, 102, 103, 104)), array(Connection::PARAM_INT_ARRAY)); + + $data = $stmt->fetchAll(PDO::FETCH_NUM); + $this->assertEquals(5, count($data)); + $this->assertEquals(array(array(100), array(101), array(102), array(103), array(104)), $data); + + $stmt = $this->_conn->executeQuery('SELECT test_int FROM fetch_table WHERE test_string IN (?)', + array(array('foo100', 'foo101', 'foo102', 'foo103', 'foo104')), array(Connection::PARAM_STR_ARRAY)); + + $data = $stmt->fetchAll(PDO::FETCH_NUM); + $this->assertEquals(5, count($data)); + $this->assertEquals(array(array(100), array(101), array(102), array(103), array(104)), $data); + } + + /** + * @group DDC-1014 + */ + public function testDateArithmetics() + { + $p = $this->_conn->getDatabasePlatform(); + $sql = 'SELECT '; + $sql .= $p->getDateDiffExpression('test_datetime', $p->getCurrentTimestampSQL()) .' AS diff, '; + $sql .= $p->getDateAddDaysExpression('test_datetime', 10) .' AS add_days, '; + $sql .= $p->getDateSubDaysExpression('test_datetime', 10) .' AS sub_days, '; + $sql .= $p->getDateAddMonthExpression('test_datetime', 2) .' AS add_month, '; + $sql .= $p->getDateSubMonthExpression('test_datetime', 2) .' AS sub_month '; + $sql .= 'FROM fetch_table'; + + $row = $this->_conn->fetchAssoc($sql); + $row = array_change_key_case($row, CASE_LOWER); + + $diff = floor( (strtotime('2010-01-01')-time()) / 3600 / 24); + $this->assertEquals($diff, (int)$row['diff'], "Date difference should be approx. ".$diff." days.", 1); + $this->assertEquals('2010-01-11', date('Y-m-d', strtotime($row['add_days'])), "Adding date should end up on 2010-01-11"); + $this->assertEquals('2009-12-22', date('Y-m-d', strtotime($row['sub_days'])), "Subtracting date should end up on 2009-12-22"); + $this->assertEquals('2010-03-01', date('Y-m-d', strtotime($row['add_month'])), "Adding month should end up on 2010-03-01"); + $this->assertEquals('2009-11-01', date('Y-m-d', strtotime($row['sub_month'])), "Adding month should end up on 2009-11-01"); + } + + public function testQuoteSQLInjection() + { + $sql = "SELECT * FROM fetch_table WHERE test_string = " . $this->_conn->quote("bar' OR '1'='1"); + $rows = $this->_conn->fetchAll($sql); + + $this->assertEquals(0, count($rows), "no result should be returned, otherwise SQL injection is possible"); + } + + /** + * @group DDC-1213 + */ + public function testBitComparisonExpressionSupport() + { + $this->_conn->executeQuery('DELETE FROM fetch_table')->execute(); + $platform = $this->_conn->getDatabasePlatform(); + $bitmap = array(); + + for ($i = 2; $i < 9; $i = $i + 2) { + $bitmap[$i] = array( + 'bit_or' => ($i | 2), + 'bit_and' => ($i & 2) + ); + $this->_conn->insert('fetch_table', array( + 'test_int' => $i, + 'test_string' => json_encode($bitmap[$i]), + 'test_datetime' => '2010-01-01 10:10:10' + )); + } + + $sql[] = 'SELECT '; + $sql[] = 'test_int, '; + $sql[] = 'test_string, '; + $sql[] = $platform->getBitOrComparisonExpression('test_int', 2) . ' AS bit_or, '; + $sql[] = $platform->getBitAndComparisonExpression('test_int', 2) . ' AS bit_and '; + $sql[] = 'FROM fetch_table'; + + $stmt = $this->_conn->executeQuery(implode(PHP_EOL, $sql)); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + + + $this->assertEquals(4, count($data)); + $this->assertEquals(count($bitmap), count($data)); + foreach ($data as $row) { + $row = array_change_key_case($row, CASE_LOWER); + + $this->assertArrayHasKey('test_int', $row); + + $id = $row['test_int']; + + $this->assertArrayHasKey($id, $bitmap); + $this->assertArrayHasKey($id, $bitmap); + + $this->assertArrayHasKey('bit_or', $row); + $this->assertArrayHasKey('bit_and', $row); + + $this->assertEquals($row['bit_or'], $bitmap[$id]['bit_or']); + $this->assertEquals($row['bit_and'], $bitmap[$id]['bit_and']); + } + } + + public function testSetDefaultFetchMode() + { + $stmt = $this->_conn->query("SELECT * FROM fetch_table"); + $stmt->setFetchMode(\PDO::FETCH_NUM); + + $row = array_keys($stmt->fetch()); + $this->assertEquals(0, count( array_filter($row, function($v) { return ! is_numeric($v); })), "should be no non-numerical elements in the result."); + } + + /** + * @group DBAL-196 + */ + public function testFetchAllSupportFetchClass() + { + $this->skipOci8AndMysqli(); + $this->setupFixture(); + + $sql = "SELECT test_int, test_string, test_datetime FROM fetch_table"; + $stmt = $this->_conn->prepare($sql); + $stmt->execute(); + + $results = $stmt->fetchAll( + \PDO::FETCH_CLASS, + __NAMESPACE__.'\\MyFetchClass' + ); + + $this->assertEquals(1, count($results)); + $this->assertInstanceOf(__NAMESPACE__.'\\MyFetchClass', $results[0]); + + $this->assertEquals(1, $results[0]->test_int); + $this->assertEquals('foo', $results[0]->test_string); + $this->assertStringStartsWith('2010-01-01 10:10:10', $results[0]->test_datetime); + } + + /** + * @group DBAL-241 + */ + public function testFetchAllStyleColumn() + { + $sql = "DELETE FROM fetch_table"; + $this->_conn->executeUpdate($sql); + + $this->_conn->insert('fetch_table', array('test_int' => 1, 'test_string' => 'foo')); + $this->_conn->insert('fetch_table', array('test_int' => 10, 'test_string' => 'foo')); + + $sql = "SELECT test_int FROM fetch_table"; + $rows = $this->_conn->query($sql)->fetchAll(\PDO::FETCH_COLUMN); + + $this->assertEquals(array(1, 10), $rows); + } + + /** + * @group DBAL-214 + */ + public function testSetFetchModeClassFetchAll() + { + $this->skipOci8AndMysqli(); + $this->setupFixture(); + + $sql = "SELECT * FROM fetch_table"; + $stmt = $this->_conn->query($sql); + $stmt->setFetchMode(\PDO::FETCH_CLASS, __NAMESPACE__ . '\\MyFetchClass', array()); + + $results = $stmt->fetchAll(); + + $this->assertEquals(1, count($results)); + $this->assertInstanceOf(__NAMESPACE__.'\\MyFetchClass', $results[0]); + + $this->assertEquals(1, $results[0]->test_int); + $this->assertEquals('foo', $results[0]->test_string); + $this->assertStringStartsWith('2010-01-01 10:10:10', $results[0]->test_datetime); + } + + /** + * @group DBAL-214 + */ + public function testSetFetchModeClassFetch() + { + $this->skipOci8AndMysqli(); + $this->setupFixture(); + + $sql = "SELECT * FROM fetch_table"; + $stmt = $this->_conn->query($sql); + $stmt->setFetchMode(\PDO::FETCH_CLASS, __NAMESPACE__ . '\\MyFetchClass', array()); + + $results = array(); + while ($row = $stmt->fetch()) { + $results[] = $row; + } + + $this->assertEquals(1, count($results)); + $this->assertInstanceOf(__NAMESPACE__.'\\MyFetchClass', $results[0]); + + $this->assertEquals(1, $results[0]->test_int); + $this->assertEquals('foo', $results[0]->test_string); + $this->assertStringStartsWith('2010-01-01 10:10:10', $results[0]->test_datetime); + } + + /** + * @group DBAL-257 + */ + public function testEmptyFetchColumnReturnsFalse() + { + $this->_conn->executeQuery('DELETE FROM fetch_table')->execute(); + $this->assertFalse($this->_conn->fetchColumn('SELECT test_int FROM fetch_table')); + $this->assertFalse($this->_conn->query('SELECT test_int FROM fetch_table')->fetchColumn()); + } + + /** + * @group DBAL-339 + */ + public function testSetFetchModeOnDbalStatement() + { + $sql = "SELECT test_int, test_string FROM fetch_table WHERE test_int = ? AND test_string = ?"; + $stmt = $this->_conn->executeQuery($sql, array(1, "foo")); + $stmt->setFetchMode(\PDO::FETCH_NUM); + + while ($row = $stmt->fetch()) { + $this->assertTrue(isset($row[0])); + $this->assertTrue(isset($row[1])); + } + } + + private function setupFixture() + { + $this->_conn->executeQuery('DELETE FROM fetch_table')->execute(); + $this->_conn->insert('fetch_table', array( + 'test_int' => 1, + 'test_string' => 'foo', + 'test_datetime' => '2010-01-01 10:10:10' + )); + } + + private function skipOci8AndMysqli() + { + if (isset($GLOBALS['db_type']) && $GLOBALS['db_type'] == "oci8") { + $this->markTestSkipped("Not supported by OCI8"); + } + if ('mysqli' == $this->_conn->getDriver()->getName()) { + $this->markTestSkipped('Mysqli driver dont support this feature.'); + } + } +} + +class MyFetchClass +{ + public $test_int, $test_string, $test_datetime; +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/LoggingTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/LoggingTest.php new file mode 100644 index 00000000..7c968131 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/LoggingTest.php @@ -0,0 +1,52 @@ +_conn->getDatabasePlatform()->getDummySelectSQL(); + + $logMock = $this->getMock('Doctrine\DBAL\Logging\SQLLogger'); + $logMock->expects($this->at(0)) + ->method('startQuery') + ->with($this->equalTo($sql), $this->equalTo(array()), $this->equalTo(array())); + $logMock->expects($this->at(1)) + ->method('stopQuery'); + $this->_conn->getConfiguration()->setSQLLogger($logMock); + $this->_conn->executeQuery($sql, array()); + } + + public function testLogExecuteUpdate() + { + $this->markTestSkipped('Test breaks MySQL but works on all other platforms (Unbuffered Queries stuff).'); + + $sql = $this->_conn->getDatabasePlatform()->getDummySelectSQL(); + + $logMock = $this->getMock('Doctrine\DBAL\Logging\SQLLogger'); + $logMock->expects($this->at(0)) + ->method('startQuery') + ->with($this->equalTo($sql), $this->equalTo(array()), $this->equalTo(array())); + $logMock->expects($this->at(1)) + ->method('stopQuery'); + $this->_conn->getConfiguration()->setSQLLogger($logMock); + $this->_conn->executeUpdate($sql, array()); + } + + public function testLogPrepareExecute() + { + $sql = $this->_conn->getDatabasePlatform()->getDummySelectSQL(); + + $logMock = $this->getMock('Doctrine\DBAL\Logging\SQLLogger'); + $logMock->expects($this->once()) + ->method('startQuery') + ->with($this->equalTo($sql), $this->equalTo(array())); + $logMock->expects($this->at(1)) + ->method('stopQuery'); + $this->_conn->getConfiguration()->setSQLLogger($logMock); + + $stmt = $this->_conn->prepare($sql); + $stmt->execute(); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php new file mode 100644 index 00000000..d1fef14c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php @@ -0,0 +1,105 @@ +_conn->getDatabasePlatform()->getName() == "sqlite") { + $this->markTestSkipped('Test does not work on sqlite.'); + } + + try { + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $table = new \Doctrine\DBAL\Schema\Table("master_slave_table"); + $table->addColumn('test_int', 'integer'); + $table->setPrimaryKey(array('test_int')); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + + + } catch(\Exception $e) { + } + + $this->_conn->executeUpdate('DELETE FROM master_slave_table'); + $this->_conn->insert('master_slave_table', array('test_int' => 1)); + } + + public function createMasterSlaveConnection($keepSlave = false) + { + $params = $this->_conn->getParams(); + $params['master'] = $params; + $params['slaves'] = array($params, $params); + $params['keepSlave'] = $keepSlave; + $params['wrapperClass'] = 'Doctrine\DBAL\Connections\MasterSlaveConnection'; + + return DriverManager::getConnection($params); + } + + public function testMasterOnConnect() + { + $conn = $this->createMasterSlaveConnection(); + + $this->assertFalse($conn->isConnectedToMaster()); + $conn->connect('slave'); + $this->assertFalse($conn->isConnectedToMaster()); + $conn->connect('master'); + $this->assertTrue($conn->isConnectedToMaster()); + } + + public function testNoMasterOnExecuteQuery() + { + $conn = $this->createMasterSlaveConnection(); + + $sql = "SELECT count(*) as num FROM master_slave_table"; + $data = $conn->fetchAll($sql); + $data[0] = array_change_key_case($data[0], CASE_LOWER); + + $this->assertEquals(1, $data[0]['num']); + $this->assertFalse($conn->isConnectedToMaster()); + } + + public function testMasterOnWriteOperation() + { + $conn = $this->createMasterSlaveConnection(); + $conn->insert('master_slave_table', array('test_int' => 30)); + + $this->assertTrue($conn->isConnectedToMaster()); + + $sql = "SELECT count(*) as num FROM master_slave_table"; + $data = $conn->fetchAll($sql); + $data[0] = array_change_key_case($data[0], CASE_LOWER); + + $this->assertEquals(2, $data[0]['num']); + $this->assertTrue($conn->isConnectedToMaster()); + } + + /** + * @group DBAL-335 + */ + public function testKeepSlaveBeginTransactionStaysOnMaster() + { + $conn = $this->createMasterSlaveConnection($keepSlave = true); + $conn->connect('slave'); + + $conn->insert('master_slave_table', array('test_int' => 40)); + + $this->assertTrue($conn->isConnectedToMaster()); + + $conn->connect(); + $this->assertTrue($conn->isConnectedToMaster()); + + $conn->connect('slave'); + $this->assertFalse($conn->isConnectedToMaster()); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ModifyLimitQueryTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ModifyLimitQueryTest.php new file mode 100644 index 00000000..69ba6ac2 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ModifyLimitQueryTest.php @@ -0,0 +1,114 @@ +addColumn('test_int', 'integer'); + $table->setPrimaryKey(array('test_int')); + + $table2 = new \Doctrine\DBAL\Schema\Table("modify_limit_table2"); + $table2->addColumn('id', 'integer', array('autoincrement' => true)); + $table2->addColumn('test_int', 'integer'); + $table2->setPrimaryKey(array('id')); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + $sm->createTable($table2); + self::$tableCreated = true; + } + $this->_conn->exec($this->_conn->getDatabasePlatform()->getTruncateTableSQL('modify_limit_table')); + $this->_conn->exec($this->_conn->getDatabasePlatform()->getTruncateTableSQL('modify_limit_table2')); + } + + public function testModifyLimitQuerySimpleQuery() + { + $this->_conn->insert('modify_limit_table', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table', array('test_int' => 2)); + $this->_conn->insert('modify_limit_table', array('test_int' => 3)); + $this->_conn->insert('modify_limit_table', array('test_int' => 4)); + + $sql = "SELECT * FROM modify_limit_table"; + + $this->assertLimitResult(array(1, 2, 3, 4), $sql, 10, 0); + $this->assertLimitResult(array(1, 2), $sql, 2, 0); + $this->assertLimitResult(array(3, 4), $sql, 2, 2); + } + + public function testModifyLimitQueryJoinQuery() + { + $this->_conn->insert('modify_limit_table', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table', array('test_int' => 2)); + + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 2)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 2)); + + $sql = "SELECT modify_limit_table.test_int FROM modify_limit_table INNER JOIN modify_limit_table2 ON modify_limit_table.test_int = modify_limit_table2.test_int"; + + $this->assertLimitResult(array(1, 1, 1, 2, 2), $sql, 10, 0); + $this->assertLimitResult(array(1, 1, 1), $sql, 3, 0); + $this->assertLimitResult(array(2, 2), $sql, 2, 3); + } + + public function testModifyLimitQueryOrderBy() + { + $this->_conn->insert('modify_limit_table', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table', array('test_int' => 2)); + $this->_conn->insert('modify_limit_table', array('test_int' => 3)); + $this->_conn->insert('modify_limit_table', array('test_int' => 4)); + + $sql = "SELECT * FROM modify_limit_table ORDER BY test_int DESC"; + + $this->assertLimitResult(array(4, 3, 2, 1), $sql, 10, 0); + $this->assertLimitResult(array(4, 3), $sql, 2, 0); + $this->assertLimitResult(array(2, 1), $sql, 2, 2); + } + + public function testModifyLimitQueryGroupBy() + { + $this->_conn->insert('modify_limit_table', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table', array('test_int' => 2)); + + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 1)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 2)); + $this->_conn->insert('modify_limit_table2', array('test_int' => 2)); + + $sql = "SELECT modify_limit_table.test_int FROM modify_limit_table " . + "INNER JOIN modify_limit_table2 ON modify_limit_table.test_int = modify_limit_table2.test_int ". + "GROUP BY modify_limit_table.test_int"; + $this->assertLimitResult(array(1, 2), $sql, 10, 0); + $this->assertLimitResult(array(1), $sql, 1, 0); + $this->assertLimitResult(array(2), $sql, 1, 1); + } + + public function assertLimitResult($expectedResults, $sql, $limit, $offset) + { + $p = $this->_conn->getDatabasePlatform(); + $data = array(); + foreach ($this->_conn->fetchAll($p->modifyLimitQuery($sql, $limit, $offset)) AS $row) { + $row = array_change_key_case($row, CASE_LOWER); + $data[] = $row['test_int']; + } + $this->assertEquals($expectedResults, $data); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/NamedParametersTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/NamedParametersTest.php new file mode 100644 index 00000000..6f9e513c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/NamedParametersTest.php @@ -0,0 +1,166 @@ +1,'bar'=> array(1, 2, 3)), + array('foo'=>PDO::PARAM_INT,'bar'=> Connection::PARAM_INT_ARRAY,), + array( + array('id'=>1,'foo'=>1,'bar'=>1), + array('id'=>2,'foo'=>1,'bar'=>2), + array('id'=>3,'foo'=>1,'bar'=>3), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.foo = :foo AND f.bar IN (:bar)', + array('foo'=>1,'bar'=> array(1, 2, 3)), + array('bar'=> Connection::PARAM_INT_ARRAY,'foo'=>PDO::PARAM_INT), + array( + array('id'=>1,'foo'=>1,'bar'=>1), + array('id'=>2,'foo'=>1,'bar'=>2), + array('id'=>3,'foo'=>1,'bar'=>3), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar IN (:bar) AND f.foo = :foo', + array('foo'=>1,'bar'=> array(1, 2, 3)), + array('bar'=> Connection::PARAM_INT_ARRAY,'foo'=>PDO::PARAM_INT), + array( + array('id'=>1,'foo'=>1,'bar'=>1), + array('id'=>2,'foo'=>1,'bar'=>2), + array('id'=>3,'foo'=>1,'bar'=>3), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar IN (:bar) AND f.foo = :foo', + array('foo'=>1,'bar'=> array('1', '2', '3')), + array('bar'=> Connection::PARAM_STR_ARRAY,'foo'=>PDO::PARAM_INT), + array( + array('id'=>1,'foo'=>1,'bar'=>1), + array('id'=>2,'foo'=>1,'bar'=>2), + array('id'=>3,'foo'=>1,'bar'=>3), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar IN (:bar) AND f.foo IN (:foo)', + array('foo'=>array('1'),'bar'=> array(1, 2, 3,4)), + array('bar'=> Connection::PARAM_STR_ARRAY,'foo'=>Connection::PARAM_INT_ARRAY), + array( + array('id'=>1,'foo'=>1,'bar'=>1), + array('id'=>2,'foo'=>1,'bar'=>2), + array('id'=>3,'foo'=>1,'bar'=>3), + array('id'=>4,'foo'=>1,'bar'=>4), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar IN (:bar) AND f.foo IN (:foo)', + array('foo'=>1,'bar'=> 2), + array('bar'=>PDO::PARAM_INT,'foo'=>PDO::PARAM_INT), + array( + array('id'=>2,'foo'=>1,'bar'=>2), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar = :arg AND f.foo <> :arg', + array('arg'=>'1'), + array('arg'=>PDO::PARAM_STR), + array( + array('id'=>5,'foo'=>2,'bar'=>1), + ) + ), + + array( + 'SELECT * FROM ddc1372_foobar f WHERE f.bar NOT IN (:arg) AND f.foo IN (:arg)', + array('arg'=>array(1, 2)), + array('arg'=>Connection::PARAM_INT_ARRAY), + array( + array('id'=>3,'foo'=>1,'bar'=>3), + array('id'=>4,'foo'=>1,'bar'=>4), + ) + ), + + ); + } + + public function setUp() + { + parent::setUp(); + + if (!$this->_conn->getSchemaManager()->tablesExist("ddc1372_foobar")) { + try { + $table = new \Doctrine\DBAL\Schema\Table("ddc1372_foobar"); + $table->addColumn('id', 'integer'); + $table->addColumn('foo','string'); + $table->addColumn('bar','string'); + $table->setPrimaryKey(array('id')); + + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 1, 'foo' => 1, 'bar' => 1 + )); + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 2, 'foo' => 1, 'bar' => 2 + )); + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 3, 'foo' => 1, 'bar' => 3 + )); + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 4, 'foo' => 1, 'bar' => 4 + )); + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 5, 'foo' => 2, 'bar' => 1 + )); + $this->_conn->insert('ddc1372_foobar', array( + 'id' => 6, 'foo' => 2, 'bar' => 2 + )); + } catch(\Exception $e) { + $this->fail($e->getMessage()); + } + } + } + + /** + * @dataProvider ticketProvider + * @param string $query + * @param array $params + * @param array $types + * @param array $expected + */ + public function testTicket($query,$params,$types,$expected) + { + $stmt = $this->_conn->executeQuery($query, $params, $types); + $result = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + foreach ($result as $k => $v) { + $result[$k] = array_change_key_case($v, CASE_LOWER); + } + + $this->assertEquals($result, $expected); + } + +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/PortabilityTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/PortabilityTest.php new file mode 100644 index 00000000..acf995bb --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/PortabilityTest.php @@ -0,0 +1,97 @@ +portableConnection) { + $this->portableConnection->close(); + } + } + + private function getPortableConnection($portabilityMode = \Doctrine\DBAL\Portability\Connection::PORTABILITY_ALL, $case = \PDO::CASE_LOWER) + { + if (!$this->portableConnection) { + $params = $this->_conn->getParams(); + $params['wrapperClass'] = 'Doctrine\DBAL\Portability\Connection'; + $params['portability'] = $portabilityMode; + $params['fetch_case'] = $case; + $this->portableConnection = DriverManager::getConnection($params, $this->_conn->getConfiguration(), $this->_conn->getEventManager()); + + try { + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $table = new \Doctrine\DBAL\Schema\Table("portability_table"); + $table->addColumn('Test_Int', 'integer'); + $table->addColumn('Test_String', 'string', array('fixed' => true, 'length' => 32)); + $table->addColumn('Test_Null', 'string', array('notnull' => false)); + $table->setPrimaryKey(array('Test_Int')); + + $sm = $this->portableConnection->getSchemaManager(); + $sm->createTable($table); + + $this->portableConnection->insert('portability_table', array('Test_Int' => 1, 'Test_String' => 'foo', 'Test_Null' => '')); + $this->portableConnection->insert('portability_table', array('Test_Int' => 2, 'Test_String' => 'foo ', 'Test_Null' => null)); + } catch(\Exception $e) { + + } + } + + return $this->portableConnection; + } + + public function testFullFetchMode() + { + $rows = $this->getPortableConnection()->fetchAll('SELECT * FROM portability_table'); + $this->assertFetchResultRows($rows); + + $stmt = $this->getPortableConnection()->query('SELECT * FROM portability_table'); + $stmt->setFetchMode(\PDO::FETCH_ASSOC); + foreach ($stmt as $row) { + $this->assertFetchResultRow($row); + } + + $stmt = $this->getPortableConnection()->query('SELECT * FROM portability_table'); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $this->assertFetchResultRow($row); + } + + $stmt = $this->getPortableConnection()->prepare('SELECT * FROM portability_table'); + $stmt->execute(); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $this->assertFetchResultRow($row); + } + } + + public function assertFetchResultRows($rows) + { + $this->assertEquals(2, count($rows)); + foreach ($rows AS $row) { + $this->assertFetchResultRow($row); + } + } + + public function assertFetchResultRow($row) + { + $this->assertTrue(in_array($row['test_int'], array(1, 2)), "Primary key test_int should either be 1 or 2."); + $this->assertArrayHasKey('test_string', $row, "Case should be lowered."); + $this->assertEquals(3, strlen($row['test_string']), "test_string should be rtrimed to length of three for CHAR(32) column."); + $this->assertNull($row['test_null']); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php new file mode 100644 index 00000000..86abe915 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php @@ -0,0 +1,208 @@ + 100, 'test_string' => 'foo'), array('test_int' => 200, 'test_string' => 'bar'), array('test_int' => 300, 'test_string' => 'baz')); + private $sqlLogger; + + public function setUp() + { + parent::setUp(); + + try { + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $table = new \Doctrine\DBAL\Schema\Table("caching"); + $table->addColumn('test_int', 'integer'); + $table->addColumn('test_string', 'string', array('notnull' => false)); + $table->setPrimaryKey(array('test_int')); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + } catch(\Exception $e) { + + } + $this->_conn->executeUpdate('DELETE FROM caching'); + foreach ($this->expectedResult AS $row) { + $this->_conn->insert('caching', $row); + } + + $config = $this->_conn->getConfiguration(); + $config->setSQLLogger($this->sqlLogger = new \Doctrine\DBAL\Logging\DebugStack); + + $cache = new \Doctrine\Common\Cache\ArrayCache; + $config->setResultCacheImpl($cache); + } + + public function testCacheFetchAssoc() + { + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($this->expectedResult, \PDO::FETCH_ASSOC); + } + + public function testFetchNum() + { + $expectedResult = array(); + foreach ($this->expectedResult AS $v) { + $expectedResult[] = array_values($v); + } + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_NUM); + } + + public function testFetchBoth() + { + $expectedResult = array(); + foreach ($this->expectedResult AS $v) { + $expectedResult[] = array_merge($v, array_values($v)); + } + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_BOTH); + } + + public function testFetchColumn() + { + $expectedResult = array(); + foreach ($this->expectedResult AS $v) { + $expectedResult[] = array_shift($v); + } + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_COLUMN); + } + + public function testMixingFetch() + { + $numExpectedResult = array(); + foreach ($this->expectedResult AS $v) { + $numExpectedResult[] = array_values($v); + } + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $data = $this->hydrateStmt($stmt, \PDO::FETCH_ASSOC); + + $this->assertEquals($this->expectedResult, $data); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $data = $this->hydrateStmt($stmt, \PDO::FETCH_NUM); + + $this->assertEquals($numExpectedResult, $data); + } + + public function testIteratorFetch() + { + $this->assertStandardAndIteratorFetchAreEqual(\PDO::FETCH_BOTH); + $this->assertStandardAndIteratorFetchAreEqual(\PDO::FETCH_ASSOC); + $this->assertStandardAndIteratorFetchAreEqual(\PDO::FETCH_NUM); + } + + public function assertStandardAndIteratorFetchAreEqual($fetchMode) + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + $data = $this->hydrateStmt($stmt, $fetchMode); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + $data_iterator = $this->hydrateStmtIterator($stmt, $fetchMode); + + $this->assertEquals($data, $data_iterator); + } + + public function testDontCloseNoCache() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $data[] = $row; + } + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_NUM)) { + $data[] = $row; + } + + $this->assertEquals(2, count($this->sqlLogger->queries)); + } + + public function testDontFinishNoCache() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $data = $this->hydrateStmt($stmt, \PDO::FETCH_NUM); + + $this->assertEquals(2, count($this->sqlLogger->queries)); + } + + public function assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, $fetchMode) + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $this->assertEquals(2, $stmt->columnCount()); + $data = $this->hydrateStmt($stmt, $fetchMode); + $this->assertEquals($expectedResult, $data); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching ORDER BY test_int ASC", array(), array(), new QueryCacheProfile(10, "testcachekey")); + + $this->assertEquals(2, $stmt->columnCount()); + $data = $this->hydrateStmt($stmt, $fetchMode); + $this->assertEquals($expectedResult, $data); + $this->assertEquals(1, count($this->sqlLogger->queries), "just one dbal hit"); + } + + public function testEmptyResultCache() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey")); + $data = $this->hydrateStmt($stmt); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey")); + $data = $this->hydrateStmt($stmt); + + $this->assertEquals(1, count($this->sqlLogger->queries), "just one dbal hit"); + } + + public function testChangeCacheImpl() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey")); + $data = $this->hydrateStmt($stmt); + + $secondCache = new \Doctrine\Common\Cache\ArrayCache; + $stmt = $this->_conn->executeQuery("SELECT * FROM caching WHERE test_int > 500", array(), array(), new QueryCacheProfile(10, "emptycachekey", $secondCache)); + $data = $this->hydrateStmt($stmt); + + $this->assertEquals(2, count($this->sqlLogger->queries), "two hits"); + $this->assertEquals(1, count($secondCache->fetch("emptycachekey"))); + } + + private function hydrateStmt($stmt, $fetchMode = \PDO::FETCH_ASSOC) + { + $data = array(); + while ($row = $stmt->fetch($fetchMode)) { + $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row; + } + $stmt->closeCursor(); + return $data; + } + + private function hydrateStmtIterator($stmt, $fetchMode = \PDO::FETCH_ASSOC) + { + $data = array(); + $stmt->setFetchMode($fetchMode); + foreach ($stmt as $row) { + $data[] = is_array($row) ? array_change_key_case($row, CASE_LOWER) : $row; + } + $stmt->closeCursor(); + return $data; + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/Db2SchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/Db2SchemaManagerTest.php new file mode 100644 index 00000000..a567900c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/Db2SchemaManagerTest.php @@ -0,0 +1,12 @@ +addColumn('foo_id', 'integer'); + $tableOld->addColumn('bar_id', 'integer'); + $tableNew = clone $tableOld; + + $this->_sm->createTable($tableOld); + $tableFetched = $this->_sm->listTableDetails("switch_primary_key_columns"); + $tableNew = clone $tableFetched; + $tableNew->setPrimaryKey(array('bar_id', 'foo_id')); + + $comparator = new \Doctrine\DBAL\Schema\Comparator; + $this->_sm->alterTable($comparator->diffTable($tableFetched, $tableNew)); + } + + public function testDiffTableBug() + { + $schema = new Schema(); + $table = $schema->createTable('diffbug_routing_translations'); + $table->addColumn('id', 'integer'); + $table->addColumn('route', 'string'); + $table->addColumn('locale', 'string'); + $table->addColumn('attribute', 'string'); + $table->addColumn('localized_value', 'string'); + $table->addColumn('original_value', 'string'); + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('route', 'locale', 'attribute')); + $table->addIndex(array('localized_value')); // this is much more selective than the unique index + + $this->_sm->createTable($table); + $tableFetched = $this->_sm->listTableDetails("diffbug_routing_translations"); + + $comparator = new \Doctrine\DBAL\Schema\Comparator; + $diff = $comparator->diffTable($tableFetched, $table); + + $this->assertFalse($diff, "no changes expected."); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/OracleSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/OracleSchemaManagerTest.php new file mode 100644 index 00000000..231b5421 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/OracleSchemaManagerTest.php @@ -0,0 +1,39 @@ +markTestSkipped('Foo'); + } + + $username = $GLOBALS['db_username']; + + $query = "GRANT ALL PRIVILEGES TO ".$username; + + $conn = \Doctrine\Tests\TestUtil::getTempConnection(); + $conn->executeUpdate($query); + } + + public function testRenameTable() + { + $this->_sm->tryMethod('DropTable', 'list_tables_test'); + $this->_sm->tryMethod('DropTable', 'list_tables_test_new_name'); + + $this->createTestTable('list_tables_test'); + $this->_sm->renameTable('list_tables_test', 'list_tables_test_new_name'); + + $tables = $this->_sm->listTables(); + + $this->assertHasTable($tables, 'list_tables_test_new_name'); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php new file mode 100644 index 00000000..2cb9bd7a --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php @@ -0,0 +1,262 @@ +_conn) { + return; + } + + $this->_conn->getConfiguration()->setFilterSchemaAssetsExpression(null); + } + + /** + * @group DBAL-177 + */ + public function testGetSearchPath() + { + $params = $this->_conn->getParams(); + + $paths = $this->_sm->getSchemaSearchPaths(); + $this->assertEquals(array($params['user'], 'public'), $paths); + } + + /** + * @group DBAL-244 + */ + public function testGetSchemaNames() + { + $names = $this->_sm->getSchemaNames(); + + $this->assertInternalType('array', $names); + $this->assertTrue(count($names) > 0); + $this->assertTrue(in_array('public', $names), "The public schema should be found."); + } + + /** + * @group DBAL-21 + */ + public function testSupportDomainTypeFallback() + { + $createDomainTypeSQL = "CREATE DOMAIN MyMoney AS DECIMAL(18,2)"; + $this->_conn->exec($createDomainTypeSQL); + + $createTableSQL = "CREATE TABLE domain_type_test (id INT PRIMARY KEY, value MyMoney)"; + $this->_conn->exec($createTableSQL); + + $table = $this->_conn->getSchemaManager()->listTableDetails('domain_type_test'); + $this->assertInstanceOf('Doctrine\DBAL\Types\DecimalType', $table->getColumn('value')->getType()); + + Type::addType('MyMoney', 'Doctrine\Tests\DBAL\Functional\Schema\MoneyType'); + $this->_conn->getDatabasePlatform()->registerDoctrineTypeMapping('MyMoney', 'MyMoney'); + + $table = $this->_conn->getSchemaManager()->listTableDetails('domain_type_test'); + $this->assertInstanceOf('Doctrine\Tests\DBAL\Functional\Schema\MoneyType', $table->getColumn('value')->getType()); + } + + /** + * @group DBAL-37 + */ + public function testDetectsAutoIncrement() + { + $autoincTable = new \Doctrine\DBAL\Schema\Table('autoinc_table'); + $column = $autoincTable->addColumn('id', 'integer'); + $column->setAutoincrement(true); + $this->_sm->createTable($autoincTable); + $autoincTable = $this->_sm->listTableDetails('autoinc_table'); + + $this->assertTrue($autoincTable->getColumn('id')->getAutoincrement()); + } + + /** + * @group DBAL-37 + */ + public function testAlterTableAutoIncrementAdd() + { + $tableFrom = new \Doctrine\DBAL\Schema\Table('autoinc_table_add'); + $column = $tableFrom->addColumn('id', 'integer'); + $this->_sm->createTable($tableFrom); + $tableFrom = $this->_sm->listTableDetails('autoinc_table_add'); + $this->assertFalse($tableFrom->getColumn('id')->getAutoincrement()); + + $tableTo = new \Doctrine\DBAL\Schema\Table('autoinc_table_add'); + $column = $tableTo->addColumn('id', 'integer'); + $column->setAutoincrement(true); + + $c = new \Doctrine\DBAL\Schema\Comparator(); + $diff = $c->diffTable($tableFrom, $tableTo); + $sql = $this->_conn->getDatabasePlatform()->getAlterTableSQL($diff); + $this->assertEquals(array( + "CREATE SEQUENCE autoinc_table_add_id_seq", + "SELECT setval('autoinc_table_add_id_seq', (SELECT MAX(id) FROM autoinc_table_add))", + "ALTER TABLE autoinc_table_add ALTER id SET DEFAULT nextval('autoinc_table_add_id_seq')", + ), $sql); + + $this->_sm->alterTable($diff); + $tableFinal = $this->_sm->listTableDetails('autoinc_table_add'); + $this->assertTrue($tableFinal->getColumn('id')->getAutoincrement()); + } + + /** + * @group DBAL-37 + */ + public function testAlterTableAutoIncrementDrop() + { + $tableFrom = new \Doctrine\DBAL\Schema\Table('autoinc_table_drop'); + $column = $tableFrom->addColumn('id', 'integer'); + $column->setAutoincrement(true); + $this->_sm->createTable($tableFrom); + $tableFrom = $this->_sm->listTableDetails('autoinc_table_drop'); + $this->assertTrue($tableFrom->getColumn('id')->getAutoincrement()); + + $tableTo = new \Doctrine\DBAL\Schema\Table('autoinc_table_drop'); + $column = $tableTo->addColumn('id', 'integer'); + + $c = new \Doctrine\DBAL\Schema\Comparator(); + $diff = $c->diffTable($tableFrom, $tableTo); + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $diff, "There should be a difference and not false being returned from the table comparison"); + $this->assertEquals(array("ALTER TABLE autoinc_table_drop ALTER id DROP DEFAULT"), $this->_conn->getDatabasePlatform()->getAlterTableSQL($diff)); + + $this->_sm->alterTable($diff); + $tableFinal = $this->_sm->listTableDetails('autoinc_table_drop'); + $this->assertFalse($tableFinal->getColumn('id')->getAutoincrement()); + } + + /** + * @group DBAL-75 + */ + public function testTableWithSchema() + { + $this->_conn->exec('CREATE SCHEMA nested'); + + $nestedRelatedTable = new \Doctrine\DBAL\Schema\Table('nested.schemarelated'); + $column = $nestedRelatedTable->addColumn('id', 'integer'); + $column->setAutoincrement(true); + $nestedRelatedTable->setPrimaryKey(array('id')); + + $nestedSchemaTable = new \Doctrine\DBAL\Schema\Table('nested.schematable'); + $column = $nestedSchemaTable->addColumn('id', 'integer'); + $column->setAutoincrement(true); + $nestedSchemaTable->setPrimaryKey(array('id')); + $nestedSchemaTable->addUnnamedForeignKeyConstraint($nestedRelatedTable, array('id'), array('id')); + + $this->_sm->createTable($nestedRelatedTable); + $this->_sm->createTable($nestedSchemaTable); + + $tables = $this->_sm->listTableNames(); + $this->assertContains('nested.schematable', $tables, "The table should be detected with its non-public schema."); + + $nestedSchemaTable = $this->_sm->listTableDetails('nested.schematable'); + $this->assertTrue($nestedSchemaTable->hasColumn('id')); + $this->assertEquals(array('id'), $nestedSchemaTable->getPrimaryKey()->getColumns()); + + $relatedFks = $nestedSchemaTable->getForeignKeys(); + $this->assertEquals(1, count($relatedFks)); + $relatedFk = array_pop($relatedFks); + $this->assertEquals("nested.schemarelated", $relatedFk->getForeignTableName()); + } + + /** + * @group DBAL-91 + * @group DBAL-88 + */ + public function testReturnQuotedAssets() + { + $sql = 'create table dbal91_something ( id integer CONSTRAINT id_something PRIMARY KEY NOT NULL ,"table" integer );'; + $this->_conn->exec($sql); + + $sql = 'ALTER TABLE dbal91_something ADD CONSTRAINT something_input FOREIGN KEY( "table" ) REFERENCES dbal91_something ON UPDATE CASCADE;'; + $this->_conn->exec($sql); + + $table = $this->_sm->listTableDetails('dbal91_something'); + + $this->assertEquals( + array( + "CREATE TABLE dbal91_something (id INT NOT NULL, \"table\" INT DEFAULT NULL, PRIMARY KEY(id))", + "CREATE INDEX IDX_A9401304ECA7352B ON dbal91_something (\"table\")", + ), + $this->_conn->getDatabasePlatform()->getCreateTableSQL($table) + ); + } + + /** + * @group DBAL-204 + */ + public function testFilterSchemaExpression() + { + $testTable = new \Doctrine\DBAL\Schema\Table('dbal204_test_prefix'); + $column = $testTable->addColumn('id', 'integer'); + $this->_sm->createTable($testTable); + $testTable = new \Doctrine\DBAL\Schema\Table('dbal204_without_prefix'); + $column = $testTable->addColumn('id', 'integer'); + $this->_sm->createTable($testTable); + + $this->_conn->getConfiguration()->setFilterSchemaAssetsExpression('#^dbal204_#'); + $names = $this->_sm->listTableNames(); + $this->assertEquals(2, count($names)); + + $this->_conn->getConfiguration()->setFilterSchemaAssetsExpression('#^dbal204_test#'); + $names = $this->_sm->listTableNames(); + $this->assertEquals(1, count($names)); + } + + public function testListForeignKeys() + { + if(!$this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Does not support foreign key constraints.'); + } + + $fkOptions = array('SET NULL', 'SET DEFAULT', 'NO ACTION','CASCADE', 'RESTRICT'); + $foreignKeys = array(); + $fkTable = $this->getTestTable('test_create_fk1'); + for($i = 0; $i < count($fkOptions); $i++) { + $fkTable->addColumn("foreign_key_test$i", 'integer'); + $foreignKeys[] = new \Doctrine\DBAL\Schema\ForeignKeyConstraint( + array("foreign_key_test$i"), 'test_create_fk2', array('id'), "foreign_key_test_$i"."_fk", array('onDelete' => $fkOptions[$i])); + } + $this->_sm->dropAndCreateTable($fkTable); + $this->createTestTable('test_create_fk2'); + + foreach($foreignKeys as $foreignKey) { + $this->_sm->createForeignKey($foreignKey, 'test_create_fk1'); + } + $fkeys = $this->_sm->listTableForeignKeys('test_create_fk1'); + $this->assertEquals(count($foreignKeys), count($fkeys), "Table 'test_create_fk1' has to have " . count($foreignKeys) . " foreign keys."); + for ($i = 0; $i < count($fkeys); $i++) { + $this->assertEquals(array("foreign_key_test$i"), array_map('strtolower', $fkeys[$i]->getLocalColumns())); + $this->assertEquals(array('id'), array_map('strtolower', $fkeys[$i]->getForeignColumns())); + $this->assertEquals('test_create_fk2', strtolower($fkeys[0]->getForeignTableName())); + if ($foreignKeys[$i]->getOption('onDelete') == 'NO ACTION') { + $this->assertFalse($fkeys[$i]->hasOption('onDelete'), 'Unexpected option: '. $fkeys[$i]->getOption('onDelete')); + } else { + $this->assertEquals($foreignKeys[$i]->getOption('onDelete'), $fkeys[$i]->getOption('onDelete')); + } + } + } +} + +class MoneyType extends Type +{ + + public function getName() + { + return "MyMoney"; + } + + public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return 'MyMoney'; + } + +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SQLServerSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SQLServerSchemaManagerTest.php new file mode 100644 index 00000000..b6372b4a --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SQLServerSchemaManagerTest.php @@ -0,0 +1,37 @@ +addColumn('id', 'integer'); + $table->addColumn('todrop', 'decimal', array('default' => 10.2)); + + $this->_sm->createTable($table); + + $diff = new TableDiff('sqlsrv_drop_column', array(), array(), array( + new Column('todrop', Type::getType('decimal')) + )); + $this->_sm->alterTable($diff); + + $columns = $this->_sm->listTableColumns('sqlsrv_drop_column'); + $this->assertEquals(1, count($columns)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php new file mode 100644 index 00000000..2f30c3cc --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SchemaManagerFunctionalTestCase.php @@ -0,0 +1,647 @@ +getPlatformName(); + + if ($this->_conn->getDatabasePlatform()->getName() !== $dbms) { + $this->markTestSkipped(get_class($this) . ' requires the use of ' . $dbms); + } + + $this->_sm = $this->_conn->getSchemaManager(); + } + + /** + * @group DBAL-195 + */ + public function testDropAndCreateSequence() + { + if(!$this->_conn->getDatabasePlatform()->supportsSequences()) { + $this->markTestSkipped($this->_conn->getDriver()->getName().' does not support sequences.'); + } + + $sequence = new \Doctrine\DBAL\Schema\Sequence('dropcreate_sequences_test_seq', 20, 10); + $this->_sm->dropAndCreateSequence($sequence); + } + + public function testListSequences() + { + if(!$this->_conn->getDatabasePlatform()->supportsSequences()) { + $this->markTestSkipped($this->_conn->getDriver()->getName().' does not support sequences.'); + } + + $sequence = new \Doctrine\DBAL\Schema\Sequence('list_sequences_test_seq', 20, 10); + $this->_sm->createSequence($sequence); + + $sequences = $this->_sm->listSequences(); + + $this->assertInternalType('array', $sequences, 'listSequences() should return an array.'); + + $foundSequence = null; + foreach($sequences AS $sequence) { + $this->assertInstanceOf('Doctrine\DBAL\Schema\Sequence', $sequence, 'Array elements of listSequences() should be Sequence instances.'); + if(strtolower($sequence->getName()) == 'list_sequences_test_seq') { + $foundSequence = $sequence; + } + } + + $this->assertNotNull($foundSequence, "Sequence with name 'list_sequences_test_seq' was not found."); + $this->assertEquals(20, $foundSequence->getAllocationSize(), "Allocation Size is expected to be 20."); + $this->assertEquals(10, $foundSequence->getInitialValue(), "Initial Value is expected to be 10."); + } + + public function testListDatabases() + { + if (!$this->_sm->getDatabasePlatform()->supportsCreateDropDatabase()) { + $this->markTestSkipped('Cannot drop Database client side with this Driver.'); + } + + $this->_sm->dropAndCreateDatabase('test_create_database'); + $databases = $this->_sm->listDatabases(); + + $databases = \array_map('strtolower', $databases); + + $this->assertEquals(true, \in_array('test_create_database', $databases)); + } + + public function testListTables() + { + $this->createTestTable('list_tables_test'); + $tables = $this->_sm->listTables(); + + $this->assertInternalType('array', $tables); + $this->assertTrue(count($tables) > 0, "List Tables has to find at least one table named 'list_tables_test'."); + + $foundTable = false; + foreach ($tables AS $table) { + $this->assertInstanceOf('Doctrine\DBAL\Schema\Table', $table); + if (strtolower($table->getName()) == 'list_tables_test') { + $foundTable = true; + + $this->assertTrue($table->hasColumn('id')); + $this->assertTrue($table->hasColumn('test')); + $this->assertTrue($table->hasColumn('foreign_key_test')); + } + } + + $this->assertTrue( $foundTable , "The 'list_tables_test' table has to be found."); + } + + public function createListTableColumns() + { + $table = new \Doctrine\DBAL\Schema\Table('list_table_columns'); + $table->addColumn('id', 'integer', array('notnull' => true)); + $table->addColumn('test', 'string', array('length' => 255, 'notnull' => false, 'default' => 'expected default')); + $table->addColumn('foo', 'text', array('notnull' => true)); + $table->addColumn('bar', 'decimal', array('precision' => 10, 'scale' => 4, 'notnull' => false)); + $table->addColumn('baz1', 'datetime'); + $table->addColumn('baz2', 'time'); + $table->addColumn('baz3', 'date'); + $table->setPrimaryKey(array('id')); + + return $table; + } + + public function testListTableColumns() + { + $table = $this->createListTableColumns(); + + $this->_sm->dropAndCreateTable($table); + + $columns = $this->_sm->listTableColumns('list_table_columns'); + + $this->assertArrayHasKey('id', $columns); + $this->assertEquals('id', strtolower($columns['id']->getname())); + $this->assertInstanceOf('Doctrine\DBAL\Types\IntegerType', $columns['id']->gettype()); + $this->assertEquals(false, $columns['id']->getunsigned()); + $this->assertEquals(true, $columns['id']->getnotnull()); + $this->assertEquals(null, $columns['id']->getdefault()); + $this->assertInternalType('array', $columns['id']->getPlatformOptions()); + + $this->assertArrayHasKey('test', $columns); + $this->assertEquals('test', strtolower($columns['test']->getname())); + $this->assertInstanceOf('Doctrine\DBAL\Types\StringType', $columns['test']->gettype()); + $this->assertEquals(255, $columns['test']->getlength()); + $this->assertEquals(false, $columns['test']->getfixed()); + $this->assertEquals(false, $columns['test']->getnotnull()); + $this->assertEquals('expected default', $columns['test']->getdefault()); + $this->assertInternalType('array', $columns['test']->getPlatformOptions()); + + $this->assertEquals('foo', strtolower($columns['foo']->getname())); + $this->assertInstanceOf('Doctrine\DBAL\Types\TextType', $columns['foo']->gettype()); + $this->assertEquals(false, $columns['foo']->getunsigned()); + $this->assertEquals(false, $columns['foo']->getfixed()); + $this->assertEquals(true, $columns['foo']->getnotnull()); + $this->assertEquals(null, $columns['foo']->getdefault()); + $this->assertInternalType('array', $columns['foo']->getPlatformOptions()); + + $this->assertEquals('bar', strtolower($columns['bar']->getname())); + $this->assertInstanceOf('Doctrine\DBAL\Types\DecimalType', $columns['bar']->gettype()); + $this->assertEquals(null, $columns['bar']->getlength()); + $this->assertEquals(10, $columns['bar']->getprecision()); + $this->assertEquals(4, $columns['bar']->getscale()); + $this->assertEquals(false, $columns['bar']->getunsigned()); + $this->assertEquals(false, $columns['bar']->getfixed()); + $this->assertEquals(false, $columns['bar']->getnotnull()); + $this->assertEquals(null, $columns['bar']->getdefault()); + $this->assertInternalType('array', $columns['bar']->getPlatformOptions()); + + $this->assertEquals('baz1', strtolower($columns['baz1']->getname())); + $this->assertInstanceOf('Doctrine\DBAL\Types\DateTimeType', $columns['baz1']->gettype()); + $this->assertEquals(true, $columns['baz1']->getnotnull()); + $this->assertEquals(null, $columns['baz1']->getdefault()); + $this->assertInternalType('array', $columns['baz1']->getPlatformOptions()); + + $this->assertEquals('baz2', strtolower($columns['baz2']->getname())); + $this->assertContains($columns['baz2']->gettype()->getName(), array('time', 'date', 'datetime')); + $this->assertEquals(true, $columns['baz2']->getnotnull()); + $this->assertEquals(null, $columns['baz2']->getdefault()); + $this->assertInternalType('array', $columns['baz2']->getPlatformOptions()); + + $this->assertEquals('baz3', strtolower($columns['baz3']->getname())); + $this->assertContains($columns['baz2']->gettype()->getName(), array('time', 'date', 'datetime')); + $this->assertEquals(true, $columns['baz3']->getnotnull()); + $this->assertEquals(null, $columns['baz3']->getdefault()); + $this->assertInternalType('array', $columns['baz3']->getPlatformOptions()); + } + + public function testListTableColumnsDispatchEvent() + { + $table = $this->createListTableColumns(); + + $this->_sm->dropAndCreateTable($table); + + $listenerMock = $this->getMock('ListTableColumnsDispatchEventListener', array('onSchemaColumnDefinition')); + $listenerMock + ->expects($this->exactly(7)) + ->method('onSchemaColumnDefinition'); + + $oldEventManager = $this->_sm->getDatabasePlatform()->getEventManager(); + + $eventManager = new EventManager(); + $eventManager->addEventListener(array(Events::onSchemaColumnDefinition), $listenerMock); + + $this->_sm->getDatabasePlatform()->setEventManager($eventManager); + + $this->_sm->listTableColumns('list_table_columns'); + + $this->_sm->getDatabasePlatform()->setEventManager($oldEventManager); + } + + public function testListTableIndexesDispatchEvent() + { + $table = $this->getTestTable('list_table_indexes_test'); + $table->addUniqueIndex(array('test'), 'test_index_name'); + $table->addIndex(array('id', 'test'), 'test_composite_idx'); + + $this->_sm->dropAndCreateTable($table); + + $listenerMock = $this->getMock('ListTableIndexesDispatchEventListener', array('onSchemaIndexDefinition')); + $listenerMock + ->expects($this->exactly(3)) + ->method('onSchemaIndexDefinition'); + + $oldEventManager = $this->_sm->getDatabasePlatform()->getEventManager(); + + $eventManager = new EventManager(); + $eventManager->addEventListener(array(Events::onSchemaIndexDefinition), $listenerMock); + + $this->_sm->getDatabasePlatform()->setEventManager($eventManager); + + $this->_sm->listTableIndexes('list_table_indexes_test'); + + $this->_sm->getDatabasePlatform()->setEventManager($oldEventManager); + } + + public function testDiffListTableColumns() + { + if ($this->_sm->getDatabasePlatform()->getName() == 'oracle') { + $this->markTestSkipped('Does not work with Oracle, since it cannot detect DateTime, Date and Time differenecs (at the moment).'); + } + + $offlineTable = $this->createListTableColumns(); + $onlineTable = $this->_sm->listTableDetails('list_table_columns'); + + $comparator = new \Doctrine\DBAL\Schema\Comparator(); + $diff = $comparator->diffTable($offlineTable, $onlineTable); + + $this->assertFalse($diff, "No differences should be detected with the offline vs online schema."); + } + + public function testListTableIndexes() + { + $table = $this->getTestCompositeTable('list_table_indexes_test'); + $table->addUniqueIndex(array('test'), 'test_index_name'); + $table->addIndex(array('id', 'test'), 'test_composite_idx'); + + $this->_sm->dropAndCreateTable($table); + + $tableIndexes = $this->_sm->listTableIndexes('list_table_indexes_test'); + + $this->assertEquals(3, count($tableIndexes)); + + $this->assertArrayHasKey('primary', $tableIndexes, 'listTableIndexes() has to return a "primary" array key.'); + $this->assertEquals(array('id', 'other_id'), array_map('strtolower', $tableIndexes['primary']->getColumns())); + $this->assertTrue($tableIndexes['primary']->isUnique()); + $this->assertTrue($tableIndexes['primary']->isPrimary()); + + $this->assertEquals('test_index_name', $tableIndexes['test_index_name']->getName()); + $this->assertEquals(array('test'), array_map('strtolower', $tableIndexes['test_index_name']->getColumns())); + $this->assertTrue($tableIndexes['test_index_name']->isUnique()); + $this->assertFalse($tableIndexes['test_index_name']->isPrimary()); + + $this->assertEquals('test_composite_idx', $tableIndexes['test_composite_idx']->getName()); + $this->assertEquals(array('id', 'test'), array_map('strtolower', $tableIndexes['test_composite_idx']->getColumns())); + $this->assertFalse($tableIndexes['test_composite_idx']->isUnique()); + $this->assertFalse($tableIndexes['test_composite_idx']->isPrimary()); + } + + public function testDropAndCreateIndex() + { + $table = $this->getTestTable('test_create_index'); + $table->addUniqueIndex(array('test'), 'test'); + $this->_sm->dropAndCreateTable($table); + + $this->_sm->dropAndCreateIndex($table->getIndex('test'), $table); + $tableIndexes = $this->_sm->listTableIndexes('test_create_index'); + $this->assertInternalType('array', $tableIndexes); + + $this->assertEquals('test', strtolower($tableIndexes['test']->getName())); + $this->assertEquals(array('test'), array_map('strtolower', $tableIndexes['test']->getColumns())); + $this->assertTrue($tableIndexes['test']->isUnique()); + $this->assertFalse($tableIndexes['test']->isPrimary()); + } + + public function testCreateTableWithForeignKeys() + { + if(!$this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Platform does not support foreign keys.'); + } + + $tableB = $this->getTestTable('test_foreign'); + + $this->_sm->dropAndCreateTable($tableB); + + $tableA = $this->getTestTable('test_create_fk'); + $tableA->addForeignKeyConstraint('test_foreign', array('foreign_key_test'), array('id')); + + $this->_sm->dropAndCreateTable($tableA); + + $fkTable = $this->_sm->listTableDetails('test_create_fk'); + $fkConstraints = $fkTable->getForeignKeys(); + $this->assertEquals(1, count($fkConstraints), "Table 'test_create_fk1' has to have one foreign key."); + + $fkConstraint = current($fkConstraints); + $this->assertInstanceOf('\Doctrine\DBAL\Schema\ForeignKeyConstraint', $fkConstraint); + $this->assertEquals('test_foreign', strtolower($fkConstraint->getForeignTableName())); + $this->assertEquals(array('foreign_key_test'), array_map('strtolower', $fkConstraint->getColumns())); + $this->assertEquals(array('id'), array_map('strtolower', $fkConstraint->getForeignColumns())); + + $this->assertTrue($fkTable->columnsAreIndexed($fkConstraint->getColumns()), "The columns of a foreign key constraint should always be indexed."); + } + + public function testListForeignKeys() + { + if(!$this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Does not support foreign key constraints.'); + } + + $this->createTestTable('test_create_fk1'); + $this->createTestTable('test_create_fk2'); + + $foreignKey = new \Doctrine\DBAL\Schema\ForeignKeyConstraint( + array('foreign_key_test'), 'test_create_fk2', array('id'), 'foreign_key_test_fk', array('onDelete' => 'CASCADE') + ); + + $this->_sm->createForeignKey($foreignKey, 'test_create_fk1'); + + $fkeys = $this->_sm->listTableForeignKeys('test_create_fk1'); + + $this->assertEquals(1, count($fkeys), "Table 'test_create_fk1' has to have one foreign key."); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\ForeignKeyConstraint', $fkeys[0]); + $this->assertEquals(array('foreign_key_test'), array_map('strtolower', $fkeys[0]->getLocalColumns())); + $this->assertEquals(array('id'), array_map('strtolower', $fkeys[0]->getForeignColumns())); + $this->assertEquals('test_create_fk2', strtolower($fkeys[0]->getForeignTableName())); + + if($fkeys[0]->hasOption('onDelete')) { + $this->assertEquals('CASCADE', $fkeys[0]->getOption('onDelete')); + } + } + + protected function getCreateExampleViewSql() + { + $this->markTestSkipped('No Create Example View SQL was defined for this SchemaManager'); + } + + public function testCreateSchema() + { + $this->createTestTable('test_table'); + + $schema = $this->_sm->createSchema(); + $this->assertTrue($schema->hasTable('test_table')); + } + + public function testAlterTableScenario() + { + if(!$this->_sm->getDatabasePlatform()->supportsAlterTable()) { + $this->markTestSkipped('Alter Table is not supported by this platform.'); + } + + $this->createTestTable('alter_table'); + $this->createTestTable('alter_table_foreign'); + + $table = $this->_sm->listTableDetails('alter_table'); + $this->assertTrue($table->hasColumn('id')); + $this->assertTrue($table->hasColumn('test')); + $this->assertTrue($table->hasColumn('foreign_key_test')); + $this->assertEquals(0, count($table->getForeignKeys())); + $this->assertEquals(1, count($table->getIndexes())); + + $tableDiff = new \Doctrine\DBAL\Schema\TableDiff("alter_table"); + $tableDiff->addedColumns['foo'] = new \Doctrine\DBAL\Schema\Column('foo', Type::getType('integer')); + $tableDiff->removedColumns['test'] = $table->getColumn('test'); + + $this->_sm->alterTable($tableDiff); + + $table = $this->_sm->listTableDetails('alter_table'); + $this->assertFalse($table->hasColumn('test')); + $this->assertTrue($table->hasColumn('foo')); + + $tableDiff = new \Doctrine\DBAL\Schema\TableDiff("alter_table"); + $tableDiff->addedIndexes[] = new \Doctrine\DBAL\Schema\Index('foo_idx', array('foo')); + + $this->_sm->alterTable($tableDiff); + + $table = $this->_sm->listTableDetails('alter_table'); + $this->assertEquals(2, count($table->getIndexes())); + $this->assertTrue($table->hasIndex('foo_idx')); + $this->assertEquals(array('foo'), array_map('strtolower', $table->getIndex('foo_idx')->getColumns())); + $this->assertFalse($table->getIndex('foo_idx')->isPrimary()); + $this->assertFalse($table->getIndex('foo_idx')->isUnique()); + + $tableDiff = new \Doctrine\DBAL\Schema\TableDiff("alter_table"); + $tableDiff->changedIndexes[] = new \Doctrine\DBAL\Schema\Index('foo_idx', array('foo', 'foreign_key_test')); + + $this->_sm->alterTable($tableDiff); + + $table = $this->_sm->listTableDetails('alter_table'); + $this->assertEquals(2, count($table->getIndexes())); + $this->assertTrue($table->hasIndex('foo_idx')); + $this->assertEquals(array('foo', 'foreign_key_test'), array_map('strtolower', $table->getIndex('foo_idx')->getColumns())); + + $tableDiff = new \Doctrine\DBAL\Schema\TableDiff("alter_table"); + $tableDiff->removedIndexes[] = new \Doctrine\DBAL\Schema\Index('foo_idx', array('foo', 'foreign_key_test')); + $fk = new \Doctrine\DBAL\Schema\ForeignKeyConstraint(array('foreign_key_test'), 'alter_table_foreign', array('id')); + $tableDiff->addedForeignKeys[] = $fk; + + $this->_sm->alterTable($tableDiff); + $table = $this->_sm->listTableDetails('alter_table'); + + // dont check for index size here, some platforms automatically add indexes for foreign keys. + $this->assertFalse($table->hasIndex('foo_idx')); + + $this->assertEquals(1, count($table->getForeignKeys())); + $fks = $table->getForeignKeys(); + $foreignKey = current($fks); + $this->assertEquals('alter_table_foreign', strtolower($foreignKey->getForeignTableName())); + $this->assertEquals(array('foreign_key_test'), array_map('strtolower', $foreignKey->getColumns())); + $this->assertEquals(array('id'), array_map('strtolower', $foreignKey->getForeignColumns())); + } + + public function testCreateAndListViews() + { + if (!$this->_sm->getDatabasePlatform()->supportsViews()) { + $this->markTestSkipped('Views is not supported by this platform.'); + } + + $this->createTestTable('view_test_table'); + + $name = "doctrine_test_view"; + $sql = "SELECT * FROM view_test_table"; + + $view = new \Doctrine\DBAL\Schema\View($name, $sql); + + $this->_sm->dropAndCreateView($view); + + $views = $this->_sm->listViews(); + } + + public function testAutoincrementDetection() + { + if (!$this->_sm->getDatabasePlatform()->supportsIdentityColumns()) { + $this->markTestSkipped('This test is only supported on platforms that have autoincrement'); + } + + $table = new \Doctrine\DBAL\Schema\Table('test_autoincrement'); + $table->setSchemaConfig($this->_sm->createSchemaConfig()); + $table->addColumn('id', 'integer', array('autoincrement' => true)); + $table->setPrimaryKey(array('id')); + + $this->_sm->createTable($table); + + $inferredTable = $this->_sm->listTableDetails('test_autoincrement'); + $this->assertTrue($inferredTable->hasColumn('id')); + $this->assertTrue($inferredTable->getColumn('id')->getAutoincrement()); + } + + /** + * @group DDC-887 + */ + public function testUpdateSchemaWithForeignKeyRenaming() + { + if (!$this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('This test is only supported on platforms that have foreign keys.'); + } + + $table = new \Doctrine\DBAL\Schema\Table('test_fk_base'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $tableFK = new \Doctrine\DBAL\Schema\Table('test_fk_rename'); + $tableFK->setSchemaConfig($this->_sm->createSchemaConfig()); + $tableFK->addColumn('id', 'integer'); + $tableFK->addColumn('fk_id', 'integer'); + $tableFK->setPrimaryKey(array('id')); + $tableFK->addIndex(array('fk_id'), 'fk_idx'); + $tableFK->addForeignKeyConstraint('test_fk_base', array('fk_id'), array('id')); + + $this->_sm->createTable($table); + $this->_sm->createTable($tableFK); + + $tableFKNew = new \Doctrine\DBAL\Schema\Table('test_fk_rename'); + $tableFKNew->setSchemaConfig($this->_sm->createSchemaConfig()); + $tableFKNew->addColumn('id', 'integer'); + $tableFKNew->addColumn('rename_fk_id', 'integer'); + $tableFKNew->setPrimaryKey(array('id')); + $tableFKNew->addIndex(array('rename_fk_id'), 'fk_idx'); + $tableFKNew->addForeignKeyConstraint('test_fk_base', array('rename_fk_id'), array('id')); + + $c = new \Doctrine\DBAL\Schema\Comparator(); + $tableDiff = $c->diffTable($tableFK, $tableFKNew); + + $this->_sm->alterTable($tableDiff); + } + + /** + * @group DBAL-42 + */ + public function testGetColumnComment() + { + if (!$this->_conn->getDatabasePlatform()->supportsInlineColumnComments() && !$this->_conn->getDatabasePlatform()->supportsCommentOnStatement()) { + $this->markTestSkipped('Database does not support column comments.'); + } + + $table = new \Doctrine\DBAL\Schema\Table('column_comment_test'); + $table->addColumn('id', 'integer', array('comment' => 'This is a comment')); + $table->setPrimaryKey(array('id')); + + $this->_sm->createTable($table); + + $columns = $this->_sm->listTableColumns("column_comment_test"); + $this->assertEquals(1, count($columns)); + $this->assertEquals('This is a comment', $columns['id']->getComment()); + } + + /** + * @group DBAL-42 + */ + public function testAutomaticallyAppendCommentOnMarkedColumns() + { + if (!$this->_conn->getDatabasePlatform()->supportsInlineColumnComments() && !$this->_conn->getDatabasePlatform()->supportsCommentOnStatement()) { + $this->markTestSkipped('Database does not support column comments.'); + } + + $table = new \Doctrine\DBAL\Schema\Table('column_comment_test2'); + $table->addColumn('id', 'integer', array('comment' => 'This is a comment')); + $table->addColumn('obj', 'object', array('comment' => 'This is a comment')); + $table->addColumn('arr', 'array', array('comment' => 'This is a comment')); + $table->setPrimaryKey(array('id')); + + $this->_sm->createTable($table); + + $columns = $this->_sm->listTableColumns("column_comment_test2"); + $this->assertEquals(3, count($columns)); + $this->assertEquals('This is a comment', $columns['id']->getComment()); + $this->assertEquals('This is a comment', $columns['obj']->getComment(), "The Doctrine2 Typehint should be stripped from comment."); + $this->assertInstanceOf('Doctrine\DBAL\Types\ObjectType', $columns['obj']->getType(), "The Doctrine2 should be detected from comment hint."); + $this->assertEquals('This is a comment', $columns['arr']->getComment(), "The Doctrine2 Typehint should be stripped from comment."); + $this->assertInstanceOf('Doctrine\DBAL\Types\ArrayType', $columns['arr']->getType(), "The Doctrine2 should be detected from comment hint."); + } + + /** + * @group DBAL-197 + */ + public function testListTableWithBlob() + { + $table = new \Doctrine\DBAL\Schema\Table('test_blob_table'); + $table->addColumn('id', 'integer', array('comment' => 'This is a comment')); + $table->addColumn('binarydata', 'blob', array()); + $table->setPrimaryKey(array('id')); + + $this->_sm->createTable($table); + $blobTable = $this->_sm->listTableDetails('test_blob_table'); + } + + /** + * @param string $name + * @param array $data + */ + protected function createTestTable($name = 'test_table', $data = array()) + { + $options = array(); + if (isset($data['options'])) { + $options = $data['options']; + } + + $table = $this->getTestTable($name, $options); + + $this->_sm->dropAndCreateTable($table); + } + + protected function getTestTable($name, $options=array()) + { + $table = new \Doctrine\DBAL\Schema\Table($name, array(), array(), array(), false, $options); + $table->setSchemaConfig($this->_sm->createSchemaConfig()); + $table->addColumn('id', 'integer', array('notnull' => true)); + $table->setPrimaryKey(array('id')); + $table->addColumn('test', 'string', array('length' => 255)); + $table->addColumn('foreign_key_test', 'integer'); + return $table; + } + + protected function getTestCompositeTable($name) + { + $table = new \Doctrine\DBAL\Schema\Table($name, array(), array(), array(), false, array()); + $table->setSchemaConfig($this->_sm->createSchemaConfig()); + $table->addColumn('id', 'integer', array('notnull' => true)); + $table->addColumn('other_id', 'integer', array('notnull' => true)); + $table->setPrimaryKey(array('id', 'other_id')); + $table->addColumn('test', 'string', array('length' => 255)); + return $table; + } + + protected function assertHasTable($tables, $tableName) + { + $foundTable = false; + foreach ($tables AS $table) { + $this->assertInstanceOf('Doctrine\DBAL\Schema\Table', $table, 'No Table instance was found in tables array.'); + if (strtolower($table->getName()) == 'list_tables_test_new_name') { + $foundTable = true; + } + } + $this->assertTrue($foundTable, "Could not find new table"); + } + + public function testListForeignKeysComposite() + { + if(!$this->_conn->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $this->markTestSkipped('Does not support foreign key constraints.'); + } + + $this->_sm->createTable($this->getTestTable('test_create_fk3')); + $this->_sm->createTable($this->getTestCompositeTable('test_create_fk4')); + + $foreignKey = new \Doctrine\DBAL\Schema\ForeignKeyConstraint( + array('id', 'foreign_key_test'), 'test_create_fk4', array('id', 'other_id'), 'foreign_key_test_fk' + ); + + $this->_sm->createForeignKey($foreignKey, 'test_create_fk3'); + + $fkeys = $this->_sm->listTableForeignKeys('test_create_fk3'); + + $this->assertEquals(1, count($fkeys), "Table 'test_create_fk3' has to have one foreign key."); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\ForeignKeyConstraint', $fkeys[0]); + $this->assertEquals(array('id', 'foreign_key_test'), array_map('strtolower', $fkeys[0]->getLocalColumns())); + $this->assertEquals(array('id', 'other_id'), array_map('strtolower', $fkeys[0]->getForeignColumns())); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php new file mode 100644 index 00000000..2dccb511 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/SqliteSchemaManagerTest.php @@ -0,0 +1,46 @@ +_sm->listDatabases(); + } + + public function testCreateAndDropDatabase() + { + $path = dirname(__FILE__).'/test_create_and_drop_sqlite_database.sqlite'; + + $this->_sm->createDatabase($path); + $this->assertEquals(true, file_exists($path)); + $this->_sm->dropDatabase($path); + $this->assertEquals(false, file_exists($path)); + } + + /** + * @expectedException \Doctrine\DBAL\DBALException + */ + public function testRenameTable() + { + $this->_sm->renameTable('oldname', 'newname'); + } + + public function testAutoincrementDetection() + { + $this->markTestSkipped( + 'There is currently no reliable way to determine whether an SQLite column is marked as ' + . 'auto-increment. So, while it does support a single identity column, we cannot with ' + . 'certainty determine which it is.'); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php new file mode 100644 index 00000000..83efe320 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php @@ -0,0 +1,58 @@ +_conn->getDatabasePlatform(); + if ($platform->getName() == "sqlite") { + $this->markTestSkipped('TableGenerator does not work with SQLite'); + } + + try { + $schema = new \Doctrine\DBAL\Schema\Schema(); + $visitor = new \Doctrine\DBAL\Id\TableGeneratorSchemaVisitor(); + $schema->visit($visitor); + + foreach ($schema->toSql($platform) as $sql) { + $this->_conn->exec($sql); + } + + } catch(\Exception $e) { + } + $this->generator = new TableGenerator($this->_conn); + } + + public function testNextVal() + { + $id1 = $this->generator->nextValue("tbl1"); + $id2 = $this->generator->nextValue("tbl1"); + $id3 = $this->generator->nextValue("tbl2"); + + $this->assertTrue($id1 > 0, "First id has to be larger than 0"); + $this->assertEquals($id1 + 1, $id2, "Second id is one larger than first one."); + $this->assertEquals($id1, $id3, "First ids from different tables are equal."); + } + + public function testNextValNotAffectedByOuterTransactions() + { + $this->_conn->beginTransaction(); + $id1 = $this->generator->nextValue("tbl1"); + $this->_conn->rollBack(); + $id2 = $this->generator->nextValue("tbl1"); + + $this->assertTrue($id1 > 0, "First id has to be larger than 0"); + $this->assertEquals($id1 + 1, $id2, "Second id is one larger than first one."); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TemporaryTableTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TemporaryTableTest.php new file mode 100644 index 00000000..9fa92c7a --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TemporaryTableTest.php @@ -0,0 +1,102 @@ +_conn->exec($this->_conn->getDatabasePlatform()->getDropTableSQL("nontemporary")); + } catch(\Exception $e) { + + } + } + + public function tearDown() + { + if ($this->_conn) { + try { + $tempTable = $this->_conn->getDatabasePlatform()->getTemporaryTableName("temporary"); + $this->_conn->exec($this->_conn->getDatabasePlatform()->getDropTemporaryTableSQL($tempTable)); + } catch(\Exception $e) { } + } + } + + /** + * @group DDC-1337 + * @return void + */ + public function testDropTemporaryTableNotAutoCommitTransaction() + { + $platform = $this->_conn->getDatabasePlatform(); + $columnDefinitions = array("id" => array("type" => Type::getType("integer"), "notnull" => true)); + $tempTable = $platform->getTemporaryTableName("temporary"); + + $createTempTableSQL = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; + $this->_conn->executeUpdate($createTempTableSQL); + + $table = new Table("nontemporary"); + $table->addColumn("id", "integer"); + $table->setPrimaryKey(array('id')); + + foreach ($platform->getCreateTableSQL($table) AS $sql) { + $this->_conn->executeQuery($sql); + } + + $this->_conn->beginTransaction(); + $this->_conn->insert("nontemporary", array("id" => 1)); + $this->_conn->exec($platform->getDropTemporaryTableSQL($tempTable)); + $this->_conn->insert("nontemporary", array("id" => 2)); + + $this->_conn->rollback(); + + $rows = $this->_conn->fetchAll('SELECT * FROM nontemporary'); + $this->assertEquals(array(), $rows, "In an event of an error this result has one row, because of an implicit commit."); + } + + /** + * @group DDC-1337 + * @return void + */ + public function testCreateTemporaryTableNotAutoCommitTransaction() + { + $platform = $this->_conn->getDatabasePlatform(); + $columnDefinitions = array("id" => array("type" => Type::getType("integer"), "notnull" => true)); + $tempTable = $platform->getTemporaryTableName("temporary"); + + $createTempTableSQL = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; + + $table = new Table("nontemporary"); + $table->addColumn("id", "integer"); + $table->setPrimaryKey(array('id')); + + foreach ($platform->getCreateTableSQL($table) AS $sql) { + $this->_conn->executeQuery($sql); + } + + $this->_conn->beginTransaction(); + $this->_conn->insert("nontemporary", array("id" => 1)); + + $this->_conn->exec($createTempTableSQL); + $this->_conn->insert("nontemporary", array("id" => 2)); + + $this->_conn->rollback(); + + try { + $this->_conn->exec($platform->getDropTemporaryTableSQL($tempTable)); + } catch(\Exception $e) { + + } + + $rows = $this->_conn->fetchAll('SELECT * FROM nontemporary'); + $this->assertEquals(array(), $rows, "In an event of an error this result has one row, because of an implicit commit."); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL168Test.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL168Test.php new file mode 100644 index 00000000..1393404b --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL168Test.php @@ -0,0 +1,27 @@ +_conn->getDatabasePlatform()->getName() != "postgresql") { + $this->markTestSkipped('PostgreSQL only test'); + } + + $table = new \Doctrine\DBAL\Schema\Table("domains"); + $table->addColumn('id', 'integer'); + $table->addColumn('parent_id', 'integer'); + $table->setPrimaryKey(array('id')); + $table->addForeignKeyConstraint('domains', array('parent_id'), array('id')); + + $this->_conn->getSchemaManager()->createTable($table); + $table = $this->_conn->getSchemaManager()->listTableDetails('domains'); + + $this->assertEquals('domains', $table->getName()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL202Test.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL202Test.php new file mode 100644 index 00000000..4448ed73 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL202Test.php @@ -0,0 +1,48 @@ +_conn->getDatabasePlatform()->getName() != 'oracle') { + $this->markTestSkipped('OCI8 only test'); + } + + if ($this->_conn->getSchemaManager()->tablesExist('DBAL202')) { + $this->_conn->executeQuery('DELETE FROM DBAL202'); + } else { + $table = new \Doctrine\DBAL\Schema\Table('DBAL202'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $this->_conn->getSchemaManager()->createTable($table); + } + } + + public function testStatementRollback() + { + $stmt = $this->_conn->prepare('INSERT INTO DBAL202 VALUES (8)'); + $this->_conn->beginTransaction(); + $stmt->execute(); + $this->_conn->rollback(); + + $this->assertEquals(0, $this->_conn->query('SELECT COUNT(1) FROM DBAL202')->fetchColumn()); + } + + public function testStatementCommit() + { + $stmt = $this->_conn->prepare('INSERT INTO DBAL202 VALUES (8)'); + $this->_conn->beginTransaction(); + $stmt->execute(); + $this->_conn->commit(); + + $this->assertEquals(1, $this->_conn->query('SELECT COUNT(1) FROM DBAL202')->fetchColumn()); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TypeConversionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TypeConversionTest.php new file mode 100644 index 00000000..67e6ddae --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TypeConversionTest.php @@ -0,0 +1,101 @@ +_conn->getSchemaManager(); + + $table = new \Doctrine\DBAL\Schema\Table("type_conversion"); + $table->addColumn('id', 'integer', array('notnull' => false)); + $table->addColumn('test_string', 'string', array('notnull' => false)); + $table->addColumn('test_boolean', 'boolean', array('notnull' => false)); + $table->addColumn('test_bigint', 'bigint', array('notnull' => false)); + $table->addColumn('test_smallint', 'bigint', array('notnull' => false)); + $table->addColumn('test_datetime', 'datetime', array('notnull' => false)); + $table->addColumn('test_datetimetz', 'datetimetz', array('notnull' => false)); + $table->addColumn('test_date', 'date', array('notnull' => false)); + $table->addColumn('test_time', 'time', array('notnull' => false)); + $table->addColumn('test_text', 'text', array('notnull' => false)); + $table->addColumn('test_array', 'array', array('notnull' => false)); + $table->addColumn('test_object', 'object', array('notnull' => false)); + $table->addColumn('test_float', 'float', array('notnull' => false)); + $table->addColumn('test_decimal', 'decimal', array('notnull' => false, 'scale' => 2, 'precision' => 10)); + $table->setPrimaryKey(array('id')); + + try { + foreach ($this->_conn->getDatabasePlatform()->getCreateTableSQL($table) AS $sql) { + $this->_conn->executeQuery($sql); + } + } catch(\Exception $e) { + + } + } + + static public function dataIdempotentDataConversion() + { + $obj = new \stdClass(); + $obj->foo = "bar"; + $obj->bar = "baz"; + + return array( + array('string', 'ABCDEFGaaaBBB', 'string'), + array('boolean', true, 'bool'), + array('boolean', false, 'bool'), + array('bigint', 12345678, 'string'), + array('smallint', 123, 'int'), + array('datetime', new \DateTime('2010-04-05 10:10:10'), 'DateTime'), + array('datetimetz', new \DateTime('2010-04-05 10:10:10'), 'DateTime'), + array('date', new \DateTime('2010-04-05'), 'DateTime'), + array('time', new \DateTime('10:10:10'), 'DateTime'), + array('text', str_repeat('foo ', 1000), 'string'), + array('array', array('foo' => 'bar'), 'array'), + array('object', $obj, 'object'), + array('float', 1.5, 'float'), + array('decimal', 1.55, 'string'), + ); + } + + /** + * @dataProvider dataIdempotentDataConversion + * @param string $type + * @param mixed $originalValue + * @param string $expectedPhpType + */ + public function testIdempotentDataConversion($type, $originalValue, $expectedPhpType) + { + $columnName = "test_" . $type; + $typeInstance = Type::getType($type); + $insertionValue = $typeInstance->convertToDatabaseValue($originalValue, $this->_conn->getDatabasePlatform()); + + $this->_conn->insert('type_conversion', array('id' => ++self::$typeCounter, $columnName => $insertionValue)); + + $sql = "SELECT " . $columnName . " FROM type_conversion WHERE id = " . self::$typeCounter; + $actualDbValue = $typeInstance->convertToPHPValue($this->_conn->fetchColumn($sql), $this->_conn->getDatabasePlatform()); + + if ($originalValue instanceof \DateTime) { + $this->assertInstanceOf($expectedPhpType, $actualDbValue, "The expected type from the conversion to and back from the database should be " . $expectedPhpType); + } else { + $this->assertInternalType($expectedPhpType, $actualDbValue, "The expected type from the conversion to and back from the database should be " . $expectedPhpType); + } + + if ($type !== "datetimetz") { + $this->assertEquals($originalValue, $actualDbValue, "Conversion between values should produce the same out as in value, but doesnt!"); + + if ($originalValue instanceof \DateTime) { + $this->assertEquals($originalValue->getTimezone()->getName(), $actualDbValue->getTimezone()->getName(), "Timezones should be the same."); + } + } + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/WriteTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/WriteTest.php new file mode 100644 index 00000000..f1a3a598 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/WriteTest.php @@ -0,0 +1,185 @@ +addColumn('id', 'integer', array('autoincrement' => true)); + $table->addColumn('test_int', 'integer'); + $table->addColumn('test_string', 'string', array('notnull' => false)); + $table->setPrimaryKey(array('id')); + + foreach ($this->_conn->getDatabasePlatform()->getCreateTableSQL($table) AS $sql) { + $this->_conn->executeQuery($sql); + } + } catch(\Exception $e) { + + } + $this->_conn->executeUpdate('DELETE FROM write_table'); + } + + /** + * @group DBAL-80 + */ + public function testExecuteUpdateFirstTypeIsNull() + { + $sql = "INSERT INTO write_table (test_string, test_int) VALUES (?, ?)"; + $this->_conn->executeUpdate($sql, array("text", 1111), array(null, PDO::PARAM_INT)); + + $sql = "SELECT * FROM write_table WHERE test_string = ? AND test_int = ?"; + $this->assertTrue((bool)$this->_conn->fetchColumn($sql, array("text", 1111))); + } + + public function testExecuteUpdate() + { + $sql = "INSERT INTO write_table (test_int) VALUES ( " . $this->_conn->quote(1) . ")"; + $affected = $this->_conn->executeUpdate($sql); + + $this->assertEquals(1, $affected, "executeUpdate() should return the number of affected rows!"); + } + + public function testExecuteUpdateWithTypes() + { + $sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)"; + $affected = $this->_conn->executeUpdate($sql, array(1, 'foo'), array(\PDO::PARAM_INT, \PDO::PARAM_STR)); + + $this->assertEquals(1, $affected, "executeUpdate() should return the number of affected rows!"); + } + + public function testPrepareRowCountReturnsAffectedRows() + { + $sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)"; + $stmt = $this->_conn->prepare($sql); + + $stmt->bindValue(1, 1); + $stmt->bindValue(2, "foo"); + $stmt->execute(); + + $this->assertEquals(1, $stmt->rowCount()); + } + + public function testPrepareWithPdoTypes() + { + $sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)"; + $stmt = $this->_conn->prepare($sql); + + $stmt->bindValue(1, 1, \PDO::PARAM_INT); + $stmt->bindValue(2, "foo", \PDO::PARAM_STR); + $stmt->execute(); + + $this->assertEquals(1, $stmt->rowCount()); + } + + public function testPrepareWithDbalTypes() + { + $sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)"; + $stmt = $this->_conn->prepare($sql); + + $stmt->bindValue(1, 1, Type::getType('integer')); + $stmt->bindValue(2, "foo", Type::getType('string')); + $stmt->execute(); + + $this->assertEquals(1, $stmt->rowCount()); + } + + public function testPrepareWithDbalTypeNames() + { + $sql = "INSERT INTO write_table (test_int, test_string) VALUES (?, ?)"; + $stmt = $this->_conn->prepare($sql); + + $stmt->bindValue(1, 1, 'integer'); + $stmt->bindValue(2, "foo", 'string'); + $stmt->execute(); + + $this->assertEquals(1, $stmt->rowCount()); + } + + public function insertRows() + { + $this->assertEquals(1, $this->_conn->insert('write_table', array('test_int' => 1, 'test_string' => 'foo'))); + $this->assertEquals(1, $this->_conn->insert('write_table', array('test_int' => 2, 'test_string' => 'bar'))); + } + + public function testInsert() + { + $this->insertRows(); + } + + public function testDelete() + { + $this->insertRows(); + + $this->assertEquals(1, $this->_conn->delete('write_table', array('test_int' => 2))); + $this->assertEquals(1, count($this->_conn->fetchAll('SELECT * FROM write_table'))); + + $this->assertEquals(1, $this->_conn->delete('write_table', array('test_int' => 1))); + $this->assertEquals(0, count($this->_conn->fetchAll('SELECT * FROM write_table'))); + } + + public function testUpdate() + { + $this->insertRows(); + + $this->assertEquals(1, $this->_conn->update('write_table', array('test_string' => 'bar'), array('test_string' => 'foo'))); + $this->assertEquals(2, $this->_conn->update('write_table', array('test_string' => 'baz'), array('test_string' => 'bar'))); + $this->assertEquals(0, $this->_conn->update('write_table', array('test_string' => 'baz'), array('test_string' => 'bar'))); + } + + public function testLastInsertId() + { + if ( ! $this->_conn->getDatabasePlatform()->prefersIdentityColumns()) { + $this->markTestSkipped('Test only works on platforms with identity columns.'); + } + + $this->assertEquals(1, $this->_conn->insert('write_table', array('test_int' => 2, 'test_string' => 'bar'))); + $num = $this->_conn->lastInsertId(); + + $this->assertNotNull($num, "LastInsertId() should not be null."); + $this->assertTrue($num > 0, "LastInsertId() should be non-negative number."); + } + + public function testLastInsertIdSequence() + { + if ( ! $this->_conn->getDatabasePlatform()->supportsSequences()) { + $this->markTestSkipped('Test only works on platforms with sequences.'); + } + + $sequence = new \Doctrine\DBAL\Schema\Sequence('write_table_id_seq'); + try { + $this->_conn->getSchemaManager()->createSequence($sequence); + } catch(\Exception $e) { + } + + $sequences = $this->_conn->getSchemaManager()->listSequences(); + $this->assertEquals(1, count(array_filter($sequences, function($sequence) { + return $sequence->getName() === 'write_table_id_seq'; + }))); + + $stmt = $this->_conn->query($this->_conn->getDatabasePlatform()->getSequenceNextValSQL('write_table_id_seq')); + $nextSequenceVal = $stmt->fetchColumn(); + + $lastInsertId = $this->_conn->lastInsertId('write_table_id_seq'); + + $this->assertTrue($lastInsertId > 0); + $this->assertEquals($nextSequenceVal, $lastInsertId); + } + + public function testLastInsertIdNoSequenceGiven() + { + if ( ! $this->_conn->getDatabasePlatform()->supportsSequences()) { + $this->markTestSkipped('Test only works on platforms with sequences.'); + } + + $this->assertFalse($this->_conn->lastInsertId( null )); + + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Logging/DebugStackTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Logging/DebugStackTest.php new file mode 100644 index 00000000..00bc0b6f --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Logging/DebugStackTest.php @@ -0,0 +1,47 @@ +logger = new \Doctrine\DBAL\Logging\DebugStack(); + } + + public function tearDown() + { + unset($this->logger); + } + + public function testLoggedQuery() + { + $this->logger->startQuery('SELECT column FROM table'); + $this->assertEquals( + array( + 1 => array( + 'sql' => 'SELECT column FROM table', + 'params' => null, + 'types' => null, + 'executionMS' => 0, + ), + ), + $this->logger->queries + ); + + $this->logger->stopQuery(); + $this->assertGreaterThan(0, $this->logger->queries[1]['executionMS']); + } + + public function testLoggedQueryDisabled() + { + $this->logger->enabled = false; + $this->logger->startQuery('SELECT column FROM table'); + $this->assertEquals(array(), $this->logger->queries); + + $this->logger->stopQuery(); + $this->assertEquals(array(), $this->logger->queries); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Mocks/MockPlatform.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Mocks/MockPlatform.php new file mode 100644 index 00000000..576430c6 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Mocks/MockPlatform.php @@ -0,0 +1,49 @@ +_platform = $this->createPlatform(); + } + + /** + * @group DDC-1360 + */ + public function testQuoteIdentifier() + { + if ($this->_platform->getName() == "mssql") { + $this->markTestSkipped('Not working this way on mssql.'); + } + + $c = $this->_platform->getIdentifierQuoteCharacter(); + $this->assertEquals($c."test".$c, $this->_platform->quoteIdentifier("test")); + $this->assertEquals($c."test".$c.".".$c."test".$c, $this->_platform->quoteIdentifier("test.test")); + $this->assertEquals(str_repeat($c, 4), $this->_platform->quoteIdentifier($c)); + } + + /** + * @group DDC-1360 + */ + public function testQuoteSingleIdentifier() + { + if ($this->_platform->getName() == "mssql") { + $this->markTestSkipped('Not working this way on mssql.'); + } + + $c = $this->_platform->getIdentifierQuoteCharacter(); + $this->assertEquals($c."test".$c, $this->_platform->quoteSingleIdentifier("test")); + $this->assertEquals($c."test.test".$c, $this->_platform->quoteSingleIdentifier("test.test")); + $this->assertEquals(str_repeat($c, 4), $this->_platform->quoteSingleIdentifier($c)); + } + + public function testGetInvalidtForeignKeyReferentialActionSQL() + { + $this->setExpectedException('InvalidArgumentException'); + $this->_platform->getForeignKeyReferentialActionSQL('unknown'); + } + + public function testGetUnknownDoctrineMappingType() + { + $this->setExpectedException('Doctrine\DBAL\DBALException'); + $this->_platform->getDoctrineTypeMapping('foobar'); + } + + public function testRegisterDoctrineMappingType() + { + $this->_platform->registerDoctrineTypeMapping('foo', 'integer'); + $this->assertEquals('integer', $this->_platform->getDoctrineTypeMapping('foo')); + } + + public function testRegisterUnknownDoctrineMappingType() + { + $this->setExpectedException('Doctrine\DBAL\DBALException'); + $this->_platform->registerDoctrineTypeMapping('foo', 'bar'); + } + + public function testCreateWithNoColumns() + { + $table = new Table('test'); + + $this->setExpectedException('Doctrine\DBAL\DBALException'); + $sql = $this->_platform->getCreateTableSQL($table); + } + + public function testGeneratesTableCreationSql() + { + $table = new Table('test'); + $table->addColumn('id', 'integer', array('notnull' => true, 'autoincrement' => true)); + $table->addColumn('test', 'string', array('notnull' => false, 'length' => 255)); + $table->setPrimaryKey(array('id')); + + $sql = $this->_platform->getCreateTableSQL($table); + $this->assertEquals($this->getGenerateTableSql(), $sql[0]); + } + + abstract public function getGenerateTableSql(); + + public function testGenerateTableWithMultiColumnUniqueIndex() + { + $table = new Table('test'); + $table->addColumn('foo', 'string', array('notnull' => false, 'length' => 255)); + $table->addColumn('bar', 'string', array('notnull' => false, 'length' => 255)); + $table->addUniqueIndex(array("foo", "bar")); + + $sql = $this->_platform->getCreateTableSQL($table); + $this->assertEquals($this->getGenerateTableWithMultiColumnUniqueIndexSql(), $sql); + } + + abstract public function getGenerateTableWithMultiColumnUniqueIndexSql(); + + public function testGeneratesIndexCreationSql() + { + $indexDef = new \Doctrine\DBAL\Schema\Index('my_idx', array('user_name', 'last_login')); + + $this->assertEquals( + $this->getGenerateIndexSql(), + $this->_platform->getCreateIndexSQL($indexDef, 'mytable') + ); + } + + abstract public function getGenerateIndexSql(); + + public function testGeneratesUniqueIndexCreationSql() + { + $indexDef = new \Doctrine\DBAL\Schema\Index('index_name', array('test', 'test2'), true); + + $sql = $this->_platform->getCreateIndexSQL($indexDef, 'test'); + $this->assertEquals($this->getGenerateUniqueIndexSql(), $sql); + } + + abstract public function getGenerateUniqueIndexSql(); + + public function testGeneratesForeignKeyCreationSql() + { + $fk = new \Doctrine\DBAL\Schema\ForeignKeyConstraint(array('fk_name_id'), 'other_table', array('id'), ''); + + $sql = $this->_platform->getCreateForeignKeySQL($fk, 'test'); + $this->assertEquals($sql, $this->getGenerateForeignKeySql()); + } + + abstract public function getGenerateForeignKeySql(); + + public function testGeneratesConstraintCreationSql() + { + $idx = new \Doctrine\DBAL\Schema\Index('constraint_name', array('test'), true, false); + $sql = $this->_platform->getCreateConstraintSQL($idx, 'test'); + $this->assertEquals($this->getGenerateConstraintUniqueIndexSql(), $sql); + + $pk = new \Doctrine\DBAL\Schema\Index('constraint_name', array('test'), true, true); + $sql = $this->_platform->getCreateConstraintSQL($pk, 'test'); + $this->assertEquals($this->getGenerateConstraintPrimaryIndexSql(), $sql); + + $fk = new \Doctrine\DBAL\Schema\ForeignKeyConstraint(array('fk_name'), 'foreign', array('id'), 'constraint_fk'); + $sql = $this->_platform->getCreateConstraintSQL($fk, 'test'); + $this->assertEquals($this->getGenerateConstraintForeignKeySql(), $sql); + } + + protected function getBitAndComparisonExpressionSql($value1, $value2) + { + return '(' . $value1 . ' & ' . $value2 . ')'; + } + + /** + * @group DDC-1213 + */ + public function testGeneratesBitAndComparisonExpressionSql() + { + $sql = $this->_platform->getBitAndComparisonExpression(2, 4); + $this->assertEquals($this->getBitAndComparisonExpressionSql(2, 4), $sql); + } + + protected function getBitOrComparisonExpressionSql($value1, $value2) + { + return '(' . $value1 . ' | ' . $value2 . ')'; + } + + /** + * @group DDC-1213 + */ + public function testGeneratesBitOrComparisonExpressionSql() + { + $sql = $this->_platform->getBitOrComparisonExpression(2, 4); + $this->assertEquals($this->getBitOrComparisonExpressionSql(2, 4), $sql); + } + + public function getGenerateConstraintUniqueIndexSql() + { + return 'ALTER TABLE test ADD CONSTRAINT constraint_name UNIQUE (test)'; + } + + public function getGenerateConstraintPrimaryIndexSql() + { + return 'ALTER TABLE test ADD CONSTRAINT constraint_name PRIMARY KEY (test)'; + } + + public function getGenerateConstraintForeignKeySql() + { + return 'ALTER TABLE test ADD CONSTRAINT constraint_fk FOREIGN KEY (fk_name) REFERENCES foreign (id)'; + } + + abstract public function getGenerateAlterTableSql(); + + public function testGeneratesTableAlterationSql() + { + $expectedSql = $this->getGenerateAlterTableSql(); + + $tableDiff = new TableDiff('mytable'); + $tableDiff->newName = 'userlist'; + $tableDiff->addedColumns['quota'] = new \Doctrine\DBAL\Schema\Column('quota', \Doctrine\DBAL\Types\Type::getType('integer'), array('notnull' => false)); + $tableDiff->removedColumns['foo'] = new \Doctrine\DBAL\Schema\Column('foo', \Doctrine\DBAL\Types\Type::getType('integer')); + $tableDiff->changedColumns['bar'] = new \Doctrine\DBAL\Schema\ColumnDiff( + 'bar', new \Doctrine\DBAL\Schema\Column( + 'baz', \Doctrine\DBAL\Types\Type::getType('string'), array('default' => 'def') + ), + array('type', 'notnull', 'default') + ); + $tableDiff->changedColumns['bloo'] = new \Doctrine\DBAL\Schema\ColumnDiff( + 'bloo', new \Doctrine\DBAL\Schema\Column( + 'bloo', \Doctrine\DBAL\Types\Type::getType('boolean'), array('default' => false) + ), + array('type', 'notnull', 'default') + ); + + $sql = $this->_platform->getAlterTableSQL($tableDiff); + + $this->assertEquals($expectedSql, $sql); + } + + public function testGetCustomColumnDeclarationSql() + { + $field = array('columnDefinition' => 'MEDIUMINT(6) UNSIGNED'); + $this->assertEquals('foo MEDIUMINT(6) UNSIGNED', $this->_platform->getColumnDeclarationSQL('foo', $field)); + } + + public function testGetCreateTableSqlDispatchEvent() + { + $listenerMock = $this->getMock('GetCreateTableSqlDispatchEvenListener', array('onSchemaCreateTable', 'onSchemaCreateTableColumn')); + $listenerMock + ->expects($this->once()) + ->method('onSchemaCreateTable'); + $listenerMock + ->expects($this->exactly(2)) + ->method('onSchemaCreateTableColumn'); + + $eventManager = new EventManager(); + $eventManager->addEventListener(array(Events::onSchemaCreateTable, Events::onSchemaCreateTableColumn), $listenerMock); + + $this->_platform->setEventManager($eventManager); + + $table = new Table('test'); + $table->addColumn('foo', 'string', array('notnull' => false, 'length' => 255)); + $table->addColumn('bar', 'string', array('notnull' => false, 'length' => 255)); + + $this->_platform->getCreateTableSQL($table); + } + + public function testGetDropTableSqlDispatchEvent() + { + $listenerMock = $this->getMock('GetDropTableSqlDispatchEventListener', array('onSchemaDropTable')); + $listenerMock + ->expects($this->once()) + ->method('onSchemaDropTable'); + + $eventManager = new EventManager(); + $eventManager->addEventListener(array(Events::onSchemaDropTable), $listenerMock); + + $this->_platform->setEventManager($eventManager); + + $this->_platform->getDropTableSQL('TABLE'); + } + + public function testGetAlterTableSqlDispatchEvent() + { + $events = array( + 'onSchemaAlterTable', + 'onSchemaAlterTableAddColumn', + 'onSchemaAlterTableRemoveColumn', + 'onSchemaAlterTableChangeColumn', + 'onSchemaAlterTableRenameColumn' + ); + + $listenerMock = $this->getMock('GetAlterTableSqlDispatchEvenListener', $events); + $listenerMock + ->expects($this->once()) + ->method('onSchemaAlterTable'); + $listenerMock + ->expects($this->once()) + ->method('onSchemaAlterTableAddColumn'); + $listenerMock + ->expects($this->once()) + ->method('onSchemaAlterTableRemoveColumn'); + $listenerMock + ->expects($this->once()) + ->method('onSchemaAlterTableChangeColumn'); + $listenerMock + ->expects($this->once()) + ->method('onSchemaAlterTableRenameColumn'); + + $eventManager = new EventManager(); + $events = array( + Events::onSchemaAlterTable, + Events::onSchemaAlterTableAddColumn, + Events::onSchemaAlterTableRemoveColumn, + Events::onSchemaAlterTableChangeColumn, + Events::onSchemaAlterTableRenameColumn + ); + $eventManager->addEventListener($events, $listenerMock); + + $this->_platform->setEventManager($eventManager); + + $tableDiff = new TableDiff('mytable'); + $tableDiff->addedColumns['added'] = new \Doctrine\DBAL\Schema\Column('added', \Doctrine\DBAL\Types\Type::getType('integer'), array()); + $tableDiff->removedColumns['removed'] = new \Doctrine\DBAL\Schema\Column('removed', \Doctrine\DBAL\Types\Type::getType('integer'), array()); + $tableDiff->changedColumns['changed'] = new \Doctrine\DBAL\Schema\ColumnDiff( + 'changed', new \Doctrine\DBAL\Schema\Column( + 'changed2', \Doctrine\DBAL\Types\Type::getType('string'), array() + ), + array() + ); + $tableDiff->renamedColumns['renamed'] = new \Doctrine\DBAL\Schema\Column('renamed2', \Doctrine\DBAL\Types\Type::getType('integer'), array()); + + $this->_platform->getAlterTableSQL($tableDiff); + } + + /** + * @group DBAL-42 + */ + public function testCreateTableColumnComments() + { + $table = new Table('test'); + $table->addColumn('id', 'integer', array('comment' => 'This is a comment')); + $table->setPrimaryKey(array('id')); + + $this->assertEquals($this->getCreateTableColumnCommentsSQL(), $this->_platform->getCreateTableSQL($table)); + } + + /** + * @group DBAL-42 + */ + public function testAlterTableColumnComments() + { + $tableDiff = new TableDiff('mytable'); + $tableDiff->addedColumns['quota'] = new \Doctrine\DBAL\Schema\Column('quota', \Doctrine\DBAL\Types\Type::getType('integer'), array('comment' => 'A comment')); + $tableDiff->changedColumns['bar'] = new \Doctrine\DBAL\Schema\ColumnDiff( + 'bar', new \Doctrine\DBAL\Schema\Column( + 'baz', \Doctrine\DBAL\Types\Type::getType('string'), array('comment' => 'B comment') + ), + array('comment') + ); + + $this->assertEquals($this->getAlterTableColumnCommentsSQL(), $this->_platform->getAlterTableSQL($tableDiff)); + } + + public function testCreateTableColumnTypeComments() + { + $table = new Table('test'); + $table->addColumn('id', 'integer'); + $table->addColumn('data', 'array'); + $table->setPrimaryKey(array('id')); + + $this->assertEquals($this->getCreateTableColumnTypeCommentsSQL(), $this->_platform->getCreateTableSQL($table)); + } + + public function getCreateTableColumnCommentsSQL() + { + $this->markTestSkipped('Platform does not support Column comments.'); + } + + public function getAlterTableColumnCommentsSQL() + { + $this->markTestSkipped('Platform does not support Column comments.'); + } + + public function getCreateTableColumnTypeCommentsSQL() + { + $this->markTestSkipped('Platform does not support Column comments.'); + } + + /** + * @group DBAL-45 + */ + public function testKeywordList() + { + $keywordList = $this->_platform->getReservedKeywordsList(); + $this->assertInstanceOf('Doctrine\DBAL\Platforms\Keywords\KeywordList', $keywordList); + + $this->assertTrue($keywordList->isKeyword('table')); + } + + /** + * @group DBAL-374 + */ + public function testQuotedColumnInPrimaryKeyPropagation() + { + $table = new Table('`quoted`'); + $table->addColumn('`key`', 'string'); + $table->setPrimaryKey(array('key')); + + $sql = $this->_platform->getCreateTableSQL($table); + $this->assertEquals($this->getQuotedColumnInPrimaryKeySQL(), $sql); + } + + abstract protected function getQuotedColumnInPrimaryKeySQL(); + abstract protected function getQuotedColumnInIndexSQL(); + + /** + * @group DBAL-374 + */ + public function testQuotedColumnInIndexPropagation() + { + $this->markTestSkipped('requires big refactoring of Platforms'); + + $table = new Table('`quoted`'); + $table->addColumn('`key`', 'string'); + $table->addIndex(array('key')); + + $sql = $this->_platform->getCreateTableSQL($table); + $this->assertEquals($this->getQuotedColumnInIndexSQL(), $sql); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php new file mode 100644 index 00000000..ab3cc839 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/MySqlPlatformTest.php @@ -0,0 +1,243 @@ +addColumn("Bar", "integer"); + + $sql = $this->_platform->getCreateTableSQL($table); + $this->assertEquals('CREATE TABLE Foo (Bar INT NOT NULL) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB', array_shift($sql)); + } + + public function getGenerateTableSql() + { + return 'CREATE TABLE test (id INT AUTO_INCREMENT NOT NULL, test VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'; + } + + public function getGenerateTableWithMultiColumnUniqueIndexSql() + { + return array( + 'CREATE TABLE test (foo VARCHAR(255) DEFAULT NULL, bar VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_D87F7E0C8C73652176FF8CAA (foo, bar)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB' + ); + } + + public function getGenerateAlterTableSql() + { + return array( + "ALTER TABLE mytable RENAME TO userlist, ADD quota INT DEFAULT NULL, DROP foo, CHANGE bar baz VARCHAR(255) DEFAULT 'def' NOT NULL, CHANGE bloo bloo TINYINT(1) DEFAULT '0' NOT NULL" + ); + } + + public function testGeneratesSqlSnippets() + { + $this->assertEquals('RLIKE', $this->_platform->getRegexpExpression(), 'Regular expression operator is not correct'); + $this->assertEquals('`', $this->_platform->getIdentifierQuoteCharacter(), 'Quote character is not correct'); + $this->assertEquals('CONCAT(column1, column2, column3)', $this->_platform->getConcatExpression('column1', 'column2', 'column3'), 'Concatenation function is not correct'); + } + + public function testGeneratesTransactionsCommands() + { + $this->assertEquals( + 'SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED), + '' + ); + $this->assertEquals( + 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED) + ); + $this->assertEquals( + 'SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ) + ); + $this->assertEquals( + 'SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE) + ); + } + + + public function testGeneratesDDLSnippets() + { + $this->assertEquals('SHOW DATABASES', $this->_platform->getShowDatabasesSQL()); + $this->assertEquals('CREATE DATABASE foobar', $this->_platform->getCreateDatabaseSQL('foobar')); + $this->assertEquals('DROP DATABASE foobar', $this->_platform->getDropDatabaseSQL('foobar')); + $this->assertEquals('DROP TABLE foobar', $this->_platform->getDropTableSQL('foobar')); + } + + public function testGeneratesTypeDeclarationForIntegers() + { + $this->assertEquals( + 'INT', + $this->_platform->getIntegerTypeDeclarationSQL(array()) + ); + $this->assertEquals( + 'INT AUTO_INCREMENT', + $this->_platform->getIntegerTypeDeclarationSQL(array('autoincrement' => true) + )); + $this->assertEquals( + 'INT AUTO_INCREMENT', + $this->_platform->getIntegerTypeDeclarationSQL( + array('autoincrement' => true, 'primary' => true) + )); + } + + public function testGeneratesTypeDeclarationForStrings() + { + $this->assertEquals( + 'CHAR(10)', + $this->_platform->getVarcharTypeDeclarationSQL( + array('length' => 10, 'fixed' => true) + )); + $this->assertEquals( + 'VARCHAR(50)', + $this->_platform->getVarcharTypeDeclarationSQL(array('length' => 50)), + 'Variable string declaration is not correct' + ); + $this->assertEquals( + 'VARCHAR(255)', + $this->_platform->getVarcharTypeDeclarationSQL(array()), + 'Long string declaration is not correct' + ); + } + + public function testPrefersIdentityColumns() + { + $this->assertTrue($this->_platform->prefersIdentityColumns()); + } + + public function testSupportsIdentityColumns() + { + $this->assertTrue($this->_platform->supportsIdentityColumns()); + } + + public function testDoesSupportSavePoints() + { + $this->assertTrue($this->_platform->supportsSavepoints()); + } + + public function getGenerateIndexSql() + { + return 'CREATE INDEX my_idx ON mytable (user_name, last_login)'; + } + + public function getGenerateUniqueIndexSql() + { + return 'CREATE UNIQUE INDEX index_name ON test (test, test2)'; + } + + public function getGenerateForeignKeySql() + { + return 'ALTER TABLE test ADD FOREIGN KEY (fk_name_id) REFERENCES other_table (id)'; + } + + /** + * @group DBAL-126 + */ + public function testUniquePrimaryKey() + { + $keyTable = new Table("foo"); + $keyTable->addColumn("bar", "integer"); + $keyTable->addColumn("baz", "string"); + $keyTable->setPrimaryKey(array("bar")); + $keyTable->addUniqueIndex(array("baz")); + + $oldTable = new Table("foo"); + $oldTable->addColumn("bar", "integer"); + $oldTable->addColumn("baz", "string"); + + $c = new \Doctrine\DBAL\Schema\Comparator; + $diff = $c->diffTable($oldTable, $keyTable); + + $sql = $this->_platform->getAlterTableSQL($diff); + + $this->assertEquals(array( + "ALTER TABLE foo ADD PRIMARY KEY (bar)", + "CREATE UNIQUE INDEX UNIQ_8C73652178240498 ON foo (baz)", + ), $sql); + } + + public function testModifyLimitQuery() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10, 0); + $this->assertEquals('SELECT * FROM user LIMIT 10 OFFSET 0', $sql); + } + + public function testModifyLimitQueryWithEmptyOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10); + $this->assertEquals('SELECT * FROM user LIMIT 10', $sql); + } + + /** + * @group DDC-118 + */ + public function testGetDateTimeTypeDeclarationSql() + { + $this->assertEquals("DATETIME", $this->_platform->getDateTimeTypeDeclarationSQL(array('version' => false))); + $this->assertEquals("TIMESTAMP", $this->_platform->getDateTimeTypeDeclarationSQL(array('version' => true))); + $this->assertEquals("DATETIME", $this->_platform->getDateTimeTypeDeclarationSQL(array())); + } + + public function getCreateTableColumnCommentsSQL() + { + return array("CREATE TABLE test (id INT NOT NULL COMMENT 'This is a comment', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB"); + } + + public function getAlterTableColumnCommentsSQL() + { + return array("ALTER TABLE mytable ADD quota INT NOT NULL COMMENT 'A comment', CHANGE bar baz VARCHAR(255) NOT NULL COMMENT 'B comment'"); + } + + public function getCreateTableColumnTypeCommentsSQL() + { + return array("CREATE TABLE test (id INT NOT NULL, data LONGTEXT NOT NULL COMMENT '(DC2Type:array)', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB"); + } + + /** + * @group DBAL-237 + */ + public function testChangeIndexWithForeignKeys() + { + $index = new Index("idx", array("col"), false); + $unique = new Index("uniq", array("col"), true); + + $diff = new TableDiff("test", array(), array(), array(), array($unique), array(), array($index)); + $sql = $this->_platform->getAlterTableSQL($diff); + $this->assertEquals(array("ALTER TABLE test DROP INDEX idx, ADD UNIQUE INDEX uniq (col)"), $sql); + + $diff = new TableDiff("test", array(), array(), array(), array($index), array(), array($unique)); + $sql = $this->_platform->getAlterTableSQL($diff); + $this->assertEquals(array("ALTER TABLE test DROP INDEX uniq, ADD INDEX idx (col)"), $sql); + } + + protected function getQuotedColumnInPrimaryKeySQL() + { + return array( + 'CREATE TABLE `quoted` (`key` VARCHAR(255) NOT NULL, PRIMARY KEY(`key`)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB' + ); + } + + protected function getQuotedColumnInIndexSQL() + { + return array( + 'CREATE TABLE `quoted` (`key` VARCHAR(255) NOT NULL, INDEX IDX_22660D028A90ABA9 (`key`)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB' + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php new file mode 100644 index 00000000..e0f6a5ac --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/OraclePlatformTest.php @@ -0,0 +1,285 @@ +createPlatform(); + $platform->assertValidIdentifier($identifier); + } + + static public function dataInvalidIdentifiers() + { + return array( + array('1'), + array('abc&'), + array('abc-def'), + array('"'), + array('"foo"bar"'), + ); + } + + /** + * @dataProvider dataInvalidIdentifiers + */ + public function testInvalidIdentifiers($identifier) + { + $this->setExpectedException('Doctrine\DBAL\DBALException'); + $platform = $this->createPlatform(); + $platform->assertValidIdentifier($identifier); + } + + public function createPlatform() + { + return new OraclePlatform; + } + + public function getGenerateTableSql() + { + return 'CREATE TABLE test (id NUMBER(10) NOT NULL, test VARCHAR2(255) DEFAULT NULL, PRIMARY KEY(id))'; + } + + public function getGenerateTableWithMultiColumnUniqueIndexSql() + { + return array( + 'CREATE TABLE test (foo VARCHAR2(255) DEFAULT NULL, bar VARCHAR2(255) DEFAULT NULL)', + 'CREATE UNIQUE INDEX UNIQ_D87F7E0C8C73652176FF8CAA ON test (foo, bar)', + ); + } + + public function getGenerateAlterTableSql() + { + return array( + 'ALTER TABLE mytable ADD (quota NUMBER(10) DEFAULT NULL)', + "ALTER TABLE mytable MODIFY (baz VARCHAR2(255) DEFAULT 'def' NOT NULL, bloo NUMBER(1) DEFAULT '0' NOT NULL)", + "ALTER TABLE mytable DROP (foo)", + "ALTER TABLE mytable RENAME TO userlist", + ); + } + + /** + * @expectedException Doctrine\DBAL\DBALException + */ + public function testRLike() + { + $this->assertEquals('RLIKE', $this->_platform->getRegexpExpression(), 'Regular expression operator is not correct'); + } + + public function testGeneratesSqlSnippets() + { + $this->assertEquals('"', $this->_platform->getIdentifierQuoteCharacter(), 'Identifier quote character is not correct'); + $this->assertEquals('column1 || column2 || column3', $this->_platform->getConcatExpression('column1', 'column2', 'column3'), 'Concatenation expression is not correct'); + } + + public function testGeneratesTransactionsCommands() + { + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE) + ); + } + + /** + * @expectedException Doctrine\DBAL\DBALException + */ + public function testShowDatabasesThrowsException() + { + $this->assertEquals('SHOW DATABASES', $this->_platform->getShowDatabasesSQL()); + } + + /** + * @expectedException Doctrine\DBAL\DBALException + */ + public function testCreateDatabaseThrowsException() + { + $this->assertEquals('CREATE DATABASE foobar', $this->_platform->getCreateDatabaseSQL('foobar')); + } + + public function testDropDatabaseThrowsException() + { + $this->assertEquals('DROP USER foobar CASCADE', $this->_platform->getDropDatabaseSQL('foobar')); + } + + public function testDropTable() + { + $this->assertEquals('DROP TABLE foobar', $this->_platform->getDropTableSQL('foobar')); + } + + public function testGeneratesTypeDeclarationForIntegers() + { + $this->assertEquals( + 'NUMBER(10)', + $this->_platform->getIntegerTypeDeclarationSQL(array()) + ); + $this->assertEquals( + 'NUMBER(10)', + $this->_platform->getIntegerTypeDeclarationSQL(array('autoincrement' => true) + )); + $this->assertEquals( + 'NUMBER(10)', + $this->_platform->getIntegerTypeDeclarationSQL( + array('autoincrement' => true, 'primary' => true) + )); + } + + public function testGeneratesTypeDeclarationsForStrings() + { + $this->assertEquals( + 'CHAR(10)', + $this->_platform->getVarcharTypeDeclarationSQL( + array('length' => 10, 'fixed' => true) + )); + $this->assertEquals( + 'VARCHAR2(50)', + $this->_platform->getVarcharTypeDeclarationSQL(array('length' => 50)), + 'Variable string declaration is not correct' + ); + $this->assertEquals( + 'VARCHAR2(255)', + $this->_platform->getVarcharTypeDeclarationSQL(array()), + 'Long string declaration is not correct' + ); + } + + public function testPrefersIdentityColumns() + { + $this->assertFalse($this->_platform->prefersIdentityColumns()); + } + + public function testSupportsIdentityColumns() + { + $this->assertFalse($this->_platform->supportsIdentityColumns()); + } + + public function testSupportsSavePoints() + { + $this->assertTrue($this->_platform->supportsSavepoints()); + } + + public function getGenerateIndexSql() + { + return 'CREATE INDEX my_idx ON mytable (user_name, last_login)'; + } + + public function getGenerateUniqueIndexSql() + { + return 'CREATE UNIQUE INDEX index_name ON test (test, test2)'; + } + + public function getGenerateForeignKeySql() + { + return 'ALTER TABLE test ADD FOREIGN KEY (fk_name_id) REFERENCES other_table (id)'; + } + + public function testModifyLimitQuery() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10, 0); + $this->assertEquals('SELECT a.* FROM (SELECT * FROM user) a WHERE ROWNUM <= 10', $sql); + } + + public function testModifyLimitQueryWithEmptyOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10); + $this->assertEquals('SELECT a.* FROM (SELECT * FROM user) a WHERE ROWNUM <= 10', $sql); + } + + public function testModifyLimitQueryWithAscOrderBy() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username ASC', 10); + $this->assertEquals('SELECT a.* FROM (SELECT * FROM user ORDER BY username ASC) a WHERE ROWNUM <= 10', $sql); + } + + public function testModifyLimitQueryWithDescOrderBy() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username DESC', 10); + $this->assertEquals('SELECT a.* FROM (SELECT * FROM user ORDER BY username DESC) a WHERE ROWNUM <= 10', $sql); + } + + public function getCreateTableColumnCommentsSQL() + { + return array( + "CREATE TABLE test (id NUMBER(10) NOT NULL, PRIMARY KEY(id))", + "COMMENT ON COLUMN test.id IS 'This is a comment'", + ); + } + + public function getCreateTableColumnTypeCommentsSQL() + { + return array( + "CREATE TABLE test (id NUMBER(10) NOT NULL, data CLOB NOT NULL, PRIMARY KEY(id))", + "COMMENT ON COLUMN test.data IS '(DC2Type:array)'" + ); + } + + public function getAlterTableColumnCommentsSQL() + { + return array( + "ALTER TABLE mytable ADD (quota NUMBER(10) NOT NULL)", + "ALTER TABLE mytable MODIFY (baz VARCHAR2(255) NOT NULL)", + "COMMENT ON COLUMN mytable.quota IS 'A comment'", + "COMMENT ON COLUMN mytable.baz IS 'B comment'", + ); + } + + public function getBitAndComparisonExpressionSql($value1, $value2) + { + return 'BITAND('.$value1 . ', ' . $value2 . ')'; + } + + public function getBitOrComparisonExpressionSql($value1, $value2) + { + return '(' . $value1 . '-' . + $this->getBitAndComparisonExpressionSql($value1, $value2) + . '+' . $value2 . ')'; + } + + protected function getQuotedColumnInPrimaryKeySQL() + { + return array('CREATE TABLE "quoted" ("key" VARCHAR2(255) NOT NULL, PRIMARY KEY("key"))'); + } + + protected function getQuotedColumnInIndexSQL() + { + return array( + 'CREATE TABLE "quoted" ("key" VARCHAR2(255) NOT NULL)', + 'CREATE INDEX IDX_22660D028A90ABA9 ON "quoted" ("key")', + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php new file mode 100644 index 00000000..433caf63 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php @@ -0,0 +1,243 @@ + 'CASCADE') + ); + $this->assertEquals( + "CONSTRAINT my_fk FOREIGN KEY (foreign_id) REFERENCES my_table (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE", + $this->_platform->getForeignKeyDeclarationSQL($foreignKey) + ); + } + + public function testGeneratesSqlSnippets() + { + $this->assertEquals('SIMILAR TO', $this->_platform->getRegexpExpression(), 'Regular expression operator is not correct'); + $this->assertEquals('"', $this->_platform->getIdentifierQuoteCharacter(), 'Identifier quote character is not correct'); + $this->assertEquals('column1 || column2 || column3', $this->_platform->getConcatExpression('column1', 'column2', 'column3'), 'Concatenation expression is not correct'); + $this->assertEquals('SUBSTR(column, 5)', $this->_platform->getSubstringExpression('column', 5), 'Substring expression without length is not correct'); + $this->assertEquals('SUBSTR(column, 0, 5)', $this->_platform->getSubstringExpression('column', 0, 5), 'Substring expression with length is not correct'); + } + + public function testGeneratesTransactionCommands() + { + $this->assertEquals( + 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ UNCOMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED) + ); + $this->assertEquals( + 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED) + ); + $this->assertEquals( + 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ) + ); + $this->assertEquals( + 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE) + ); + } + + public function testGeneratesDDLSnippets() + { + $this->assertEquals('CREATE DATABASE foobar', $this->_platform->getCreateDatabaseSQL('foobar')); + $this->assertEquals('DROP DATABASE foobar', $this->_platform->getDropDatabaseSQL('foobar')); + $this->assertEquals('DROP TABLE foobar', $this->_platform->getDropTableSQL('foobar')); + } + + public function testGenerateTableWithAutoincrement() + { + $table = new \Doctrine\DBAL\Schema\Table('autoinc_table'); + $column = $table->addColumn('id', 'integer'); + $column->setAutoincrement(true); + + $this->assertEquals(array('CREATE TABLE autoinc_table (id SERIAL NOT NULL)'), $this->_platform->getCreateTableSQL($table)); + } + + public function testGeneratesTypeDeclarationForIntegers() + { + $this->assertEquals( + 'INT', + $this->_platform->getIntegerTypeDeclarationSQL(array()) + ); + $this->assertEquals( + 'SERIAL', + $this->_platform->getIntegerTypeDeclarationSQL(array('autoincrement' => true) + )); + $this->assertEquals( + 'SERIAL', + $this->_platform->getIntegerTypeDeclarationSQL( + array('autoincrement' => true, 'primary' => true) + )); + } + + public function testGeneratesTypeDeclarationForStrings() + { + $this->assertEquals( + 'CHAR(10)', + $this->_platform->getVarcharTypeDeclarationSQL( + array('length' => 10, 'fixed' => true)) + ); + $this->assertEquals( + 'VARCHAR(50)', + $this->_platform->getVarcharTypeDeclarationSQL(array('length' => 50)), + 'Variable string declaration is not correct' + ); + $this->assertEquals( + 'VARCHAR(255)', + $this->_platform->getVarcharTypeDeclarationSQL(array()), + 'Long string declaration is not correct' + ); + } + + public function getGenerateUniqueIndexSql() + { + return 'CREATE UNIQUE INDEX index_name ON test (test, test2)'; + } + + public function testGeneratesSequenceSqlCommands() + { + $sequence = new \Doctrine\DBAL\Schema\Sequence('myseq', 20, 1); + $this->assertEquals( + 'CREATE SEQUENCE myseq INCREMENT BY 20 MINVALUE 1 START 1', + $this->_platform->getCreateSequenceSQL($sequence) + ); + $this->assertEquals( + 'DROP SEQUENCE myseq', + $this->_platform->getDropSequenceSQL('myseq') + ); + $this->assertEquals( + "SELECT NEXTVAL('myseq')", + $this->_platform->getSequenceNextValSQL('myseq') + ); + } + + public function testDoesNotPreferIdentityColumns() + { + $this->assertFalse($this->_platform->prefersIdentityColumns()); + } + + public function testPrefersSequences() + { + $this->assertTrue($this->_platform->prefersSequences()); + } + + public function testSupportsIdentityColumns() + { + $this->assertTrue($this->_platform->supportsIdentityColumns()); + } + + public function testSupportsSavePoints() + { + $this->assertTrue($this->_platform->supportsSavepoints()); + } + + public function testSupportsSequences() + { + $this->assertTrue($this->_platform->supportsSequences()); + } + + public function testModifyLimitQuery() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10, 0); + $this->assertEquals('SELECT * FROM user LIMIT 10 OFFSET 0', $sql); + } + + public function testModifyLimitQueryWithEmptyOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10); + $this->assertEquals('SELECT * FROM user LIMIT 10', $sql); + } + + public function getCreateTableColumnCommentsSQL() + { + return array( + "CREATE TABLE test (id INT NOT NULL, PRIMARY KEY(id))", + "COMMENT ON COLUMN test.id IS 'This is a comment'", + ); + } + + public function getAlterTableColumnCommentsSQL() + { + return array( + "ALTER TABLE mytable ADD quota INT NOT NULL", + "COMMENT ON COLUMN mytable.quota IS 'A comment'", + "COMMENT ON COLUMN mytable.baz IS 'B comment'", + ); + } + + public function getCreateTableColumnTypeCommentsSQL() + { + return array( + "CREATE TABLE test (id INT NOT NULL, data TEXT NOT NULL, PRIMARY KEY(id))", + "COMMENT ON COLUMN test.data IS '(DC2Type:array)'" + ); + } + + protected function getQuotedColumnInPrimaryKeySQL() + { + return array( + 'CREATE TABLE "quoted" ("key" VARCHAR(255) NOT NULL, PRIMARY KEY("key"))', + ); + } + + protected function getQuotedColumnInIndexSQL() + { + return array( + 'CREATE TABLE "quoted" ("key" VARCHAR(255) NOT NULL)', + 'CREATE INDEX IDX_22660D028A90ABA9 ON "quoted" ("key")', + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/ReservedKeywordsValidatorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/ReservedKeywordsValidatorTest.php new file mode 100644 index 00000000..388115f6 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/ReservedKeywordsValidatorTest.php @@ -0,0 +1,47 @@ +validator = new ReservedKeywordsValidator(array( + new \Doctrine\DBAL\Platforms\Keywords\MySQLKeywords() + )); + } + + public function testReservedTableName() + { + $table = new Table("TABLE"); + $this->validator->acceptTable($table); + + $this->assertEquals( + array('Table TABLE keyword violations: MySQL'), + $this->validator->getViolations() + ); + } + + public function testReservedColumnName() + { + $table = new Table("TABLE"); + $column = $table->addColumn('table', 'string'); + + $this->validator->acceptColumn($table, $column); + + $this->assertEquals( + array('Table TABLE column table keyword violations: MySQL'), + $this->validator->getViolations() + ); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php new file mode 100644 index 00000000..f81938da --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php @@ -0,0 +1,28 @@ +platform = new \Doctrine\DBAL\Platforms\SQLAzurePlatform(); + } + + public function testCreateFederatedOnTable() + { + $table = new \Doctrine\DBAL\Schema\Table("tbl"); + $table->addColumn("id", "integer"); + $table->addOption('azure.federatedOnDistributionName', 'TblId'); + $table->addOption('azure.federatedOnColumnName', 'id'); + + $this->assertEquals(array('CREATE TABLE tbl (id INT NOT NULL) FEDERATED ON (TblId = id)'), $this->platform->getCreateTableSQL($table)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php new file mode 100644 index 00000000..397e4cfc --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php @@ -0,0 +1,245 @@ +assertEquals('RLIKE', $this->_platform->getRegexpExpression(), 'Regular expression operator is not correct'); + $this->assertEquals('"', $this->_platform->getIdentifierQuoteCharacter(), 'Identifier quote character is not correct'); + $this->assertEquals('(column1 + column2 + column3)', $this->_platform->getConcatExpression('column1', 'column2', 'column3'), 'Concatenation expression is not correct'); + } + + public function testGeneratesTransactionsCommands() + { + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL READ COMMITTED', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ) + ); + $this->assertEquals( + 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE) + ); + } + + public function testGeneratesDDLSnippets() + { + $dropDatabaseExpectation = 'DROP DATABASE foobar'; + + $this->assertEquals('SHOW DATABASES', $this->_platform->getShowDatabasesSQL()); + $this->assertEquals('CREATE DATABASE foobar', $this->_platform->getCreateDatabaseSQL('foobar')); + $this->assertEquals($dropDatabaseExpectation, $this->_platform->getDropDatabaseSQL('foobar')); + $this->assertEquals('DROP TABLE foobar', $this->_platform->getDropTableSQL('foobar')); + } + + public function testGeneratesTypeDeclarationForIntegers() + { + $this->assertEquals( + 'INT', + $this->_platform->getIntegerTypeDeclarationSQL(array()) + ); + $this->assertEquals( + 'INT IDENTITY', + $this->_platform->getIntegerTypeDeclarationSQL(array('autoincrement' => true) + )); + $this->assertEquals( + 'INT IDENTITY', + $this->_platform->getIntegerTypeDeclarationSQL( + array('autoincrement' => true, 'primary' => true) + )); + } + + public function testGeneratesTypeDeclarationsForStrings() + { + $this->assertEquals( + 'NCHAR(10)', + $this->_platform->getVarcharTypeDeclarationSQL( + array('length' => 10, 'fixed' => true) + )); + $this->assertEquals( + 'NVARCHAR(50)', + $this->_platform->getVarcharTypeDeclarationSQL(array('length' => 50)), + 'Variable string declaration is not correct' + ); + $this->assertEquals( + 'NVARCHAR(255)', + $this->_platform->getVarcharTypeDeclarationSQL(array()), + 'Long string declaration is not correct' + ); + } + + public function testPrefersIdentityColumns() + { + $this->assertTrue($this->_platform->prefersIdentityColumns()); + } + + public function testSupportsIdentityColumns() + { + $this->assertTrue($this->_platform->supportsIdentityColumns()); + } + + public function testDoesNotSupportSavePoints() + { + $this->assertTrue($this->_platform->supportsSavepoints()); + } + + public function getGenerateIndexSql() + { + return 'CREATE INDEX my_idx ON mytable (user_name, last_login)'; + } + + public function getGenerateUniqueIndexSql() + { + return 'CREATE UNIQUE INDEX index_name ON test (test, test2) WHERE test IS NOT NULL AND test2 IS NOT NULL'; + } + + public function getGenerateForeignKeySql() + { + return 'ALTER TABLE test ADD FOREIGN KEY (fk_name_id) REFERENCES other_table (id)'; + } + + public function testModifyLimitQuery() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10, 0); + $this->assertEquals('SELECT TOP 10 * FROM user', $sql); + } + + public function testModifyLimitQueryWithEmptyOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10); + $this->assertEquals('SELECT TOP 10 * FROM user', $sql); + } + + public function testModifyLimitQueryWithOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username DESC', 10, 5); + $this->assertEquals('SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY username DESC) AS doctrine_rownum, * FROM user) AS doctrine_tbl WHERE doctrine_rownum BETWEEN 6 AND 15', $sql); + } + + public function testModifyLimitQueryWithAscOrderBy() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username ASC', 10); + $this->assertEquals('SELECT TOP 10 * FROM user ORDER BY username ASC', $sql); + } + + public function testModifyLimitQueryWithDescOrderBy() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username DESC', 10); + $this->assertEquals('SELECT TOP 10 * FROM user ORDER BY username DESC', $sql); + } + + /** + * @group DDC-1360 + */ + public function testQuoteIdentifier() + { + $this->assertEquals('[fo][o]', $this->_platform->quoteIdentifier('fo]o')); + $this->assertEquals('[test]', $this->_platform->quoteIdentifier('test')); + $this->assertEquals('[test].[test]', $this->_platform->quoteIdentifier('test.test')); + } + + /** + * @group DDC-1360 + */ + public function testQuoteSingleIdentifier() + { + $this->assertEquals('[fo][o]', $this->_platform->quoteSingleIdentifier('fo]o')); + $this->assertEquals('[test]', $this->_platform->quoteSingleIdentifier('test')); + $this->assertEquals('[test.test]', $this->_platform->quoteSingleIdentifier('test.test')); + } + + /** + * @group DBAL-220 + */ + public function testCreateClusteredIndex() + { + $idx = new \Doctrine\DBAL\Schema\Index('idx', array('id')); + $idx->addFlag('clustered'); + $this->assertEquals('CREATE CLUSTERED INDEX idx ON tbl (id)', $this->_platform->getCreateIndexSQL($idx, 'tbl')); + } + + /** + * @group DBAL-220 + */ + public function testCreateNonClusteredPrimaryKeyInTable() + { + $table = new \Doctrine\DBAL\Schema\Table("tbl"); + $table->addColumn("id", "integer"); + $table->setPrimaryKey(Array("id")); + $table->getIndex('primary')->addFlag('nonclustered'); + + $this->assertEquals(array('CREATE TABLE tbl (id INT NOT NULL, PRIMARY KEY NONCLUSTERED (id))'), $this->_platform->getCreateTableSQL($table)); + } + + /** + * @group DBAL-220 + */ + public function testCreateNonClusteredPrimaryKey() + { + $idx = new \Doctrine\DBAL\Schema\Index('idx', array('id'), false, true); + $idx->addFlag('nonclustered'); + $this->assertEquals('ALTER TABLE tbl ADD PRIMARY KEY NONCLUSTERED (id)', $this->_platform->getCreatePrimaryKeySQL($idx, 'tbl')); + } + + public function testAlterAddPrimaryKey() + { + $idx = new \Doctrine\DBAL\Schema\Index('idx', array('id'), false, true); + $this->assertEquals('ALTER TABLE tbl ADD PRIMARY KEY (id)', $this->_platform->getCreateIndexSQL($idx, 'tbl')); + } + + protected function getQuotedColumnInPrimaryKeySQL() + { + return array( + 'CREATE TABLE [quoted] ([key] NVARCHAR(255) NOT NULL, PRIMARY KEY ([key]))', + ); + } + + protected function getQuotedColumnInIndexSQL() + { + return array( + 'CREATE TABLE [quoted] ([key] NVARCHAR(255) NOT NULL)', + 'CREATE INDEX IDX_22660D028A90ABA9 ON [quoted] ([key])', + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SqlitePlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SqlitePlatformTest.php new file mode 100644 index 00000000..aae3465e --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SqlitePlatformTest.php @@ -0,0 +1,170 @@ +assertEquals('RLIKE', $this->_platform->getRegexpExpression(), 'Regular expression operator is not correct'); + $this->assertEquals('SUBSTR(column, 5, LENGTH(column))', $this->_platform->getSubstringExpression('column', 5), 'Substring expression without length is not correct'); + $this->assertEquals('SUBSTR(column, 0, 5)', $this->_platform->getSubstringExpression('column', 0, 5), 'Substring expression with length is not correct'); + } + + public function testGeneratesTransactionCommands() + { + $this->assertEquals( + 'PRAGMA read_uncommitted = 0', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED) + ); + $this->assertEquals( + 'PRAGMA read_uncommitted = 1', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED) + ); + $this->assertEquals( + 'PRAGMA read_uncommitted = 1', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ) + ); + $this->assertEquals( + 'PRAGMA read_uncommitted = 1', + $this->_platform->getSetTransactionIsolationSQL(\Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE) + ); + } + + public function testPrefersIdentityColumns() + { + $this->assertTrue($this->_platform->prefersIdentityColumns()); + } + + public function testGeneratesTypeDeclarationForIntegers() + { + $this->assertEquals( + 'INTEGER', + $this->_platform->getIntegerTypeDeclarationSQL(array()) + ); + $this->assertEquals( + 'INTEGER', + $this->_platform->getIntegerTypeDeclarationSQL(array('autoincrement' => true)) + ); + $this->assertEquals( + 'INTEGER', + $this->_platform->getIntegerTypeDeclarationSQL( + array('autoincrement' => true, 'primary' => true)) + ); + } + + public function testGeneratesTypeDeclarationForStrings() + { + $this->assertEquals( + 'CHAR(10)', + $this->_platform->getVarcharTypeDeclarationSQL( + array('length' => 10, 'fixed' => true)) + ); + $this->assertEquals( + 'VARCHAR(50)', + $this->_platform->getVarcharTypeDeclarationSQL(array('length' => 50)), + 'Variable string declaration is not correct' + ); + $this->assertEquals( + 'VARCHAR(255)', + $this->_platform->getVarcharTypeDeclarationSQL(array()), + 'Long string declaration is not correct' + ); + } + + public function getGenerateIndexSql() + { + return 'CREATE INDEX my_idx ON mytable (user_name, last_login)'; + } + + public function getGenerateUniqueIndexSql() + { + return 'CREATE UNIQUE INDEX index_name ON test (test, test2)'; + } + + public function getGenerateForeignKeySql() + { + $this->markTestSkipped('SQLite does not support ForeignKeys.'); + } + + public function testModifyLimitQuery() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10, 0); + $this->assertEquals('SELECT * FROM user LIMIT 10 OFFSET 0', $sql); + } + + public function testModifyLimitQueryWithEmptyOffset() + { + $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user', 10); + $this->assertEquals('SELECT * FROM user LIMIT 10', $sql); + } + + public function getGenerateAlterTableSql() + { + $this->markTestSkipped('SQlite does not support ALTER Table.'); + } + + public function testGetAlterTableSqlDispatchEvent() + { + $this->markTestSkipped('SQlite does not support ALTER Table.'); + } + + /** + * @group DDC-1845 + */ + public function testGenerateTableSqlShouldNotAutoQuotePrimaryKey() + { + $table = new \Doctrine\DBAL\Schema\Table('test'); + $table->addColumn('"like"', 'integer', array('notnull' => true, 'autoincrement' => true)); + $table->setPrimaryKey(array('"like"')); + + $createTableSQL = $this->_platform->getCreateTableSQL($table); + $this->assertEquals( + 'CREATE TABLE test ("like" INTEGER NOT NULL, PRIMARY KEY("like"))', + $createTableSQL[0] + ); + + $this->assertEquals( + 'ALTER TABLE test ADD PRIMARY KEY ("like")', + $this->_platform->getCreatePrimaryKeySQL($table->getIndex('primary'), 'test') + ); + } + + protected function getQuotedColumnInPrimaryKeySQL() + { + return array( + 'CREATE TABLE "quoted" ("key" VARCHAR(255) NOT NULL, PRIMARY KEY("key"))', + ); + } + + protected function getQuotedColumnInIndexSQL() + { + return array( + 'CREATE TABLE "quoted" ("key" VARCHAR(255) NOT NULL)', + 'CREATE INDEX IDX_22660D028A90ABA9 ON "quoted" ("key")', + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/CompositeExpressionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/CompositeExpressionTest.php new file mode 100644 index 00000000..99396598 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/CompositeExpressionTest.php @@ -0,0 +1,82 @@ +assertEquals(1, count($expr)); + + $expr->add('u.group_id = 2'); + + $this->assertEquals(2, count($expr)); + } + + /** + * @dataProvider provideDataForConvertToString + */ + public function testCompositeUsageAndGeneration($type, $parts, $expects) + { + $expr = new CompositeExpression($type, $parts); + + $this->assertEquals($expects, (string) $expr); + } + + public function provideDataForConvertToString() + { + return array( + array( + CompositeExpression::TYPE_AND, + array('u.user = 1'), + 'u.user = 1' + ), + array( + CompositeExpression::TYPE_AND, + array('u.user = 1', 'u.group_id = 1'), + '(u.user = 1) AND (u.group_id = 1)' + ), + array( + CompositeExpression::TYPE_OR, + array('u.user = 1'), + 'u.user = 1' + ), + array( + CompositeExpression::TYPE_OR, + array('u.group_id = 1', 'u.group_id = 2'), + '(u.group_id = 1) OR (u.group_id = 2)' + ), + array( + CompositeExpression::TYPE_AND, + array( + 'u.user = 1', + new CompositeExpression( + CompositeExpression::TYPE_OR, + array('u.group_id = 1', 'u.group_id = 2') + ) + ), + '(u.user = 1) AND ((u.group_id = 1) OR (u.group_id = 2))' + ), + array( + CompositeExpression::TYPE_OR, + array( + 'u.group_id = 1', + new CompositeExpression( + CompositeExpression::TYPE_AND, + array('u.user = 1', 'u.group_id = 2') + ) + ), + '(u.group_id = 1) OR ((u.user = 1) AND (u.group_id = 2))' + ), + ); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/ExpressionBuilderTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/ExpressionBuilderTest.php new file mode 100644 index 00000000..1893e97f --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/Expression/ExpressionBuilderTest.php @@ -0,0 +1,201 @@ +getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + + $this->expr = new ExpressionBuilder($conn); + + $conn->expects($this->any()) + ->method('getExpressionBuilder') + ->will($this->returnValue($this->expr)); + } + + /** + * @dataProvider provideDataForAndX + */ + public function testAndX($parts, $expected) + { + $composite = $this->expr->andX(); + + foreach ($parts as $part) { + $composite->add($part); + } + + $this->assertEquals($expected, (string) $composite); + } + + public function provideDataForAndX() + { + return array( + array( + array('u.user = 1'), + 'u.user = 1' + ), + array( + array('u.user = 1', 'u.group_id = 1'), + '(u.user = 1) AND (u.group_id = 1)' + ), + array( + array('u.user = 1'), + 'u.user = 1' + ), + array( + array('u.group_id = 1', 'u.group_id = 2'), + '(u.group_id = 1) AND (u.group_id = 2)' + ), + array( + array( + 'u.user = 1', + new CompositeExpression( + CompositeExpression::TYPE_OR, + array('u.group_id = 1', 'u.group_id = 2') + ) + ), + '(u.user = 1) AND ((u.group_id = 1) OR (u.group_id = 2))' + ), + array( + array( + 'u.group_id = 1', + new CompositeExpression( + CompositeExpression::TYPE_AND, + array('u.user = 1', 'u.group_id = 2') + ) + ), + '(u.group_id = 1) AND ((u.user = 1) AND (u.group_id = 2))' + ), + ); + } + + /** + * @dataProvider provideDataForOrX + */ + public function testOrX($parts, $expected) + { + $composite = $this->expr->orX(); + + foreach ($parts as $part) { + $composite->add($part); + } + + $this->assertEquals($expected, (string) $composite); + } + + public function provideDataForOrX() + { + return array( + array( + array('u.user = 1'), + 'u.user = 1' + ), + array( + array('u.user = 1', 'u.group_id = 1'), + '(u.user = 1) OR (u.group_id = 1)' + ), + array( + array('u.user = 1'), + 'u.user = 1' + ), + array( + array('u.group_id = 1', 'u.group_id = 2'), + '(u.group_id = 1) OR (u.group_id = 2)' + ), + array( + array( + 'u.user = 1', + new CompositeExpression( + CompositeExpression::TYPE_OR, + array('u.group_id = 1', 'u.group_id = 2') + ) + ), + '(u.user = 1) OR ((u.group_id = 1) OR (u.group_id = 2))' + ), + array( + array( + 'u.group_id = 1', + new CompositeExpression( + CompositeExpression::TYPE_AND, + array('u.user = 1', 'u.group_id = 2') + ) + ), + '(u.group_id = 1) OR ((u.user = 1) AND (u.group_id = 2))' + ), + ); + } + + /** + * @dataProvider provideDataForComparison + */ + public function testComparison($leftExpr, $operator, $rightExpr, $expected) + { + $part = $this->expr->comparison($leftExpr, $operator, $rightExpr); + + $this->assertEquals($expected, (string) $part); + } + + public function provideDataForComparison() + { + return array( + array('u.user_id', ExpressionBuilder::EQ, '1', 'u.user_id = 1'), + array('u.user_id', ExpressionBuilder::NEQ, '1', 'u.user_id <> 1'), + array('u.salary', ExpressionBuilder::LT, '10000', 'u.salary < 10000'), + array('u.salary', ExpressionBuilder::LTE, '10000', 'u.salary <= 10000'), + array('u.salary', ExpressionBuilder::GT, '10000', 'u.salary > 10000'), + array('u.salary', ExpressionBuilder::GTE, '10000', 'u.salary >= 10000'), + ); + } + + public function testEq() + { + $this->assertEquals('u.user_id = 1', $this->expr->eq('u.user_id', '1')); + } + + public function testNeq() + { + $this->assertEquals('u.user_id <> 1', $this->expr->neq('u.user_id', '1')); + } + + public function testLt() + { + $this->assertEquals('u.salary < 10000', $this->expr->lt('u.salary', '10000')); + } + + public function testLte() + { + $this->assertEquals('u.salary <= 10000', $this->expr->lte('u.salary', '10000')); + } + + public function testGt() + { + $this->assertEquals('u.salary > 10000', $this->expr->gt('u.salary', '10000')); + } + + public function testGte() + { + $this->assertEquals('u.salary >= 10000', $this->expr->gte('u.salary', '10000')); + } + + public function testIsNull() + { + $this->assertEquals('u.deleted IS NULL', $this->expr->isNull('u.deleted')); + } + + public function testIsNotNull() + { + $this->assertEquals('u.updated IS NOT NULL', $this->expr->isNotNull('u.updated')); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php new file mode 100644 index 00000000..98f16167 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php @@ -0,0 +1,572 @@ +conn = $this->getMock('Doctrine\DBAL\Connection', array(), array(), '', false); + + $expressionBuilder = new ExpressionBuilder($this->conn); + + $this->conn->expects($this->any()) + ->method('getExpressionBuilder') + ->will($this->returnValue($expressionBuilder)); + } + + public function testSimpleSelect() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.id') + ->from('users', 'u'); + + $this->assertEquals('SELECT u.id FROM users u', (string) $qb); + } + + public function testSelectWithSimpleWhere() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.id') + ->from('users', 'u') + ->where($expr->andX($expr->eq('u.nickname', '?'))); + + $this->assertEquals("SELECT u.id FROM users u WHERE u.nickname = ?", (string) $qb); + } + + public function testSelectWithLeftJoin() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->leftJoin('u', 'phones', 'p', $expr->eq('p.user_id', 'u.id')); + + $this->assertEquals('SELECT u.*, p.* FROM users u LEFT JOIN phones p ON p.user_id = u.id', (string) $qb); + } + + public function testSelectWithJoin() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->Join('u', 'phones', 'p', $expr->eq('p.user_id', 'u.id')); + + $this->assertEquals('SELECT u.*, p.* FROM users u INNER JOIN phones p ON p.user_id = u.id', (string) $qb); + } + + public function testSelectWithInnerJoin() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->innerJoin('u', 'phones', 'p', $expr->eq('p.user_id', 'u.id')); + + $this->assertEquals('SELECT u.*, p.* FROM users u INNER JOIN phones p ON p.user_id = u.id', (string) $qb); + } + + public function testSelectWithRightJoin() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->rightJoin('u', 'phones', 'p', $expr->eq('p.user_id', 'u.id')); + + $this->assertEquals('SELECT u.*, p.* FROM users u RIGHT JOIN phones p ON p.user_id = u.id', (string) $qb); + } + + public function testSelectWithAndWhereConditions() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->where('u.username = ?') + ->andWhere('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u WHERE (u.username = ?) AND (u.name = ?)', (string) $qb); + } + + public function testSelectWithOrWhereConditions() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->where('u.username = ?') + ->orWhere('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u WHERE (u.username = ?) OR (u.name = ?)', (string) $qb); + } + + public function testSelectWithOrOrWhereConditions() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->orWhere('u.username = ?') + ->orWhere('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u WHERE (u.username = ?) OR (u.name = ?)', (string) $qb); + } + + public function testSelectWithAndOrWhereConditions() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->where('u.username = ?') + ->andWhere('u.username = ?') + ->orWhere('u.name = ?') + ->andWhere('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u WHERE (((u.username = ?) AND (u.username = ?)) OR (u.name = ?)) AND (u.name = ?)', (string) $qb); + } + + public function testSelectGroupBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id', (string) $qb); + } + + public function testSelectEmptyGroupBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->groupBy(array()) + ->from('users', 'u'); + + $this->assertEquals('SELECT u.*, p.* FROM users u', (string) $qb); + } + + public function testSelectEmptyAddGroupBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->addGroupBy(array()) + ->from('users', 'u'); + + $this->assertEquals('SELECT u.*, p.* FROM users u', (string) $qb); + } + + public function testSelectAddGroupBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->addGroupBy('u.foo'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id, u.foo', (string) $qb); + } + + public function testSelectAddGroupBys() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->addGroupBy('u.foo', 'u.bar'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id, u.foo, u.bar', (string) $qb); + } + + public function testSelectHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->having('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING u.name = ?', (string) $qb); + } + + public function testSelectAndHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->andHaving('u.name = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING u.name = ?', (string) $qb); + } + + public function testSelectHavingAndHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->having('u.name = ?') + ->andHaving('u.username = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING (u.name = ?) AND (u.username = ?)', (string) $qb); + } + + public function testSelectHavingOrHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->having('u.name = ?') + ->orHaving('u.username = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING (u.name = ?) OR (u.username = ?)', (string) $qb); + } + + public function testSelectOrHavingOrHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->orHaving('u.name = ?') + ->orHaving('u.username = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING (u.name = ?) OR (u.username = ?)', (string) $qb); + } + + public function testSelectHavingAndOrHaving() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->groupBy('u.id') + ->having('u.name = ?') + ->orHaving('u.username = ?') + ->andHaving('u.username = ?'); + + $this->assertEquals('SELECT u.*, p.* FROM users u GROUP BY u.id HAVING ((u.name = ?) OR (u.username = ?)) AND (u.username = ?)', (string) $qb); + } + + public function testSelectOrderBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->orderBy('u.name'); + + $this->assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC', (string) $qb); + } + + public function testSelectAddOrderBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->orderBy('u.name') + ->addOrderBy('u.username', 'DESC'); + + $this->assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb); + } + + public function testSelectAddAddOrderBy() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*', 'p.*') + ->from('users', 'u') + ->addOrderBy('u.name') + ->addOrderBy('u.username', 'DESC'); + + $this->assertEquals('SELECT u.*, p.* FROM users u ORDER BY u.name ASC, u.username DESC', (string) $qb); + } + + public function testEmptySelect() + { + $qb = new QueryBuilder($this->conn); + $qb2 = $qb->select(); + + $this->assertSame($qb, $qb2); + $this->assertEquals(QueryBuilder::SELECT, $qb->getType()); + } + + public function testSelectAddSelect() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*') + ->addSelect('p.*') + ->from('users', 'u'); + + $this->assertEquals('SELECT u.*, p.* FROM users u', (string) $qb); + } + + public function testEmptyAddSelect() + { + $qb = new QueryBuilder($this->conn); + $qb2 = $qb->addSelect(); + + $this->assertSame($qb, $qb2); + $this->assertEquals(QueryBuilder::SELECT, $qb->getType()); + } + + public function testSelectMultipleFrom() + { + $qb = new QueryBuilder($this->conn); + $expr = $qb->expr(); + + $qb->select('u.*') + ->addSelect('p.*') + ->from('users', 'u') + ->from('phonenumbers', 'p'); + + $this->assertEquals('SELECT u.*, p.* FROM users u, phonenumbers p', (string) $qb); + } + + public function testUpdate() + { + $qb = new QueryBuilder($this->conn); + $qb->update('users', 'u') + ->set('u.foo', '?') + ->set('u.bar', '?'); + + $this->assertEquals(QueryBuilder::UPDATE, $qb->getType()); + $this->assertEquals('UPDATE users u SET u.foo = ?, u.bar = ?', (string) $qb); + } + + public function testUpdateWithoutAlias() + { + $qb = new QueryBuilder($this->conn); + $qb->update('users') + ->set('foo', '?') + ->set('bar', '?'); + + $this->assertEquals('UPDATE users SET foo = ?, bar = ?', (string) $qb); + } + + public function testUpdateWhere() + { + $qb = new QueryBuilder($this->conn); + $qb->update('users', 'u') + ->set('u.foo', '?') + ->where('u.foo = ?'); + + $this->assertEquals('UPDATE users u SET u.foo = ? WHERE u.foo = ?', (string) $qb); + } + + public function testEmptyUpdate() + { + $qb = new QueryBuilder($this->conn); + $qb2 = $qb->update(); + + $this->assertEquals(QueryBuilder::UPDATE, $qb->getType()); + $this->assertSame($qb2, $qb); + } + + public function testDelete() + { + $qb = new QueryBuilder($this->conn); + $qb->delete('users', 'u'); + + $this->assertEquals(QueryBuilder::DELETE, $qb->getType()); + $this->assertEquals('DELETE FROM users u', (string) $qb); + } + + public function testDeleteWithoutAlias() + { + $qb = new QueryBuilder($this->conn); + $qb->delete('users'); + + $this->assertEquals(QueryBuilder::DELETE, $qb->getType()); + $this->assertEquals('DELETE FROM users', (string) $qb); + } + + public function testDeleteWhere() + { + $qb = new QueryBuilder($this->conn); + $qb->delete('users', 'u') + ->where('u.foo = ?'); + + $this->assertEquals('DELETE FROM users u WHERE u.foo = ?', (string) $qb); + } + + public function testEmptyDelete() + { + $qb = new QueryBuilder($this->conn); + $qb2 = $qb->delete(); + + $this->assertEquals(QueryBuilder::DELETE, $qb->getType()); + $this->assertSame($qb2, $qb); + } + + public function testGetConnection() + { + $qb = new QueryBuilder($this->conn); + $this->assertSame($this->conn, $qb->getConnection()); + } + + public function testGetState() + { + $qb = new QueryBuilder($this->conn); + + $this->assertEquals(QueryBuilder::STATE_CLEAN, $qb->getState()); + + $qb->select('u.*')->from('users', 'u'); + + $this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState()); + + $sql1 = $qb->getSQL(); + + $this->assertEquals(QueryBuilder::STATE_CLEAN, $qb->getState()); + $this->assertEquals($sql1, $qb->getSQL()); + } + + public function testSetMaxResults() + { + $qb = new QueryBuilder($this->conn); + $qb->setMaxResults(10); + + $this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState()); + $this->assertEQuals(10, $qb->getMaxResults()); + } + + public function testSetFirstResult() + { + $qb = new QueryBuilder($this->conn); + $qb->setFirstResult(10); + + $this->assertEquals(QueryBuilder::STATE_DIRTY, $qb->getState()); + $this->assertEQuals(10, $qb->getFirstResult()); + } + + public function testResetQueryPart() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.*')->from('users', 'u')->where('u.name = ?'); + + $this->assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string)$qb); + $qb->resetQueryPart('where'); + $this->assertEquals('SELECT u.* FROM users u', (string)$qb); + } + + public function testResetQueryParts() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.*')->from('users', 'u')->where('u.name = ?')->orderBy('u.name'); + + $this->assertEquals('SELECT u.* FROM users u WHERE u.name = ? ORDER BY u.name ASC', (string)$qb); + $qb->resetQueryParts(array('where', 'orderBy')); + $this->assertEquals('SELECT u.* FROM users u', (string)$qb); + } + + public function testCreateNamedParameter() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.*')->from('users', 'u')->where( + $qb->expr()->eq('u.name', $qb->createNamedParameter(10, \PDO::PARAM_INT)) + ); + + $this->assertEquals('SELECT u.* FROM users u WHERE u.name = :dcValue1', (string)$qb); + $this->assertEquals(10, $qb->getParameter('dcValue1')); + } + + public function testCreateNamedParameterCustomPlaceholder() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.*')->from('users', 'u')->where( + $qb->expr()->eq('u.name', $qb->createNamedParameter(10, \PDO::PARAM_INT, ':test')) + ); + + $this->assertEquals('SELECT u.* FROM users u WHERE u.name = :test', (string)$qb); + $this->assertEquals(10, $qb->getParameter('test')); + } + + public function testCreatePositionalParameter() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('u.*')->from('users', 'u')->where( + $qb->expr()->eq('u.name', $qb->createPositionalParameter(10, \PDO::PARAM_INT)) + ); + + $this->assertEquals('SELECT u.* FROM users u WHERE u.name = ?', (string)$qb); + $this->assertEquals(10, $qb->getParameter(1)); + } + + /** + * @group DBAL-172 + */ + public function testReferenceJoinFromJoin() + { + $qb = new QueryBuilder($this->conn); + + $qb->select("l.id", "mdsh.xcode", "mdso.xcode") + ->from("location_tree", "l") + ->join("l", "location_tree_pos", "p", "l.id = p.tree_id") + ->rightJoin("l", "hotel", "h", "h.location_id = l.id") + ->leftJoin("l", "offer_location", "ol", "l.id=ol.location_id") + ->leftJoin("ol", "mds_offer", "mdso", "ol.offer_id = mdso.offer_id") + ->leftJoin("h", "mds_hotel", "mdsh", "h.id = mdsh.hotel_id") + ->where("p.parent_id IN (:ids)") + ->andWhere("(mdso.xcode IS NOT NULL OR mdsh.xcode IS NOT NULL)"); + + $this->setExpectedException('Doctrine\DBAL\Query\QueryException', "The given alias 'ol' is not part of any FROM clause table. The currently registered FROM-clause aliases are: l"); + $this->assertEquals('', $qb->getSQL()); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php new file mode 100644 index 00000000..0e74c7de --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php @@ -0,0 +1,250 @@ + 'foo', 17 => 'bar')), + array('SELECT * FROM Foo WHERE bar IN (:name1, :name2)', false, array(32 => 'name1', 40 => 'name2')), + array('SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)', false, array(37 => 'name1', 45 => 'name2')), + array("SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)", false, array(37 => 'name1', 45 => 'name2')), + array('SELECT :foo_id', false, array(7 => 'foo_id')), // Ticket DBAL-231 + ); + } + + /** + * @dataProvider dataGetPlaceholderPositions + * @param type $query + * @param type $isPositional + * @param type $expectedParamPos + */ + public function testGetPlaceholderPositions($query, $isPositional, $expectedParamPos) + { + $actualParamPos = SQLParserUtils::getPlaceholderPositions($query, $isPositional); + $this->assertEquals($expectedParamPos, $actualParamPos); + } + + static public function dataExpandListParameters() + { + return array( + // Positional: Very simple with one needle + array( + "SELECT * FROM Foo WHERE foo IN (?)", + array(array(1, 2, 3)), + array(Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?)', + array(1, 2, 3), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Positional: One non-list before d one after list-needle + array( + "SELECT * FROM Foo WHERE foo = ? AND bar IN (?)", + array("string", array(1, 2, 3)), + array(\PDO::PARAM_STR, Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?)', + array("string", 1, 2, 3), + array(\PDO::PARAM_STR, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Positional: One non-list after list-needle + array( + "SELECT * FROM Foo WHERE bar IN (?) AND baz = ?", + array(array(1, 2, 3), "foo"), + array(Connection::PARAM_INT_ARRAY, \PDO::PARAM_STR), + 'SELECT * FROM Foo WHERE bar IN (?, ?, ?) AND baz = ?', + array(1, 2, 3, "foo"), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR) + ), + // Positional: One non-list before and one after list-needle + array( + "SELECT * FROM Foo WHERE foo = ? AND bar IN (?) AND baz = ?", + array(1, array(1, 2, 3), 4), + array(\PDO::PARAM_INT, Connection::PARAM_INT_ARRAY, \PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ?', + array(1, 1, 2, 3, 4), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Positional: Two lists + array( + "SELECT * FROM Foo WHERE foo IN (?, ?)", + array(array(1, 2, 3), array(4, 5)), + array(Connection::PARAM_INT_ARRAY, Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?, ?, ?)', + array(1, 2, 3, 4, 5), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Positional : Empty "integer" array DDC-1978 + array( + "SELECT * FROM Foo WHERE foo IN (?)", + array('foo'=>array()), + array('foo'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?)', + array(), + array() + ), + // Positional : Empty "str" array DDC-1978 + array( + "SELECT * FROM Foo WHERE foo IN (?)", + array('foo'=>array()), + array('foo'=>Connection::PARAM_STR_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?)', + array(), + array() + ), + // Named parameters : Very simple with param int + array( + "SELECT * FROM Foo WHERE foo = :foo", + array('foo'=>1), + array('foo'=>\PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE foo = ?', + array(1), + array(\PDO::PARAM_INT) + ), + + // Named parameters : Very simple with param int and string + array( + "SELECT * FROM Foo WHERE foo = :foo AND bar = :bar", + array('bar'=>'Some String','foo'=>1), + array('foo'=>\PDO::PARAM_INT,'bar'=>\PDO::PARAM_STR), + 'SELECT * FROM Foo WHERE foo = ? AND bar = ?', + array(1,'Some String'), + array(\PDO::PARAM_INT, \PDO::PARAM_STR) + ), + + // Named parameters : Very simple with one needle + array( + "SELECT * FROM Foo WHERE foo IN (:foo)", + array('foo'=>array(1, 2, 3)), + array('foo'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?)', + array(1, 2, 3), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Named parameters: One non-list before d one after list-needle + array( + "SELECT * FROM Foo WHERE foo = :foo AND bar IN (:bar)", + array('foo'=>"string", 'bar'=>array(1, 2, 3)), + array('foo'=>\PDO::PARAM_STR, 'bar'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?)', + array("string", 1, 2, 3), + array(\PDO::PARAM_STR, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Named parameters: One non-list after list-needle + array( + "SELECT * FROM Foo WHERE bar IN (:bar) AND baz = :baz", + array('bar'=>array(1, 2, 3), 'baz'=>"foo"), + array('bar'=>Connection::PARAM_INT_ARRAY, 'baz'=>\PDO::PARAM_STR), + 'SELECT * FROM Foo WHERE bar IN (?, ?, ?) AND baz = ?', + array(1, 2, 3, "foo"), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR) + ), + // Named parameters: One non-list before and one after list-needle + array( + "SELECT * FROM Foo WHERE foo = :foo AND bar IN (:bar) AND baz = :baz", + array('bar'=>array(1, 2, 3),'foo'=>1, 'baz'=>4), + array('bar'=>Connection::PARAM_INT_ARRAY, 'foo'=>\PDO::PARAM_INT, 'baz'=>\PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE foo = ? AND bar IN (?, ?, ?) AND baz = ?', + array(1, 1, 2, 3, 4), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Named parameters: Two lists + array( + "SELECT * FROM Foo WHERE foo IN (:a, :b)", + array('b'=>array(4, 5),'a'=>array(1, 2, 3)), + array('a'=>Connection::PARAM_INT_ARRAY, 'b'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?, ?, ?)', + array(1, 2, 3, 4, 5), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Named parameters : With the same name arg type string + array( + "SELECT * FROM Foo WHERE foo <> :arg AND bar = :arg", + array('arg'=>"Some String"), + array('arg'=>\PDO::PARAM_STR), + 'SELECT * FROM Foo WHERE foo <> ? AND bar = ?', + array("Some String","Some String"), + array(\PDO::PARAM_STR,\PDO::PARAM_STR,) + ), + // Named parameters : With the same name arg + array( + "SELECT * FROM Foo WHERE foo IN (:arg) AND NOT bar IN (:arg)", + array('arg'=>array(1, 2, 3)), + array('arg'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?, ?) AND NOT bar IN (?, ?, ?)', + array(1, 2, 3, 1, 2, 3), + array(\PDO::PARAM_INT,\PDO::PARAM_INT, \PDO::PARAM_INT,\PDO::PARAM_INT,\PDO::PARAM_INT, \PDO::PARAM_INT) + ), + + // Named parameters : Same name, other name in between DBAL-299 + array( + "SELECT * FROM Foo WHERE (:foo = 2) AND (:bar = 3) AND (:foo = 2)", + array('foo'=>2,'bar'=>3), + array('foo'=>\PDO::PARAM_INT,'bar'=>\PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE (? = 2) AND (? = 3) AND (? = 2)', + array(2, 3, 2), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT) + ), + // Named parameters : Empty "integer" array DDC-1978 + array( + "SELECT * FROM Foo WHERE foo IN (:foo)", + array('foo'=>array()), + array('foo'=>Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?)', + array(), + array() + ), + // Named parameters : Two empty "str" array DDC-1978 + array( + "SELECT * FROM Foo WHERE foo IN (:foo) OR bar IN (:bar)", + array('foo'=>array(), 'bar'=>array()), + array('foo'=>Connection::PARAM_STR_ARRAY, 'bar'=>Connection::PARAM_STR_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?) OR bar IN (?)', + array(), + array() + ), + ); + } + + /** + * @dataProvider dataExpandListParameters + * @param type $q + * @param type $p + * @param type $t + * @param type $expectedQuery + * @param type $expectedParams + * @param type $expectedTypes + */ + public function testExpandListParameters($q, $p, $t, $expectedQuery, $expectedParams, $expectedTypes) + { + list($query, $params, $types) = SQLParserUtils::expandListParameters($q, $p, $t); + + $this->assertEquals($expectedQuery, $query, "Query was not rewritten correctly."); + $this->assertEquals($expectedParams, $params, "Params dont match"); + $this->assertEquals($expectedTypes, $types, "Types dont match"); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php new file mode 100644 index 00000000..20510616 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ColumnTest.php @@ -0,0 +1,114 @@ +createColumn(); + + $this->assertEquals("foo", $column->getName()); + $this->assertSame(Type::getType('string'), $column->getType()); + + $this->assertEquals(200, $column->getLength()); + $this->assertEquals(5, $column->getPrecision()); + $this->assertEquals(2, $column->getScale()); + $this->assertTrue($column->getUnsigned()); + $this->assertFalse($column->getNotNull()); + $this->assertTrue($column->getFixed()); + $this->assertEquals("baz", $column->getDefault()); + + $this->assertEquals(array('foo' => 'bar'), $column->getPlatformOptions()); + $this->assertTrue($column->hasPlatformOption('foo')); + $this->assertEquals('bar', $column->getPlatformOption('foo')); + $this->assertFalse($column->hasPlatformOption('bar')); + + $this->assertEquals(array('bar' => 'baz'), $column->getCustomSchemaOptions()); + $this->assertTrue($column->hasCustomSchemaOption('bar')); + $this->assertEquals('baz', $column->getCustomSchemaOption('bar')); + $this->assertFalse($column->hasCustomSchemaOption('foo')); + } + + public function testToArray() + { + $expected = array( + 'name' => 'foo', + 'type' => Type::getType('string'), + 'default' => 'baz', + 'notnull' => false, + 'length' => 200, + 'precision' => 5, + 'scale' => 2, + 'fixed' => true, + 'unsigned' => true, + 'autoincrement' => false, + 'columnDefinition' => null, + 'comment' => null, + 'foo' => 'bar', + 'bar' => 'baz' + ); + + $this->assertEquals($expected, $this->createColumn()->toArray()); + } + + /** + * @return Column + */ + public function createColumn() + { + $options = array( + 'length' => 200, + 'precision' => 5, + 'scale' => 2, + 'unsigned' => true, + 'notnull' => false, + 'fixed' => true, + 'default' => 'baz', + 'platformOptions' => array('foo' => 'bar'), + 'customSchemaOptions' => array('bar' => 'baz'), + ); + + $string = Type::getType('string'); + return new Column("foo", $string, $options); + } + + /** + * @group DBAL-64 + */ + public function testQuotedColumnName() + { + $string = Type::getType('string'); + $column = new Column("`bar`", $string, array()); + + $mysqlPlatform = new \Doctrine\DBAL\Platforms\MySqlPlatform(); + $sqlitePlatform = new \Doctrine\DBAL\Platforms\SqlitePlatform(); + + $this->assertEquals('bar', $column->getName()); + $this->assertEquals('`bar`', $column->getQuotedName($mysqlPlatform)); + $this->assertEquals('"bar"', $column->getQuotedName($sqlitePlatform)); + } + + /** + * @group DBAL-42 + */ + public function testColumnComment() + { + $column = new Column("bar", Type::getType('string')); + $this->assertNull($column->getComment()); + + $column->setComment("foo"); + $this->assertEquals("foo", $column->getComment()); + + $columnArray = $column->toArray(); + $this->assertArrayHasKey('comment', $columnArray); + $this->assertEquals('foo', $columnArray['comment']); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php new file mode 100644 index 00000000..5301bd8e --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php @@ -0,0 +1,839 @@ +. + */ + +namespace Doctrine\Tests\DBAL\Schema; + +require_once __DIR__ . '/../../TestInit.php'; + +use Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\SchemaConfig, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\SchemaDiff, + Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Comparator, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Schema\ForeignKeyConstraint; + +/** + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ComparatorTest extends \PHPUnit_Framework_TestCase +{ + public function testCompareSame1() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer' ) ), + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer') ), + ) + ), + ) ); + + $this->assertEquals(new SchemaDiff(), Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareSame2() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + ) + ), + ) ); + $this->assertEquals(new SchemaDiff(), Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareMissingTable() + { + $schemaConfig = new \Doctrine\DBAL\Schema\SchemaConfig; + $table = new Table('bugdb', array ('integerfield1' => new Column('integerfield1', Type::getType('integer')))); + $table->setSchemaConfig($schemaConfig); + + $schema1 = new Schema( array($table), array(), $schemaConfig ); + $schema2 = new Schema( array(), array(), $schemaConfig ); + + $expected = new SchemaDiff( array(), array(), array('bugdb' => $table) ); + + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareNewTable() + { + $schemaConfig = new \Doctrine\DBAL\Schema\SchemaConfig; + $table = new Table('bugdb', array ('integerfield1' => new Column('integerfield1', Type::getType('integer')))); + $table->setSchemaConfig($schemaConfig); + + $schema1 = new Schema( array(), array(), $schemaConfig ); + $schema2 = new Schema( array($table), array(), $schemaConfig ); + + $expected = new SchemaDiff( array('bugdb' => $table), array(), array() ); + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareOnlyAutoincrementChanged() + { + $column1 = new Column('foo', Type::getType('integer'), array('autoincrement' => true)); + $column2 = new Column('foo', Type::getType('integer'), array('autoincrement' => false)); + + $comparator = new Comparator(); + $changedProperties = $comparator->diffColumn($column1, $column2); + + $this->assertEquals(array('autoincrement'), $changedProperties); + } + + public function testCompareMissingField() + { + $missingColumn = new Column('integerfield1', Type::getType('integer')); + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => $missingColumn, + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff( 'bugdb', array(), array(), + array ( + 'integerfield1' => $missingColumn, + ) + ) + ) + ); + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareNewField() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff ('bugdb', + array ( + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) + ); + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareChangedColumns_ChangeType() + { + $column1 = new Column('charfield1', Type::getType('string')); + $column2 = new Column('charfield1', Type::getType('integer')); + + $c = new Comparator(); + $this->assertEquals(array('type'), $c->diffColumn($column1, $column2)); + $this->assertEquals(array(), $c->diffColumn($column1, $column1)); + } + + public function testCompareChangedColumns_ChangeCustomSchemaOption() + { + $column1 = new Column('charfield1', Type::getType('string')); + $column2 = new Column('charfield1', Type::getType('string')); + + $column1->setCustomSchemaOption('foo', 'bar'); + $column2->setCustomSchemaOption('foo', 'bar'); + + $column1->setCustomSchemaOption('foo1', 'bar1'); + $column2->setCustomSchemaOption('foo2', 'bar2'); + + $c = new Comparator(); + $this->assertEquals(array('foo1', 'foo2'), $c->diffColumn($column1, $column2)); + $this->assertEquals(array(), $c->diffColumn($column1, $column1)); + } + + public function testCompareRemovedIndex() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1' + ), + true + ) + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff( 'bugdb', array(), array(), array(), array(), array(), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1' + ), + true + ) + ) + ), + ) + ); + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareNewIndex() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1' + ), + true + ) + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff( 'bugdb', array(), array(), array(), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1' + ), + true + ) + ) + ), + ) + ); + $this->assertEquals($expected, Comparator::compareSchemas( $schema1, $schema2 ) ); + } + + public function testCompareChangedIndex() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1' + ), + true + ) + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', + array('integerfield1', 'integerfield2'), + true + ) + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff( 'bugdb', array(), array(), array(), array(), + array ( + 'primary' => new Index('primary', + array( + 'integerfield1', + 'integerfield2' + ), + true + ) + ) + ), + ) + ); + $actual = Comparator::compareSchemas( $schema1, $schema2 ); + $this->assertEquals($expected, $actual); + } + + public function testCompareChangedIndexFieldPositions() + { + $schema1 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', array('integerfield1', 'integerfield2'), true) + ) + ), + ) ); + $schema2 = new Schema( array( + 'bugdb' => new Table('bugdb', + array ( + 'integerfield1' => new Column('integerfield1', Type::getType('integer')), + 'integerfield2' => new Column('integerfield2', Type::getType('integer')), + ), + array ( + 'primary' => new Index('primary', array('integerfield2', 'integerfield1'), true) + ) + ), + ) ); + + $expected = new SchemaDiff ( array(), + array ( + 'bugdb' => new TableDiff('bugdb', array(), array(), array(), array(), + array ( + 'primary' => new Index('primary', array('integerfield2', 'integerfield1'), true) + ) + ), + ) + ); + $actual = Comparator::compareSchemas( $schema1, $schema2 ); + $this->assertEquals($expected, $actual); + } + + public function testCompareSequences() + { + $seq1 = new Sequence('foo', 1, 1); + $seq2 = new Sequence('foo', 1, 2); + $seq3 = new Sequence('foo', 2, 1); + + $c = new Comparator(); + + $this->assertTrue($c->diffSequence($seq1, $seq2)); + $this->assertTrue($c->diffSequence($seq1, $seq3)); + } + + public function testRemovedSequence() + { + $schema1 = new Schema(); + $seq = $schema1->createSequence('foo'); + + $schema2 = new Schema(); + + $c = new Comparator(); + $diffSchema = $c->compare($schema1, $schema2); + + $this->assertEquals(1, count($diffSchema->removedSequences)); + $this->assertSame($seq, $diffSchema->removedSequences[0]); + } + + public function testAddedSequence() + { + $schema1 = new Schema(); + + $schema2 = new Schema(); + $seq = $schema2->createSequence('foo'); + + $c = new Comparator(); + $diffSchema = $c->compare($schema1, $schema2); + + $this->assertEquals(1, count($diffSchema->newSequences)); + $this->assertSame($seq, $diffSchema->newSequences[0]); + } + + public function testTableAddForeignKey() + { + $tableForeign = new Table("bar"); + $tableForeign->addColumn('id', 'integer'); + + $table1 = new Table("foo"); + $table1->addColumn('fk', 'integer'); + + $table2 = new Table("foo"); + $table2->addColumn('fk', 'integer'); + $table2->addForeignKeyConstraint($tableForeign, array('fk'), array('id')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($table1, $table2); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertEquals(1, count($tableDiff->addedForeignKeys)); + } + + public function testTableRemoveForeignKey() + { + $tableForeign = new Table("bar"); + $tableForeign->addColumn('id', 'integer'); + + $table1 = new Table("foo"); + $table1->addColumn('fk', 'integer'); + + $table2 = new Table("foo"); + $table2->addColumn('fk', 'integer'); + $table2->addForeignKeyConstraint($tableForeign, array('fk'), array('id')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($table2, $table1); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertEquals(1, count($tableDiff->removedForeignKeys)); + } + + public function testTableUpdateForeignKey() + { + $tableForeign = new Table("bar"); + $tableForeign->addColumn('id', 'integer'); + + $table1 = new Table("foo"); + $table1->addColumn('fk', 'integer'); + $table1->addForeignKeyConstraint($tableForeign, array('fk'), array('id')); + + $table2 = new Table("foo"); + $table2->addColumn('fk', 'integer'); + $table2->addForeignKeyConstraint($tableForeign, array('fk'), array('id'), array('onUpdate' => 'CASCADE')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($table1, $table2); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertEquals(1, count($tableDiff->changedForeignKeys)); + } + + public function testTablesCaseInsensitive() + { + $schemaA = new Schema(); + $schemaA->createTable('foo'); + $schemaA->createTable('bAr'); + $schemaA->createTable('BAZ'); + $schemaA->createTable('new'); + + $schemaB = new Schema(); + $schemaB->createTable('FOO'); + $schemaB->createTable('bar'); + $schemaB->createTable('Baz'); + $schemaB->createTable('old'); + + $c = new Comparator(); + $diff = $c->compare($schemaA, $schemaB); + + $this->assertSchemaTableChangeCount($diff, 1, 0, 1); + } + + public function testSequencesCaseInsenstive() + { + $schemaA = new Schema(); + $schemaA->createSequence('foo'); + $schemaA->createSequence('BAR'); + $schemaA->createSequence('Baz'); + $schemaA->createSequence('new'); + + $schemaB = new Schema(); + $schemaB->createSequence('FOO'); + $schemaB->createSequence('Bar'); + $schemaB->createSequence('baz'); + $schemaB->createSequence('old'); + + $c = new Comparator(); + $diff = $c->compare($schemaA, $schemaB); + + $this->assertSchemaSequenceChangeCount($diff, 1, 0, 1); + } + + public function testCompareColumnCompareCaseInsensitive() + { + $tableA = new Table("foo"); + $tableA->addColumn('id', 'integer'); + + $tableB = new Table("foo"); + $tableB->addColumn('ID', 'integer'); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertFalse($tableDiff); + } + + public function testCompareIndexBasedOnPropertiesNotName() + { + $tableA = new Table("foo"); + $tableA->addColumn('id', 'integer'); + $tableA->addIndex(array("id"), "foo_bar_idx"); + + $tableB = new Table("foo"); + $tableB->addColumn('ID', 'integer'); + $tableB->addIndex(array("id"), "bar_foo_idx"); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertFalse($tableDiff); + } + + public function testCompareForeignKeyBasedOnPropertiesNotName() + { + $tableA = new Table("foo"); + $tableA->addColumn('id', 'integer'); + $tableA->addNamedForeignKeyConstraint('foo_constraint', 'bar', array('id'), array('id')); + + $tableB = new Table("foo"); + $tableB->addColumn('ID', 'integer'); + $tableB->addNamedForeignKeyConstraint('bar_constraint', 'bar', array('id'), array('id')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertFalse($tableDiff); + } + + public function testCompareForeignKey_RestrictNoAction_AreTheSame() + { + $fk1 = new ForeignKeyConstraint(array("foo"), "bar", array("baz"), "fk1", array('onDelete' => 'NO ACTION')); + $fk2 = new ForeignKeyConstraint(array("foo"), "bar", array("baz"), "fk1", array('onDelete' => 'RESTRICT')); + + $c = new Comparator(); + $this->assertFalse($c->diffForeignKey($fk1, $fk2)); + } + + public function testDetectRenameColumn() + { + $tableA = new Table("foo"); + $tableA->addColumn('foo', 'integer'); + + $tableB = new Table("foo"); + $tableB->addColumn('bar', 'integer'); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertEquals(0, count($tableDiff->addedColumns)); + $this->assertEquals(0, count($tableDiff->removedColumns)); + $this->assertArrayHasKey('foo', $tableDiff->renamedColumns); + $this->assertEquals('bar', $tableDiff->renamedColumns['foo']->getName()); + } + + /** + * You can easily have ambiguouties in the column renaming. If these + * are detected no renaming should take place, instead adding and dropping + * should be used exclusively. + * + * @group DBAL-24 + */ + public function testDetectRenameColumnAmbiguous() + { + $tableA = new Table("foo"); + $tableA->addColumn('foo', 'integer'); + $tableA->addColumn('bar', 'integer'); + + $tableB = new Table("foo"); + $tableB->addColumn('baz', 'integer'); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertEquals(1, count($tableDiff->addedColumns), "'baz' should be added, not created through renaming!"); + $this->assertArrayHasKey('baz', $tableDiff->addedColumns, "'baz' should be added, not created through renaming!"); + $this->assertEquals(2, count($tableDiff->removedColumns), "'foo' and 'bar' should both be dropped, an ambigouty exists which one could be renamed to 'baz'."); + $this->assertArrayHasKey('foo', $tableDiff->removedColumns, "'foo' should be removed."); + $this->assertArrayHasKey('bar', $tableDiff->removedColumns, "'bar' should be removed."); + $this->assertEquals(0, count($tableDiff->renamedColumns), "no renamings should take place."); + } + + public function testDetectChangeIdentifierType() + { + $this->markTestSkipped('DBAL-2 was reopened, this test cannot work anymore.'); + + $tableA = new Table("foo"); + $tableA->addColumn('id', 'integer', array('autoincrement' => false)); + + $tableB = new Table("foo"); + $tableB->addColumn('id', 'integer', array('autoincrement' => true)); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertArrayHasKey('id', $tableDiff->changedColumns); + } + + + /** + * @group DBAL-105 + */ + public function testDiff() + { + $table = new \Doctrine\DBAL\Schema\Table('twitter_users'); + $table->addColumn('id', 'integer', array('autoincrement' => true)); + $table->addColumn('twitterId', 'integer', array('nullable' => false)); + $table->addColumn('displayName', 'string', array('nullable' => false)); + $table->setPrimaryKey(array('id')); + + $newtable = new \Doctrine\DBAL\Schema\Table('twitter_users'); + $newtable->addColumn('id', 'integer', array('autoincrement' => true)); + $newtable->addColumn('twitter_id', 'integer', array('nullable' => false)); + $newtable->addColumn('display_name', 'string', array('nullable' => false)); + $newtable->addColumn('logged_in_at', 'datetime', array('nullable' => true)); + $newtable->setPrimaryKey(array('id')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($table, $newtable); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertEquals(array('twitterid', 'displayname'), array_keys($tableDiff->renamedColumns)); + $this->assertEquals(array('logged_in_at'), array_keys($tableDiff->addedColumns)); + $this->assertEquals(0, count($tableDiff->removedColumns)); + } + + + /** + * @group DBAL-112 + */ + public function testChangedSequence() + { + $schema = new Schema(); + $sequence = $schema->createSequence('baz'); + + $schemaNew = clone $schema; + /* @var $schemaNew Schema */ + $schemaNew->getSequence('baz')->setAllocationSize(20); + + $c = new \Doctrine\DBAL\Schema\Comparator; + $diff = $c->compare($schema, $schemaNew); + + $this->assertSame($diff->changedSequences[0] , $schemaNew->getSequence('baz')); + } + + /** + * @group DBAL-106 + */ + public function testDiffDecimalWithNullPrecision() + { + $column = new Column('foo', Type::getType('decimal')); + $column->setPrecision(null); + + $column2 = new Column('foo', Type::getType('decimal')); + + $c = new Comparator(); + $this->assertEquals(array(), $c->diffColumn($column, $column2)); + } + + /** + * @group DBAL-204 + */ + public function testFqnSchemaComparision() + { + $config = new SchemaConfig(); + $config->setName("foo"); + + $oldSchema = new Schema(array(), array(), $config); + $oldSchema->createTable('bar'); + + $newSchema= new Schema(array(), array(), $config); + $newSchema->createTable('foo.bar'); + + $c = new Comparator(); + $this->assertEquals(new SchemaDiff(), $c->compare($oldSchema, $newSchema)); + } + + /** + * @group DBAL-204 + */ + public function testFqnSchemaComparisionDifferentSchemaNameButSameTableNoDiff() + { + $config = new SchemaConfig(); + $config->setName("foo"); + + $oldSchema = new Schema(array(), array(), $config); + $oldSchema->createTable('foo.bar'); + + $newSchema = new Schema(); + $newSchema->createTable('bar'); + + $c = new Comparator(); + $diff = $c->compare($oldSchema, $newSchema); + $this->assertEquals(new SchemaDiff(), $c->compare($oldSchema, $newSchema)); + } + + /** + * @group DBAL-204 + */ + public function testFqnSchemaComparisionNoSchemaSame() + { + $config = new SchemaConfig(); + $config->setName("foo"); + $oldSchema = new Schema(array(), array(), $config); + $oldSchema->createTable('bar'); + + $newSchema = new Schema(); + $newSchema->createTable('bar'); + + $c = new Comparator(); + $diff = $c->compare($oldSchema, $newSchema); + + $this->assertEquals(new SchemaDiff(), $c->compare($oldSchema, $newSchema)); + } + + /** + * @group DDC-1657 + */ + public function testAutoIncremenetSequences() + { + $oldSchema = new Schema(); + $table = $oldSchema->createTable("foo"); + $table->addColumn("id", "integer", array("autoincrement" => true)); + $table->setPrimaryKey(array("id")); + $oldSchema->createSequence("foo_id_seq"); + + $newSchema = new Schema(); + $table = $newSchema->createTable("foo"); + $table->addColumn("id", "integer", array("autoincrement" => true)); + $table->setPrimaryKey(array("id")); + + $c = new Comparator(); + $diff = $c->compare($oldSchema, $newSchema); + + $this->assertCount(0, $diff->removedSequences); + } + + + /** + * You can get multiple drops for a FK when a table referenced by a foreign + * key is deleted, as this FK is referenced twice, once on the orphanedForeignKeys + * array because of the dropped table, and once on changedTables array. We + * now check that the key is present once. + */ + public function testAvoidMultipleDropForeignKey() + { + $oldSchema = new Schema(); + + $tableForeign = $oldSchema->createTable('foreign'); + $tableForeign->addColumn('id', 'integer'); + + $table = $oldSchema->createTable('foo'); + $table->addColumn('fk', 'integer'); + $table->addForeignKeyConstraint($tableForeign, array('fk'), array('id')); + + + $newSchema = new Schema(); + $table = $newSchema->createTable('foo'); + + $c = new Comparator(); + $diff = $c->compare($oldSchema, $newSchema); + + $this->assertCount(0, $diff->changedTables['foo']->removedForeignKeys); + $this->assertCount(1, $diff->orphanedForeignKeys); + } + + + /** + * @param SchemaDiff $diff + * @param int $newTableCount + * @param int $changeTableCount + * @param int $removeTableCount + */ + public function assertSchemaTableChangeCount($diff, $newTableCount=0, $changeTableCount=0, $removeTableCount=0) + { + $this->assertEquals($newTableCount, count($diff->newTables)); + $this->assertEquals($changeTableCount, count($diff->changedTables)); + $this->assertEquals($removeTableCount, count($diff->removedTables)); + } + + /** + * @param SchemaDiff $diff + * @param int $newSequenceCount + * @param int $changeSequenceCount + * @param int $changeSequenceCount + */ + public function assertSchemaSequenceChangeCount($diff, $newSequenceCount=0, $changeSequenceCount=0, $removeSequenceCount=0) + { + $this->assertEquals($newSequenceCount, count($diff->newSequences), "Expected number of new sequences is wrong."); + $this->assertEquals($changeSequenceCount, count($diff->changedSequences), "Expected number of changed sequences is wrong."); + $this->assertEquals($removeSequenceCount, count($diff->removedSequences), "Expected number of removed sequences is wrong."); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/IndexTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/IndexTest.php new file mode 100644 index 00000000..ed6b070f --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/IndexTest.php @@ -0,0 +1,115 @@ +createIndex(); + $this->assertEquals("foo", $idx->getName()); + $columns = $idx->getColumns(); + $this->assertEquals(2, count($columns)); + $this->assertEquals(array("bar", "baz"), $columns); + $this->assertFalse($idx->isUnique()); + $this->assertFalse($idx->isPrimary()); + } + + public function testCreatePrimary() + { + $idx = $this->createIndex(false, true); + $this->assertTrue($idx->isUnique()); + $this->assertTrue($idx->isPrimary()); + } + + public function testCreateUnique() + { + $idx = $this->createIndex(true, false); + $this->assertTrue($idx->isUnique()); + $this->assertFalse($idx->isPrimary()); + } + + /** + * @group DBAL-50 + */ + public function testFullfilledByUnique() + { + $idx1 = $this->createIndex(true, false); + $idx2 = $this->createIndex(true, false); + $idx3 = $this->createIndex(); + + $this->assertTrue($idx1->isFullfilledBy($idx2)); + $this->assertFalse($idx1->isFullfilledBy($idx3)); + } + + /** + * @group DBAL-50 + */ + public function testFullfilledByPrimary() + { + $idx1 = $this->createIndex(true, true); + $idx2 = $this->createIndex(true, true); + $idx3 = $this->createIndex(true, false); + + $this->assertTrue($idx1->isFullfilledBy($idx2)); + $this->assertFalse($idx1->isFullfilledBy($idx3)); + } + + /** + * @group DBAL-50 + */ + public function testFullfilledByIndex() + { + $idx1 = $this->createIndex(); + $idx2 = $this->createIndex(); + $pri = $this->createIndex(true, true); + $uniq = $this->createIndex(true); + + $this->assertTrue($idx1->isFullfilledBy($idx2)); + $this->assertTrue($idx1->isFullfilledBy($pri)); + $this->assertTrue($idx1->isFullfilledBy($uniq)); + } + + /** + * @group DBAL-220 + */ + public function testFlags() + { + $idx1 = $this->createIndex(); + $this->assertFalse($idx1->hasFlag('clustered')); + + $idx1->addFlag('clustered'); + $this->assertTrue($idx1->hasFlag('clustered')); + $this->assertTrue($idx1->hasFlag('CLUSTERED')); + + $idx1->removeFlag('clustered'); + $this->assertFalse($idx1->hasFlag('clustered')); + } + + /** + * @group DBAL-285 + */ + public function testIndexQuotes() + { + $index = new Index("foo", array("`bar`", "`baz`")); + + $this->assertTrue($index->spansColumns(array("bar", "baz"))); + $this->assertTrue($index->hasColumnAtPosition("bar", 0)); + $this->assertTrue($index->hasColumnAtPosition("baz", 1)); + + $this->assertFalse($index->hasColumnAtPosition("bar", 1)); + $this->assertFalse($index->hasColumnAtPosition("baz", 0)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/MySqlSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/MySqlSchemaManagerTest.php new file mode 100644 index 00000000..ff093fb9 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/MySqlSchemaManagerTest.php @@ -0,0 +1,74 @@ +getMock('Doctrine\DBAL\Driver'); + $platform = $this->getMock('Doctrine\DBAL\Platforms\MySqlPlatform'); + $this->conn = $this->getMock( + 'Doctrine\DBAL\Connection', + array('fetchAll'), + array(array('platform' => $platform), $driverMock, new Configuration(), $eventManager) + ); + $this->manager = new MySqlSchemaManager($this->conn); + } + + public function testCompositeForeignKeys() + { + $this->conn->expects($this->once())->method('fetchAll')->will($this->returnValue($this->getFKDefinition())); + $fkeys = $this->manager->listTableForeignKeys('dummy'); + $this->assertEquals(1, count($fkeys), "Table has to have one foreign key."); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\ForeignKeyConstraint', $fkeys[0]); + $this->assertEquals(array('column_1', 'column_2', 'column_3'), array_map('strtolower', $fkeys[0]->getLocalColumns())); + $this->assertEquals(array('column_1', 'column_2', 'column_3'), array_map('strtolower', $fkeys[0]->getForeignColumns())); + } + + public function getFKDefinition() + { + return array( + array( + "CONSTRAINT_NAME" => "FK_C1B1712387FE737264DE5A5511B8B3E", + "COLUMN_NAME" => "column_1", + "REFERENCED_TABLE_NAME" => "dummy", + "REFERENCED_COLUMN_NAME" => "column_1", + "update_rule" => "RESTRICT", + "delete_rule" => "RESTRICT", + ), + array( + "CONSTRAINT_NAME" => "FK_C1B1712387FE737264DE5A5511B8B3E", + "COLUMN_NAME" => "column_2", + "REFERENCED_TABLE_NAME" => "dummy", + "REFERENCED_COLUMN_NAME" => "column_2", + "update_rule" => "RESTRICT", + "delete_rule" => "RESTRICT", + ), + array( + "CONSTRAINT_NAME" => "FK_C1B1712387FE737264DE5A5511B8B3E", + "COLUMN_NAME" => "column_3", + "REFERENCED_TABLE_NAME" => "dummy", + "REFERENCED_COLUMN_NAME" => "column_3", + "update_rule" => "RESTRICT", + "delete_rule" => "RESTRICT", + ) + ); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Platforms/MySQLSchemaTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Platforms/MySQLSchemaTest.php new file mode 100644 index 00000000..651a5ee2 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Platforms/MySQLSchemaTest.php @@ -0,0 +1,88 @@ +comparator = new \Doctrine\DBAL\Schema\Comparator; + $this->platform = new \Doctrine\DBAL\Platforms\MySqlPlatform; + } + + public function testSwitchPrimaryKeyOrder() + { + $tableOld = new Table("test"); + $tableOld->addColumn('foo_id', 'integer'); + $tableOld->addColumn('bar_id', 'integer'); + $tableNew = clone $tableOld; + + $tableOld->setPrimaryKey(array('foo_id', 'bar_id')); + $tableNew->setPrimaryKey(array('bar_id', 'foo_id')); + + $diff = $this->comparator->diffTable($tableOld, $tableNew); + $sql = $this->platform->getAlterTableSQL($diff); + + $this->assertEquals( + array( + 'ALTER TABLE test DROP PRIMARY KEY', + 'ALTER TABLE test ADD PRIMARY KEY (bar_id, foo_id)' + ), $sql + ); + } + + /** + * @group DBAL-132 + */ + public function testGenerateForeignKeySQL() + { + $tableOld = new Table("test"); + $tableOld->addColumn('foo_id', 'integer'); + $tableOld->addUnnamedForeignKeyConstraint('test_foreign', array('foo_id'), array('foo_id')); + + $sqls = array(); + foreach ($tableOld->getForeignKeys() AS $fk) { + $sqls[] = $this->platform->getCreateForeignKeySQL($fk, $tableOld); + } + + $this->assertEquals(array("ALTER TABLE test ADD CONSTRAINT FK_D87F7E0C8E48560F FOREIGN KEY (foo_id) REFERENCES test_foreign (foo_id)"), $sqls); + } + + /** + * @group DDC-1737 + */ + public function testClobNoAlterTable() + { + $tableOld = new Table("test"); + $tableOld->addColumn('id', 'integer'); + $tableOld->addColumn('description', 'string', array('length' => 65536)); + $tableNew = clone $tableOld; + + $tableNew->setPrimaryKey(array('id')); + + $diff = $this->comparator->diffTable($tableOld, $tableNew); + $sql = $this->platform->getAlterTableSQL($diff); + + $this->assertEquals( + array('ALTER TABLE test ADD PRIMARY KEY (id)'), + $sql + ); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaDiffTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaDiffTest.php new file mode 100644 index 00000000..16bf92a2 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaDiffTest.php @@ -0,0 +1,109 @@ +createSchemaDiff(); + $platform = $this->createPlatform(true); + + $sql = $diff->toSql($platform); + + $expected = array('drop_orphan_fk', 'alter_seq', 'drop_seq', 'create_seq', 'create_table', 'create_foreign_key', 'drop_table', 'alter_table'); + + $this->assertEquals($expected, $sql); + } + + public function testSchemaDiffToSaveSql() + { + $diff = $this->createSchemaDiff(); + $platform = $this->createPlatform(false); + + $sql = $diff->toSaveSql($platform); + + $expected = array('alter_seq', 'create_seq', 'create_table', 'create_foreign_key', 'alter_table'); + + $this->assertEquals($expected, $sql); + } + + public function createPlatform($unsafe = false) + { + $platform = $this->getMock('Doctrine\Tests\DBAL\Mocks\MockPlatform'); + if ($unsafe) { + $platform->expects($this->exactly(1)) + ->method('getDropSequenceSql') + ->with($this->isInstanceOf('Doctrine\DBAL\Schema\Sequence')) + ->will($this->returnValue('drop_seq')); + } + $platform->expects($this->exactly(1)) + ->method('getAlterSequenceSql') + ->with($this->isInstanceOf('Doctrine\DBAL\Schema\Sequence')) + ->will($this->returnValue('alter_seq')); + $platform->expects($this->exactly(1)) + ->method('getCreateSequenceSql') + ->with($this->isInstanceOf('Doctrine\DBAL\Schema\Sequence')) + ->will($this->returnValue('create_seq')); + if ($unsafe) { + $platform->expects($this->exactly(1)) + ->method('getDropTableSql') + ->with($this->isInstanceof('Doctrine\DBAL\Schema\Table')) + ->will($this->returnValue('drop_table')); + } + $platform->expects($this->exactly(1)) + ->method('getCreateTableSql') + ->with($this->isInstanceof('Doctrine\DBAL\Schema\Table')) + ->will($this->returnValue(array('create_table'))); + $platform->expects($this->exactly(1)) + ->method('getCreateForeignKeySQL') + ->with($this->isInstanceOf('Doctrine\DBAL\Schema\ForeignKeyConstraint')) + ->will($this->returnValue('create_foreign_key')); + $platform->expects($this->exactly(1)) + ->method('getAlterTableSql') + ->with($this->isInstanceOf('Doctrine\DBAL\Schema\TableDiff')) + ->will($this->returnValue(array('alter_table'))); + if ($unsafe) { + $platform->expects($this->exactly(1)) + ->method('getDropForeignKeySql') + ->with($this->isInstanceof('Doctrine\DBAL\Schema\ForeignKeyConstraint'), $this->equalTo('local_table')) + ->will($this->returnValue('drop_orphan_fk')); + } + $platform->expects($this->exactly(1)) + ->method('supportsSequences') + ->will($this->returnValue(true)); + $platform->expects($this->exactly(2)) + ->method('supportsForeignKeyConstraints') + ->will($this->returnValue(true)); + return $platform; + } + + public function createSchemaDiff() + { + $diff = new SchemaDiff(); + $diff->changedSequences['foo_seq'] = new Sequence('foo_seq'); + $diff->newSequences['bar_seq'] = new Sequence('bar_seq'); + $diff->removedSequences['baz_seq'] = new Sequence('baz_seq'); + $diff->newTables['foo_table'] = new Table('foo_table'); + $diff->removedTables['bar_table'] = new Table('bar_table'); + $diff->changedTables['baz_table'] = new TableDiff('baz_table'); + $diff->newTables['foo_table']->addColumn('foreign_id', 'integer'); + $diff->newTables['foo_table']->addForeignKeyConstraint('foreign_table', array('foreign_id'), array('id')); + $fk = new \Doctrine\DBAL\Schema\ForeignKeyConstraint(array('id'), 'foreign_table', array('id')); + $fk->setLocalTable(new Table('local_table')); + $diff->orphanedForeignKeys[] = $fk; + return $diff; + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaTest.php new file mode 100644 index 00000000..096ede79 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SchemaTest.php @@ -0,0 +1,224 @@ +assertTrue($schema->hasTable($tableName)); + + $tables = $schema->getTables(); + $this->assertTrue( isset($tables[$tableName]) ); + $this->assertSame($table, $tables[$tableName]); + $this->assertSame($table, $schema->getTable($tableName)); + $this->assertTrue($schema->hasTable($tableName)); + } + + public function testTableMatchingCaseInsenstive() + { + $table = new Table("Foo"); + + $schema = new Schema(array($table)); + $this->assertTrue($schema->hasTable("foo")); + $this->assertTrue($schema->hasTable("FOO")); + + $this->assertSame($table, $schema->getTable('FOO')); + $this->assertSame($table, $schema->getTable('foo')); + $this->assertSame($table, $schema->getTable('Foo')); + } + + public function testGetUnknownTableThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $schema = new Schema(); + $schema->getTable("unknown"); + } + + public function testCreateTableTwiceThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $tableName = "foo"; + $table = new Table($tableName); + $tables = array($table, $table); + + $schema = new Schema($tables); + } + + public function testRenameTable() + { + $tableName = "foo"; + $table = new Table($tableName); + $schema = new Schema(array($table)); + + $this->assertTrue($schema->hasTable("foo")); + $schema->renameTable("foo", "bar"); + $this->assertFalse($schema->hasTable("foo")); + $this->assertTrue($schema->hasTable("bar")); + $this->assertSame($table, $schema->getTable("bar")); + } + + public function testDropTable() + { + $tableName = "foo"; + $table = new Table($tableName); + $schema = new Schema(array($table)); + + $this->assertTrue($schema->hasTable("foo")); + + $schema->dropTable("foo"); + + $this->assertFalse($schema->hasTable("foo")); + } + + public function testCreateTable() + { + $schema = new Schema(); + + $this->assertFalse($schema->hasTable("foo")); + + $table = $schema->createTable("foo"); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\Table', $table); + $this->assertEquals("foo", $table->getName()); + $this->assertTrue($schema->hasTable("foo")); + } + + public function testAddSequences() + { + $sequence = new Sequence("a_seq", 1, 1); + + $schema = new Schema(array(), array($sequence)); + + $this->assertTrue($schema->hasSequence("a_seq")); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Sequence', $schema->getSequence("a_seq")); + + $sequences = $schema->getSequences(); + $this->assertArrayHasKey('public.a_seq', $sequences); + } + + public function testSequenceAccessCaseInsensitive() + { + $sequence = new Sequence("a_Seq"); + + $schema = new Schema(array(), array($sequence)); + $this->assertTrue($schema->hasSequence('a_seq')); + $this->assertTrue($schema->hasSequence('a_Seq')); + $this->assertTrue($schema->hasSequence('A_SEQ')); + + $this->assertEquals($sequence, $schema->getSequence('a_seq')); + $this->assertEquals($sequence, $schema->getSequence('a_Seq')); + $this->assertEquals($sequence, $schema->getSequence('A_SEQ')); + } + + public function testGetUnknownSequenceThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $schema = new Schema(); + $schema->getSequence("unknown"); + } + + public function testCreateSequence() + { + $schema = new Schema(); + $sequence = $schema->createSequence('a_seq', 10, 20); + + $this->assertEquals('a_seq', $sequence->getName()); + $this->assertEquals(10, $sequence->getAllocationSize()); + $this->assertEquals(20, $sequence->getInitialValue()); + + $this->assertTrue($schema->hasSequence("a_seq")); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Sequence', $schema->getSequence("a_seq")); + + $sequences = $schema->getSequences(); + $this->assertArrayHasKey('public.a_seq', $sequences); + } + + public function testDropSequence() + { + $sequence = new Sequence("a_seq", 1, 1); + + $schema = new Schema(array(), array($sequence)); + + $schema->dropSequence("a_seq"); + $this->assertFalse($schema->hasSequence("a_seq")); + } + + public function testAddSequenceTwiceThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $sequence = new Sequence("a_seq", 1, 1); + + $schema = new Schema(array(), array($sequence, $sequence)); + } + + public function testConfigMaxIdentifierLength() + { + $schemaConfig = new \Doctrine\DBAL\Schema\SchemaConfig(); + $schemaConfig->setMaxIdentifierLength(5); + + $schema = new Schema(array(), array(), $schemaConfig); + $table = $schema->createTable("smalltable"); + $table->addColumn('long_id', 'integer'); + $table->addIndex(array('long_id')); + + $index = current($table->getIndexes()); + $this->assertEquals(5, strlen($index->getName())); + } + + public function testDeepClone() + { + $schema = new Schema(); + $sequence = $schema->createSequence('baz'); + + $tableA = $schema->createTable('foo'); + $tableA->addColumn('id', 'integer'); + + $tableB = $schema->createTable('bar'); + $tableB->addColumn('id', 'integer'); + $tableB->addColumn('foo_id', 'integer'); + $tableB->addForeignKeyConstraint($tableA, array('foo_id'), array('id')); + + $schemaNew = clone $schema; + + $this->assertNotSame($sequence, $schemaNew->getSequence('baz')); + + $this->assertNotSame($tableA, $schemaNew->getTable('foo')); + $this->assertNotSame($tableA->getColumn('id'), $schemaNew->getTable('foo')->getColumn('id')); + + $this->assertNotSame($tableB, $schemaNew->getTable('bar')); + $this->assertNotSame($tableB->getColumn('id'), $schemaNew->getTable('bar')->getColumn('id')); + + $fk = $schemaNew->getTable('bar')->getForeignKeys(); + $fk = current($fk); + $this->assertSame($schemaNew->getTable('bar'), $this->readAttribute($fk, '_localTable')); + } + + /** + * @group DBAL-219 + */ + public function testHasTableForQuotedAsset() + { + $schema = new Schema(); + + $tableA = $schema->createTable('foo'); + $tableA->addColumn('id', 'integer'); + + $this->assertTrue($schema->hasTable('`foo`')); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php new file mode 100644 index 00000000..ba74ad52 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php @@ -0,0 +1,27 @@ +addColumn("id", "integer", array("autoincrement" => true)); + $table->setPrimaryKey(array("id")); + + $sequence = new Sequence("foo_id_seq"); + $sequence2 = new Sequence("bar_id_seq"); + $sequence3 = new Sequence("other.foo_id_seq"); + + $this->assertTrue($sequence->isAutoIncrementsFor($table)); + $this->assertFalse($sequence2->isAutoIncrementsFor($table)); + $this->assertFalse($sequence3->isAutoIncrementsFor($table)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php new file mode 100644 index 00000000..465668d6 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php @@ -0,0 +1,87 @@ +. + */ + +namespace Doctrine\Tests\DBAL\Schema\Synchronizer; + +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Synchronizer\SingleDatabaseSynchronizer; + +class SingleDatabaseSynchronizerTest extends \PHPUnit_Framework_TestCase +{ + private $conn; + private $synchronizer; + + public function setUp() + { + $this->conn = DriverManager::getConnection(array( + 'driver' => 'pdo_sqlite', + 'memory' => true, + )); + $this->synchronizer = new SingleDatabaseSynchronizer($this->conn); + } + + public function testGetCreateSchema() + { + $schema = new Schema(); + $table = $schema->createTable('test'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $sql = $this->synchronizer->getCreateSchema($schema); + $this->assertEquals(array('CREATE TABLE test (id INTEGER NOT NULL, PRIMARY KEY(id))'), $sql); + } + + public function testGetUpdateSchema() + { + $schema = new Schema(); + $table = $schema->createTable('test'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $sql = $this->synchronizer->getUpdateSchema($schema); + $this->assertEquals(array('CREATE TABLE test (id INTEGER NOT NULL, PRIMARY KEY(id))'), $sql); + } + + public function testGetDropSchema() + { + $schema = new Schema(); + $table = $schema->createTable('test'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $this->synchronizer->createSchema($schema); + + $sql = $this->synchronizer->getDropSchema($schema); + $this->assertEquals(array('DROP TABLE test'), $sql); + } + + public function testGetDropAllSchema() + { + $schema = new Schema(); + $table = $schema->createTable('test'); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $this->synchronizer->createSchema($schema); + + $sql = $this->synchronizer->getDropAllSchema(); + $this->assertEquals(array('DROP TABLE test'), $sql); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/TableTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/TableTest.php new file mode 100644 index 00000000..79f22056 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/TableTest.php @@ -0,0 +1,528 @@ +setExpectedException('Doctrine\DBAL\DBALException'); + $table = new \Doctrine\DBAL\Schema\Table(''); + } + + public function testGetName() + { + $table = new Table("foo", array(), array(), array()); + $this->assertEquals("foo", $table->getName()); + } + + public function testColumns() + { + $type = Type::getType('integer'); + $columns = array(); + $columns[] = new Column("foo", $type); + $columns[] = new Column("bar", $type); + $table = new Table("foo", $columns, array(), array()); + + $this->assertTrue($table->hasColumn("foo")); + $this->assertTrue($table->hasColumn("bar")); + $this->assertFalse($table->hasColumn("baz")); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\Column', $table->getColumn("foo")); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Column', $table->getColumn("bar")); + + $this->assertEquals(2, count($table->getColumns())); + } + + public function testColumnsCaseInsensitive() + { + $table = new Table("foo"); + $column = $table->addColumn('Foo', 'integer'); + + $this->assertTrue($table->hasColumn('Foo')); + $this->assertTrue($table->hasColumn('foo')); + $this->assertTrue($table->hasColumn('FOO')); + + $this->assertSame($column, $table->getColumn('Foo')); + $this->assertSame($column, $table->getColumn('foo')); + $this->assertSame($column, $table->getColumn('FOO')); + } + + public function testCreateColumn() + { + $type = Type::getType('integer'); + + $table = new Table("foo"); + + $this->assertFalse($table->hasColumn("bar")); + $table->addColumn("bar", 'integer'); + $this->assertTrue($table->hasColumn("bar")); + $this->assertSame($type, $table->getColumn("bar")->getType()); + } + + public function testDropColumn() + { + $type = Type::getType('integer'); + $columns = array(); + $columns[] = new Column("foo", $type); + $columns[] = new Column("bar", $type); + $table = new Table("foo", $columns, array(), array()); + + $this->assertTrue($table->hasColumn("foo")); + $this->assertTrue($table->hasColumn("bar")); + + $table->dropColumn("foo")->dropColumn("bar"); + + $this->assertFalse($table->hasColumn("foo")); + $this->assertFalse($table->hasColumn("bar")); + } + + public function testGetUnknownColumnThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo", array(), array(), array()); + $table->getColumn('unknown'); + } + + public function testAddColumnTwiceThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array(); + $columns[] = new Column("foo", $type); + $columns[] = new Column("foo", $type); + $table = new Table("foo", $columns, array(), array()); + } + + public function testCreateIndex() + { + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array(new Column("foo", $type), new Column("bar", $type), new Column("baz", $type)); + $table = new Table("foo", $columns); + + $table->addIndex(array("foo", "bar"), "foo_foo_bar_idx"); + $table->addUniqueIndex(array("bar", "baz"), "foo_bar_baz_uniq"); + + $this->assertTrue($table->hasIndex("foo_foo_bar_idx")); + $this->assertTrue($table->hasIndex("foo_bar_baz_uniq")); + } + + public function testIndexCaseInsensitive() + { + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array( + new Column("foo", $type), + new Column("bar", $type), + new Column("baz", $type) + ); + $table = new Table("foo", $columns); + + $table->addIndex(array("foo", "bar", "baz"), "Foo_Idx"); + + $this->assertTrue($table->hasIndex('foo_idx')); + $this->assertTrue($table->hasIndex('Foo_Idx')); + $this->assertTrue($table->hasIndex('FOO_IDX')); + } + + public function testAddIndexes() + { + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array( + new Column("foo", $type), + new Column("bar", $type), + ); + $indexes = array( + new Index("the_primary", array("foo"), true, true), + new Index("bar_idx", array("bar"), false, false), + ); + $table = new Table("foo", $columns, $indexes, array()); + + $this->assertTrue($table->hasIndex("the_primary")); + $this->assertTrue($table->hasIndex("bar_idx")); + $this->assertFalse($table->hasIndex("some_idx")); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\Index', $table->getPrimaryKey()); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Index', $table->getIndex('the_primary')); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Index', $table->getIndex('bar_idx')); + } + + public function testGetUnknownIndexThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo", array(), array(), array()); + $table->getIndex("unknownIndex"); + } + + public function testAddTwoPrimaryThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array(new Column("foo", $type), new Column("bar", $type)); + $indexes = array( + new Index("the_primary", array("foo"), true, true), + new Index("other_primary", array("bar"), true, true), + ); + $table = new Table("foo", $columns, $indexes, array()); + } + + public function testAddTwoIndexesWithSameNameThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $type = \Doctrine\DBAL\Types\Type::getType('integer'); + $columns = array(new Column("foo", $type), new Column("bar", $type)); + $indexes = array( + new Index("an_idx", array("foo"), false, false), + new Index("an_idx", array("bar"), false, false), + ); + $table = new Table("foo", $columns, $indexes, array()); + } + + public function testConstraints() + { + $constraint = new ForeignKeyConstraint(array(), "foo", array()); + + $tableA = new Table("foo", array(), array(), array($constraint)); + $constraints = $tableA->getForeignKeys(); + + $this->assertEquals(1, count($constraints)); + $this->assertSame($constraint, array_shift($constraints)); + } + + public function testOptions() + { + $table = new Table("foo", array(), array(), array(), false, array("foo" => "bar")); + + $this->assertTrue($table->hasOption("foo")); + $this->assertEquals("bar", $table->getOption("foo")); + } + + public function testBuilderSetPrimaryKey() + { + $table = new Table("foo"); + + $table->addColumn("bar", 'integer'); + $table->setPrimaryKey(array("bar")); + + $this->assertTrue($table->hasIndex("primary")); + $this->assertInstanceOf('Doctrine\DBAL\Schema\Index', $table->getPrimaryKey()); + $this->assertTrue($table->getIndex("primary")->isUnique()); + $this->assertTrue($table->getIndex("primary")->isPrimary()); + } + + public function testBuilderAddUniqueIndex() + { + $table = new Table("foo"); + + $table->addColumn("bar", 'integer'); + $table->addUniqueIndex(array("bar"), "my_idx"); + + $this->assertTrue($table->hasIndex("my_idx")); + $this->assertTrue($table->getIndex("my_idx")->isUnique()); + $this->assertFalse($table->getIndex("my_idx")->isPrimary()); + } + + public function testBuilderAddIndex() + { + $table = new Table("foo"); + + $table->addColumn("bar", 'integer'); + $table->addIndex(array("bar"), "my_idx"); + + $this->assertTrue($table->hasIndex("my_idx")); + $this->assertFalse($table->getIndex("my_idx")->isUnique()); + $this->assertFalse($table->getIndex("my_idx")->isPrimary()); + } + + public function testBuilderAddIndexWithInvalidNameThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo"); + $table->addColumn("bar",'integer'); + $table->addIndex(array("bar"), "invalid name %&/"); + } + + public function testBuilderAddIndexWithUnknownColumnThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo"); + $table->addIndex(array("bar"), "invalidName"); + } + + public function testBuilderOptions() + { + $table = new Table("foo"); + $table->addOption("foo", "bar"); + $this->assertTrue($table->hasOption("foo")); + $this->assertEquals("bar", $table->getOption("foo")); + } + + public function testAddForeignKeyConstraint_UnknownLocalColumn_ThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo"); + $table->addColumn("id", 'integer'); + + $foreignTable = new Table("bar"); + $foreignTable->addColumn("id", 'integer'); + + $table->addForeignKeyConstraint($foreignTable, array("foo"), array("id")); + } + + public function testAddForeignKeyConstraint_UnknownForeignColumn_ThrowsException() + { + $this->setExpectedException("Doctrine\DBAL\Schema\SchemaException"); + + $table = new Table("foo"); + $table->addColumn("id", 'integer'); + + $foreignTable = new Table("bar"); + $foreignTable->addColumn("id", 'integer'); + + $table->addForeignKeyConstraint($foreignTable, array("id"), array("foo")); + } + + public function testAddForeignKeyConstraint() + { + $table = new Table("foo"); + $table->addColumn("id", 'integer'); + + $foreignTable = new Table("bar"); + $foreignTable->addColumn("id", 'integer'); + + $table->addForeignKeyConstraint($foreignTable, array("id"), array("id"), array("foo" => "bar")); + + $constraints = $table->getForeignKeys(); + $this->assertEquals(1, count($constraints)); + $constraint = current($constraints); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\ForeignKeyConstraint', $constraint); + + $this->assertTrue($constraint->hasOption("foo")); + $this->assertEquals("bar", $constraint->getOption("foo")); + } + + public function testAddIndexWithCaseSensitiveColumnProblem() + { + $table = new Table("foo"); + $table->addColumn("id", 'integer'); + + $table->addIndex(array("ID"), "my_idx"); + + $this->assertTrue($table->hasIndex('my_idx')); + $this->assertEquals(array("ID"), $table->getIndex("my_idx")->getColumns()); + $this->assertTrue($table->getIndex('my_idx')->spansColumns(array('id'))); + } + + public function testAddPrimaryKey_ColumnsAreExplicitlySetToNotNull() + { + $table = new Table("foo"); + $column = $table->addColumn("id", 'integer', array('notnull' => false)); + + $this->assertFalse($column->getNotnull()); + + $table->setPrimaryKey(array('id')); + + $this->assertTrue($column->getNotnull()); + } + + /** + * @group DDC-133 + */ + public function testAllowImplicitSchemaTableInAutogeneratedIndexNames() + { + $table = new Table("foo.bar"); + $table->addColumn('baz', 'integer', array()); + $table->addIndex(array('baz')); + + $this->assertEquals(1, count($table->getIndexes())); + } + + /** + * @group DBAL-50 + */ + public function testAddIndexTwice_IgnoreSecond() + { + $table = new Table("foo.bar"); + $table->addColumn('baz', 'integer', array()); + $table->addIndex(array('baz')); + $table->addIndex(array('baz')); + + $this->assertEquals(1, count($table->getIndexes())); + } + + /** + * @group DBAL-50 + */ + public function testAddForeignKeyIndexImplicitly() + { + $table = new Table("foo"); + $table->addColumn("id", 'integer'); + + $foreignTable = new Table("bar"); + $foreignTable->addColumn("id", 'integer'); + + $table->addForeignKeyConstraint($foreignTable, array("id"), array("id"), array("foo" => "bar")); + + $indexes = $table->getIndexes(); + $this->assertEquals(1, count($indexes)); + $index = current($indexes); + + $this->assertTrue($table->hasIndex($index->getName())); + $this->assertEquals(array('id'), $index->getColumns()); + } + + /** + * @group DBAL-50 + */ + public function testOverruleIndex() + { + $table = new Table("bar"); + $table->addColumn('baz', 'integer', array()); + $table->addIndex(array('baz')); + + $indexes = $table->getIndexes(); + $this->assertEquals(1, count($indexes)); + $index = current($indexes); + + $table->addUniqueIndex(array('baz')); + $this->assertEquals(1, count($table->getIndexes())); + $this->assertFalse($table->hasIndex($index->getName())); + } + + public function testPrimaryKeyOverrulesUniqueIndex() + { + $table = new Table("bar"); + $table->addColumn('baz', 'integer', array()); + $table->addUniqueIndex(array('baz')); + + $table->setPrimaryKey(array('baz')); + + $indexes = $table->getIndexes(); + $this->assertEquals(1, count($indexes), "Table should only contain the primary key table index, not the unique one anymore, because it was overruled."); + + $index = current($indexes); + $this->assertTrue($index->isPrimary()); + } + + /** + * @group DBAL-64 + */ + public function testQuotedTableName() + { + $table = new Table("`bar`"); + + $mysqlPlatform = new \Doctrine\DBAL\Platforms\MySqlPlatform(); + $sqlitePlatform = new \Doctrine\DBAL\Platforms\SqlitePlatform(); + + $this->assertEquals('bar', $table->getName()); + $this->assertEquals('`bar`', $table->getQuotedName($mysqlPlatform)); + $this->assertEquals('"bar"', $table->getQuotedName($sqlitePlatform)); + } + + /** + * @group DBAL-79 + */ + public function testTableHasPrimaryKey() + { + $table = new Table("test"); + + $this->assertFalse($table->hasPrimaryKey()); + + $table->addColumn("foo", "integer"); + $table->setPrimaryKey(array("foo")); + + $this->assertTrue($table->hasPrimaryKey()); + } + + /** + * @group DBAL-91 + */ + public function testAddIndexWithQuotedColumns() + { + $table = new Table("test"); + $table->addColumn('"foo"', 'integer'); + $table->addColumn('bar', 'integer'); + $table->addIndex(array('"foo"', '"bar"')); + } + + /** + * @group DBAL-91 + */ + public function testAddForeignKeyWithQuotedColumnsAndTable() + { + $table = new Table("test"); + $table->addColumn('"foo"', 'integer'); + $table->addColumn('bar', 'integer'); + $table->addForeignKeyConstraint('"boing"', array('"foo"', '"bar"'), array("id")); + } + + /** + * @group DBAL-177 + */ + public function testQuoteSchemaPrefixed() + { + $table = new Table("`test`.`test`"); + $this->assertEquals("test.test", $table->getName()); + $this->assertEquals("`test`.`test`", $table->getQuotedName(new \Doctrine\DBAL\Platforms\MySqlPlatform)); + } + + /** + * @group DBAL-204 + */ + public function testFullQualifiedTableName() + { + $table = new Table("`test`.`test`"); + $this->assertEquals('test.test', $table->getFullQualifiedName("test")); + $this->assertEquals('test.test', $table->getFullQualifiedName("other")); + + $table = new Table("test"); + $this->assertEquals('test.test', $table->getFullQualifiedName("test")); + $this->assertEquals('other.test', $table->getFullQualifiedName("other")); + } + + /** + * @group DBAL-224 + */ + public function testDropIndex() + { + $table = new Table("test"); + $table->addColumn('id', 'integer'); + $table->addIndex(array('id'), 'idx'); + + $this->assertTrue($table->hasIndex('idx')); + + $table->dropIndex('idx'); + $this->assertFalse($table->hasIndex('idx')); + } + + /** + * @group DBAL-224 + */ + public function testDropPrimaryKey() + { + $table = new Table("test"); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $this->assertTrue($table->hasPrimaryKey()); + + $table->dropPrimaryKey(); + $this->assertFalse($table->hasPrimaryKey()); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php new file mode 100644 index 00000000..9773c0bd --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php @@ -0,0 +1,76 @@ +setName("test"); + $schema = new Schema(array(), array(), $config); + + $schema->createTable("test.test"); + $schema->createTable("foo.bar"); + $schema->createTable("baz"); + + $schema->visit(new RemoveNamespacedAssets()); + + $tables = $schema->getTables(); + $this->assertEquals(array("test.test", "test.baz"), array_keys($tables), "Only 2 tables should be present, both in 'test' namespace."); + } + + /** + * @group DBAL-204 + */ + public function testCleanupForeignKeys() + { + $config = new SchemaConfig; + $config->setName("test"); + $schema = new Schema(array(), array(), $config); + + $fooTable = $schema->createTable("foo.bar"); + $fooTable->addColumn('id', 'integer'); + + $testTable = $schema->createTable("test.test"); + $testTable->addColumn('id', 'integer'); + + $testTable->addForeignKeyConstraint("foo.bar", array("id"), array("id")); + + $schema->visit(new RemoveNamespacedAssets()); + + $sql = $schema->toSql(new MySqlPlatform()); + $this->assertEquals(1, count($sql), "Just one CREATE TABLE statement, no foreign key and table to foo.bar"); + } + + /** + * @group DBAL-204 + */ + public function testCleanupForeignKeysDifferentOrder() + { + $config = new SchemaConfig; + $config->setName("test"); + $schema = new Schema(array(), array(), $config); + + $testTable = $schema->createTable("test.test"); + $testTable->addColumn('id', 'integer'); + + $fooTable = $schema->createTable("foo.bar"); + $fooTable->addColumn('id', 'integer'); + + $testTable->addForeignKeyConstraint("foo.bar", array("id"), array("id")); + + $schema->visit(new RemoveNamespacedAssets()); + + $sql = $schema->toSql(new MySqlPlatform()); + $this->assertEquals(1, count($sql), "Just one CREATE TABLE statement, no foreign key and table to foo.bar"); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/SchemaSqlCollectorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/SchemaSqlCollectorTest.php new file mode 100644 index 00000000..8dfd2b26 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/SchemaSqlCollectorTest.php @@ -0,0 +1,80 @@ +getMock( + 'Doctrine\DBAL\Platforms\MySqlPlatform', + array('getCreateTableSql', 'getCreateSequenceSql', 'getCreateForeignKeySql') + ); + $platformMock->expects($this->exactly(2)) + ->method('getCreateTableSql') + ->will($this->returnValue(array("foo"))); + $platformMock->expects($this->exactly(1)) + ->method('getCreateSequenceSql') + ->will($this->returnValue(array("bar"))); + $platformMock->expects($this->exactly(1)) + ->method('getCreateForeignKeySql') + ->will($this->returnValue(array("baz"))); + + $schema = $this->createFixtureSchema(); + + $sql = $schema->toSql($platformMock); + + $this->assertEquals(array("foo", "foo", "bar", "baz"), $sql); + } + + public function testDropSchema() + { + $platformMock = $this->getMock( + 'Doctrine\DBAL\Platforms\MySqlPlatform', + array('getDropTableSql', 'getDropSequenceSql', 'getDropForeignKeySql') + ); + $platformMock->expects($this->exactly(2)) + ->method('getDropTableSql') + ->will($this->returnValue("tbl")); + $platformMock->expects($this->exactly(1)) + ->method('getDropSequenceSql') + ->will($this->returnValue("seq")); + $platformMock->expects($this->exactly(1)) + ->method('getDropForeignKeySql') + ->will($this->returnValue("fk")); + + $schema = $this->createFixtureSchema(); + + $sql = $schema->toDropSql($platformMock); + + $this->assertEquals(array("fk", "seq", "tbl", "tbl"), $sql); + } + + /** + * @return Schema + */ + public function createFixtureSchema() + { + $schema = new Schema(); + $tableA = $schema->createTable("foo"); + $tableA->addColumn("id", 'integer'); + $tableA->addColumn("bar", 'string', array('length' => 255)); + $tableA->setPrimaryKey(array("id")); + + $schema->createSequence("foo_seq"); + + $tableB = $schema->createTable("bar"); + $tableB->addColumn("id", 'integer'); + $tableB->setPrimaryKey(array("id")); + + $tableA->addForeignKeyConstraint($tableB, array("bar"), array("id")); + + return $schema; + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php new file mode 100644 index 00000000..de734e74 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php @@ -0,0 +1,181 @@ +. + */ + +namespace Doctrine\Tests\DBAL\Sharding; + +use Doctrine\DBAL\DriverManager; + +class PoolingShardConnectionTest extends \PHPUnit_Framework_TestCase +{ + public function testConnect() + { + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 1, 'memory' => true), + array('id' => 2, 'memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + + $this->assertFalse($conn->isConnected(0)); + $conn->connect(0); + $this->assertEquals(1, $conn->fetchColumn('SELECT 1')); + $this->assertTrue($conn->isConnected(0)); + + $this->assertFalse($conn->isConnected(1)); + $conn->connect(1); + $this->assertEquals(1, $conn->fetchColumn('SELECT 1')); + $this->assertTrue($conn->isConnected(1)); + + $this->assertFalse($conn->isConnected(2)); + $conn->connect(2); + $this->assertEquals(1, $conn->fetchColumn('SELECT 1')); + $this->assertTrue($conn->isConnected(2)); + + $conn->close(); + $this->assertFalse($conn->isConnected(0)); + $this->assertFalse($conn->isConnected(1)); + $this->assertFalse($conn->isConnected(2)); + } + + public function testNoGlobalServerException() + { + $this->setExpectedException('InvalidArgumentException', "Connection Parameters require 'global' and 'shards' configurations."); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'shards' => array( + array('id' => 1, 'memory' => true), + array('id' => 2, 'memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + } + + public function testNoShardsServersExecption() + { + $this->setExpectedException('InvalidArgumentException', "Connection Parameters require 'global' and 'shards' configurations."); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + } + + public function testNoShardsChoserExecption() + { + $this->setExpectedException('InvalidArgumentException', "Missing Shard Choser configuration 'shardChoser'"); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 1, 'memory' => true), + array('id' => 2, 'memory' => true), + ), + )); + } + + public function testShardChoserWrongInstance() + { + $this->setExpectedException('InvalidArgumentException', "The 'shardChoser' configuration is not a valid instance of Doctrine\DBAL\Sharding\ShardChoser\ShardChoser"); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 1, 'memory' => true), + array('id' => 2, 'memory' => true), + ), + 'shardChoser' => new \stdClass, + )); + } + + public function testShardNonNumericId() + { + $this->setExpectedException('InvalidArgumentException', "Shard Id has to be a non-negative number."); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 'foo', 'memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + } + + public function testShardMissingId() + { + $this->setExpectedException('InvalidArgumentException', "Missing 'id' for one configured shard. Please specificy a unique shard-id."); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + } + + public function testDuplicateShardId() + { + $this->setExpectedException('InvalidArgumentException', "Shard 1 is duplicated in the configuration."); + + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 1, 'memory' => true), + array('id' => 1, 'memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + } + + public function testSwitchShardWithOpenTransactionException() + { + $conn = DriverManager::getConnection(array( + 'wrapperClass' => 'Doctrine\DBAL\Sharding\PoolingShardConnection', + 'driver' => 'pdo_sqlite', + 'global' => array('memory' => true), + 'shards' => array( + array('id' => 1, 'memory' => true), + ), + 'shardChoser' => 'Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser', + )); + + $conn->beginTransaction(); + + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'Cannot switch shard when transaction is active.'); + $conn->connect(1); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php new file mode 100644 index 00000000..b2789526 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php @@ -0,0 +1,107 @@ +. + */ +namespace Doctrine\Tests\DBAL\Sharding; + +use Doctrine\DBAL\Sharding\PoolingShardManager; + +class PoolingShardManagerTest extends \PHPUnit_Framework_TestCase +{ + private function createConnectionMock() + { + return $this->getMock('Doctrine\DBAL\Sharding\PoolingShardConnection', array('connect', 'getParams', 'fetchAll'), array(), '', false); + } + + private function createPassthroughShardChoser() + { + $mock = $this->getMock('Doctrine\DBAL\Sharding\ShardChoser\ShardChoser'); + $mock->expects($this->any()) + ->method('pickShard') + ->will($this->returnCallback(function($value) { return $value; })); + return $mock; + } + + public function testSelectGlobal() + { + $conn = $this->createConnectionMock(); + $conn->expects($this->once())->method('connect')->with($this->equalTo(0)); + + $shardManager = new PoolingShardManager($conn, $this->createPassthroughShardChoser()); + $shardManager->selectGlobal(); + + $this->assertNull($shardManager->getCurrentDistributionValue()); + } + + public function testSelectShard() + { + $shardId = 10; + $conn = $this->createConnectionMock(); + $conn->expects($this->at(0))->method('getParams')->will($this->returnValue(array('shardChoser' => $this->createPassthroughShardChoser()))); + $conn->expects($this->at(1))->method('connect')->with($this->equalTo($shardId)); + + $shardManager = new PoolingShardManager($conn); + $shardManager->selectShard($shardId); + + $this->assertEquals($shardId, $shardManager->getCurrentDistributionValue()); + } + + public function testGetShards() + { + $conn = $this->createConnectionMock(); + $conn->expects($this->any())->method('getParams')->will( + $this->returnValue( + array('shards' => array( array('id' => 1), array('id' => 2) ), 'shardChoser' => $this->createPassthroughShardChoser()) + ) + ); + + $shardManager = new PoolingShardManager($conn, $this->createPassthroughShardChoser()); + $shards = $shardManager->getShards(); + + $this->assertEquals(array(array('id' => 1), array('id' => 2)), $shards); + } + + public function testQueryAll() + { + $sql = "SELECT * FROM table"; + $params = array(1); + $types = array(1); + + $conn = $this->createConnectionMock(); + $conn->expects($this->at(0))->method('getParams')->will($this->returnValue( + array('shards' => array( array('id' => 1), array('id' => 2) ), 'shardChoser' => $this->createPassthroughShardChoser()) + )); + $conn->expects($this->at(1))->method('getParams')->will($this->returnValue( + array('shards' => array( array('id' => 1), array('id' => 2) ), 'shardChoser' => $this->createPassthroughShardChoser()) + )); + $conn->expects($this->at(2))->method('connect')->with($this->equalTo(1)); + $conn->expects($this->at(3)) + ->method('fetchAll') + ->with($this->equalTo($sql), $this->equalTo($params), $this->equalTo($types)) + ->will($this->returnValue(array( array('id' => 1) ) )); + $conn->expects($this->at(4))->method('connect')->with($this->equalTo(2)); + $conn->expects($this->at(5)) + ->method('fetchAll') + ->with($this->equalTo($sql), $this->equalTo($params), $this->equalTo($types)) + ->will($this->returnValue(array( array('id' => 2) ) )); + + $shardManager = new PoolingShardManager($conn, $this->createPassthroughShardChoser()); + $result = $shardManager->queryAll($sql, $params, $types); + + $this->assertEquals(array(array('id' => 1), array('id' => 2)), $result); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/AbstractTestCase.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/AbstractTestCase.php new file mode 100644 index 00000000..ccf185c5 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/AbstractTestCase.php @@ -0,0 +1,82 @@ +markTestSkipped('No driver or sqlserver driver specified.'); + } + + $params = array( + 'driver' => $GLOBALS['db_type'], + 'dbname' => $GLOBALS['db_name'], + 'user' => $GLOBALS['db_username'], + 'password' => $GLOBALS['db_password'], + 'host' => $GLOBALS['db_host'], + 'sharding' => array( + 'federationName' => 'Orders_Federation', + 'distributionKey' => 'CustID', + 'distributionType' => 'integer', + 'filteringEnabled' => false, + ), + 'driverOptions' => array('MultipleActiveResultSets' => false) + ); + $this->conn = DriverManager::getConnection($params); + // assume database is created and schema is: + // Global products table + // Customers, Orders, OrderItems federation tables. + // See http://cloud.dzone.com/articles/using-sql-azure-federations + $this->sm = new SQLAzureShardManager($this->conn); + } + + public function createShopSchema() + { + $schema = new Schema(); + + $products = $schema->createTable('Products'); + $products->addColumn('ProductID', 'integer'); + $products->addColumn('SupplierID', 'integer'); + $products->addColumn('ProductName', 'string'); + $products->addColumn('Price', 'decimal', array('scale' => 2, 'precision' => 12)); + $products->setPrimaryKey(array('ProductID')); + $products->addOption('azure.federated', true); + + $customers = $schema->createTable('Customers'); + $customers->addColumn('CustomerID', 'integer'); + $customers->addColumn('CompanyName', 'string'); + $customers->addColumn('FirstName', 'string'); + $customers->addColumn('LastName', 'string'); + $customers->setPrimaryKey(array('CustomerID')); + $customers->addOption('azure.federated', true); + $customers->addOption('azure.federatedOnColumnName', 'CustomerID'); + + $orders = $schema->createTable('Orders'); + $orders->addColumn('CustomerID', 'integer'); + $orders->addColumn('OrderID', 'integer'); + $orders->addColumn('OrderDate', 'datetime'); + $orders->setPrimaryKey(array('CustomerID', 'OrderID')); + $orders->addOption('azure.federated', true); + $orders->addOption('azure.federatedOnColumnName', 'CustomerID'); + + $orderItems = $schema->createTable('OrderItems'); + $orderItems->addColumn('CustomerID', 'integer'); + $orderItems->addColumn('OrderID', 'integer'); + $orderItems->addColumn('ProductID', 'integer'); + $orderItems->addColumn('Quantity', 'integer'); + $orderItems->setPrimaryKey(array('CustomerID', 'OrderID', 'ProductID')); + $orderItems->addOption('azure.federated', true); + $orderItems->addOption('azure.federatedOnColumnName', 'CustomerID'); + + return $schema; + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php new file mode 100644 index 00000000..9c74c81c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php @@ -0,0 +1,43 @@ +createShopSchema(); + + $synchronizer = new SQLAzureSchemaSynchronizer($this->conn, $this->sm); + $synchronizer->dropAllSchema(); + $synchronizer->createSchema($schema); + + $this->sm->selectShard(0); + + $this->conn->insert("Products", array( + "ProductID" => 1, + "SupplierID" => 2, + "ProductName" => "Test", + "Price" => 10.45 + )); + + $this->conn->insert("Customers", array( + "CustomerID" => 1, + "CompanyName" => "Foo", + "FirstName" => "Benjamin", + "LastName" => "E.", + )); + + $query = "SELECT * FROM Products"; + $data = $this->conn->fetchAll($query); + $this->assertTrue(count($data) > 0); + + $query = "SELECT * FROM Customers"; + $data = $this->conn->fetchAll($query); + $this->assertTrue(count($data) > 0); + + $data = $this->sm->queryAll("SELECT * FROM Customers"); + $this->assertTrue(count($data) > 0); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php new file mode 100644 index 00000000..5489d02f --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\Tests\DBAL\Sharding\SQLAzure; + +use Doctrine\DBAL\Platforms\SQLAzurePlatform; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Sharding\SQLAzure\Schema\MultiTenantVisitor; + +class MultiTenantVisitorTest extends \PHPUnit_Framework_TestCase +{ + public function testMultiTenantPrimaryKey() + { + $platform = new SQLAzurePlatform(); + $visitor = new MultiTenantVisitor(); + + $schema = new Schema(); + $foo = $schema->createTable('foo'); + $foo->addColumn('id', 'string'); + $foo->setPrimaryKey(array('id')); + $schema->visit($visitor); + + $this->assertEquals(array('id', 'tenant_id'), $foo->getPrimaryKey()->getColumns()); + $this->assertTrue($foo->hasColumn('tenant_id')); + } + + public function testMultiTenantNonPrimaryKey() + { + $platform = new SQLAzurePlatform(); + $visitor = new MultiTenantVisitor(); + + $schema = new Schema(); + $foo = $schema->createTable('foo'); + $foo->addColumn('id', 'string'); + $foo->addColumn('created', 'datetime'); + $foo->setPrimaryKey(array('id')); + $foo->addIndex(array('created'), 'idx'); + + $foo->getPrimaryKey()->addFlag('nonclustered'); + $foo->getIndex('idx')->addFlag('clustered'); + + $schema->visit($visitor); + + $this->assertEquals(array('id'), $foo->getPrimaryKey()->getColumns()); + $this->assertTrue($foo->hasColumn('tenant_id')); + $this->assertEquals(array('created', 'tenant_id'), $foo->getIndex('idx')->getColumns()); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php new file mode 100644 index 00000000..4c1ada72 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php @@ -0,0 +1,49 @@ +createShopSchema(); + + $synchronizer = new SQLAzureFederationsSynchronizer($this->conn, $this->sm); + $sql = $synchronizer->getCreateSchema($schema); + + $this->assertEquals(array ( + "--Create Federation\nCREATE FEDERATION Orders_Federation (CustID INT RANGE)", + "USE FEDERATION Orders_Federation (CustID = 0) WITH RESET, FILTERING = OFF;", + "CREATE TABLE Products (ProductID INT NOT NULL, SupplierID INT NOT NULL, ProductName NVARCHAR(255) NOT NULL, Price NUMERIC(12, 2) NOT NULL, PRIMARY KEY (ProductID))", + "CREATE TABLE Customers (CustomerID INT NOT NULL, CompanyName NVARCHAR(255) NOT NULL, FirstName NVARCHAR(255) NOT NULL, LastName NVARCHAR(255) NOT NULL, PRIMARY KEY (CustomerID))", + "CREATE TABLE Orders (CustomerID INT NOT NULL, OrderID INT NOT NULL, OrderDate DATETIME2(6) NOT NULL, PRIMARY KEY (CustomerID, OrderID))", + "CREATE TABLE OrderItems (CustomerID INT NOT NULL, OrderID INT NOT NULL, ProductID INT NOT NULL, Quantity INT NOT NULL, PRIMARY KEY (CustomerID, OrderID, ProductID))", + ), $sql); + } + + public function testUpdateSchema() + { + $schema = $this->createShopSchema(); + + $synchronizer = new SQLAzureFederationsSynchronizer($this->conn, $this->sm); + $synchronizer->dropAllSchema(); + + $sql = $synchronizer->getUpdateSchema($schema); + + $this->assertEquals(array(), $sql); + } + + public function testDropSchema() + { + $schema = $this->createShopSchema(); + + $synchronizer = new SQLAzureFederationsSynchronizer($this->conn, $this->sm); + $synchronizer->dropAllSchema(); + $synchronizer->createSchema($schema); + $sql = $synchronizer->getDropSchema($schema); + + $this->assertEQuals(5, count($sql)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php new file mode 100644 index 00000000..ece7538d --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php @@ -0,0 +1,92 @@ +setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'SQLAzure requires a federation name to be set during sharding configuration.'); + + $conn = $this->createConnection(array('sharding' => array('distributionKey' => 'abc', 'distributionType' => 'integer'))); + $sm = new SQLAzureShardManager($conn); + } + + public function testNoDistributionKey() + { + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'SQLAzure requires a distribution key to be set during sharding configuration.'); + + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionType' => 'integer'))); + $sm = new SQLAzureShardManager($conn); + } + + public function testNoDistributionType() + { + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException'); + + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo'))); + $sm = new SQLAzureShardManager($conn); + } + + public function testGetDefaultDistributionValue() + { + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo', 'distributionType' => 'integer'))); + + $sm = new SQLAzureShardManager($conn); + $this->assertNull($sm->getCurrentDistributionValue()); + } + + public function testSelectGlobalTransactionActive() + { + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo', 'distributionType' => 'integer'))); + $conn->expects($this->at(1))->method('isTransactionActive')->will($this->returnValue(true)); + + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'Cannot switch shard during an active transaction.'); + + $sm = new SQLAzureShardManager($conn); + $sm->selectGlobal(); + } + + public function testSelectGlobal() + { + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo', 'distributionType' => 'integer'))); + $conn->expects($this->at(1))->method('isTransactionActive')->will($this->returnValue(false)); + $conn->expects($this->at(2))->method('exec')->with($this->equalTo('USE FEDERATION ROOT WITH RESET')); + + $sm = new SQLAzureShardManager($conn); + $sm->selectGlobal(); + } + + public function testSelectShard() + { + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo', 'distributionType' => 'integer'))); + $conn->expects($this->at(1))->method('isTransactionActive')->will($this->returnValue(true)); + + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'Cannot switch shard during an active transaction.'); + + $sm = new SQLAzureShardManager($conn); + $sm->selectShard(1234); + + $this->assertEquals(1234, $sm->getCurrentDistributionValue()); + } + + public function testSelectShardNoDistriubtionValue() + { + $conn = $this->createConnection(array('sharding' => array('federationName' => 'abc', 'distributionKey' => 'foo', 'distributionType' => 'integer'))); + $conn->expects($this->at(1))->method('isTransactionActive')->will($this->returnValue(false)); + + $this->setExpectedException('Doctrine\DBAL\Sharding\ShardingException', 'You have to specify a string or integer as shard distribution value.'); + + $sm = new SQLAzureShardManager($conn); + $sm->selectShard(null); + } + + private function createConnection(array $params) + { + $conn = $this->getMock('Doctrine\DBAL\Connection', array('getParams', 'exec', 'isTransactionActive'), array(), '', false); + $conn->expects($this->at(0))->method('getParams')->will($this->returnValue($params)); + return $conn; + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php new file mode 100644 index 00000000..dfc75e71 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php @@ -0,0 +1,39 @@ +. + */ + +namespace Doctrine\Tests\DBAL\Sharding\ShardChoser; + +use Doctrine\DBAL\Sharding\ShardChoser\MultiTenantShardChoser; + +class MultiTenantShardChoserTest extends \PHPUnit_Framework_TestCase +{ + public function testPickShard() + { + $choser = new MultiTenantShardChoser(); + $conn = $this->createConnectionMock(); + + $this->assertEquals(1, $choser->pickShard(1, $conn)); + $this->assertEquals(2, $choser->pickShard(2, $conn)); + } + + private function createConnectionMock() + { + return $this->getMock('Doctrine\DBAL\Sharding\PoolingShardConnection', array('connect', 'getParams', 'fetchAll'), array(), '', false); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ArrayTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ArrayTest.php new file mode 100644 index 00000000..ac4244d9 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ArrayTest.php @@ -0,0 +1,61 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('array'); + } + + public function tearDown() + { + error_reporting(-1); // reactive all error levels + } + + + public function testArrayConvertsToDatabaseValue() + { + $this->assertTrue( + is_string($this->_type->convertToDatabaseValue(array(), $this->_platform)) + ); + } + + public function testArrayConvertsToPHPValue() + { + $this->assertTrue( + is_array($this->_type->convertToPHPValue(serialize(array()), $this->_platform)) + ); + } + + public function testConversionFailure() + { + error_reporting( (E_ALL | E_STRICT) - \E_NOTICE ); + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + /** + * @group DBAL-73 + */ + public function testFalseConversion() + { + $this->assertFalse($this->_type->convertToPHPValue(serialize(false), $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BlobTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BlobTest.php new file mode 100644 index 00000000..756487dd --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BlobTest.php @@ -0,0 +1,26 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('blob'); + } + + public function testBlobNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BooleanTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BooleanTest.php new file mode 100644 index 00000000..753cfb46 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/BooleanTest.php @@ -0,0 +1,36 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('boolean'); + } + + public function testBooleanConvertsToDatabaseValue() + { + $this->assertInternalType('integer', $this->_type->convertToDatabaseValue(1, $this->_platform)); + } + + public function testBooleanConvertsToPHPValue() + { + $this->assertInternalType('bool', $this->_type->convertToPHPValue(0, $this->_platform)); + } + + public function testBooleanNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTest.php new file mode 100644 index 00000000..e3763fd8 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTest.php @@ -0,0 +1,81 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('date'); + $this->_tz = date_default_timezone_get(); + } + + public function tearDown() + { + date_default_timezone_set($this->_tz); + } + + public function testDateConvertsToDatabaseValue() + { + $this->assertTrue( + is_string($this->_type->convertToDatabaseValue(new \DateTime(), $this->_platform)) + ); + } + + public function testDateConvertsToPHPValue() + { + // Birthday of jwage and also birthday of Doctrine. Send him a present ;) + $this->assertTrue( + $this->_type->convertToPHPValue('1985-09-01', $this->_platform) + instanceof \DateTime + ); + } + + public function testDateResetsNonDatePartsToZeroUnixTimeValues() + { + $date = $this->_type->convertToPHPValue('1985-09-01', $this->_platform); + + $this->assertEquals('00:00:00', $date->format('H:i:s')); + } + + public function testDateRests_SummerTimeAffection() + { + date_default_timezone_set('Europe/Berlin'); + + $date = $this->_type->convertToPHPValue('2009-08-01', $this->_platform); + $this->assertEquals('00:00:00', $date->format('H:i:s')); + $this->assertEquals('2009-08-01', $date->format('Y-m-d')); + + $date = $this->_type->convertToPHPValue('2009-11-01', $this->_platform); + $this->assertEquals('00:00:00', $date->format('H:i:s')); + $this->assertEquals('2009-11-01', $date->format('Y-m-d')); + } + + public function testInvalidDateFormatConversion() + { + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testConvertDateTimeToPHPValue() + { + $date = new \DateTime("now"); + $this->assertSame($date, $this->_type->convertToPHPValue($date, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTest.php new file mode 100644 index 00000000..58129d21 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTest.php @@ -0,0 +1,56 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('datetime'); + } + + public function testDateTimeConvertsToDatabaseValue() + { + $date = new \DateTime('1985-09-01 10:10:10'); + + $expected = $date->format($this->_platform->getDateTimeTzFormatString()); + $actual = $this->_type->convertToDatabaseValue($date, $this->_platform); + + $this->assertEquals($expected, $actual); + } + + public function testDateTimeConvertsToPHPValue() + { + // Birthday of jwage and also birthday of Doctrine. Send him a present ;) + $date = $this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform); + $this->assertInstanceOf('DateTime', $date); + $this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s')); + } + + public function testInvalidDateTimeFormatConversion() + { + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testConvertDateTimeToPHPValue() + { + $date = new \DateTime("now"); + $this->assertSame($date, $this->_type->convertToPHPValue($date, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTzTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTzTest.php new file mode 100644 index 00000000..08c250a7 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DateTimeTzTest.php @@ -0,0 +1,56 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('datetimetz'); + } + + public function testDateTimeConvertsToDatabaseValue() + { + $date = new \DateTime('1985-09-01 10:10:10'); + + $expected = $date->format($this->_platform->getDateTimeTzFormatString()); + $actual = $this->_type->convertToDatabaseValue($date, $this->_platform); + + $this->assertEquals($expected, $actual); + } + + public function testDateTimeConvertsToPHPValue() + { + // Birthday of jwage and also birthday of Doctrine. Send him a present ;) + $date = $this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform); + $this->assertInstanceOf('DateTime', $date); + $this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s')); + } + + public function testInvalidDateFormatConversion() + { + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testConvertDateTimeToPHPValue() + { + $date = new \DateTime("now"); + $this->assertSame($date, $this->_type->convertToPHPValue($date, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DecimalTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DecimalTest.php new file mode 100644 index 00000000..983029c1 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/DecimalTest.php @@ -0,0 +1,31 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('decimal'); + } + + public function testDecimalConvertsToPHPValue() + { + $this->assertInternalType('string', $this->_type->convertToPHPValue('5.5', $this->_platform)); + } + + public function testDecimalNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/FloatTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/FloatTest.php new file mode 100644 index 00000000..38d92375 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/FloatTest.php @@ -0,0 +1,39 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('float'); + } + + public function testFloatConvertsToPHPValue() + { + $this->assertInternalType('float', $this->_type->convertToPHPValue('5.5', $this->_platform)); + } + + public function testFloatNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testFloatConvertToDatabaseValue() + { + $this->assertInternalType('float', $this->_type->convertToDatabaseValue(5.5, $this->_platform)); + } + + public function testFloatNullConvertToDatabaseValue() + { + $this->assertNull($this->_type->convertToDatabaseValue(null, $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php new file mode 100644 index 00000000..317c3d28 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php @@ -0,0 +1,30 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('guid'); + } + + public function testConvertToPHPValue() + { + $this->assertInternalType("string", $this->_type->convertToPHPValue("foo", $this->_platform)); + $this->assertInternalType("string", $this->_type->convertToPHPValue("", $this->_platform)); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/IntegerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/IntegerTest.php new file mode 100644 index 00000000..adcde536 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/IntegerTest.php @@ -0,0 +1,32 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('integer'); + } + + public function testIntegerConvertsToPHPValue() + { + $this->assertInternalType('integer', $this->_type->convertToPHPValue('1', $this->_platform)); + $this->assertInternalType('integer', $this->_type->convertToPHPValue('0', $this->_platform)); + } + + public function testIntegerNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ObjectTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ObjectTest.php new file mode 100644 index 00000000..d25c49ae --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/ObjectTest.php @@ -0,0 +1,56 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('object'); + } + + public function tearDown() + { + error_reporting(-1); // reactive all error levels + } + + public function testObjectConvertsToDatabaseValue() + { + $this->assertInternalType('string', $this->_type->convertToDatabaseValue(new \stdClass(), $this->_platform)); + } + + public function testObjectConvertsToPHPValue() + { + $this->assertInternalType('object', $this->_type->convertToPHPValue(serialize(new \stdClass), $this->_platform)); + } + + public function testConversionFailure() + { + error_reporting( (E_ALL | E_STRICT) - \E_NOTICE ); + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + /** + * @group DBAL-73 + */ + public function testFalseConversion() + { + $this->assertFalse($this->_type->convertToPHPValue(serialize(false), $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/SmallIntTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/SmallIntTest.php new file mode 100644 index 00000000..399095cb --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/SmallIntTest.php @@ -0,0 +1,32 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('smallint'); + } + + public function testSmallIntConvertsToPHPValue() + { + $this->assertInternalType('integer', $this->_type->convertToPHPValue('1', $this->_platform)); + $this->assertInternalType('integer', $this->_type->convertToPHPValue('0', $this->_platform)); + } + + public function testSmallIntNullConvertsToPHPValue() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/StringTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/StringTest.php new file mode 100644 index 00000000..52d2955b --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/StringTest.php @@ -0,0 +1,49 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('string'); + } + + public function testReturnsSqlDeclarationFromPlatformVarchar() + { + $this->assertEquals("DUMMYVARCHAR()", $this->_type->getSqlDeclaration(array(), $this->_platform)); + } + + public function testReturnsDefaultLengthFromPlatformVarchar() + { + $this->assertEquals(255, $this->_type->getDefaultLength($this->_platform)); + } + + public function testConvertToPHPValue() + { + $this->assertInternalType("string", $this->_type->convertToPHPValue("foo", $this->_platform)); + $this->assertInternalType("string", $this->_type->convertToPHPValue("", $this->_platform)); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testSQLConversion() + { + $this->assertFalse($this->_type->canRequireSQLConversion(), "String type can never require SQL conversion to work."); + $this->assertEquals('t.foo', $this->_type->convertToDatabaseValueSQL('t.foo', $this->_platform)); + $this->assertEquals('t.foo', $this->_type->convertToPHPValueSQL('t.foo', $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/TimeTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/TimeTest.php new file mode 100644 index 00000000..8a07b9b9 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/TimeTest.php @@ -0,0 +1,53 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + $this->_type = Type::getType('time'); + } + + public function testTimeConvertsToDatabaseValue() + { + $this->assertTrue( + is_string($this->_type->convertToDatabaseValue(new \DateTime(), $this->_platform)) + ); + } + + public function testTimeConvertsToPHPValue() + { + $this->assertTrue( + $this->_type->convertToPHPValue('5:30:55', $this->_platform) + instanceof \DateTime + ); + } + + public function testInvalidTimeFormatConversion() + { + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testConvertDateTimeToPHPValue() + { + $date = new \DateTime("now"); + $this->assertSame($date, $this->_type->convertToPHPValue($date, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/VarDateTimeTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/VarDateTimeTest.php new file mode 100644 index 00000000..329ef8cc --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/VarDateTimeTest.php @@ -0,0 +1,68 @@ +_platform = new \Doctrine\Tests\DBAL\Mocks\MockPlatform(); + if (!Type::hasType('vardatetime')) { + Type::addType('vardatetime', 'Doctrine\DBAL\Types\VarDateTimeType'); + } + $this->_type = Type::getType('vardatetime'); + } + + public function testDateTimeConvertsToDatabaseValue() + { + $date = new \DateTime('1985-09-01 10:10:10'); + + $expected = $date->format($this->_platform->getDateTimeTzFormatString()); + $actual = $this->_type->convertToDatabaseValue($date, $this->_platform); + + $this->assertEquals($expected, $actual); + } + + public function testDateTimeConvertsToPHPValue() + { + // Birthday of jwage and also birthday of Doctrine. Send him a present ;) + $date = $this->_type->convertToPHPValue('1985-09-01 00:00:00', $this->_platform); + $this->assertInstanceOf('DateTime', $date); + $this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s')); + $this->assertEquals('000000', $date->format('u')); + } + + public function testInvalidDateTimeFormatConversion() + { + $this->setExpectedException('Doctrine\DBAL\Types\ConversionException'); + $this->_type->convertToPHPValue('abcdefg', $this->_platform); + } + + public function testConversionWithMicroseconds() + { + $date = $this->_type->convertToPHPValue('1985-09-01 00:00:00.123456', $this->_platform); + $this->assertInstanceOf('DateTime', $date); + $this->assertEquals('1985-09-01 00:00:00', $date->format('Y-m-d H:i:s')); + $this->assertEquals('123456', $date->format('u')); + } + + public function testNullConversion() + { + $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); + } + + public function testConvertDateTimeToPHPValue() + { + $date = new \DateTime("now"); + $this->assertSame($date, $this->_type->convertToPHPValue($date, $this->_platform)); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/UtilTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/UtilTest.php new file mode 100644 index 00000000..b793e357 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/UtilTest.php @@ -0,0 +1,78 @@ + ':param1') + ), + array( + 'SELECT name FROM users WHERE id = ? AND status = ?', + 'SELECT name FROM users WHERE id = :param1 AND status = :param2', + array(1 => ':param1', 2 => ':param2'), + ), + array( + "UPDATE users SET name = '???', status = ?", + "UPDATE users SET name = '???', status = :param1", + array(1 => ':param1'), + ), + array( + "UPDATE users SET status = ?, name = '???'", + "UPDATE users SET status = :param1, name = '???'", + array(1 => ':param1'), + ), + array( + "UPDATE users SET foo = ?, name = '???', status = ?", + "UPDATE users SET foo = :param1, name = '???', status = :param2", + array(1 => ':param1', 2 => ':param2'), + ), + array( + 'UPDATE users SET name = "???", status = ?', + 'UPDATE users SET name = "???", status = :param1', + array(1 => ':param1'), + ), + array( + 'UPDATE users SET status = ?, name = "???"', + 'UPDATE users SET status = :param1, name = "???"', + array(1 => ':param1'), + ), + array( + 'UPDATE users SET foo = ?, name = "???", status = ?', + 'UPDATE users SET foo = :param1, name = "???", status = :param2', + array(1 => ':param1', 2 => ':param2'), + ), + array( + 'SELECT * FROM users WHERE id = ? AND name = "" AND status = ?', + 'SELECT * FROM users WHERE id = :param1 AND name = "" AND status = :param2', + array(1 => ':param1', 2 => ':param2'), + ), + array( + "SELECT * FROM users WHERE id = ? AND name = '' AND status = ?", + "SELECT * FROM users WHERE id = :param1 AND name = '' AND status = :param2", + array(1 => ':param1', 2 => ':param2'), + ) + ); + } + + /** + * @dataProvider dataConvertPositionalToNamedParameters + * @param string $inputSQL + * @param string $expectedOutputSQL + * @param array $expectedOutputParamsMap + */ + public function testConvertPositionalToNamedParameters($inputSQL, $expectedOutputSQL, $expectedOutputParamsMap) + { + list($statement, $params) = \Doctrine\DBAL\Driver\OCI8\OCI8Statement::convertPositionalToNamedPlaceholders($inputSQL); + + $this->assertEquals($expectedOutputSQL, $statement); + $this->assertEquals($expectedOutputParamsMap, $params); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DbalFunctionalTestCase.php b/doctrine/dbal/tests/Doctrine/Tests/DbalFunctionalTestCase.php new file mode 100644 index 00000000..27dd1495 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DbalFunctionalTestCase.php @@ -0,0 +1,77 @@ +close(); + self::$_sharedConn = null; + } + } + + protected function setUp() + { + if ( ! isset(self::$_sharedConn)) { + self::$_sharedConn = TestUtil::getConnection(); + } + $this->_conn = self::$_sharedConn; + + $this->_sqlLoggerStack = new \Doctrine\DBAL\Logging\DebugStack(); + $this->_conn->getConfiguration()->setSQLLogger($this->_sqlLoggerStack); + } + + protected function onNotSuccessfulTest(\Exception $e) + { + if ($e instanceof \PHPUnit_Framework_AssertionFailedError) { + throw $e; + } + + if(isset($this->_sqlLoggerStack->queries) && count($this->_sqlLoggerStack->queries)) { + $queries = ""; + $i = count($this->_sqlLoggerStack->queries); + foreach (array_reverse($this->_sqlLoggerStack->queries) AS $query) { + $params = array_map(function($p) { if (is_object($p)) return get_class($p); else return "'".$p."'"; }, $query['params'] ?: array()); + $queries .= ($i+1).". SQL: '".$query['sql']."' Params: ".implode(", ", $params).PHP_EOL; + $i--; + } + + $trace = $e->getTrace(); + $traceMsg = ""; + foreach($trace AS $part) { + if(isset($part['file'])) { + if(strpos($part['file'], "PHPUnit/") !== false) { + // Beginning with PHPUnit files we don't print the trace anymore. + break; + } + + $traceMsg .= $part['file'].":".$part['line'].PHP_EOL; + } + } + + $message = "[".get_class($e)."] ".$e->getMessage().PHP_EOL.PHP_EOL."With queries:".PHP_EOL.$queries.PHP_EOL."Trace:".PHP_EOL.$traceMsg; + + throw new \Exception($message, (int)$e->getCode(), $e); + } + throw $e; + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DbalTestCase.php b/doctrine/dbal/tests/Doctrine/Tests/DbalTestCase.php new file mode 100644 index 00000000..2478e7bc --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DbalTestCase.php @@ -0,0 +1,10 @@ +_platformMock = new DatabasePlatformMock(); + + parent::__construct($params, $driver, $config, $eventManager); + + // Override possible assignment of platform to database platform mock + $this->_platform = $this->_platformMock; + } + + /** + * @override + */ + public function getDatabasePlatform() + { + return $this->_platformMock; + } + + /** + * @override + */ + public function insert($tableName, array $data, array $types = array()) + { + $this->_inserts[$tableName][] = $data; + } + + /** + * @override + */ + public function lastInsertId($seqName = null) + { + return $this->_lastInsertId; + } + + /** + * @override + */ + public function fetchColumn($statement, array $params = array(), $colnum = 0) + { + return $this->_fetchOneResult; + } + + /** + * @override + */ + public function quote($input, $type = null) + { + if (is_string($input)) { + return "'" . $input . "'"; + } + return $input; + } + + /* Mock API */ + + public function setFetchOneResult($fetchOneResult) + { + $this->_fetchOneResult = $fetchOneResult; + } + + public function setDatabasePlatform($platform) + { + $this->_platformMock = $platform; + } + + public function setLastInsertId($id) + { + $this->_lastInsertId = $id; + } + + public function getInserts() + { + return $this->_inserts; + } + + public function reset() + { + $this->_inserts = array(); + $this->_lastInsertId = 0; + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php b/doctrine/dbal/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php new file mode 100644 index 00000000..0153f9bb --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/Mocks/DatabasePlatformMock.php @@ -0,0 +1,98 @@ +_prefersIdentityColumns; + } + + /** + * @override + */ + public function prefersSequences() + { + return $this->_prefersSequences; + } + + /** @override */ + public function getSequenceNextValSQL($sequenceName) + { + return $this->_sequenceNextValSql; + } + + /** @override */ + public function getBooleanTypeDeclarationSQL(array $field) {} + + /** @override */ + public function getIntegerTypeDeclarationSQL(array $field) {} + + /** @override */ + public function getBigIntTypeDeclarationSQL(array $field) {} + + /** @override */ + public function getSmallIntTypeDeclarationSQL(array $field) {} + + /** @override */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) {} + + /** @override */ + public function getVarcharTypeDeclarationSQL(array $field) {} + + /** @override */ + public function getClobTypeDeclarationSQL(array $field) {} + + /* MOCK API */ + + public function setPrefersIdentityColumns($bool) + { + $this->_prefersIdentityColumns = $bool; + } + + public function setPrefersSequences($bool) + { + $this->_prefersSequences = $bool; + } + + public function setSequenceNextValSql($sql) + { + $this->_sequenceNextValSql = $sql; + } + + public function getName() + { + return 'mock'; + } + protected function initializeDoctrineTypeMappings() { + } + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + + } + /** + * Gets the SQL Snippet used to declare a BLOB column type. + */ + public function getBlobTypeDeclarationSQL(array $field) + { + throw DBALException::notSupported(__METHOD__); + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/Mocks/DriverConnectionMock.php b/doctrine/dbal/tests/Doctrine/Tests/Mocks/DriverConnectionMock.php new file mode 100644 index 00000000..03d44caa --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/Mocks/DriverConnectionMock.php @@ -0,0 +1,17 @@ +_platformMock) { + $this->_platformMock = new DatabasePlatformMock; + } + return $this->_platformMock; + } + + /** + * @override + */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + if($this->_schemaManagerMock == null) { + return new SchemaManagerMock($conn); + } else { + return $this->_schemaManagerMock; + } + } + + /* MOCK API */ + + public function setDatabasePlatform(\Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $this->_platformMock = $platform; + } + + public function setSchemaManager(\Doctrine\DBAL\Schema\AbstractSchemaManager $sm) + { + $this->_schemaManagerMock = $sm; + } + + public function getName() + { + return 'mock'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + return; + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php b/doctrine/dbal/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php new file mode 100644 index 00000000..629352fb --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/Mocks/HydratorMockStatement.php @@ -0,0 +1,101 @@ + + */ +class HydratorMockStatement implements \Doctrine\DBAL\Driver\Statement +{ + private $_resultSet; + + /** + * Creates a new mock statement that will serve the provided fake result set to clients. + * + * @param array $resultSet The faked SQL result set. + */ + public function __construct(array $resultSet) + { + $this->_resultSet = $resultSet; + } + + /** + * Fetches all rows from the result set. + * + * @return array + */ + public function fetchAll($fetchMode = null, $columnIndex = null, array $ctorArgs = null) + { + return $this->_resultSet; + } + + public function fetchColumn($columnNumber = 0) + { + $row = current($this->_resultSet); + if ( ! is_array($row)) return false; + $val = array_shift($row); + return $val !== null ? $val : false; + } + + /** + * Fetches the next row in the result set. + * + */ + public function fetch($fetchMode = null) + { + $current = current($this->_resultSet); + next($this->_resultSet); + return $current; + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean + */ + public function closeCursor() + { + return true; + } + + public function setResultSet(array $resultSet) + { + reset($resultSet); + $this->_resultSet = $resultSet; + } + + public function bindColumn($column, &$param, $type = null) + { + } + + public function bindValue($param, $value, $type = null) + { + } + + public function bindParam($column, &$variable, $type = null, $length = null, $driverOptions = array()) + { + } + + public function columnCount() + { + } + + public function errorCode() + { + } + + public function errorInfo() + { + } + + public function execute($params = array()) + { + } + + public function rowCount() + { + } +} \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/Mocks/SchemaManagerMock.php b/doctrine/dbal/tests/Doctrine/Tests/Mocks/SchemaManagerMock.php new file mode 100644 index 00000000..d4c3c28c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/Mocks/SchemaManagerMock.php @@ -0,0 +1,13 @@ +. + */ + +namespace Doctrine\Tests\Mocks; + +use Doctrine\Common\Cli\AbstractNamespace; + +/** + * TaskMock used for testing the CLI interface. + * @author Nils Adermann + */ +class TaskMock extends \Doctrine\Common\Cli\Tasks\AbstractTask +{ + /** + * Since instances of this class can be created elsewhere all instances + * register themselves in this array for later inspection. + * + * @var array(TaskMock) + */ + static public $instances = array(); + + private $runCounter = 0; + + /** + * Constructor of Task Mock Object. + * Makes sure the object can be inspected later. + * + * @param AbstractNamespace CLI Namespace, passed to parent constructor + */ + function __construct(AbstractNamespace $namespace) + { + self::$instances[] = $this; + + parent::__construct($namespace); + } + + /** + * Returns the number of times run() was called on this object. + * + * @return int + */ + public function getRunCounter() + { + return $this->runCounter; + } + + /* Mock API */ + + /** + * Method invoked by CLI to run task. + */ + public function run() + { + $this->runCounter++; + } + + /** + * Method supposed to generate the CLI Task Documentation + */ + public function buildDocumentation() + { + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/TestInit.php b/doctrine/dbal/tests/Doctrine/Tests/TestInit.php new file mode 100644 index 00000000..6763cf61 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/TestInit.php @@ -0,0 +1,21 @@ +register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\DBAL', __DIR__ . '/../../../lib'); +$classLoader->register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\Tests', __DIR__ . '/../../'); +$classLoader->register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Symfony', __DIR__ . "/../../../lib/vendor"); +$classLoader->register(); diff --git a/doctrine/dbal/tests/Doctrine/Tests/TestUtil.php b/doctrine/dbal/tests/Doctrine/Tests/TestUtil.php new file mode 100644 index 00000000..8701c0a2 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/TestUtil.php @@ -0,0 +1,130 @@ +real database connection using the following parameters + * of the $GLOBALS array: + * + * 'db_type' : The name of the Doctrine DBAL database driver to use. + * 'db_username' : The username to use for connecting. + * 'db_password' : The password to use for connecting. + * 'db_host' : The hostname of the database to connect to. + * 'db_name' : The name of the database to connect to. + * 'db_port' : The port of the database to connect to. + * + * Usually these variables of the $GLOBALS array are filled by PHPUnit based + * on an XML configuration file. If no such parameters exist, an SQLite + * in-memory database is used. + * + * IMPORTANT: + * 1) Each invocation of this method returns a NEW database connection. + * 2) The database is dropped and recreated to ensure it's clean. + * + * @return \Doctrine\DBAL\Connection The database connection instance. + */ + public static function getConnection() + { + if (isset($GLOBALS['db_type'], $GLOBALS['db_username'], $GLOBALS['db_password'], + $GLOBALS['db_host'], $GLOBALS['db_name'], $GLOBALS['db_port']) && + isset($GLOBALS['tmpdb_type'], $GLOBALS['tmpdb_username'], $GLOBALS['tmpdb_password'], + $GLOBALS['tmpdb_host'], $GLOBALS['tmpdb_name'], $GLOBALS['tmpdb_port'])) { + $realDbParams = array( + 'driver' => $GLOBALS['db_type'], + 'user' => $GLOBALS['db_username'], + 'password' => $GLOBALS['db_password'], + 'host' => $GLOBALS['db_host'], + 'dbname' => $GLOBALS['db_name'], + 'port' => $GLOBALS['db_port'] + ); + $tmpDbParams = array( + 'driver' => $GLOBALS['tmpdb_type'], + 'user' => $GLOBALS['tmpdb_username'], + 'password' => $GLOBALS['tmpdb_password'], + 'host' => $GLOBALS['tmpdb_host'], + 'dbname' => $GLOBALS['tmpdb_name'], + 'port' => $GLOBALS['tmpdb_port'] + ); + + if (isset($GLOBALS['db_unix_socket'])) { + $realDbParams['unix_socket'] = $GLOBALS['db_unix_socket']; + } + + if (isset($GLOBALS['tmpdb_unix_socket'])) { + $tmpDbParams['unix_socket'] = $GLOBALS['tmpdb_unix_socket']; + } + + $realConn = \Doctrine\DBAL\DriverManager::getConnection($realDbParams); + + $platform = $realConn->getDatabasePlatform(); + + if ($platform->supportsCreateDropDatabase()) { + $dbname = $realConn->getDatabase(); + // Connect to tmpdb in order to drop and create the real test db. + $tmpConn = \Doctrine\DBAL\DriverManager::getConnection($tmpDbParams); + $realConn->close(); + + $tmpConn->getSchemaManager()->dropAndCreateDatabase($dbname); + + $tmpConn->close(); + } else { + $sm = $realConn->getSchemaManager(); + + /* @var $schema Schema */ + $schema = $sm->createSchema(); + $stmts = $schema->toDropSql($realConn->getDatabasePlatform()); + + foreach ($stmts AS $stmt) { + $realConn->exec($stmt); + } + } + + $conn = \Doctrine\DBAL\DriverManager::getConnection($realDbParams, null, null); + } else { + $params = array( + 'driver' => 'pdo_sqlite', + 'memory' => true + ); + if (isset($GLOBALS['db_path'])) { + $params['path'] = $GLOBALS['db_path']; + unlink($GLOBALS['db_path']); + } + $conn = \Doctrine\DBAL\DriverManager::getConnection($params); + } + + if (isset($GLOBALS['db_event_subscribers'])) { + $evm = $conn->getEventManager(); + foreach (explode(",", $GLOBALS['db_event_subscribers']) AS $subscriberClass) { + $subscriberInstance = new $subscriberClass(); + $evm->addEventSubscriber($subscriberInstance); + } + } + + return $conn; + } + + /** + * @return \Doctrine\DBAL\Connection + */ + public static function getTempConnection() + { + $tmpDbParams = array( + 'driver' => $GLOBALS['tmpdb_type'], + 'user' => $GLOBALS['tmpdb_username'], + 'password' => $GLOBALS['tmpdb_password'], + 'host' => $GLOBALS['tmpdb_host'], + 'dbname' => $GLOBALS['tmpdb_name'], + 'port' => $GLOBALS['tmpdb_port'] + ); + + // Connect to tmpdb in order to drop and create the real test db. + return \Doctrine\DBAL\DriverManager::getConnection($tmpDbParams); + } +} diff --git a/doctrine/dbal/tests/README.markdown b/doctrine/dbal/tests/README.markdown new file mode 100644 index 00000000..c1027ace --- /dev/null +++ b/doctrine/dbal/tests/README.markdown @@ -0,0 +1,25 @@ +# Running the Doctrine 2 Testsuite + +## Setting up a PHPUnit Configuration XML + +.. + +## Testing Lock-Support + +The Lock support in Doctrine 2 is tested using Gearman, which allows to run concurrent tasks in parallel. +Install Gearman with PHP as follows: + +1. Go to http://www.gearman.org and download the latest Gearman Server +2. Compile it and then call ldconfig +3. Start it up "gearmand -vvvv" +4. Install pecl/gearman by calling "gearman-beta" + +You can then go into tests/ and start up two workers: + + php Doctrine/Tests/ORM/Functional/Locking/LockAgentWorker.php + +Then run the locking test-suite: + + phpunit --configuration Doctrine/Tests/ORM/Functional/Locking/GearmanLockTest.php + +This can run considerable time, because it is using sleep() to test for the timing ranges of locks. \ No newline at end of file diff --git a/doctrine/dbal/tests/travis/mysql.travis.xml b/doctrine/dbal/tests/travis/mysql.travis.xml new file mode 100644 index 00000000..15fe6023 --- /dev/null +++ b/doctrine/dbal/tests/travis/mysql.travis.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/DBAL + + + + + performance + locking_functional + + + + diff --git a/doctrine/dbal/tests/travis/mysqli.travis.xml b/doctrine/dbal/tests/travis/mysqli.travis.xml new file mode 100644 index 00000000..0a72d880 --- /dev/null +++ b/doctrine/dbal/tests/travis/mysqli.travis.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/DBAL + + + + + performance + locking_functional + + + + diff --git a/doctrine/dbal/tests/travis/pgsql.travis.xml b/doctrine/dbal/tests/travis/pgsql.travis.xml new file mode 100644 index 00000000..e3bd7ead --- /dev/null +++ b/doctrine/dbal/tests/travis/pgsql.travis.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + ./../Doctrine/Tests/DBAL + + + + + performance + locking_functional + + + \ No newline at end of file diff --git a/doctrine/dbal/tests/travis/sqlite.travis.xml b/doctrine/dbal/tests/travis/sqlite.travis.xml new file mode 100644 index 00000000..944f137a --- /dev/null +++ b/doctrine/dbal/tests/travis/sqlite.travis.xml @@ -0,0 +1,14 @@ + + + + + ./../Doctrine/Tests/DBAL + + + + + performance + locking_functional + + + \ No newline at end of file From f6016686aced5e3759bce68b446a947b06efcf09 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Tue, 26 Feb 2013 17:55:19 +0100 Subject: [PATCH 02/11] Backport of Doctrine Sqlite ALTER TABLE workaround --- .../DBAL/Platforms/SqlitePlatform.php | 470 +++++++++++++++++- .../lib/Doctrine/DBAL/Schema/ColumnDiff.php | 8 +- .../lib/Doctrine/DBAL/Schema/Comparator.php | 7 + .../lib/Doctrine/DBAL/Schema/SchemaDiff.php | 9 +- .../DBAL/Schema/SqliteSchemaManager.php | 183 ++++++- .../lib/Doctrine/DBAL/Schema/TableDiff.php | 9 +- 6 files changed, 663 insertions(+), 23 deletions(-) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index 7ed52017..8a316470 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -20,6 +20,11 @@ namespace Doctrine\DBAL\Platforms; use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\Constraint; /** * The SqlitePlatform class describes the specifics and dialects of the SQLite @@ -28,6 +33,7 @@ * @since 2.0 * @author Roman Borschel * @author Benjamin Eberlei + * @author Martin Hasoň * @todo Rename: SQLitePlatform */ class SqlitePlatform extends AbstractPlatform @@ -263,16 +269,32 @@ protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) */ protected function _getCreateTableSQL($name, array $columns, array $options = array()) { - $name = str_replace(".", "__", $name); + $name = str_replace('.', '__', $name); $queryFields = $this->getColumnDeclarationListSQL($columns); + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + if (isset($options['primary']) && ! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; } + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $foreignKey) { + $queryFields.= ', '.$this->getForeignKeyDeclarationSQL($foreignKey); + } + } + $query[] = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')'; + if (isset($options['alter']) && true === $options['alter']) { + return $query; + } + if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index => $indexDef) { $query[] = $this->getCreateIndexSQL($indexDef, $name); @@ -305,16 +327,22 @@ public function getClobTypeDeclarationSQL(array $field) return 'CLOB'; } + /** + * {@inheritDoc} + */ public function getListTableConstraintsSQL($table) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = '$table' AND sql NOT NULL ORDER BY name"; } + /** + * {@inheritDoc} + */ public function getListTableColumnsSQL($table, $currentDatabase = null) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "PRAGMA table_info($table)"; } @@ -324,11 +352,14 @@ public function getListTableColumnsSQL($table, $currentDatabase = null) */ public function getListTableIndexesSQL($table, $currentDatabase = null) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "PRAGMA index_list($table)"; } + /** + * {@inheritDoc} + */ public function getListTablesSQL() { return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' " @@ -344,34 +375,33 @@ public function getListViewsSQL($database) return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; } + /** + * {@inheritDoc} + */ public function getCreateViewSQL($name, $sql) { return 'CREATE VIEW ' . $name . ' AS ' . $sql; } - public function getDropViewSQL($name) - { - return 'DROP VIEW '. $name; - } - /** * {@inheritDoc} - * - * SQLite does support foreign key constraints, but only in CREATE TABLE statements... - * This really limits their usefulness and requires SQLite specific handling, so - * we simply say that SQLite does NOT support foreign keys for now... */ - public function supportsForeignKeyConstraints() + public function getDropViewSQL($name) { - return false; + return 'DROP VIEW '. $name; } /** * {@inheritDoc} */ - public function supportsAlterTable() + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) { - return false; + $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + $query .= (($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) ? ' ' : ' NOT ') . 'DEFERRABLE'; + $query .= ' INITIALLY ' . (($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) ? 'DEFERRED' : 'IMMEDIATE'); + + return $query; } /** @@ -395,7 +425,8 @@ public function getName() */ public function getTruncateTableSQL($tableName, $cascade = false) { - $tableName = str_replace(".", "__", $tableName); + $tableName = str_replace('.', '__', $tableName); + return 'DELETE FROM '.$tableName; } @@ -441,6 +472,9 @@ static public function udfLocate($str, $substr, $offset = 0) return 0; } + /** + * {@inheritDoc} + */ public function getForUpdateSql() { return ''; @@ -495,6 +529,47 @@ protected function getReservedKeywordsClass() return 'Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords'; } + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + if ( ! $diff->fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $sql = array(); + foreach ($diff->fromTable->getIndexes() as $index) { + if ( ! $index->isPrimary()) { + $sql[] = $this->getDropIndexSQL($index, $diff->name); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + if ( ! $diff->fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $sql = array(); + $tableName = $diff->newName ?: $diff->name; + foreach ($this->getIndexesInAlteredTable($diff) as $indexName => $index) { + if ($index->isPrimary()) { + continue; + } + + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + + return $sql; + } + /** * {@inheritDoc} */ @@ -508,7 +583,7 @@ public function getBlobTypeDeclarationSQL(array $field) */ public function getTemporaryTableName($tableName) { - $tableName = str_replace(".", "__", $tableName); + $tableName = str_replace('.', '__', $tableName); return $tableName; } @@ -526,4 +601,361 @@ public function canEmulateSchemas() { return true; } + + /** + * {@inheritDoc} + */ + public function supportsForeignKeyConstraints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + throw new DBALException('Sqlite platform does not support alter primary key.'); + } + + /** + * {@inheritdoc} + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + throw new DBALException('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritdoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + throw new DBALException('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritDoc} + */ + public function getCreateConstraintSQL(Constraint $constraint, $table) + { + throw new DBALException('Sqlite platform does not support alter constraint.'); + } + + /** + * {@inheritDoc} + */ + public function getCreateTableSQL(Table $table, $createFlags = null) + { + $createFlags = null === $createFlags ? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS : $createFlags; + + return parent::getCreateTableSQL($table, $createFlags); + } + + /** + * {@inheritDoc} + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + $table = str_replace('.', '__', $table); + + return "PRAGMA foreign_key_list($table)"; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = $this->getSimpleAlterTableSQL($diff); + if (false !== $sql) { + return $sql; + } + + $fromTable = $diff->fromTable; + if ( ! $fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $table = clone $fromTable; + + $columns = array(); + $oldColumnNames = array(); + $newColumnNames = array(); + $columnSql = array(); + + foreach ($table->getColumns() as $columnName => $column) { + $columnName = strtolower($columnName); + $columns[$columnName] = $column; + $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); + } + + foreach ($diff->removedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $columnName = strtolower($columnName); + if (isset($columns[$columnName])) { + unset($columns[$columnName]); + unset($oldColumnNames[$columnName]); + unset($newColumnNames[$columnName]); + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = strtolower($oldColumnName); + if (isset($columns[$oldColumnName])) { + unset($columns[$oldColumnName]); + } + + $columns[strtolower($column->getName())] = $column; + + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $column->getQuotedName($this); + } + } + + foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + if (isset($columns[$oldColumnName])) { + unset($columns[$oldColumnName]); + } + + $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column; + + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this); + } + } + + foreach ($diff->addedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columns[strtolower($columnName)] = $column; + } + + $sql = array(); + $tableSql = array(); + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + $newTableName = $diff->newName ?: $diff->name; + + $dataTable = new Table('__temp__'.$table->getName()); + + $newTable = new Table($table->getName(), $columns, $this->getPrimaryIndexInAlteredTable($diff), $this->getForeignKeysInAlteredTable($diff), 0, $table->getOptions()); + $newTable->addOption('alter', true); + + $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); + //$sql = array_merge($sql, $this->getCreateTableSQL($dataTable, 0)); + $sql[] = sprintf('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this)); + $sql[] = $this->getDropTableSQL($fromTable); + + $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); + $sql[] = sprintf('INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this)); + $sql[] = $this->getDropTableSQL($dataTable); + + if ($diff->newName && $diff->newName != $diff->name) { + $renamedTable = new Table($diff->newName); + $sql[] = 'ALTER TABLE '.$newTable->getQuotedName($this).' RENAME TO '.$renamedTable->getQuotedName($this); + } + + $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + private function getSimpleAlterTableSQL(TableDiff $diff) + { + if ( ! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes) + || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes) + || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes) + ) { + return false; + } + + $table = new Table($diff->name); + + $sql = array(); + $tableSql = array(); + $columnSql = array(); + + foreach ($diff->addedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $field = array_merge(array('unique' => null, 'autoincrement' => null, 'default' => null), $column->toArray()); + $type = (string) $field['type']; + switch (true) { + case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']: + case $type == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL(): + case $type == 'Date' && $field['default'] == $this->getCurrentDateSQL(): + case $type == 'Time' && $field['default'] == $this->getCurrentTimeSQL(): + return false; + } + + $field['name'] = $column->getQuotedName($this); + if (strtolower($field['type']) == 'string' && $field['length'] === null) { + $field['length'] = 255; + } + + $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' ADD COLUMN '.$this->getColumnDeclarationSQL($field['name'], $field); + } + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if ($diff->newName !== false) { + $newTable = new Table($diff->newName); + $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' RENAME TO '.$newTable->getQuotedName($this); + } + } + + return array_merge($sql, $tableSql, $columnSql); + } + + private function getColumnNamesInAlteredTable(TableDiff $diff) + { + $columns = array(); + + foreach ($diff->fromTable->getColumns() as $columnName => $column) { + $columns[strtolower($columnName)] = $column->getName(); + } + + foreach ($diff->removedColumns as $columnName => $column) { + $columnName = strtolower($columnName); + if (isset($columns[$columnName])) { + unset($columns[$columnName]); + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + $columnName = $column->getName(); + $columns[strtolower($oldColumnName)] = $columnName; + $columns[strtolower($columnName)] = $columnName; + } + + foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { + $columnName = $columnDiff->column->getName(); + $columns[strtolower($oldColumnName)] = $columnName; + $columns[strtolower($columnName)] = $columnName; + } + + foreach ($diff->addedColumns as $columnName => $column) { + $columns[strtolower($columnName)] = $columnName; + } + + return $columns; + } + + private function getIndexesInAlteredTable(TableDiff $diff) + { + $indexes = $diff->fromTable->getIndexes(); + $columnNames = $this->getColumnNamesInAlteredTable($diff); + + foreach ($indexes as $key => $index) { + $changed = false; + $indexColumns = array(); + foreach ($index->getColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if ( ! isset($columnNames[$normalizedColumnName])) { + unset($indexes[$key]); + continue 2; + } else { + $indexColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName !== $columnNames[$normalizedColumnName]) { + $changed = true; + } + } + } + + if ($changed) { + $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags()); + } + } + + foreach ($diff->removedIndexes as $index) { + $indexName = strtolower($index->getName()); + if (strlen($indexName) && isset($indexes[$indexName])) { + unset($indexes[$indexName]); + } + } + + foreach (array_merge($diff->changedIndexes, $diff->addedIndexes) as $index) { + $indexName = strtolower($index->getName()); + if (strlen($indexName)) { + $indexes[$indexName] = $index; + } else { + $indexes[] = $index; + } + } + + return $indexes; + } + + private function getForeignKeysInAlteredTable(TableDiff $diff) + { + $foreignKeys = $diff->fromTable->getForeignKeys(); + $columnNames = $this->getColumnNamesInAlteredTable($diff); + + foreach ($foreignKeys as $key => $constraint) { + $changed = false; + $localColumns = array(); + foreach ($constraint->getLocalColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if ( ! isset($columnNames[$normalizedColumnName])) { + unset($foreignKeys[$key]); + continue 2; + } else { + $localColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName !== $columnNames[$normalizedColumnName]) { + $changed = true; + } + } + } + + if ($changed) { + $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions()); + } + } + + foreach ($diff->removedForeignKeys as $constraint) { + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName) && isset($foreignKeys[$constraintName])) { + unset($foreignKeys[$constraintName]); + } + } + + foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) { + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName)) { + $foreignKeys[$constraintName] = $constraint; + } else { + $foreignKeys[] = $constraint; + } + } + + return $foreignKeys; + } + + private function getPrimaryIndexInAlteredTable(TableDiff $diff) + { + $primaryIndex = array(); + + foreach ($this->getIndexesInAlteredTable($diff) as $index) { + if ($index->isPrimary()) { + $primaryIndex = array($index->getName() => $index); + } + } + + return $primaryIndex; + } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php index e8cfa1de..2bde1bd7 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php @@ -44,11 +44,17 @@ class ColumnDiff */ public $changedProperties = array(); - public function __construct($oldColumnName, Column $column, array $changedProperties = array()) + /** + * @var Column + */ + public $fromColumn; + + public function __construct($oldColumnName, Column $column, array $changedProperties = array(), Column $fromColumn = null) { $this->oldColumnName = $oldColumnName; $this->column = $column; $this->changedProperties = $changedProperties; + $this->fromColumn = $fromColumn; } public function hasChanged($propertyName) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php index f73dd51e..0effaa38 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php @@ -58,6 +58,7 @@ static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) public function compare(Schema $fromSchema, Schema $toSchema) { $diff = new SchemaDiff(); + $diff->fromSchema = $fromSchema; $foreignKeysToTable = array(); @@ -179,6 +180,7 @@ public function diffTable(Table $table1, Table $table2) { $changes = 0; $tableDifferences = new TableDiff($table1->getName()); + $tableDifferences->fromTable = $table1; $table1Columns = $table1->getColumns(); $table2Columns = $table2->getColumns(); @@ -203,6 +205,7 @@ public function diffTable(Table $table1, Table $table2) $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); if (count($changedProperties) ) { $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); + $columnDiff->fromColumn = $column; $tableDifferences->changedColumns[$column->getName()] = $columnDiff; $changes++; } @@ -317,6 +320,10 @@ public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint return true; } + if (strtolower($key1->getForeignTableName()) != strtolower($key2->getForeignTableName())) { + return true; + } + if ($key1->onUpdate() != $key2->onUpdate()) { return true; } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php index 5cb5bc6a..0fd0ba0b 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php @@ -34,6 +34,11 @@ */ class SchemaDiff { + /** + * @var Schema + */ + public $fromSchema; + /** * All added tables * @@ -81,12 +86,14 @@ class SchemaDiff * @param array(string=>Table) $newTables * @param array(string=>TableDiff) $changedTables * @param array(string=>bool) $removedTables + * @param Schema $fromSchema */ - public function __construct($newTables = array(), $changedTables = array(), $removedTables = array()) + public function __construct($newTables = array(), $changedTables = array(), $removedTables = array(), Schema $fromSchema = null) { $this->newTables = $newTables; $this->changedTables = $changedTables; $this->removedTables = $removedTables; + $this->fromSchema = $fromSchema; } /** diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php index 41a941d5..a253e60e 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -19,6 +19,8 @@ namespace Doctrine\DBAL\Schema; +use Doctrine\DBAL\DBALException; + /** * SqliteSchemaManager * @@ -26,6 +28,7 @@ * @author Konsta Vesterinen * @author Lukas Smith (PEAR MDB2 library) * @author Jonathan H. Wage + * @author Martin Hasoň * @version $Revision$ * @since 2.0 */ @@ -61,6 +64,93 @@ public function createDatabase($database) $conn->close(); } + /** + * {@inheritdoc} + */ + public function renameTable($name, $newName) + { + $tableDiff = new TableDiff($name); + $tableDiff->fromTable = $this->listTableDetails($name); + $tableDiff->newName = $newName; + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->addedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->changedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->removedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function listTableForeignKeys($table, $database = null) + { + if (null === $database) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); + $tableForeignKeys = $this->_conn->fetchAll($sql); + + if ( ! empty($tableForeignKeys)) { + $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'"); + $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : ''; + if (preg_match_all('# + (?:CONSTRAINT\s+([^\s]+)\s+)? + (?:FOREIGN\s+KEY[^\)]+\)\s*)? + REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $createSql, $match)) { + + $names = array_reverse($match[1]); + $deferrable = array_reverse($match[2]); + $deferred = array_reverse($match[3]); + } else { + $names = $deferrable = $deferred = array(); + } + + foreach ($tableForeignKeys as $key => $value) { + $id = $value['id']; + $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id; + $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false; + $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false; + } + } + + return $this->_getPortableTableForeignKeysList($tableForeignKeys); + } + protected function _getPortableTableDefinition($table) { return $table['name']; @@ -122,6 +212,31 @@ protected function _getPortableTableIndexDefinition($tableIndex) ); } + protected function _getPortableTableColumnList($table, $database, $tableColumns) + { + $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); + $autoincrementColumn = null; + $autoincrementCount = 0; + foreach ($tableColumns as $tableColumn) { + if ('1' == $tableColumn['pk']) { + $autoincrementCount++; + if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) { + $autoincrementColumn = $tableColumn['name']; + } + } + } + + if (1 == $autoincrementCount && null !== $autoincrementColumn) { + foreach ($list as $column) { + if ($autoincrementColumn == $column->getName()) { + $column->setAutoincrement(true); + } + } + } + + return $list; + } + protected function _getPortableTableColumnDefinition($tableColumn) { $e = explode('(', $tableColumn['type']); @@ -163,7 +278,10 @@ protected function _getPortableTableColumnDefinition($tableColumn) case 'decimal': case 'numeric': if (isset($tableColumn['length'])) { - list($precision, $scale) = array_map('trim', explode(', ', $tableColumn['length'])); + if (strpos($tableColumn['length'], ',') === false) { + $tableColumn['length'] .= ",0"; + } + list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length'])); } $length = null; break; @@ -187,4 +305,67 @@ protected function _getPortableViewDefinition($view) { return new View($view['name'], $view['sql']); } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $key => $value) { + $value = array_change_key_case($value, CASE_LOWER); + $name = $value['constraint_name']; + if ( ! isset($list[$name])) { + if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") { + $value['on_delete'] = null; + } + if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") { + $value['on_update'] = null; + } + + $list[$name] = array( + 'name' => $name, + 'local' => array(), + 'foreign' => array(), + 'foreignTable' => $value['table'], + 'onDelete' => $value['on_delete'], + 'onUpdate' => $value['on_update'], + 'deferrable' => $value['deferrable'], + 'deferred'=> $value['deferred'], + ); + } + $list[$name]['local'][] = $value['from']; + $list[$name]['foreign'][] = $value['to']; + } + + $result = array(); + foreach($list as $constraint) { + $result[] = new ForeignKeyConstraint( + array_values($constraint['local']), $constraint['foreignTable'], + array_values($constraint['foreign']), $constraint['name'], + array( + 'onDelete' => $constraint['onDelete'], + 'onUpdate' => $constraint['onUpdate'], + 'deferrable' => $constraint['deferrable'], + 'deferred'=> $constraint['deferred'], + ) + ); + } + + return $result; + } + + private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + if ( ! $table instanceof Table) { + $tableDetails = $this->tryMethod('listTableDetails', $table); + if (false === $table) { + throw new \DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table)); + } + + $table = $tableDetails; + } + + $tableDiff = new TableDiff($table->getName()); + $tableDiff->fromTable = $table; + + return $tableDiff; + } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php index f5604683..1f5d1daf 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php @@ -111,6 +111,11 @@ class TableDiff */ public $removedForeignKeys = array(); + /** + * @var Table + */ + public $fromTable; + /** * Constructs an TableDiff object. * @@ -120,10 +125,11 @@ class TableDiff * @param array(string=>Index) $addedIndexes * @param array(string=>Index) $changedIndexes * @param array(string=>bool) $removedIndexes + * @param Table $fromTable */ public function __construct($tableName, $addedColumns = array(), $changedColumns = array(), $removedColumns = array(), $addedIndexes = array(), - $changedIndexes = array(), $removedIndexes = array()) + $changedIndexes = array(), $removedIndexes = array(), Table $fromTable = null) { $this->name = $tableName; $this->addedColumns = $addedColumns; @@ -132,5 +138,6 @@ public function __construct($tableName, $addedColumns = array(), $this->addedIndexes = $addedIndexes; $this->changedIndexes = $changedIndexes; $this->removedIndexes = $removedIndexes; + $this->fromTable = $fromTable; } } From 12ea21d4514a8622fba8caace9c78cd4d2109a4e Mon Sep 17 00:00:00 2001 From: Michael Gapczynski Date: Fri, 17 May 2013 10:06:27 -0400 Subject: [PATCH 03/11] Remove old Google Drive libs --- Google/LICENSE.txt | 21 -- Google/OAuth.php | 751 ------------------------------------------ Google/common.inc.php | 185 ----------- 3 files changed, 957 deletions(-) delete mode 100644 Google/LICENSE.txt delete mode 100755 Google/OAuth.php delete mode 100755 Google/common.inc.php diff --git a/Google/LICENSE.txt b/Google/LICENSE.txt deleted file mode 100644 index 8891c7dd..00000000 --- a/Google/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2007 Andy Smith - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/Google/OAuth.php b/Google/OAuth.php deleted file mode 100755 index c7e75dd8..00000000 --- a/Google/OAuth.php +++ /dev/null @@ -1,751 +0,0 @@ -key = $key; - $this->secret = $secret; - $this->callback_url = $callback_url; - }/*}}}*/ -}/*}}}*/ - -class OAuthToken {/*{{{*/ - // access tokens and request tokens - public $key; - public $secret; - - /** - * key = the token - * secret = the token secret - */ - function __construct($key, $secret) {/*{{{*/ - $this->key = $key; - $this->secret = $secret; - }/*}}}*/ - - /** - * generates the basic string serialization of a token that a server - * would respond to request_token and access_token calls with - */ - function to_string() {/*{{{*/ - return "oauth_token=" . OAuthUtil::urlencodeRFC3986($this->key) . - "&oauth_token_secret=" . OAuthUtil::urlencodeRFC3986($this->secret); - }/*}}}*/ - - function __toString() {/*{{{*/ - return $this->to_string(); - }/*}}}*/ -}/*}}}*/ - -class OAuthSignatureMethod {/*{{{*/ - public function check_signature(&$request, $consumer, $token, $signature) { - $built = $this->build_signature($request, $consumer, $token); - return $built == $signature; - } -}/*}}}*/ - -class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/ - function get_name() {/*{{{*/ - return "HMAC-SHA1"; - }/*}}}*/ - - public function build_signature($request, $consumer, $token, $privKey=NULL) {/*{{{*/ - $base_string = $request->get_signature_base_string(); - $request->base_string = $base_string; - - $key_parts = array( - $consumer->secret, - ($token) ? $token->secret : "" - ); - - $key_parts = array_map(array('OAuthUtil','urlencodeRFC3986'), $key_parts); - $key = implode('&', $key_parts); - - return base64_encode( hash_hmac('sha1', $base_string, $key, true)); - }/*}}}*/ -}/*}}}*/ - -class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/ - public function get_name() {/*{{{*/ - return "RSA-SHA1"; - }/*}}}*/ - - protected function fetch_public_cert(&$request) {/*{{{*/ - // not implemented yet, ideas are: - // (1) do a lookup in a table of trusted certs keyed off of consumer - // (2) fetch via http using a url provided by the requester - // (3) some sort of specific discovery code based on request - // - // either way should return a string representation of the certificate - throw Exception("fetch_public_cert not implemented"); - }/*}}}*/ - - protected function fetch_private_cert($privKey) {//&$request) {/*{{{*/ - // not implemented yet, ideas are: - // (1) do a lookup in a table of trusted certs keyed off of consumer - // - // either way should return a string representation of the certificate - throw Exception("fetch_private_cert not implemented"); - }/*}}}*/ - - public function build_signature(&$request, $consumer, $token, $privKey) {/*{{{*/ - $base_string = $request->get_signature_base_string(); - - // Fetch the private key cert based on the request - //$cert = $this->fetch_private_cert($consumer->privKey); - - //Pull the private key ID from the certificate - //$privatekeyid = openssl_get_privatekey($cert); - - // hacked in - if ($privKey == '') { - $fp = fopen($GLOBALS['PRIV_KEY_FILE'], "r"); - $privKey = fread($fp, 8192); - fclose($fp); - } - $privatekeyid = openssl_get_privatekey($privKey); - - //Check the computer signature against the one passed in the query - $ok = openssl_sign($base_string, $signature, $privatekeyid); - - //Release the key resource - openssl_free_key($privatekeyid); - - return base64_encode($signature); - } /*}}}*/ - - public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/ - $decoded_sig = base64_decode($signature); - - $base_string = $request->get_signature_base_string(); - - // Fetch the public key cert based on the request - $cert = $this->fetch_public_cert($request); - - //Pull the public key ID from the certificate - $publickeyid = openssl_get_publickey($cert); - - //Check the computer signature against the one passed in the query - $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); - - //Release the key resource - openssl_free_key($publickeyid); - - return $ok == 1; - } /*}}}*/ -}/*}}}*/ - -class OAuthRequest {/*{{{*/ - private $parameters; - private $http_method; - private $http_url; - // for debug purposes - public $base_string; - public static $version = '1.0'; - - function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/ - @$parameters or $parameters = array(); - $this->parameters = $parameters; - $this->http_method = $http_method; - $this->http_url = $http_url; - }/*}}}*/ - - - /** - * attempt to build up a request from what was passed to the server - */ - public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/ - $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https'; - @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; - @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; - - $request_headers = OAuthRequest::get_headers(); - - // let the library user override things however they'd like, if they know - // which parameters to use then go for it, for example XMLRPC might want to - // do this - if ($parameters) { - $req = new OAuthRequest($http_method, $http_url, $parameters); - } - // next check for the auth header, we need to do some extra stuff - // if that is the case, namely suck in the parameters from GET or POST - // so that we can include them in the signature - else if (@substr($request_headers['Authorization'], 0, 5) == "OAuth") { - $header_parameters = OAuthRequest::split_header($request_headers['Authorization']); - if ($http_method == "GET") { - $req_parameters = $_GET; - } - else if ($http_method = "POST") { - $req_parameters = $_POST; - } - $parameters = array_merge($header_parameters, $req_parameters); - $req = new OAuthRequest($http_method, $http_url, $parameters); - } - else if ($http_method == "GET") { - $req = new OAuthRequest($http_method, $http_url, $_GET); - } - else if ($http_method == "POST") { - $req = new OAuthRequest($http_method, $http_url, $_POST); - } - return $req; - }/*}}}*/ - - /** - * pretty much a helper function to set up the request - */ - public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {/*{{{*/ - @$parameters or $parameters = array(); - $defaults = array("oauth_version" => OAuthRequest::$version, - "oauth_nonce" => OAuthRequest::generate_nonce(), - "oauth_timestamp" => OAuthRequest::generate_timestamp(), - "oauth_consumer_key" => $consumer->key); - $parameters = array_merge($defaults, $parameters); - - if ($token) { - $parameters['oauth_token'] = $token->key; - } - - // oauth v1.0a - /*if (isset($_REQUEST['oauth_verifier'])) { - $parameters['oauth_verifier'] = $_REQUEST['oauth_verifier']; - }*/ - - - return new OAuthRequest($http_method, $http_url, $parameters); - }/*}}}*/ - - public function set_parameter($name, $value) {/*{{{*/ - $this->parameters[$name] = $value; - }/*}}}*/ - - public function get_parameter($name) {/*{{{*/ - return $this->parameters[$name]; - }/*}}}*/ - - public function get_parameters() {/*{{{*/ - return $this->parameters; - }/*}}}*/ - - /** - * Returns the normalized parameters of the request - * - * This will be all (except oauth_signature) parameters, - * sorted first by key, and if duplicate keys, then by - * value. - * - * The returned string will be all the key=value pairs - * concated by &. - * - * @return string - */ - public function get_signable_parameters() {/*{{{*/ - // Grab all parameters - $params = $this->parameters; - - // Remove oauth_signature if present - if (isset($params['oauth_signature'])) { - unset($params['oauth_signature']); - } - - // Urlencode both keys and values - $keys = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_keys($params)); - $values = array_map(array('OAuthUtil', 'urlencodeRFC3986'), array_values($params)); - $params = array_combine($keys, $values); - - // Sort by keys (natsort) - uksort($params, 'strnatcmp'); - -if(isset($params['title']) && isset($params['title-exact'])) { - $temp = $params['title-exact']; - $title = $params['title']; - - unset($params['title']); - unset($params['title-exact']); - - $params['title-exact'] = $temp; - $params['title'] = $title; -} - - // Generate key=value pairs - $pairs = array(); - foreach ($params as $key=>$value ) { - if (is_array($value)) { - // If the value is an array, it's because there are multiple - // with the same key, sort them, then add all the pairs - natsort($value); - foreach ($value as $v2) { - $pairs[] = $key . '=' . $v2; - } - } else { - $pairs[] = $key . '=' . $value; - } - } - - // Return the pairs, concated with & - return implode('&', $pairs); - }/*}}}*/ - - /** - * Returns the base string of this request - * - * The base string defined as the method, the url - * and the parameters (normalized), each urlencoded - * and the concated with &. - */ - public function get_signature_base_string() {/*{{{*/ - $parts = array( - $this->get_normalized_http_method(), - $this->get_normalized_http_url(), - $this->get_signable_parameters() - ); - - $parts = array_map(array('OAuthUtil', 'urlencodeRFC3986'), $parts); - - return implode('&', $parts); - }/*}}}*/ - - /** - * just uppercases the http method - */ - public function get_normalized_http_method() {/*{{{*/ - return strtoupper($this->http_method); - }/*}}}*/ - -/** - * parses the url and rebuilds it to be - * scheme://host/path - */ - public function get_normalized_http_url() { - $parts = parse_url($this->http_url); - - $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; - $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); - $host = (isset($parts['host'])) ? strtolower($parts['host']) : ''; - $path = (isset($parts['path'])) ? $parts['path'] : ''; - - if (($scheme == 'https' && $port != '443') - || ($scheme == 'http' && $port != '80')) { - $host = "$host:$port"; - } - return "$scheme://$host$path"; - } - - /** - * builds a url usable for a GET request - */ - public function to_url() {/*{{{*/ - $out = $this->get_normalized_http_url() . "?"; - $out .= $this->to_postdata(); - return $out; - }/*}}}*/ - - /** - * builds the data one would send in a POST request - */ - public function to_postdata() {/*{{{*/ - $total = array(); - foreach ($this->parameters as $k => $v) { - $total[] = OAuthUtil::urlencodeRFC3986($k) . "=" . OAuthUtil::urlencodeRFC3986($v); - } - $out = implode("&", $total); - return $out; - }/*}}}*/ - - /** - * builds the Authorization: header - */ - public function to_header() {/*{{{*/ - $out ='Authorization: OAuth '; - $total = array(); - - /* - $sig = $this->parameters['oauth_signature']; - unset($this->parameters['oauth_signature']); - uksort($this->parameters, 'strnatcmp'); - $this->parameters['oauth_signature'] = $sig; - */ - - foreach ($this->parameters as $k => $v) { - if (substr($k, 0, 5) != "oauth") continue; - $out .= OAuthUtil::urlencodeRFC3986($k) . '="' . OAuthUtil::urlencodeRFC3986($v) . '", '; - } - $out = substr_replace($out, '', strlen($out) - 2); - - return $out; - }/*}}}*/ - - public function __toString() {/*{{{*/ - return $this->to_url(); - }/*}}}*/ - - - public function sign_request($signature_method, $consumer, $token, $privKey=NULL) {/*{{{*/ - $this->set_parameter("oauth_signature_method", $signature_method->get_name()); - $signature = $this->build_signature($signature_method, $consumer, $token, $privKey); - $this->set_parameter("oauth_signature", $signature); - }/*}}}*/ - - public function build_signature($signature_method, $consumer, $token, $privKey=NULL) {/*{{{*/ - $signature = $signature_method->build_signature($this, $consumer, $token, $privKey); - return $signature; - }/*}}}*/ - - /** - * util function: current timestamp - */ - private static function generate_timestamp() {/*{{{*/ - return time(); - }/*}}}*/ - - /** - * util function: current nonce - */ - private static function generate_nonce() {/*{{{*/ - $mt = microtime(); - $rand = mt_rand(); - - return md5($mt . $rand); // md5s look nicer than numbers - }/*}}}*/ - - /** - * util function for turning the Authorization: header into - * parameters, has to do some unescaping - */ - private static function split_header($header) {/*{{{*/ - // this should be a regex - // error cases: commas in parameter values - $parts = explode(",", $header); - $out = array(); - foreach ($parts as $param) { - $param = ltrim($param); - // skip the "realm" param, nobody ever uses it anyway - if (substr($param, 0, 5) != "oauth") continue; - - $param_parts = explode("=", $param); - - // rawurldecode() used because urldecode() will turn a "+" in the - // value into a space - $out[$param_parts[0]] = rawurldecode(substr($param_parts[1], 1, -1)); - } - return $out; - }/*}}}*/ - - /** - * helper to try to sort out headers for people who aren't running apache - */ - private static function get_headers() {/*{{{*/ - if (function_exists('apache_request_headers')) { - // we need this to get the actual Authorization: header - // because apache tends to tell us it doesn't exist - return apache_request_headers(); - } - // otherwise we don't have apache and are just going to have to hope - // that $_SERVER actually contains what we need - $out = array(); - foreach ($_SERVER as $key => $value) { - if (substr($key, 0, 5) == "HTTP_") { - // this is chaos, basically it is just there to capitalize the first - // letter of every word that is not an initial HTTP and strip HTTP - // code from przemek - $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5))))); - $out[$key] = $value; - } - } - return $out; - }/*}}}*/ -}/*}}}*/ - -class OAuthServer {/*{{{*/ - protected $timestamp_threshold = 300; // in seconds, five minutes - protected $version = 1.0; // hi blaine - protected $signature_methods = array(); - - protected $data_store; - - function __construct($data_store) {/*{{{*/ - $this->data_store = $data_store; - }/*}}}*/ - - public function add_signature_method($signature_method) {/*{{{*/ - $this->signature_methods[$signature_method->get_name()] = - $signature_method; - }/*}}}*/ - - // high level functions - - /** - * process a request_token request - * returns the request token on success - */ - public function fetch_request_token(&$request) {/*{{{*/ - $this->get_version($request); - - $consumer = $this->get_consumer($request); - - // no token required for the initial token request - $token = NULL; - - $this->check_signature($request, $consumer, $token); - - $new_token = $this->data_store->new_request_token($consumer); - - return $new_token; - }/*}}}*/ - - /** - * process an access_token request - * returns the access token on success - */ - public function fetch_access_token(&$request) {/*{{{*/ - $this->get_version($request); - - $consumer = $this->get_consumer($request); - - // requires authorized request token - $token = $this->get_token($request, $consumer, "request"); - - $this->check_signature($request, $consumer, $token); - - $new_token = $this->data_store->new_access_token($token, $consumer); - - return $new_token; - }/*}}}*/ - - /** - * verify an api call, checks all the parameters - */ - public function verify_request(&$request) {/*{{{*/ - $this->get_version($request); - $consumer = $this->get_consumer($request); - $token = $this->get_token($request, $consumer, "access"); - $this->check_signature($request, $consumer, $token); - return array($consumer, $token); - }/*}}}*/ - - // Internals from here - /** - * version 1 - */ - private function get_version(&$request) {/*{{{*/ - $version = $request->get_parameter("oauth_version"); - if (!$version) { - $version = 1.0; - } - if ($version && $version != $this->version) { - throw new OAuthException("OAuth version '$version' not supported"); - } - return $version; - }/*}}}*/ - - /** - * figure out the signature with some defaults - */ - private function get_signature_method(&$request) {/*{{{*/ - $signature_method = - @$request->get_parameter("oauth_signature_method"); - if (!$signature_method) { - $signature_method = "PLAINTEXT"; - } - if (!in_array($signature_method, - array_keys($this->signature_methods))) { - throw new OAuthException( - "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods)) - ); - } - return $this->signature_methods[$signature_method]; - }/*}}}*/ - - /** - * try to find the consumer for the provided request's consumer key - */ - private function get_consumer(&$request) {/*{{{*/ - $consumer_key = @$request->get_parameter("oauth_consumer_key"); - if (!$consumer_key) { - throw new OAuthException("Invalid consumer key"); - } - - $consumer = $this->data_store->lookup_consumer($consumer_key); - if (!$consumer) { - throw new OAuthException("Invalid consumer"); - } - - return $consumer; - }/*}}}*/ - - /** - * try to find the token for the provided request's token key - */ - private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/ - $token_field = @$request->get_parameter('oauth_token'); - $token = $this->data_store->lookup_token( - $consumer, $token_type, $token_field - ); - if (!$token) { - throw new OAuthException("Invalid $token_type token: $token_field"); - } - return $token; - }/*}}}*/ - - /** - * all-in-one function to check the signature on a request - * should guess the signature method appropriately - */ - private function check_signature(&$request, $consumer, $token) {/*{{{*/ - // this should probably be in a different method - $timestamp = @$request->get_parameter('oauth_timestamp'); - $nonce = @$request->get_parameter('oauth_nonce'); - - $this->check_timestamp($timestamp); - $this->check_nonce($consumer, $token, $nonce, $timestamp); - - $signature_method = $this->get_signature_method($request); - - $signature = $request->get_parameter('oauth_signature'); - $valid_sig = $signature_method->check_signature( - $request, - $consumer, - $token, - $signature - ); - - if (!$valid_sig) { - throw new OAuthException("Invalid signature"); - } - }/*}}}*/ - - /** - * check that the timestamp is new enough - */ - private function check_timestamp($timestamp) {/*{{{*/ - // verify that timestamp is recentish - $now = time(); - if ($now - $timestamp > $this->timestamp_threshold) { - throw new OAuthException("Expired timestamp, yours $timestamp, ours $now"); - } - }/*}}}*/ - - /** - * check that the nonce is not repeated - */ - private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ - // verify that the nonce is uniqueish - $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp); - if ($found) { - throw new OAuthException("Nonce already used: $nonce"); - } - }/*}}}*/ - - - -}/*}}}*/ - -class OAuthDataStore {/*{{{*/ - function lookup_consumer($consumer_key) {/*{{{*/ - // implement me - }/*}}}*/ - - function lookup_token($consumer, $token_type, $token) {/*{{{*/ - // implement me - }/*}}}*/ - - function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ - // implement me - }/*}}}*/ - - function fetch_request_token($consumer) {/*{{{*/ - // return a new token attached to this consumer - }/*}}}*/ - - function fetch_access_token($token, $consumer) {/*{{{*/ - // return a new access token attached to this consumer - // for the user associated with this token if the request token - // is authorized - // should also invalidate the request token - }/*}}}*/ - -}/*}}}*/ - - -/* A very naive dbm-based oauth storage - */ -class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/ - private $dbh; - - function __construct($path = "oauth.gdbm") {/*{{{*/ - $this->dbh = dba_popen($path, 'c', 'gdbm'); - }/*}}}*/ - - function __destruct() {/*{{{*/ - dba_close($this->dbh); - }/*}}}*/ - - function lookup_consumer($consumer_key) {/*{{{*/ - $rv = dba_fetch("consumer_$consumer_key", $this->dbh); - if ($rv === FALSE) { - return NULL; - } - $obj = unserialize($rv); - if (!($obj instanceof OAuthConsumer)) { - return NULL; - } - return $obj; - }/*}}}*/ - - function lookup_token($consumer, $token_type, $token) {/*{{{*/ - $rv = dba_fetch("${token_type}_${token}", $this->dbh); - if ($rv === FALSE) { - return NULL; - } - $obj = unserialize($rv); - if (!($obj instanceof OAuthToken)) { - return NULL; - } - return $obj; - }/*}}}*/ - - function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ - return dba_exists("nonce_$nonce", $this->dbh); - }/*}}}*/ - - function new_token($consumer, $type="request") {/*{{{*/ - $key = md5(time()); - $secret = time() + time(); - $token = new OAuthToken($key, md5(md5($secret))); - if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) { - throw new OAuthException("doooom!"); - } - return $token; - }/*}}}*/ - - function new_request_token($consumer) {/*{{{*/ - return $this->new_token($consumer, "request"); - }/*}}}*/ - - function new_access_token($token, $consumer) {/*{{{*/ - - $token = $this->new_token($consumer, 'access'); - dba_delete("request_" . $token->key, $this->dbh); - return $token; - }/*}}}*/ -}/*}}}*/ - -class OAuthUtil {/*{{{*/ - public static function urlencodeRFC3986($string) {/*{{{*/ - return str_replace('%7E', '~', rawurlencode($string)); - }/*}}}*/ - - public static function urldecodeRFC3986($string) {/*{{{*/ - return rawurldecode($string); - }/*}}}*/ -}/*}}}*/ - -?> \ No newline at end of file diff --git a/Google/common.inc.php b/Google/common.inc.php deleted file mode 100755 index 57185cdc..00000000 --- a/Google/common.inc.php +++ /dev/null @@ -1,185 +0,0 @@ - - */ - -$PRIV_KEY_FILE = '/path/to/your/rsa_private_key.pem'; - -// OAuth library - http://oauth.googlecode.com/svn/code/php/ -require_once('OAuth.php'); - -// Google's accepted signature methods -$hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); -$rsa_method = new OAuthSignatureMethod_RSA_SHA1(); -$SIG_METHODS = array($rsa_method->get_name() => $rsa_method, - $hmac_method->get_name() => $hmac_method); - -/** - * Makes an HTTP request to the specified URL - * - * @param string $http_method The HTTP method (GET, POST, PUT, DELETE) - * @param string $url Full URL of the resource to access - * @param array $extraHeaders (optional) Additional headers to include in each - * request. Elements are header/value pair strings ('Host: example.com') - * @param string $postData (optional) POST/PUT request body - * @param bool $returnResponseHeaders True if resp. headers should be returned. - * @return string Response body from the server - */ -function send_signed_request($http_method, $url, $extraHeaders=null, - $postData=null, $returnResponseHeaders=true) { - $curl = curl_init($url); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_FAILONERROR, false); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - - // Return request headers in the reponse -// curl_setopt($curl, CURLINFO_HEADER_OUT, true); - - // Return response headers ni the response? - if ($returnResponseHeaders) { - curl_setopt($curl, CURLOPT_HEADER, true); - } - - $headers = array(); - //$headers[] = 'GData-Version: 2.0'; // use GData v2 by default - if (is_array($extraHeaders)) { - $headers = array_merge($headers, $extraHeaders); - } - - // Setup default curl options for each type of HTTP request. - // This is also a great place to add additional headers for each request. - switch($http_method) { - case 'GET': - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - break; - case 'POST': - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_POST, 1); - curl_setopt($curl, CURLOPT_POSTFIELDS, $postData); - break; - case 'PUT': - $headers[] = 'If-Match: *'; - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $http_method); - curl_setopt($curl, CURLOPT_POSTFIELDS, $postData); - break; - case 'DELETE': - $headers[] = 'If-Match: *'; - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $http_method); - break; - default: - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); - } - - // Execute the request. If an error occures, fill the response body with it. - $response = curl_exec($curl); - if (!$response) { - $response = curl_error($curl); - } - - // Add server's response headers to our response body - $response = curl_getinfo($curl, CURLINFO_HEADER_OUT) . $response; - - curl_close($curl); - - return $response; -} - -/** -* Takes XML as a string and returns it nicely indented -* -* @param string $xml The xml to beautify -* @param boolean $html_output True if returned XML should be escaped for HTML. -* @return string The beautified xml -*/ -function xml_pretty_printer($xml, $html_output=false) { - $xml_obj = new SimpleXMLElement($xml); - $level = 2; - - // Get an array containing each XML element - $xml = explode("\n", preg_replace('/>\s*\n<", $xml_obj->asXML())); - - // Hold current indentation level - $indent = 0; - - $pretty = array(); - - // Shift off opening XML tag if present - if (count($xml) && preg_match('/^<\?\s*xml/', $xml[0])) { - $pretty[] = array_shift($xml); - } - - foreach ($xml as $el) { - if (preg_match('/^<([\w])+[^>\/]*>$/U', $el)) { - // opening tag, increase indent - $pretty[] = str_repeat(' ', $indent) . $el; - $indent += $level; - } else { - if (preg_match('/^<\/.+>$/', $el)) { - $indent -= $level; // closing tag, decrease indent - } - if ($indent < 0) { - $indent += $level; - } - $pretty[] = str_repeat(' ', $indent) . $el; - } - } - - $xml = implode("\n", $pretty); - return $html_output ? htmlentities($xml) : $xml; -} - -/** - * Joins key/value pairs by $inner_glue and each pair together by $outer_glue. - * - * Example: implode_assoc('=', '&', array('a' => 1, 'b' => 2)) === 'a=1&b=2' - * - * @param string $inner_glue What to implode each key/value pair with - * @param string $outer_glue What to impode each key/value string subset with - * @param array $array Associative array of query parameters - * @return string Urlencoded string of query parameters - */ -function implode_assoc($inner_glue, $outer_glue, $array) { - $output = array(); - foreach($array as $key => $item) { - $output[] = $key . $inner_glue . urlencode($item); - } - return implode($outer_glue, $output); -} - -/** - * Explodes a string of key/value url parameters into an associative array. - * This method performs the compliment operations of implode_assoc(). - * - * Example: explode_assoc('=', '&', 'a=1&b=2') === array('a' => 1, 'b' => 2) - * - * @param string $inner_glue What each key/value pair is joined with - * @param string $outer_glue What each set of key/value pairs is joined with. - * @param array $array Associative array of query parameters - * @return array Urlencoded string of query parameters - */ -function explode_assoc($inner_glue, $outer_glue, $params) { - $tempArr = explode($outer_glue, $params); - foreach($tempArr as $val) { - $pos = strpos($val, $inner_glue); - $key = substr($val, 0, $pos); - $array2[$key] = substr($val, $pos + 1, strlen($val)); - } - return $array2; -} - -?> \ No newline at end of file From dfc919bd664fb06d1a881ee2eb3895f751a367bc Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Wed, 5 Jun 2013 17:58:37 +0200 Subject: [PATCH 04/11] Update Doctrine DBAL to 2.3.4 --- doctrine/dbal/.travis.yml | 3 +- doctrine/dbal/bin/doctrine.php | 2 +- doctrine/dbal/composer.json | 4 +- .../dbal/docs/design/AZURE_FEDERATIONS.md | 188 +++---- .../dbal/docs/examples/sharding/README.md | 1 + .../dbal/docs/examples/sharding/bootstrap.php | 1 + .../docs/examples/sharding/create_schema.php | 1 + .../dbal/lib/Doctrine/DBAL/Connection.php | 2 +- .../dbal/lib/Doctrine/DBAL/DBALException.php | 24 +- .../Doctrine/DBAL/Driver/ResultStatement.php | 1 + .../Doctrine/DBAL/Driver/SQLSrv/Driver.php | 1 + .../DBAL/Driver/SQLSrv/LastInsertId.php | 1 + .../DBAL/Driver/SQLSrv/SQLSrvConnection.php | 1 + .../DBAL/Driver/SQLSrv/SQLSrvException.php | 1 + .../DBAL/Driver/SQLSrv/SQLSrvStatement.php | 1 + .../lib/Doctrine/DBAL/Id/TableGenerator.php | 1 + .../DBAL/Id/TableGeneratorSchemaVisitor.php | 1 + .../lib/Doctrine/DBAL/Logging/DebugStack.php | 3 +- .../Doctrine/DBAL/Logging/EchoSQLLogger.php | 6 +- .../lib/Doctrine/DBAL/Logging/LoggerChain.php | 3 +- .../lib/Doctrine/DBAL/Logging/SQLLogger.php | 2 +- .../DBAL/Platforms/Keywords/DB2Keywords.php | 1 + .../Platforms/Keywords/DrizzleKeywords.php | 2 +- .../DBAL/Platforms/Keywords/MySQLKeywords.php | 1 + .../Platforms/Keywords/OracleKeywords.php | 1 + .../DBAL/Platforms/PostgreSqlPlatform.php | 8 +- .../DBAL/Platforms/SQLAzurePlatform.php | 1 + .../DBAL/Platforms/SQLServer2005Platform.php | 1 + .../DBAL/Platforms/SQLServerPlatform.php | 4 +- .../DBAL/Platforms/SqlitePlatform.php | 471 +----------------- .../lib/Doctrine/DBAL/Query/QueryBuilder.php | 24 +- .../Doctrine/DBAL/Query/QueryException.php | 8 +- .../dbal/lib/Doctrine/DBAL/SQLParserUtils.php | 101 +++- .../Doctrine/DBAL/SQLParserUtilsException.php | 43 ++ .../DBAL/Schema/AbstractSchemaManager.php | 6 +- .../dbal/lib/Doctrine/DBAL/Schema/Column.php | 2 +- .../lib/Doctrine/DBAL/Schema/ColumnDiff.php | 10 +- .../lib/Doctrine/DBAL/Schema/Comparator.php | 13 +- .../DBAL/Schema/DrizzleSchemaManager.php | 1 + .../DBAL/Schema/ForeignKeyConstraint.php | 11 + .../dbal/lib/Doctrine/DBAL/Schema/Index.php | 1 + .../DBAL/Schema/PostgreSqlSchemaManager.php | 8 +- .../DBAL/Schema/SQLServerSchemaManager.php | 2 + .../lib/Doctrine/DBAL/Schema/SchemaConfig.php | 2 +- .../lib/Doctrine/DBAL/Schema/SchemaDiff.php | 11 +- .../lib/Doctrine/DBAL/Schema/Sequence.php | 2 +- .../DBAL/Schema/SqliteSchemaManager.php | 180 +------ .../AbstractSchemaSynchronizer.php | 3 +- .../Synchronizer/SchemaSynchronizer.php | 1 + .../SingleDatabaseSynchronizer.php | 1 + .../dbal/lib/Doctrine/DBAL/Schema/Table.php | 2 +- .../lib/Doctrine/DBAL/Schema/TableDiff.php | 11 +- .../Schema/Visitor/DropSchemaSqlCollector.php | 2 +- .../Doctrine/DBAL/Schema/Visitor/Visitor.php | 2 +- .../DBAL/Sharding/PoolingShardConnection.php | 3 +- .../DBAL/Sharding/PoolingShardManager.php | 1 + .../SQLAzureFederationsSynchronizer.php | 1 + .../SQLAzure/SQLAzureShardManager.php | 1 + .../SQLAzure/Schema/MultiTenantVisitor.php | 1 + .../ShardChoser/MultiTenantShardChoser.php | 1 + .../DBAL/Sharding/ShardChoser/ShardChoser.php | 1 + .../Doctrine/DBAL/Sharding/ShardManager.php | 1 + .../DBAL/Sharding/ShardingException.php | 1 + .../Tools/Console/Command/ImportCommand.php | 2 +- .../Tools/Console/Command/RunSqlCommand.php | 2 +- .../Tools/Console/Helper/ConnectionHelper.php | 2 +- .../lib/Doctrine/DBAL/Types/BigIntType.php | 6 +- .../dbal/lib/Doctrine/DBAL/Types/BlobType.php | 6 +- .../dbal/lib/Doctrine/DBAL/Types/GuidType.php | 1 + .../dbal/lib/Doctrine/DBAL/Types/Type.php | 1 + doctrine/dbal/lib/Doctrine/DBAL/Version.php | 4 +- doctrine/dbal/phpunit.xml.dist | 2 +- doctrine/dbal/run-all.sh | 6 +- .../Doctrine/Tests/DBAL/DBALExceptionTest.php | 14 + .../Tests/DBAL/Functional/ResultCacheTest.php | 2 +- .../Schema/PostgreSqlSchemaManagerTest.php | 17 + .../DBAL/Functional/TableGeneratorTest.php | 1 + .../DBAL/Functional/Ticket/DBAL510Test.php | 37 ++ .../DBAL/Platforms/PostgreSqlPlatformTest.php | 2 +- .../DBAL/Platforms/SQLAzurePlatformTest.php | 1 + .../DBAL/Platforms/SQLServerPlatformTest.php | 2 +- .../Tests/DBAL/Query/QueryBuilderTest.php | 37 +- .../Tests/DBAL/SQLParserUtilsTest.php | 109 ++++ .../Tests/DBAL/Schema/ComparatorTest.php | 55 ++ .../Tests/DBAL/Schema/SequenceTest.php | 1 + .../SingleDatabaseSynchronizerTest.php | 1 + .../Visitor/RemoveNamespacedAssetsTest.php | 1 + .../Sharding/PoolingShardConnectionTest.php | 1 + .../DBAL/Sharding/PoolingShardManagerTest.php | 1 + .../DBAL/Sharding/SQLAzure/FunctionalTest.php | 1 + .../SQLAzure/MultiTenantVisitorTest.php | 1 + .../SQLAzureFederationsSynchronizerTest.php | 1 + .../SQLAzure/SQLAzureShardManagerTest.php | 1 + .../MultiTenantShardChoserTest.php | 1 + .../Tests/DBAL/Types/GuidTypeTest.php | 1 + .../dbal/tests/Doctrine/Tests/TestInit.php | 20 +- doctrine/dbal/tests/travis/mysql.travis.xml | 1 + doctrine/dbal/tests/travis/mysqli.travis.xml | 1 + 98 files changed, 673 insertions(+), 865 deletions(-) create mode 100644 doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtilsException.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/DBALExceptionTest.php create mode 100644 doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL510Test.php diff --git a/doctrine/dbal/.travis.yml b/doctrine/dbal/.travis.yml index 6cdd9ff6..1a438ca2 100644 --- a/doctrine/dbal/.travis.yml +++ b/doctrine/dbal/.travis.yml @@ -10,12 +10,13 @@ env: - DB=mysqli before_script: + - composer update --dev --prefer-source - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi" - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests_tmp;' -U postgres; fi" - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests;' -U postgres; fi" - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests_tmp;' -U postgres; fi" - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" - sh -c "if [ '$DB' = 'mysqli' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" - - git submodule update --init script: phpunit --configuration tests/travis/$DB.travis.xml + diff --git a/doctrine/dbal/bin/doctrine.php b/doctrine/dbal/bin/doctrine.php index b017703c..cd3184d0 100644 --- a/doctrine/dbal/bin/doctrine.php +++ b/doctrine/dbal/bin/doctrine.php @@ -19,7 +19,7 @@ } require $configFile; - + foreach ($GLOBALS as $helperSetCandidate) { if ($helperSetCandidate instanceof \Symfony\Component\Console\Helper\HelperSet) { $helperSet = $helperSetCandidate; diff --git a/doctrine/dbal/composer.json b/doctrine/dbal/composer.json index ae2409b1..22f9d53f 100644 --- a/doctrine/dbal/composer.json +++ b/doctrine/dbal/composer.json @@ -1,6 +1,6 @@ { "name": "doctrine/dbal", - "type": "library","version":"2.3.1", + "type": "library","version":"2.3.4", "description": "Database Abstraction Layer", "keywords": ["dbal", "database", "persistence", "queryobject"], "homepage": "http://www.doctrine-project.org", @@ -13,7 +13,7 @@ ], "require": { "php": ">=5.3.2", - "doctrine/common": "2.3.*" + "doctrine/common": ">=2.3.0,<2.5-dev" }, "autoload": { "psr-0": { "Doctrine\\DBAL": "lib/" } diff --git a/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md b/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md index 16e65d90..99d7e3cf 100644 --- a/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md +++ b/doctrine/dbal/docs/design/AZURE_FEDERATIONS.md @@ -1,94 +1,94 @@ -# Azure Federations - -Implementing Federations inside a new Doctrine Sharding Extension. Some extensions to the DBAL and ORM core have to be done to get this working. - -1. DBAL (Database Abstraction Layer) - -* Add support for Database Schema Operations - * CREATE FEDERATION - * CREATE TABLE ... FEDERATED ON - * Add support to create a multi-tenent schema from any given schema -* Add API to pick a shard based on distribution key and atomic value -* Add API to ask about federations, federation members and so on. -* Add Sharding Abstraction - * If a shard is picked via distribution key and atomic value fire queries against this only - * Or query the global database. - -2. ORM (Object-Relational Mapper) - -* Federation Key has to be part of the clustered index of the table - * Test with a pure Multi-Tenent App with Filtering = ON (TaskList) - * Test with sharded app (Weather) - -## Implementation Details - -SQL Azure requires one and exactly one clustered index. It makes no difference if the primary key -or any other key is the clustered index. Sharding requires an external ID generation (no auto-increment) -such as GUIDs. GUIDs have negative properties with regard to clustered index performance, so that -typically you would add a "created" timestamp for example that holds the clustered index instead -of making the GUID a clustered index. - -## Example API: - - @@@ php - 'tcp:dbname.database.windows.net', - 'sharding' => array( - 'federationName' => 'Orders_Federation', - 'distributionKey' => 'CustID', - 'distributionType' => 'integer', - 'filteringEnabled' => false, - ), - // ... - ); - - $conn = DriverManager::getConnection($dbParams); - $shardManager = $conn->getShardManager(); - - // Example 1: query against root database - $sql = "SELECT * FROM Products"; - $rows = $conn->executeQuery($sql); - - // Example 2: query against the selected shard with CustomerId = 100 - $aCustomerID = 100; - $shardManager->selectShard($aCustomerID); // Using Default federationName and distributionKey - // Query: "USE FEDERATION Orders_Federation (CustID = $aCustomerID) WITH RESET, FILTERING OFF;" - - $sql = "SELECT * FROM Customers"; - $rows = $conn->executeQuery($sql); - - // Example 3: Reset API to root database again - $shardManager->selectGlobal(); - -## ID Generation - -With sharding all the ids have to be generated for global uniqueness. There are three strategies for this. - -1. Use GUIDs as described here http://blogs.msdn.com/b/cbiyikoglu/archive/2011/06/20/id-generation-in-federations-identity-sequences-and-guids-uniqueidentifier.aspx -2. Having a central table that is accessed with a second connection to generate sequential ids -3. Using natural keys from the domain. - -The second approach has the benefit of having numerical primary keys, however also a central failure location. The third strategy can seldom be used, because the domains dont allow this. Identity columns cannot be used at all. - - @@@ php - 'dbname.database.windows.net', - // ... - ); - $conn = DriverManager::getConnection($dbParams); - - $idGenerator = new TableHiLoIdGenerator($conn, 'id_table_name', $multiplicator = 1); - // only once, create this table - $idGenerator->createTable(); - - $nextId = $idGenerator->generateId('for_table_name'); - $nextOtherId = $idGenerator->generateId('for_other_table'); - -The connection for the table generator has to be a different one than the one used for the main app to avoid transaction clashes. +# Azure Federations + +Implementing Federations inside a new Doctrine Sharding Extension. Some extensions to the DBAL and ORM core have to be done to get this working. + +1. DBAL (Database Abstraction Layer) + +* Add support for Database Schema Operations + * CREATE FEDERATION + * CREATE TABLE ... FEDERATED ON + * Add support to create a multi-tenent schema from any given schema +* Add API to pick a shard based on distribution key and atomic value +* Add API to ask about federations, federation members and so on. +* Add Sharding Abstraction + * If a shard is picked via distribution key and atomic value fire queries against this only + * Or query the global database. + +2. ORM (Object-Relational Mapper) + +* Federation Key has to be part of the clustered index of the table + * Test with a pure Multi-Tenent App with Filtering = ON (TaskList) + * Test with sharded app (Weather) + +## Implementation Details + +SQL Azure requires one and exactly one clustered index. It makes no difference if the primary key +or any other key is the clustered index. Sharding requires an external ID generation (no auto-increment) +such as GUIDs. GUIDs have negative properties with regard to clustered index performance, so that +typically you would add a "created" timestamp for example that holds the clustered index instead +of making the GUID a clustered index. + +## Example API: + + @@@ php + 'tcp:dbname.database.windows.net', + 'sharding' => array( + 'federationName' => 'Orders_Federation', + 'distributionKey' => 'CustID', + 'distributionType' => 'integer', + 'filteringEnabled' => false, + ), + // ... + ); + + $conn = DriverManager::getConnection($dbParams); + $shardManager = $conn->getShardManager(); + + // Example 1: query against root database + $sql = "SELECT * FROM Products"; + $rows = $conn->executeQuery($sql); + + // Example 2: query against the selected shard with CustomerId = 100 + $aCustomerID = 100; + $shardManager->selectShard($aCustomerID); // Using Default federationName and distributionKey + // Query: "USE FEDERATION Orders_Federation (CustID = $aCustomerID) WITH RESET, FILTERING OFF;" + + $sql = "SELECT * FROM Customers"; + $rows = $conn->executeQuery($sql); + + // Example 3: Reset API to root database again + $shardManager->selectGlobal(); + +## ID Generation + +With sharding all the ids have to be generated for global uniqueness. There are three strategies for this. + +1. Use GUIDs as described here http://blogs.msdn.com/b/cbiyikoglu/archive/2011/06/20/id-generation-in-federations-identity-sequences-and-guids-uniqueidentifier.aspx +2. Having a central table that is accessed with a second connection to generate sequential ids +3. Using natural keys from the domain. + +The second approach has the benefit of having numerical primary keys, however also a central failure location. The third strategy can seldom be used, because the domains dont allow this. Identity columns cannot be used at all. + + @@@ php + 'dbname.database.windows.net', + // ... + ); + $conn = DriverManager::getConnection($dbParams); + + $idGenerator = new TableHiLoIdGenerator($conn, 'id_table_name', $multiplicator = 1); + // only once, create this table + $idGenerator->createTable(); + + $nextId = $idGenerator->generateId('for_table_name'); + $nextOtherId = $idGenerator->generateId('for_other_table'); + +The connection for the table generator has to be a different one than the one used for the main app to avoid transaction clashes. diff --git a/doctrine/dbal/docs/examples/sharding/README.md b/doctrine/dbal/docs/examples/sharding/README.md index 73ba195b..3680e544 100644 --- a/doctrine/dbal/docs/examples/sharding/README.md +++ b/doctrine/dbal/docs/examples/sharding/README.md @@ -23,3 +23,4 @@ Change "examples/sharding/bootstrap.php" to contain Database connection. 5. insert_data_after_split.php 6. query_filtering_off.php 7. query_filtering_on.php + diff --git a/doctrine/dbal/docs/examples/sharding/bootstrap.php b/doctrine/dbal/docs/examples/sharding/bootstrap.php index ad62aa79..fe174f1c 100644 --- a/doctrine/dbal/docs/examples/sharding/bootstrap.php +++ b/doctrine/dbal/docs/examples/sharding/bootstrap.php @@ -23,3 +23,4 @@ $conn = DriverManager::getConnection($config); $shardManager = new SQLAzureShardManager($conn); + diff --git a/doctrine/dbal/docs/examples/sharding/create_schema.php b/doctrine/dbal/docs/examples/sharding/create_schema.php index cc4c8a7b..ac6b66cd 100644 --- a/doctrine/dbal/docs/examples/sharding/create_schema.php +++ b/doctrine/dbal/docs/examples/sharding/create_schema.php @@ -48,3 +48,4 @@ echo implode("\n", $synchronizer->getCreateSchema($schema)); $synchronizer->createSchema($schema); + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Connection.php b/doctrine/dbal/lib/Doctrine/DBAL/Connection.php index 4aeb1dc7..80efa94b 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Connection.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Connection.php @@ -34,7 +34,7 @@ * events, transaction isolation levels, configuration, emulated transaction nesting, * lazy connecting and more. * - * + * * @link www.doctrine-project.org * @since 2.0 * @author Guilherme Blanco diff --git a/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php b/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php index c8e430b0..2b4151e0 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php @@ -40,13 +40,35 @@ public static function driverExceptionDuringQuery(\Exception $driverEx, $sql, ar { $msg = "An exception occurred while executing '".$sql."'"; if ($params) { - $msg .= " with params ".json_encode($params); + $msg .= " with params " . self::formatParameters($params); } $msg .= ":\n\n".$driverEx->getMessage(); return new self($msg, 0, $driverEx); } + /** + * Returns a human-readable representation of an array of parameters. + * This properly handles binary data by returning a hex representation. + * + * @param array $params + * + * @return string + */ + private static function formatParameters(array $params) + { + return '[' . implode(', ', array_map(function($param) { + $json = @json_encode($param); + + if (! is_string($json) || $json == 'null' && is_string($param)) { + // JSON encoding failed, this is not a UTF-8 string. + return '"\x' . implode('\x', str_split(bin2hex($param), 2)) . '"'; + } + + return $json; + }, $params)) . ']'; + } + public static function invalidWrapperClass($wrapperClass) { return new self("The given 'wrapperClass' ".$wrapperClass." has to be a ". diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php index d0780482..561dc353 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/ResultStatement.php @@ -90,3 +90,4 @@ function fetchAll($fetchMode = null); */ function fetchColumn($columnIndex = 0); } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php index 364f811e..c7043cc6 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/Driver.php @@ -69,3 +69,4 @@ public function getDatabase(\Doctrine\DBAL\Connection $conn) return $params['dbname']; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php index 3f13c38a..421d0715 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/LastInsertId.php @@ -39,3 +39,4 @@ public function getId() return $this->id; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php index 5faca1fb..82544ca1 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvConnection.php @@ -158,3 +158,4 @@ public function errorInfo() return sqlsrv_errors(SQLSRV_ERR_ERRORS); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php index 6a129444..67778774 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvException.php @@ -40,3 +40,4 @@ static public function fromSqlSrvErrors() return new self(rtrim($message)); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php index 1adf266a..76a156b3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Driver/SQLSrv/SQLSrvStatement.php @@ -248,3 +248,4 @@ public function rowCount() return sqlsrv_rows_affected($this->stmt); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php index 9f254029..c52a40bd 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGenerator.php @@ -157,3 +157,4 @@ public function nextValue($sequenceName) return $value; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php index 119705d3..e3403976 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Id/TableGeneratorSchemaVisitor.php @@ -87,3 +87,4 @@ public function acceptSequence(Sequence $sequence) { } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php index cb4b7c1b..7d70b907 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/DebugStack.php @@ -24,7 +24,7 @@ /** * Includes executed SQLs in a Debug Stack * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ @@ -66,3 +66,4 @@ public function stopQuery() } } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php index e6969aaf..a332258f 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/EchoSQLLogger.php @@ -24,7 +24,7 @@ /** * A SQL logger that logs to the standard output using echo/var_dump. * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ @@ -40,11 +40,11 @@ class EchoSQLLogger implements SQLLogger */ public function startQuery($sql, array $params = null, array $types = null) { - echo $sql . PHP_EOL; + echo $sql . PHP_EOL; if ($params) { var_dump($params); - } + } if ($types) { var_dump($types); diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php index 76f02859..6725cc5e 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/LoggerChain.php @@ -22,7 +22,7 @@ /** * Chains multiple SQLLogger * - * + * * @link www.doctrine-project.org * @since 2.2 * @author Christophe Coevoet @@ -61,3 +61,4 @@ public function stopQuery() } } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php b/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php index e7ab2882..9564f4c9 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Logging/SQLLogger.php @@ -24,7 +24,7 @@ /** * Interface for SQL loggers. * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php index 53ab5e57..77c1c674 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DB2Keywords.php @@ -435,3 +435,4 @@ protected function getKeywords() ); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php index 391ef41e..c6d3187b 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/DrizzleKeywords.php @@ -23,7 +23,7 @@ /** * Drizzle Keywordlist * - * @author Kim Hemsø Rasmussen + * @author Kim Hemsø Rasmussen */ class DrizzleKeywords extends KeywordList { diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php index 71704f6b..c4ad5d68 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/MySQLKeywords.php @@ -190,6 +190,7 @@ protected function getKeywords() 'PROCEDURE', 'PURGE', 'RAID0', + 'RANGE', 'READ', 'READS', 'REAL', diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php index 525286e8..9f34ba64 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/Keywords/OracleKeywords.php @@ -151,6 +151,7 @@ protected function getKeywords() 'MODE', 'ROWS', 'WITH', + 'RANGE', ); } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index 36bbcb72..2f907a2a 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -254,7 +254,7 @@ private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias list($schema, $table) = explode(".", $table); $schema = "'" . $schema . "'"; } else { - $schema = "ANY(string_to_array((select setting from pg_catalog.pg_settings where name = 'search_path'),','))"; + $schema = "ANY(string_to_array((select replace(setting,'\"\$user\"',user) from pg_catalog.pg_settings where name = 'search_path'),','))"; } $whereClause .= "$classAlias.relname = '" . $table . "' AND $namespaceAlias.nspname = $schema"; @@ -269,8 +269,8 @@ public function getListTableColumnsSQL($table, $database = null) t.typname AS type, format_type(a.atttypid, a.atttypmod) AS complete_type, (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, - (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM pg_catalog.pg_type t2 - WHERE t2.typtype = 'd' AND t2.typname = format_type(a.atttypid, a.atttypmod)) AS domain_complete_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, a.attnotnull AS isnotnull, (SELECT 't' FROM pg_index @@ -463,7 +463,7 @@ public function getDropSequenceSQL($sequence) if ($sequence instanceof \Doctrine\DBAL\Schema\Sequence) { $sequence = $sequence->getQuotedName($this); } - return 'DROP SEQUENCE ' . $sequence; + return 'DROP SEQUENCE ' . $sequence . ' CASCADE'; } /** diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php index 71829cd0..238e54f4 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLAzurePlatform.php @@ -48,3 +48,4 @@ public function getCreateTableSQL(Table $table, $createFlags=self::CREATE_INDEXE return $sql; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php index 9923ef9d..be3725b3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServer2005Platform.php @@ -51,3 +51,4 @@ public function getClobTypeDeclarationSQL(array $field) return 'VARCHAR(MAX)'; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php index acac55ba..c0b31c76 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SQLServerPlatform.php @@ -687,12 +687,12 @@ protected function doModifyLimitQuery($query, $limit, $offset = null) // Remove ORDER BY clause from $query $query = preg_replace('/\s+ORDER BY(.*)/', '', $query); - $query = preg_replace('/^SELECT\s/', '', $query); + $query = preg_replace('/\sFROM/i', ", ROW_NUMBER() OVER ($over) AS doctrine_rownum FROM", $query); $start = $offset + 1; $end = $offset + $limit; - $query = "SELECT * FROM (SELECT ROW_NUMBER() OVER ($over) AS doctrine_rownum, $query) AS doctrine_tbl WHERE doctrine_rownum BETWEEN $start AND $end"; + $query = "SELECT * FROM ($query) AS doctrine_tbl WHERE doctrine_rownum BETWEEN $start AND $end"; } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index 8a316470..6fba88d7 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -20,11 +20,6 @@ namespace Doctrine\DBAL\Platforms; use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Schema\TableDiff; -use Doctrine\DBAL\Schema\Table; -use Doctrine\DBAL\Schema\ForeignKeyConstraint; -use Doctrine\DBAL\Schema\Index; -use Doctrine\DBAL\Schema\Constraint; /** * The SqlitePlatform class describes the specifics and dialects of the SQLite @@ -33,7 +28,6 @@ * @since 2.0 * @author Roman Borschel * @author Benjamin Eberlei - * @author Martin Hasoň * @todo Rename: SQLitePlatform */ class SqlitePlatform extends AbstractPlatform @@ -269,32 +263,16 @@ protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) */ protected function _getCreateTableSQL($name, array $columns, array $options = array()) { - $name = str_replace('.', '__', $name); + $name = str_replace(".", "__", $name); $queryFields = $this->getColumnDeclarationListSQL($columns); - if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { - foreach ($options['uniqueConstraints'] as $name => $definition) { - $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); - } - } - if (isset($options['primary']) && ! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; } - if (isset($options['foreignKeys'])) { - foreach ($options['foreignKeys'] as $foreignKey) { - $queryFields.= ', '.$this->getForeignKeyDeclarationSQL($foreignKey); - } - } - $query[] = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')'; - if (isset($options['alter']) && true === $options['alter']) { - return $query; - } - if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index => $indexDef) { $query[] = $this->getCreateIndexSQL($indexDef, $name); @@ -327,22 +305,16 @@ public function getClobTypeDeclarationSQL(array $field) return 'CLOB'; } - /** - * {@inheritDoc} - */ public function getListTableConstraintsSQL($table) { - $table = str_replace('.', '__', $table); + $table = str_replace(".", "__", $table); return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = '$table' AND sql NOT NULL ORDER BY name"; } - /** - * {@inheritDoc} - */ public function getListTableColumnsSQL($table, $currentDatabase = null) { - $table = str_replace('.', '__', $table); + $table = str_replace(".", "__", $table); return "PRAGMA table_info($table)"; } @@ -352,14 +324,11 @@ public function getListTableColumnsSQL($table, $currentDatabase = null) */ public function getListTableIndexesSQL($table, $currentDatabase = null) { - $table = str_replace('.', '__', $table); + $table = str_replace(".", "__", $table); return "PRAGMA index_list($table)"; } - /** - * {@inheritDoc} - */ public function getListTablesSQL() { return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' " @@ -375,17 +344,11 @@ public function getListViewsSQL($database) return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; } - /** - * {@inheritDoc} - */ public function getCreateViewSQL($name, $sql) { return 'CREATE VIEW ' . $name . ' AS ' . $sql; } - /** - * {@inheritDoc} - */ public function getDropViewSQL($name) { return 'DROP VIEW '. $name; @@ -393,15 +356,22 @@ public function getDropViewSQL($name) /** * {@inheritDoc} + * + * SQLite does support foreign key constraints, but only in CREATE TABLE statements... + * This really limits their usefulness and requires SQLite specific handling, so + * we simply say that SQLite does NOT support foreign keys for now... */ - public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + public function supportsForeignKeyConstraints() { - $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); - - $query .= (($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) ? ' ' : ' NOT ') . 'DEFERRABLE'; - $query .= ' INITIALLY ' . (($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) ? 'DEFERRED' : 'IMMEDIATE'); + return false; + } - return $query; + /** + * {@inheritDoc} + */ + public function supportsAlterTable() + { + return false; } /** @@ -425,8 +395,7 @@ public function getName() */ public function getTruncateTableSQL($tableName, $cascade = false) { - $tableName = str_replace('.', '__', $tableName); - + $tableName = str_replace(".", "__", $tableName); return 'DELETE FROM '.$tableName; } @@ -472,9 +441,6 @@ static public function udfLocate($str, $substr, $offset = 0) return 0; } - /** - * {@inheritDoc} - */ public function getForUpdateSql() { return ''; @@ -518,6 +484,7 @@ protected function initializeDoctrineTypeMappings() 'decimal' => 'decimal', 'numeric' => 'decimal', 'blob' => 'blob', + 'integer unsigned' => 'integer', ); } @@ -529,47 +496,6 @@ protected function getReservedKeywordsClass() return 'Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords'; } - /** - * {@inheritDoc} - */ - protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) - { - if ( ! $diff->fromTable instanceof Table) { - throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); - } - - $sql = array(); - foreach ($diff->fromTable->getIndexes() as $index) { - if ( ! $index->isPrimary()) { - $sql[] = $this->getDropIndexSQL($index, $diff->name); - } - } - - return $sql; - } - - /** - * {@inheritDoc} - */ - protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) - { - if ( ! $diff->fromTable instanceof Table) { - throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); - } - - $sql = array(); - $tableName = $diff->newName ?: $diff->name; - foreach ($this->getIndexesInAlteredTable($diff) as $indexName => $index) { - if ($index->isPrimary()) { - continue; - } - - $sql[] = $this->getCreateIndexSQL($index, $tableName); - } - - return $sql; - } - /** * {@inheritDoc} */ @@ -583,7 +509,7 @@ public function getBlobTypeDeclarationSQL(array $field) */ public function getTemporaryTableName($tableName) { - $tableName = str_replace('.', '__', $tableName); + $tableName = str_replace(".", "__", $tableName); return $tableName; } @@ -601,361 +527,4 @@ public function canEmulateSchemas() { return true; } - - /** - * {@inheritDoc} - */ - public function supportsForeignKeyConstraints() - { - return false; - } - - /** - * {@inheritDoc} - */ - public function getCreatePrimaryKeySQL(Index $index, $table) - { - throw new DBALException('Sqlite platform does not support alter primary key.'); - } - - /** - * {@inheritdoc} - */ - public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) - { - throw new DBALException('Sqlite platform does not support alter foreign key.'); - } - - /** - * {@inheritdoc} - */ - public function getDropForeignKeySQL($foreignKey, $table) - { - throw new DBALException('Sqlite platform does not support alter foreign key.'); - } - - /** - * {@inheritDoc} - */ - public function getCreateConstraintSQL(Constraint $constraint, $table) - { - throw new DBALException('Sqlite platform does not support alter constraint.'); - } - - /** - * {@inheritDoc} - */ - public function getCreateTableSQL(Table $table, $createFlags = null) - { - $createFlags = null === $createFlags ? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS : $createFlags; - - return parent::getCreateTableSQL($table, $createFlags); - } - - /** - * {@inheritDoc} - */ - public function getListTableForeignKeysSQL($table, $database = null) - { - $table = str_replace('.', '__', $table); - - return "PRAGMA foreign_key_list($table)"; - } - - /** - * {@inheritDoc} - */ - public function getAlterTableSQL(TableDiff $diff) - { - $sql = $this->getSimpleAlterTableSQL($diff); - if (false !== $sql) { - return $sql; - } - - $fromTable = $diff->fromTable; - if ( ! $fromTable instanceof Table) { - throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); - } - - $table = clone $fromTable; - - $columns = array(); - $oldColumnNames = array(); - $newColumnNames = array(); - $columnSql = array(); - - foreach ($table->getColumns() as $columnName => $column) { - $columnName = strtolower($columnName); - $columns[$columnName] = $column; - $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); - } - - foreach ($diff->removedColumns as $columnName => $column) { - if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { - continue; - } - - $columnName = strtolower($columnName); - if (isset($columns[$columnName])) { - unset($columns[$columnName]); - unset($oldColumnNames[$columnName]); - unset($newColumnNames[$columnName]); - } - } - - foreach ($diff->renamedColumns as $oldColumnName => $column) { - if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { - continue; - } - - $oldColumnName = strtolower($oldColumnName); - if (isset($columns[$oldColumnName])) { - unset($columns[$oldColumnName]); - } - - $columns[strtolower($column->getName())] = $column; - - if (isset($newColumnNames[$oldColumnName])) { - $newColumnNames[$oldColumnName] = $column->getQuotedName($this); - } - } - - foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { - if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { - continue; - } - - if (isset($columns[$oldColumnName])) { - unset($columns[$oldColumnName]); - } - - $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column; - - if (isset($newColumnNames[$oldColumnName])) { - $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this); - } - } - - foreach ($diff->addedColumns as $columnName => $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $columns[strtolower($columnName)] = $column; - } - - $sql = array(); - $tableSql = array(); - if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { - $newTableName = $diff->newName ?: $diff->name; - - $dataTable = new Table('__temp__'.$table->getName()); - - $newTable = new Table($table->getName(), $columns, $this->getPrimaryIndexInAlteredTable($diff), $this->getForeignKeysInAlteredTable($diff), 0, $table->getOptions()); - $newTable->addOption('alter', true); - - $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); - //$sql = array_merge($sql, $this->getCreateTableSQL($dataTable, 0)); - $sql[] = sprintf('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this)); - $sql[] = $this->getDropTableSQL($fromTable); - - $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); - $sql[] = sprintf('INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this)); - $sql[] = $this->getDropTableSQL($dataTable); - - if ($diff->newName && $diff->newName != $diff->name) { - $renamedTable = new Table($diff->newName); - $sql[] = 'ALTER TABLE '.$newTable->getQuotedName($this).' RENAME TO '.$renamedTable->getQuotedName($this); - } - - $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); - } - - return array_merge($sql, $tableSql, $columnSql); - } - - private function getSimpleAlterTableSQL(TableDiff $diff) - { - if ( ! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes) - || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes) - || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes) - ) { - return false; - } - - $table = new Table($diff->name); - - $sql = array(); - $tableSql = array(); - $columnSql = array(); - - foreach ($diff->addedColumns as $columnName => $column) { - if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { - continue; - } - - $field = array_merge(array('unique' => null, 'autoincrement' => null, 'default' => null), $column->toArray()); - $type = (string) $field['type']; - switch (true) { - case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']: - case $type == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL(): - case $type == 'Date' && $field['default'] == $this->getCurrentDateSQL(): - case $type == 'Time' && $field['default'] == $this->getCurrentTimeSQL(): - return false; - } - - $field['name'] = $column->getQuotedName($this); - if (strtolower($field['type']) == 'string' && $field['length'] === null) { - $field['length'] = 255; - } - - $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' ADD COLUMN '.$this->getColumnDeclarationSQL($field['name'], $field); - } - - if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { - if ($diff->newName !== false) { - $newTable = new Table($diff->newName); - $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' RENAME TO '.$newTable->getQuotedName($this); - } - } - - return array_merge($sql, $tableSql, $columnSql); - } - - private function getColumnNamesInAlteredTable(TableDiff $diff) - { - $columns = array(); - - foreach ($diff->fromTable->getColumns() as $columnName => $column) { - $columns[strtolower($columnName)] = $column->getName(); - } - - foreach ($diff->removedColumns as $columnName => $column) { - $columnName = strtolower($columnName); - if (isset($columns[$columnName])) { - unset($columns[$columnName]); - } - } - - foreach ($diff->renamedColumns as $oldColumnName => $column) { - $columnName = $column->getName(); - $columns[strtolower($oldColumnName)] = $columnName; - $columns[strtolower($columnName)] = $columnName; - } - - foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { - $columnName = $columnDiff->column->getName(); - $columns[strtolower($oldColumnName)] = $columnName; - $columns[strtolower($columnName)] = $columnName; - } - - foreach ($diff->addedColumns as $columnName => $column) { - $columns[strtolower($columnName)] = $columnName; - } - - return $columns; - } - - private function getIndexesInAlteredTable(TableDiff $diff) - { - $indexes = $diff->fromTable->getIndexes(); - $columnNames = $this->getColumnNamesInAlteredTable($diff); - - foreach ($indexes as $key => $index) { - $changed = false; - $indexColumns = array(); - foreach ($index->getColumns() as $columnName) { - $normalizedColumnName = strtolower($columnName); - if ( ! isset($columnNames[$normalizedColumnName])) { - unset($indexes[$key]); - continue 2; - } else { - $indexColumns[] = $columnNames[$normalizedColumnName]; - if ($columnName !== $columnNames[$normalizedColumnName]) { - $changed = true; - } - } - } - - if ($changed) { - $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags()); - } - } - - foreach ($diff->removedIndexes as $index) { - $indexName = strtolower($index->getName()); - if (strlen($indexName) && isset($indexes[$indexName])) { - unset($indexes[$indexName]); - } - } - - foreach (array_merge($diff->changedIndexes, $diff->addedIndexes) as $index) { - $indexName = strtolower($index->getName()); - if (strlen($indexName)) { - $indexes[$indexName] = $index; - } else { - $indexes[] = $index; - } - } - - return $indexes; - } - - private function getForeignKeysInAlteredTable(TableDiff $diff) - { - $foreignKeys = $diff->fromTable->getForeignKeys(); - $columnNames = $this->getColumnNamesInAlteredTable($diff); - - foreach ($foreignKeys as $key => $constraint) { - $changed = false; - $localColumns = array(); - foreach ($constraint->getLocalColumns() as $columnName) { - $normalizedColumnName = strtolower($columnName); - if ( ! isset($columnNames[$normalizedColumnName])) { - unset($foreignKeys[$key]); - continue 2; - } else { - $localColumns[] = $columnNames[$normalizedColumnName]; - if ($columnName !== $columnNames[$normalizedColumnName]) { - $changed = true; - } - } - } - - if ($changed) { - $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions()); - } - } - - foreach ($diff->removedForeignKeys as $constraint) { - $constraintName = strtolower($constraint->getName()); - if (strlen($constraintName) && isset($foreignKeys[$constraintName])) { - unset($foreignKeys[$constraintName]); - } - } - - foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) { - $constraintName = strtolower($constraint->getName()); - if (strlen($constraintName)) { - $foreignKeys[$constraintName] = $constraint; - } else { - $foreignKeys[] = $constraint; - } - } - - return $foreignKeys; - } - - private function getPrimaryIndexInAlteredTable(TableDiff $diff) - { - $primaryIndex = array(); - - foreach ($this->getIndexesInAlteredTable($diff) as $index) { - if ($index->isPrimary()) { - $primaryIndex = array($index->getName() => $index); - } - } - - return $primaryIndex; - } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php index e0c5af14..3d803d57 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryBuilder.php @@ -946,29 +946,37 @@ private function getSQLForSelect() $query = 'SELECT ' . implode(', ', $this->sqlParts['select']) . ' FROM '; $fromClauses = array(); + $joinsPending = true; + $joinAliases = array(); // Loop through all FROM clauses foreach ($this->sqlParts['from'] as $from) { $fromClause = $from['table'] . ' ' . $from['alias']; - if (isset($this->sqlParts['join'][$from['alias']])) { - foreach ($this->sqlParts['join'][$from['alias']] as $join) { - $fromClause .= ' ' . strtoupper($join['joinType']) - . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'] - . ' ON ' . ((string) $join['joinCondition']); + if ($joinsPending && isset($this->sqlParts['join'][$from['alias']])) { + foreach ($this->sqlParts['join'] as $joins) { + foreach ($joins as $join) { + $fromClause .= ' ' . strtoupper($join['joinType']) + . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias'] + . ' ON ' . ((string) $join['joinCondition']); + $joinAliases[$join['joinAlias']] = true; + } } + $joinsPending = false; } $fromClauses[$from['alias']] = $fromClause; } - // loop through all JOIN clasues for validation purpose + // loop through all JOIN clauses for validation purpose + $knownAliases = array_merge($fromClauses,$joinAliases); foreach ($this->sqlParts['join'] as $fromAlias => $joins) { - if ( ! isset($fromClauses[$fromAlias]) ) { - throw QueryException::unknownFromAlias($fromAlias, array_keys($fromClauses)); + if ( ! isset($knownAliases[$fromAlias]) ) { + throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases)); } } + $query .= implode(', ', $fromClauses) . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : '') . ($this->sqlParts['groupBy'] ? ' GROUP BY ' . implode(', ', $this->sqlParts['groupBy']) : '') diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php index addc7456..e2c2b328 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Query/QueryException.php @@ -29,12 +29,10 @@ */ class QueryException extends DBALException { - static public function unknownFromAlias($alias, $registeredAliases) + static public function unknownAlias($alias, $registeredAliases) { return new self("The given alias '" . $alias . "' is not part of " . - "any FROM clause table. The currently registered FROM-clause " . - "aliases are: " . implode(", ", $registeredAliases) . ". Join clauses " . - "are bound to from clauses to provide support for mixing of multiple " . - "from and join clauses."); + "any FROM or JOIN clause table. The currently registered " . + "aliases are: " . implode(", ", $registeredAliases) . "."); } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php index a4cd1009..20cafa93 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtils.php @@ -32,6 +32,13 @@ */ class SQLParserUtils { + const POSITIONAL_TOKEN = '\?'; + const NAMED_TOKEN = '(? $paramName) { - $paramLen = strlen($paramName) + 1; - $value = $params[$paramName]; + $paramLen = strlen($paramName) + 1; + $value = static::extractParam($paramName, $params, true); - if ( ! isset($arrayPositions[$paramName])) { + if ( ! isset($arrayPositions[$paramName]) && ! isset($arrayPositions[':' . $paramName])) { $pos += $queryOffset; $queryOffset -= ($paramLen - 1); $paramsOrd[] = $value; - $typesOrd[] = $types[$paramName]; + $typesOrd[] = static::extractParam($paramName, $types, false, \PDO::PARAM_STR); $query = substr($query, 0, $pos) . '?' . substr($query, ($pos + $paramLen)); continue; @@ -170,7 +171,7 @@ static public function expandListParameters($query, $params, $types) foreach ($value as $val) { $paramsOrd[] = $val; - $typesOrd[] = $types[$paramName] - Connection::ARRAY_PARAM_OFFSET; + $typesOrd[] = static::extractParam($paramName, $types, false) - Connection::ARRAY_PARAM_OFFSET; } $pos += $queryOffset; @@ -180,4 +181,54 @@ static public function expandListParameters($query, $params, $types) return array($query, $paramsOrd, $typesOrd); } -} \ No newline at end of file + + /** + * Slice the SQL statement around pairs of quotes and + * return string fragments of SQL outside of quoted literals. + * Each fragment is captured as a 2-element array: + * + * 0 => matched fragment string, + * 1 => offset of fragment in $statement + * + * @param string $statement + * @return array + */ + static private function getUnquotedStatementFragments($statement) + { + $literal = self::ESCAPED_SINGLE_QUOTED_TEXT . '|' . self::ESCAPED_DOUBLE_QUOTED_TEXT; + preg_match_all("/([^'\"]+)(?:$literal)?/s", $statement, $fragments, PREG_OFFSET_CAPTURE); + + return $fragments[1]; + } + + /** + * @param string $paramName The name of the parameter (without a colon in front) + * @param array $paramsOrTypes A hash of parameters or types + * @param bool $isParam + * @param mixed $defaultValue An optional default value. If omitted, an exception is thrown + * + * @throws SQLParserUtilsException + * @return mixed + */ + static private function extractParam($paramName, $paramsOrTypes, $isParam, $defaultValue = null) + { + if (isset($paramsOrTypes[$paramName])) { + return $paramsOrTypes[$paramName]; + } + + // Hash keys can be prefixed with a colon for compatibility + if (isset($paramsOrTypes[':' . $paramName])) { + return $paramsOrTypes[':' . $paramName]; + } + + if (null !== $defaultValue) { + return $defaultValue; + } + + if ($isParam) { + throw SQLParserUtilsException::missingParam($paramName); + } + + throw SQLParserUtilsException::missingType($paramName); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtilsException.php b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtilsException.php new file mode 100644 index 00000000..4a74f6cd --- /dev/null +++ b/doctrine/dbal/lib/Doctrine/DBAL/SQLParserUtilsException.php @@ -0,0 +1,43 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Doctrine\DBAL\ConnectionException + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.4 + * @author Lars Strojny + */ +class SQLParserUtilsException extends DBALException +{ + public static function missingParam($paramName) + { + return new self(sprintf('Value for :%1$s not found in params array. Params array key should be "%1$s"', $paramName)); + } + + public static function missingType($typeName) + { + return new self(sprintf('Value for :%1$s not found in types array. Types array key should be "%1$s"', $typeName)); + } +} diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php index 425426e0..068d6b5d 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/AbstractSchemaManager.php @@ -59,10 +59,10 @@ abstract class AbstractSchemaManager * * @param \Doctrine\DBAL\Connection $conn */ - public function __construct(\Doctrine\DBAL\Connection $conn) + public function __construct(\Doctrine\DBAL\Connection $conn, AbstractPlatform $platform = null) { - $this->_conn = $conn; - $this->_platform = $this->_conn->getDatabasePlatform(); + $this->_conn = $conn; + $this->_platform = $platform ?: $this->_conn->getDatabasePlatform(); } /** diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php index 47ce78f9..fa6f70b3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Column.php @@ -25,7 +25,7 @@ /** * Object representation of a database column * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php index 2bde1bd7..4fc4b9c3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php @@ -24,7 +24,7 @@ /** * Represent the change of a column * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ @@ -44,17 +44,11 @@ class ColumnDiff */ public $changedProperties = array(); - /** - * @var Column - */ - public $fromColumn; - - public function __construct($oldColumnName, Column $column, array $changedProperties = array(), Column $fromColumn = null) + public function __construct($oldColumnName, Column $column, array $changedProperties = array()) { $this->oldColumnName = $oldColumnName; $this->column = $column; $this->changedProperties = $changedProperties; - $this->fromColumn = $fromColumn; } public function hasChanged($propertyName) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php index 0effaa38..b61a69a3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php @@ -58,7 +58,6 @@ static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) public function compare(Schema $fromSchema, Schema $toSchema) { $diff = new SchemaDiff(); - $diff->fromSchema = $fromSchema; $foreignKeysToTable = array(); @@ -180,7 +179,6 @@ public function diffTable(Table $table1, Table $table2) { $changes = 0; $tableDifferences = new TableDiff($table1->getName()); - $tableDifferences->fromTable = $table1; $table1Columns = $table1->getColumns(); $table2Columns = $table2->getColumns(); @@ -205,7 +203,6 @@ public function diffTable(Table $table1, Table $table2) $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); if (count($changedProperties) ) { $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); - $columnDiff->fromColumn = $column; $tableDifferences->changedColumns[$column->getName()] = $columnDiff; $changes++; } @@ -298,9 +295,11 @@ private function detectColumnRenamings(TableDiff $tableDifferences) $removedColumnName = strtolower($removedColumn->getName()); $addedColumnName = strtolower($addedColumn->getName()); - $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; - unset($tableDifferences->addedColumns[$addedColumnName]); - unset($tableDifferences->removedColumns[$removedColumnName]); + if ( ! isset($tableDifferences->renamedColumns[$removedColumnName])) { + $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; + unset($tableDifferences->addedColumns[$addedColumnName]); + unset($tableDifferences->removedColumns[$removedColumnName]); + } } } } @@ -320,7 +319,7 @@ public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint return true; } - if (strtolower($key1->getForeignTableName()) != strtolower($key2->getForeignTableName())) { + if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { return true; } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php index 6278a736..f73b223c 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/DrizzleSchemaManager.php @@ -93,3 +93,4 @@ protected function _getPortableTableIndexesList($tableIndexes, $tableName = null return parent::_getPortableTableIndexesList($indexes, $tableName); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php index 481d6930..0375582b 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ForeignKeyConstraint.php @@ -110,6 +110,17 @@ public function getForeignTableName() return $this->_foreignTableName; } + /** + * Return the non-schema qualified foreign table name. + * + * @return string + */ + public function getUnqualifiedForeignTableName() + { + $parts = explode(".", $this->_foreignTableName); + return strtolower(end($parts)); + } + /** * Get the quoted representation of this asset but only if it was defined with one. Otherwise * return the plain unquoted value as inserted. diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php index 625e4039..56b9ea86 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Index.php @@ -239,3 +239,4 @@ public function removeFlag($flag) unset($this->flags[strtolower($flag)]); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php index 800fee27..c67e8cd9 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php @@ -58,10 +58,12 @@ public function getSchemaSearchPaths() { $params = $this->_conn->getParams(); $schema = explode(",", $this->_conn->fetchColumn('SHOW search_path')); + if (isset($params['user'])) { $schema = str_replace('"$user"', $params['user'], $schema); } - return $schema; + + return array_map('trim', $schema); } /** @@ -256,6 +258,10 @@ protected function _getPortableTableColumnDefinition($tableColumn) $autoincrement = true; } + if (preg_match("/^'(.*)'::.*$/", $tableColumn['default'], $matches)) { + $tableColumn['default'] = $matches[1]; + } + if (stripos($tableColumn['default'], 'NULL') === 0) { $tableColumn['default'] = null; } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php index 174dc11f..3075fc9a 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SQLServerSchemaManager.php @@ -47,6 +47,8 @@ protected function _getPortableTableColumnDefinition($tableColumn) $autoincrement = true; } + $dbType = strtok($dbType, '(), '); + $type = array(); $unsigned = $fixed = null; diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php index 4b054d45..d90da206 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaConfig.php @@ -22,7 +22,7 @@ /** * Configuration for a Schema * - * + * * @link www.doctrine-project.org * @since 2.0 * @author Benjamin Eberlei diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php index 0fd0ba0b..ba0d9e6f 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php @@ -24,7 +24,7 @@ /** * Schema Diff * - * + * * @link www.doctrine-project.org * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. * @license http://ez.no/licenses/new_bsd New BSD License @@ -34,11 +34,6 @@ */ class SchemaDiff { - /** - * @var Schema - */ - public $fromSchema; - /** * All added tables * @@ -86,14 +81,12 @@ class SchemaDiff * @param array(string=>Table) $newTables * @param array(string=>TableDiff) $changedTables * @param array(string=>bool) $removedTables - * @param Schema $fromSchema */ - public function __construct($newTables = array(), $changedTables = array(), $removedTables = array(), Schema $fromSchema = null) + public function __construct($newTables = array(), $changedTables = array(), $removedTables = array()) { $this->newTables = $newTables; $this->changedTables = $changedTables; $this->removedTables = $removedTables; - $this->fromSchema = $fromSchema; } /** diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php index b4fb39c0..87ff0fa7 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Sequence.php @@ -24,7 +24,7 @@ /** * Sequence Structure * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php index a253e60e..f980f957 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -19,8 +19,6 @@ namespace Doctrine\DBAL\Schema; -use Doctrine\DBAL\DBALException; - /** * SqliteSchemaManager * @@ -28,7 +26,6 @@ * @author Konsta Vesterinen * @author Lukas Smith (PEAR MDB2 library) * @author Jonathan H. Wage - * @author Martin Hasoň * @version $Revision$ * @since 2.0 */ @@ -64,93 +61,6 @@ public function createDatabase($database) $conn->close(); } - /** - * {@inheritdoc} - */ - public function renameTable($name, $newName) - { - $tableDiff = new TableDiff($name); - $tableDiff->fromTable = $this->listTableDetails($name); - $tableDiff->newName = $newName; - $this->alterTable($tableDiff); - } - - /** - * {@inheritdoc} - */ - public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) - { - $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); - $tableDiff->addedForeignKeys[] = $foreignKey; - - $this->alterTable($tableDiff); - } - - /** - * {@inheritdoc} - */ - public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) - { - $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); - $tableDiff->changedForeignKeys[] = $foreignKey; - - $this->alterTable($tableDiff); - } - - /** - * {@inheritdoc} - */ - public function dropForeignKey($foreignKey, $table) - { - $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); - $tableDiff->removedForeignKeys[] = $foreignKey; - - $this->alterTable($tableDiff); - } - - /** - * {@inheritdoc} - */ - public function listTableForeignKeys($table, $database = null) - { - if (null === $database) { - $database = $this->_conn->getDatabase(); - } - $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); - $tableForeignKeys = $this->_conn->fetchAll($sql); - - if ( ! empty($tableForeignKeys)) { - $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'"); - $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : ''; - if (preg_match_all('# - (?:CONSTRAINT\s+([^\s]+)\s+)? - (?:FOREIGN\s+KEY[^\)]+\)\s*)? - REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))? - (?: - [^,]*? - (NOT\s+DEFERRABLE|DEFERRABLE) - (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? - )?#isx', - $createSql, $match)) { - - $names = array_reverse($match[1]); - $deferrable = array_reverse($match[2]); - $deferred = array_reverse($match[3]); - } else { - $names = $deferrable = $deferred = array(); - } - - foreach ($tableForeignKeys as $key => $value) { - $id = $value['id']; - $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id; - $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false; - $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false; - } - } - - return $this->_getPortableTableForeignKeysList($tableForeignKeys); - } - protected function _getPortableTableDefinition($table) { return $table['name']; @@ -212,31 +122,6 @@ protected function _getPortableTableIndexDefinition($tableIndex) ); } - protected function _getPortableTableColumnList($table, $database, $tableColumns) - { - $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); - $autoincrementColumn = null; - $autoincrementCount = 0; - foreach ($tableColumns as $tableColumn) { - if ('1' == $tableColumn['pk']) { - $autoincrementCount++; - if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) { - $autoincrementColumn = $tableColumn['name']; - } - } - } - - if (1 == $autoincrementCount && null !== $autoincrementColumn) { - foreach ($list as $column) { - if ($autoincrementColumn == $column->getName()) { - $column->setAutoincrement(true); - } - } - } - - return $list; - } - protected function _getPortableTableColumnDefinition($tableColumn) { $e = explode('(', $tableColumn['type']); @@ -280,7 +165,7 @@ protected function _getPortableTableColumnDefinition($tableColumn) if (isset($tableColumn['length'])) { if (strpos($tableColumn['length'], ',') === false) { $tableColumn['length'] .= ",0"; - } + } list($precision, $scale) = array_map('trim', explode(',', $tableColumn['length'])); } $length = null; @@ -305,67 +190,4 @@ protected function _getPortableViewDefinition($view) { return new View($view['name'], $view['sql']); } - - protected function _getPortableTableForeignKeysList($tableForeignKeys) - { - $list = array(); - foreach ($tableForeignKeys as $key => $value) { - $value = array_change_key_case($value, CASE_LOWER); - $name = $value['constraint_name']; - if ( ! isset($list[$name])) { - if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") { - $value['on_delete'] = null; - } - if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") { - $value['on_update'] = null; - } - - $list[$name] = array( - 'name' => $name, - 'local' => array(), - 'foreign' => array(), - 'foreignTable' => $value['table'], - 'onDelete' => $value['on_delete'], - 'onUpdate' => $value['on_update'], - 'deferrable' => $value['deferrable'], - 'deferred'=> $value['deferred'], - ); - } - $list[$name]['local'][] = $value['from']; - $list[$name]['foreign'][] = $value['to']; - } - - $result = array(); - foreach($list as $constraint) { - $result[] = new ForeignKeyConstraint( - array_values($constraint['local']), $constraint['foreignTable'], - array_values($constraint['foreign']), $constraint['name'], - array( - 'onDelete' => $constraint['onDelete'], - 'onUpdate' => $constraint['onUpdate'], - 'deferrable' => $constraint['deferrable'], - 'deferred'=> $constraint['deferred'], - ) - ); - } - - return $result; - } - - private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table) - { - if ( ! $table instanceof Table) { - $tableDetails = $this->tryMethod('listTableDetails', $table); - if (false === $table) { - throw new \DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table)); - } - - $table = $tableDetails; - } - - $tableDiff = new TableDiff($table->getName()); - $tableDiff->fromTable = $table; - - return $tableDiff; - } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php index 2ee743a9..16fb0339 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/AbstractSchemaSynchronizer.php @@ -53,5 +53,6 @@ protected function processSql(array $sql) $this->conn->exec($s); } } - + } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php index f932f20d..c2498152 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SchemaSynchronizer.php @@ -93,3 +93,4 @@ function dropSchema(Schema $dropSchema); */ function dropAllSchema(); } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php index a090ecd8..38ea53ac 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizer.php @@ -194,3 +194,4 @@ public function dropAllSchema() $this->processSql($this->getDropAllSchema()); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php index a7047b78..d0763c5c 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Table.php @@ -28,7 +28,7 @@ /** * Object Representation of a table * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php index 1f5d1daf..257a3bd0 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php @@ -22,7 +22,7 @@ /** * Table Diff * - * + * * @link www.doctrine-project.org * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. * @license http://ez.no/licenses/new_bsd New BSD License @@ -111,11 +111,6 @@ class TableDiff */ public $removedForeignKeys = array(); - /** - * @var Table - */ - public $fromTable; - /** * Constructs an TableDiff object. * @@ -125,11 +120,10 @@ class TableDiff * @param array(string=>Index) $addedIndexes * @param array(string=>Index) $changedIndexes * @param array(string=>bool) $removedIndexes - * @param Table $fromTable */ public function __construct($tableName, $addedColumns = array(), $changedColumns = array(), $removedColumns = array(), $addedIndexes = array(), - $changedIndexes = array(), $removedIndexes = array(), Table $fromTable = null) + $changedIndexes = array(), $removedIndexes = array()) { $this->name = $tableName; $this->addedColumns = $addedColumns; @@ -138,6 +132,5 @@ public function __construct($tableName, $addedColumns = array(), $this->addedIndexes = $addedIndexes; $this->changedIndexes = $changedIndexes; $this->removedIndexes = $removedIndexes; - $this->fromTable = $fromTable; } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php index 6edb7989..3d74cb90 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php @@ -34,7 +34,7 @@ /** * Gather SQL statements that allow to completly drop the current schema. * - * + * * @link www.doctrine-project.org * @since 2.0 * @author Benjamin Eberlei diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php index 7840fd74..45eff819 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Visitor/Visitor.php @@ -33,7 +33,7 @@ /** * Schema Visitor used for Validation or Generation purposes. * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php index 8ef2e100..6453e638 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardConnection.php @@ -101,7 +101,7 @@ public function __construct(array $params, Driver $driver, Configuration $config } if ( ! ($params['shardChoser'] instanceof ShardChoser)) { - throw new \InvalidArgumentException("The 'shardChoser' configuration is not a valid instance of Doctrine\DBAL\Sharding\ShardChoser\ShardChoser"); + throw new \InvalidArgumentException("The 'shardChoser' configuration is not a valid instance of Doctrine\DBAL\Sharding\ShardChoser\ShardChoser"); } $this->connections[0] = array_merge($params, $params['global']); @@ -198,3 +198,4 @@ public function close() $this->activeConnections = null; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php index 4b514b96..6f6c5d2c 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/PoolingShardManager.php @@ -95,3 +95,4 @@ public function queryAll($sql, array $params, array $types) return $result; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php index cbd6c71a..4001379f 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizer.php @@ -293,3 +293,4 @@ private function getCreateFederationStatement() "CREATE FEDERATION " . $this->shardManager->getFederationName() . " (" . $this->shardManager->getDistributionKey() . " " . $federationTypeSql ." RANGE)"; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php index 3d76ef9d..80ca3d92 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/SQLAzureShardManager.php @@ -235,3 +235,4 @@ public function splitFederation($splitDistributionValue) $this->conn->exec($sql); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php index 5770be28..2b2b4578 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/SQLAzure/Schema/MultiTenantVisitor.php @@ -158,3 +158,4 @@ public function acceptSequence(Sequence $sequence) { } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php index 9714ee54..c6cdabfb 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/MultiTenantShardChoser.php @@ -34,3 +34,4 @@ public function pickShard($distributionValue, PoolingShardConnection $conn) return $distributionValue; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php index 924daa5d..2aa9f74e 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardChoser/ShardChoser.php @@ -38,3 +38,4 @@ interface ShardChoser */ function pickShard($distributionValue, PoolingShardConnection $conn); } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php index 1d10e0e1..aa67992a 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardManager.php @@ -92,3 +92,4 @@ function getShards(); */ function queryAll($sql, array $params, array $types); } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php index 2dc1d21a..06dd1695 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Sharding/ShardingException.php @@ -58,3 +58,4 @@ static public function missingDistributionType() return new self("You have to specify a sharding distribution type such as 'integer', 'string', 'guid'."); } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php index c2795f2e..6b2b8c27 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php @@ -26,7 +26,7 @@ * Task for executing arbitrary SQL that can come from a file or directly from * the command line. * - * + * * @link www.doctrine-project.org * @since 2.0 * @author Benjamin Eberlei diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php index ca84b275..b1af34bd 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php @@ -27,7 +27,7 @@ * Task for executing arbitrary SQL that can come from a file or directly from * the command line. * - * + * * @link www.doctrine-project.org * @since 2.0 * @author Benjamin Eberlei diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php index d3fa431d..877cb64f 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php @@ -27,7 +27,7 @@ /** * Doctrine CLI Connection Helper. * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php index f4603812..7648bef6 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/BigIntType.php @@ -46,11 +46,11 @@ public function getBindingType() return \PDO::PARAM_STR; } - /** + /** * {@inheritdoc} - */ + */ public function convertToPHPValue($value, AbstractPlatform $platform) - { + { return (null === $value) ? null : (string) $value; } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php index 32c93a1d..ff046556 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/BlobType.php @@ -47,11 +47,15 @@ public function convertToPHPValue($value, AbstractPlatform $platform) if (null === $value) { return null; } + if (is_string($value)) { $value = fopen('data://text/plain;base64,' . base64_encode($value), 'r'); - } else if ( ! is_resource($value)) { + } + + if ( ! is_resource($value)) { throw ConversionException::conversionFailed($value, self::BLOB); } + return $value; } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php index af340a86..a8251dd3 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/GuidType.php @@ -39,3 +39,4 @@ public function getName() return Type::GUID; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php b/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php index d137c605..29947bef 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Types/Type.php @@ -303,3 +303,4 @@ public function requiresSQLCommentHint(AbstractPlatform $platform) return false; } } + diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Version.php b/doctrine/dbal/lib/Doctrine/DBAL/Version.php index 343c7616..36376316 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Version.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Version.php @@ -22,7 +22,7 @@ /** * Class to store and retrieve the version of Doctrine * - * + * * @link www.doctrine-project.org * @since 2.0 * @version $Revision$ @@ -36,7 +36,7 @@ class Version /** * Current Doctrine Version */ - const VERSION = '2.3.1'; + const VERSION = '2.3.4'; /** * Compares a Doctrine version with the current one. diff --git a/doctrine/dbal/phpunit.xml.dist b/doctrine/dbal/phpunit.xml.dist index 2290d75a..fe1d515e 100644 --- a/doctrine/dbal/phpunit.xml.dist +++ b/doctrine/dbal/phpunit.xml.dist @@ -33,7 +33,7 @@ --> - + diff --git a/doctrine/dbal/run-all.sh b/doctrine/dbal/run-all.sh index d0140c84..80712eeb 100755 --- a/doctrine/dbal/run-all.sh +++ b/doctrine/dbal/run-all.sh @@ -4,8 +4,8 @@ # Just create the phpunit.xmls as described in the array below and configure the specific files section # to connect to that database. Just omit a file if you dont have that database and the tests will be skipped. -configs[1]="mysql.phpunit.xml" -configs[2]='postgres.phpunit.xml' +configs[1]="mysql.phpunit.xml" +configs[2]='postgres.phpunit.xml' configs[3]='sqlite.phpunit.xml' configs[4]='oracle.phpunit.xml' configs[5]='db2.phpunit.xml' @@ -16,6 +16,6 @@ for i in "${configs[@]}"; do if [ -f "$i" ]; then echo "RUNNING TESTS WITH CONFIG $i" - phpunit -c "$i" "$@" + phpunit -c "$i" "$@" fi; done diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/DBALExceptionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/DBALExceptionTest.php new file mode 100644 index 00000000..19796c7c --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/DBALExceptionTest.php @@ -0,0 +1,14 @@ +assertContains('with params ["ABC", "\x80"]', $e->getMessage()); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php index 86abe915..dc829115 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php @@ -65,7 +65,7 @@ public function testFetchBoth() } $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_BOTH); } - + public function testFetchColumn() { $expectedResult = array(); diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php index 2cb9bd7a..0b6c2606 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Schema/PostgreSqlSchemaManagerTest.php @@ -244,6 +244,23 @@ public function testListForeignKeys() } } } + + /** + * @group DBAL-511 + */ + public function testDefaultValueCharacterVarying() + { + $testTable = new \Doctrine\DBAL\Schema\Table('dbal511_default'); + $testTable->addColumn('id', 'integer'); + $testTable->addColumn('def', 'string', array('default' => 'foo')); + $testTable->setPrimaryKey(array('id')); + + $this->_sm->createTable($testTable); + + $databaseTable = $this->_sm->listTableDetails($testTable->getName()); + + $this->assertEquals('foo', $databaseTable->getColumn('def')->getDefault()); + } } class MoneyType extends Type diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php index 83efe320..ea9ac533 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/TableGeneratorTest.php @@ -56,3 +56,4 @@ public function testNextValNotAffectedByOuterTransactions() $this->assertEquals($id1 + 1, $id2, "Second id is one larger than first one."); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL510Test.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL510Test.php new file mode 100644 index 00000000..3eb10ff4 --- /dev/null +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Functional/Ticket/DBAL510Test.php @@ -0,0 +1,37 @@ +_conn->getDatabasePlatform()->getName() !== "postgresql") { + $this->markTestSkipped('PostgreSQL Only test'); + } + } + + public function testSearchPathSchemaChanges() + { + $table = new Table("dbal510tbl"); + $table->addColumn('id', 'integer'); + $table->setPrimaryKey(array('id')); + + $this->_conn->getSchemaManager()->createTable($table); + + $onlineTable = $this->_conn->getSchemaManager()->listTableDetails('dbal510tbl'); + + $comparator = new Comparator(); + $diff = $comparator->diffTable($onlineTable, $table); + + $this->assertFalse($diff); + } +} diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php index 433caf63..8f967f60 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/PostgreSqlPlatformTest.php @@ -155,7 +155,7 @@ public function testGeneratesSequenceSqlCommands() $this->_platform->getCreateSequenceSQL($sequence) ); $this->assertEquals( - 'DROP SEQUENCE myseq', + 'DROP SEQUENCE myseq CASCADE', $this->_platform->getDropSequenceSQL('myseq') ); $this->assertEquals( diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php index f81938da..f66a0f84 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLAzurePlatformTest.php @@ -26,3 +26,4 @@ public function testCreateFederatedOnTable() $this->assertEquals(array('CREATE TABLE tbl (id INT NOT NULL) FEDERATED ON (TblId = id)'), $this->platform->getCreateTableSQL($table)); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php index 397e4cfc..21c42ad5 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Platforms/SQLServerPlatformTest.php @@ -154,7 +154,7 @@ public function testModifyLimitQueryWithEmptyOffset() public function testModifyLimitQueryWithOffset() { $sql = $this->_platform->modifyLimitQuery('SELECT * FROM user ORDER BY username DESC', 10, 5); - $this->assertEquals('SELECT * FROM (SELECT ROW_NUMBER() OVER (ORDER BY username DESC) AS doctrine_rownum, * FROM user) AS doctrine_tbl WHERE doctrine_rownum BETWEEN 6 AND 15', $sql); + $this->assertEquals('SELECT * FROM (SELECT *, ROW_NUMBER() OVER (ORDER BY username DESC) AS doctrine_rownum FROM user) AS doctrine_tbl WHERE doctrine_rownum BETWEEN 6 AND 15', $sql); } public function testModifyLimitQueryWithAscOrderBy() diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php index 98f16167..74f208a8 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Query/QueryBuilderTest.php @@ -556,17 +556,32 @@ public function testReferenceJoinFromJoin() { $qb = new QueryBuilder($this->conn); - $qb->select("l.id", "mdsh.xcode", "mdso.xcode") - ->from("location_tree", "l") - ->join("l", "location_tree_pos", "p", "l.id = p.tree_id") - ->rightJoin("l", "hotel", "h", "h.location_id = l.id") - ->leftJoin("l", "offer_location", "ol", "l.id=ol.location_id") - ->leftJoin("ol", "mds_offer", "mdso", "ol.offer_id = mdso.offer_id") - ->leftJoin("h", "mds_hotel", "mdsh", "h.id = mdsh.hotel_id") - ->where("p.parent_id IN (:ids)") - ->andWhere("(mdso.xcode IS NOT NULL OR mdsh.xcode IS NOT NULL)"); - - $this->setExpectedException('Doctrine\DBAL\Query\QueryException', "The given alias 'ol' is not part of any FROM clause table. The currently registered FROM-clause aliases are: l"); + $qb->select('COUNT(DISTINCT news.id)') + ->from('cb_newspages', 'news') + ->innerJoin('news', 'nodeversion', 'nv', 'nv.refId = news.id AND nv.refEntityname=\'News\'') + ->innerJoin('invalid', 'nodetranslation', 'nt', 'nv.nodetranslation = nt.id') + ->innerJoin('nt', 'node', 'n', 'nt.node = n.id') + ->where('nt.lang = :lang AND n.deleted != 1'); + + $this->setExpectedException('Doctrine\DBAL\Query\QueryException', "The given alias 'invalid' is not part of any FROM or JOIN clause table. The currently registered aliases are: news, nv, nt, n."); $this->assertEquals('', $qb->getSQL()); } + + /** + * @group DBAL-172 + */ + public function testSelectFromMasterWithWhereOnJoinedTables() + { + $qb = new QueryBuilder($this->conn); + + $qb->select('COUNT(DISTINCT news.id)') + ->from('newspages', 'news') + ->innerJoin('news', 'nodeversion', 'nv', "nv.refId = news.id AND nv.refEntityname='Entity\\News'") + ->innerJoin('nv', 'nodetranslation', 'nt', 'nv.nodetranslation = nt.id') + ->innerJoin('nt', 'node', 'n', 'nt.node = n.id') + ->where('nt.lang = ?') + ->andWhere('n.deleted = 0'); + + $this->assertEquals("SELECT COUNT(DISTINCT news.id) FROM newspages news INNER JOIN nodeversion nv ON nv.refId = news.id AND nv.refEntityname='Entity\\News' INNER JOIN nodetranslation nt ON nv.nodetranslation = nt.id INNER JOIN node n ON nt.node = n.id WHERE (nt.lang = ?) AND (n.deleted = 0)", $qb->getSQL()); + } } \ No newline at end of file diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php index 0e74c7de..96a7c6b1 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/SQLParserUtilsTest.php @@ -28,6 +28,13 @@ static public function dataGetPlaceholderPositions() array("SELECT '?' FROM foo", true, array()), array('SELECT "?" FROM foo WHERE bar = ?', true, array(32)), array("SELECT '?' FROM foo WHERE bar = ?", true, array(32)), + array( +<<<'SQLDATA' +SELECT * FROM foo WHERE bar = 'it\'s a trap? \\' OR bar = ? +AND baz = "\"quote\" me on it? \\" OR baz = ? +SQLDATA + , true, array(58, 104) + ), // named array('SELECT :foo FROM :bar', false, array(7 => 'foo', 17 => 'bar')), @@ -35,6 +42,10 @@ static public function dataGetPlaceholderPositions() array('SELECT ":foo" FROM Foo WHERE bar IN (:name1, :name2)', false, array(37 => 'name1', 45 => 'name2')), array("SELECT ':foo' FROM Foo WHERE bar IN (:name1, :name2)", false, array(37 => 'name1', 45 => 'name2')), array('SELECT :foo_id', false, array(7 => 'foo_id')), // Ticket DBAL-231 + array('SELECT @rank := 1', false, array()), // Ticket DBAL-398 + array('SELECT @rank := 1 AS rank, :foo AS foo FROM :bar', false, array(27 => 'foo', 44 => 'bar')), // Ticket DBAL-398 + array('SELECT * FROM Foo WHERE bar > :start_date AND baz > :start_date', false, array(30 => 'start_date', 52 => 'start_date')), // Ticket GH-113 + array('SELECT foo::date as date FROM Foo WHERE bar > :start_date AND baz > :start_date', false, array(46 => 'start_date', 68 => 'start_date')) // Ticket GH-259 ); } @@ -227,6 +238,55 @@ static public function dataExpandListParameters() array(), array() ), + array( + "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar OR baz = :baz", + array('foo' => array(1, 2), 'bar' => 'bar', 'baz' => 'baz'), + array('foo' => Connection::PARAM_INT_ARRAY, 'baz' => 'string'), + 'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ? OR baz = ?', + array(1, 2, 'bar', 'baz'), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR, 'string') + ), + array( + "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar", + array('foo' => array(1, 2), 'bar' => 'bar'), + array('foo' => Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?', + array(1, 2, 'bar'), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR) + ), + // Params/types with colons + array( + "SELECT * FROM Foo WHERE foo = :foo OR bar = :bar", + array(':foo' => 'foo', ':bar' => 'bar'), + array(':foo' => \PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE foo = ? OR bar = ?', + array('foo', 'bar'), + array(\PDO::PARAM_INT, \PDO::PARAM_STR) + ), + array( + "SELECT * FROM Foo WHERE foo = :foo OR bar = :bar", + array(':foo' => 'foo', ':bar' => 'bar'), + array(':foo' => \PDO::PARAM_INT, 'bar' => \PDO::PARAM_INT), + 'SELECT * FROM Foo WHERE foo = ? OR bar = ?', + array('foo', 'bar'), + array(\PDO::PARAM_INT, \PDO::PARAM_INT) + ), + array( + "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar", + array(':foo' => array(1, 2), ':bar' => 'bar'), + array('foo' => Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?', + array(1, 2, 'bar'), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR) + ), + array( + "SELECT * FROM Foo WHERE foo IN (:foo) OR bar = :bar", + array('foo' => array(1, 2), 'bar' => 'bar'), + array(':foo' => Connection::PARAM_INT_ARRAY), + 'SELECT * FROM Foo WHERE foo IN (?, ?) OR bar = ?', + array(1, 2, 'bar'), + array(\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_STR) + ), ); } @@ -247,4 +307,53 @@ public function testExpandListParameters($q, $p, $t, $expectedQuery, $expectedPa $this->assertEquals($expectedParams, $params, "Params dont match"); $this->assertEquals($expectedTypes, $types, "Types dont match"); } + + public static function dataQueryWithMissingParameters() + { + return array( + array( + "SELECT * FROM foo WHERE bar = :param", + array('other' => 'val'), + array(), + ), + array( + "SELECT * FROM foo WHERE bar = :param", + array(), + array(), + ), + array( + "SELECT * FROM foo WHERE bar = :param", + array(), + array('param' => Connection::PARAM_INT_ARRAY), + ), + array( + "SELECT * FROM foo WHERE bar = :param", + array(), + array(':param' => Connection::PARAM_INT_ARRAY), + ), + array( + "SELECT * FROM foo WHERE bar = :param", + array(), + array('bar' => Connection::PARAM_INT_ARRAY), + ), + array( + "SELECT * FROM foo WHERE bar = :param", + array('bar' => 'value'), + array('bar' => Connection::PARAM_INT_ARRAY), + ), + ); + } + + /** + * @dataProvider dataQueryWithMissingParameters + */ + public function testExceptionIsThrownForMissingParam($query, $params, $types = array()) + { + $this->setExpectedException( + 'Doctrine\DBAL\SQLParserUtilsException', + 'Value for :param not found in params array. Params array key should be "param"' + ); + + SQLParserUtils::expandListParameters($query, $params, $types); + } } diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php index 5301bd8e..32a303bb 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php @@ -210,6 +210,26 @@ public function testCompareChangedColumns_ChangeCustomSchemaOption() $this->assertEquals(array(), $c->diffColumn($column1, $column1)); } + public function testCompareChangeColumns_MultipleNewColumnsRename() + { + $tableA = new Table("foo"); + $tableA->addColumn('datefield1', 'datetime'); + + $tableB = new Table("foo"); + $tableB->addColumn('new_datefield1', 'datetime'); + $tableB->addColumn('new_datefield2', 'datetime'); + + $c = new Comparator(); + $tableDiff = $c->diffTable($tableA, $tableB); + + $this->assertCount(1, $tableDiff->renamedColumns, "we should have one rename datefield1 => new_datefield1."); + $this->assertArrayHasKey('datefield1', $tableDiff->renamedColumns, "'datefield1' should be set to be renamed to new_datefield1"); + $this->assertCount(1, $tableDiff->addedColumns, "'new_datefield2' should be added"); + $this->assertArrayHasKey('new_datefield2', $tableDiff->addedColumns, "'new_datefield2' should be added, not created through renaming!"); + $this->assertCount(0, $tableDiff->removedColumns, "Nothing should be removed."); + $this->assertCount(0, $tableDiff->changedColumns, "Nothing should be changed as all fields old & new have diff names."); + } + public function testCompareRemovedIndex() { $schema1 = new Schema( array( @@ -486,6 +506,29 @@ public function testTableUpdateForeignKey() $this->assertEquals(1, count($tableDiff->changedForeignKeys)); } + public function testMovedForeignKeyForeignTable() + { + $tableForeign = new Table("bar"); + $tableForeign->addColumn('id', 'integer'); + + $tableForeign2 = new Table("bar2"); + $tableForeign2->addColumn('id', 'integer'); + + $table1 = new Table("foo"); + $table1->addColumn('fk', 'integer'); + $table1->addForeignKeyConstraint($tableForeign, array('fk'), array('id')); + + $table2 = new Table("foo"); + $table2->addColumn('fk', 'integer'); + $table2->addForeignKeyConstraint($tableForeign2, array('fk'), array('id')); + + $c = new Comparator(); + $tableDiff = $c->diffTable($table1, $table2); + + $this->assertInstanceOf('Doctrine\DBAL\Schema\TableDiff', $tableDiff); + $this->assertEquals(1, count($tableDiff->changedForeignKeys)); + } + public function testTablesCaseInsensitive() { $schemaA = new Schema(); @@ -581,6 +624,18 @@ public function testCompareForeignKey_RestrictNoAction_AreTheSame() $this->assertFalse($c->diffForeignKey($fk1, $fk2)); } + /** + * @group DBAL-492 + */ + public function testCompareForeignKeyNamesUnqualified_AsNoSchemaInformationIsAvailable() + { + $fk1 = new ForeignKeyConstraint(array("foo"), "foo.bar", array("baz"), "fk1"); + $fk2 = new ForeignKeyConstraint(array("foo"), "baz.bar", array("baz"), "fk1"); + + $c = new Comparator(); + $this->assertFalse($c->diffForeignKey($fk1, $fk2)); + } + public function testDetectRenameColumn() { $tableA = new Table("foo"); diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php index ba74ad52..3ee09374 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/SequenceTest.php @@ -25,3 +25,4 @@ public function testIsAutoincrementFor() $this->assertFalse($sequence3->isAutoIncrementsFor($table)); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php index 465668d6..e2f52c47 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Synchronizer/SingleDatabaseSynchronizerTest.php @@ -85,3 +85,4 @@ public function testGetDropAllSchema() $this->assertEquals(array('DROP TABLE test'), $sql); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php index 9773c0bd..e5b660f6 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Schema/Visitor/RemoveNamespacedAssetsTest.php @@ -74,3 +74,4 @@ public function testCleanupForeignKeysDifferentOrder() $this->assertEquals(1, count($sql), "Just one CREATE TABLE statement, no foreign key and table to foo.bar"); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php index de734e74..59259fd8 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardConnectionTest.php @@ -179,3 +179,4 @@ public function testSwitchShardWithOpenTransactionException() $conn->connect(1); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php index b2789526..002f9f0e 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/PoolingShardManagerTest.php @@ -105,3 +105,4 @@ public function testQueryAll() $this->assertEquals(array(array('id' => 1), array('id' => 2)), $result); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php index 9c74c81c..1051efb5 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/FunctionalTest.php @@ -41,3 +41,4 @@ public function testSharding() $this->assertTrue(count($data) > 0); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php index 5489d02f..7eb038fc 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/MultiTenantVisitorTest.php @@ -62,3 +62,4 @@ public function testMultiTenantNonPrimaryKey() $this->assertEquals(array('created', 'tenant_id'), $foo->getIndex('idx')->getColumns()); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php index 4c1ada72..091a3636 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureFederationsSynchronizerTest.php @@ -47,3 +47,4 @@ public function testDropSchema() $this->assertEQuals(5, count($sql)); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php index ece7538d..f73e4940 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/SQLAzure/SQLAzureShardManagerTest.php @@ -90,3 +90,4 @@ private function createConnection(array $params) return $conn; } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php index dfc75e71..4e06f8d4 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Sharding/ShardChoser/MultiTenantShardChoserTest.php @@ -37,3 +37,4 @@ private function createConnectionMock() return $this->getMock('Doctrine\DBAL\Sharding\PoolingShardConnection', array('connect', 'getParams', 'fetchAll'), array(), '', false); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php index 317c3d28..0f611839 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php +++ b/doctrine/dbal/tests/Doctrine/Tests/DBAL/Types/GuidTypeTest.php @@ -28,3 +28,4 @@ public function testNullConversion() $this->assertNull($this->_type->convertToPHPValue(null, $this->_platform)); } } + diff --git a/doctrine/dbal/tests/Doctrine/Tests/TestInit.php b/doctrine/dbal/tests/Doctrine/Tests/TestInit.php index 6763cf61..cb4cc278 100644 --- a/doctrine/dbal/tests/Doctrine/Tests/TestInit.php +++ b/doctrine/dbal/tests/Doctrine/Tests/TestInit.php @@ -6,16 +6,12 @@ error_reporting(E_ALL | E_STRICT); -require_once __DIR__ . '/../../../lib/vendor/doctrine-common/lib/Doctrine/Common/ClassLoader.php'; +if (file_exists(__DIR__ . '/../../../vendor/autoload.php')) { + $loader = require_once __DIR__ . '/../../../vendor/autoload.php'; +} elseif (file_exists(__DIR__ . '/../../../../../autoload.php')) { + $loader = require __DIR__ . '/../../../vendor/autoload.php'; +} else { + throw new \RuntimeException('Could not locate composer autoloader'); +} -$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\Common', __DIR__ . '/../../../lib/vendor/doctrine-common/lib'); -$classLoader->register(); - -$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\DBAL', __DIR__ . '/../../../lib'); -$classLoader->register(); - -$classLoader = new \Doctrine\Common\ClassLoader('Doctrine\Tests', __DIR__ . '/../../'); -$classLoader->register(); - -$classLoader = new \Doctrine\Common\ClassLoader('Symfony', __DIR__ . "/../../../lib/vendor"); -$classLoader->register(); +$loader->add('Doctrine\Tests', __DIR__ . '/../../'); diff --git a/doctrine/dbal/tests/travis/mysql.travis.xml b/doctrine/dbal/tests/travis/mysql.travis.xml index 15fe6023..c80f4d29 100644 --- a/doctrine/dbal/tests/travis/mysql.travis.xml +++ b/doctrine/dbal/tests/travis/mysql.travis.xml @@ -29,3 +29,4 @@ + diff --git a/doctrine/dbal/tests/travis/mysqli.travis.xml b/doctrine/dbal/tests/travis/mysqli.travis.xml index 0a72d880..ace5df46 100644 --- a/doctrine/dbal/tests/travis/mysqli.travis.xml +++ b/doctrine/dbal/tests/travis/mysqli.travis.xml @@ -29,3 +29,4 @@ + From 1bab5d4b5b5e6ce81b94bc1c359cff46119c406e Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Tue, 26 Feb 2013 17:55:19 +0100 Subject: [PATCH 05/11] Backport of Doctrine Sqlite ALTER TABLE workaround Conflicts: doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php --- .../DBAL/Platforms/SqlitePlatform.php | 470 +++++++++++++++++- .../lib/Doctrine/DBAL/Schema/ColumnDiff.php | 8 +- .../lib/Doctrine/DBAL/Schema/Comparator.php | 3 + .../lib/Doctrine/DBAL/Schema/SchemaDiff.php | 9 +- .../DBAL/Schema/SqliteSchemaManager.php | 178 +++++++ .../lib/Doctrine/DBAL/Schema/TableDiff.php | 9 +- 6 files changed, 655 insertions(+), 22 deletions(-) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php index 6fba88d7..58390898 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -20,6 +20,11 @@ namespace Doctrine\DBAL\Platforms; use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\ForeignKeyConstraint; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\Constraint; /** * The SqlitePlatform class describes the specifics and dialects of the SQLite @@ -28,6 +33,7 @@ * @since 2.0 * @author Roman Borschel * @author Benjamin Eberlei + * @author Martin Hasoň * @todo Rename: SQLitePlatform */ class SqlitePlatform extends AbstractPlatform @@ -263,16 +269,32 @@ protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) */ protected function _getCreateTableSQL($name, array $columns, array $options = array()) { - $name = str_replace(".", "__", $name); + $name = str_replace('.', '__', $name); $queryFields = $this->getColumnDeclarationListSQL($columns); + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + if (isset($options['primary']) && ! empty($options['primary'])) { $keyColumns = array_unique(array_values($options['primary'])); $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; } + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $foreignKey) { + $queryFields.= ', '.$this->getForeignKeyDeclarationSQL($foreignKey); + } + } + $query[] = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')'; + if (isset($options['alter']) && true === $options['alter']) { + return $query; + } + if (isset($options['indexes']) && ! empty($options['indexes'])) { foreach ($options['indexes'] as $index => $indexDef) { $query[] = $this->getCreateIndexSQL($indexDef, $name); @@ -305,16 +327,22 @@ public function getClobTypeDeclarationSQL(array $field) return 'CLOB'; } + /** + * {@inheritDoc} + */ public function getListTableConstraintsSQL($table) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = '$table' AND sql NOT NULL ORDER BY name"; } + /** + * {@inheritDoc} + */ public function getListTableColumnsSQL($table, $currentDatabase = null) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "PRAGMA table_info($table)"; } @@ -324,11 +352,14 @@ public function getListTableColumnsSQL($table, $currentDatabase = null) */ public function getListTableIndexesSQL($table, $currentDatabase = null) { - $table = str_replace(".", "__", $table); + $table = str_replace('.', '__', $table); return "PRAGMA index_list($table)"; } + /** + * {@inheritDoc} + */ public function getListTablesSQL() { return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND name != 'geometry_columns' AND name != 'spatial_ref_sys' " @@ -344,34 +375,33 @@ public function getListViewsSQL($database) return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; } + /** + * {@inheritDoc} + */ public function getCreateViewSQL($name, $sql) { return 'CREATE VIEW ' . $name . ' AS ' . $sql; } - public function getDropViewSQL($name) - { - return 'DROP VIEW '. $name; - } - /** * {@inheritDoc} - * - * SQLite does support foreign key constraints, but only in CREATE TABLE statements... - * This really limits their usefulness and requires SQLite specific handling, so - * we simply say that SQLite does NOT support foreign keys for now... */ - public function supportsForeignKeyConstraints() + public function getDropViewSQL($name) { - return false; + return 'DROP VIEW '. $name; } /** * {@inheritDoc} */ - public function supportsAlterTable() + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) { - return false; + $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + $query .= (($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) ? ' ' : ' NOT ') . 'DEFERRABLE'; + $query .= ' INITIALLY ' . (($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) ? 'DEFERRED' : 'IMMEDIATE'); + + return $query; } /** @@ -395,7 +425,8 @@ public function getName() */ public function getTruncateTableSQL($tableName, $cascade = false) { - $tableName = str_replace(".", "__", $tableName); + $tableName = str_replace('.', '__', $tableName); + return 'DELETE FROM '.$tableName; } @@ -441,6 +472,9 @@ static public function udfLocate($str, $substr, $offset = 0) return 0; } + /** + * {@inheritDoc} + */ public function getForUpdateSql() { return ''; @@ -496,6 +530,47 @@ protected function getReservedKeywordsClass() return 'Doctrine\DBAL\Platforms\Keywords\SQLiteKeywords'; } + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + if ( ! $diff->fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $sql = array(); + foreach ($diff->fromTable->getIndexes() as $index) { + if ( ! $index->isPrimary()) { + $sql[] = $this->getDropIndexSQL($index, $diff->name); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + if ( ! $diff->fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $sql = array(); + $tableName = $diff->newName ?: $diff->name; + foreach ($this->getIndexesInAlteredTable($diff) as $indexName => $index) { + if ($index->isPrimary()) { + continue; + } + + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + + return $sql; + } + /** * {@inheritDoc} */ @@ -509,7 +584,7 @@ public function getBlobTypeDeclarationSQL(array $field) */ public function getTemporaryTableName($tableName) { - $tableName = str_replace(".", "__", $tableName); + $tableName = str_replace('.', '__', $tableName); return $tableName; } @@ -527,4 +602,361 @@ public function canEmulateSchemas() { return true; } + + /** + * {@inheritDoc} + */ + public function supportsForeignKeyConstraints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + throw new DBALException('Sqlite platform does not support alter primary key.'); + } + + /** + * {@inheritdoc} + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + throw new DBALException('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritdoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + throw new DBALException('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritDoc} + */ + public function getCreateConstraintSQL(Constraint $constraint, $table) + { + throw new DBALException('Sqlite platform does not support alter constraint.'); + } + + /** + * {@inheritDoc} + */ + public function getCreateTableSQL(Table $table, $createFlags = null) + { + $createFlags = null === $createFlags ? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS : $createFlags; + + return parent::getCreateTableSQL($table, $createFlags); + } + + /** + * {@inheritDoc} + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + $table = str_replace('.', '__', $table); + + return "PRAGMA foreign_key_list($table)"; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = $this->getSimpleAlterTableSQL($diff); + if (false !== $sql) { + return $sql; + } + + $fromTable = $diff->fromTable; + if ( ! $fromTable instanceof Table) { + throw new DBALException('Sqlite platform requires for alter table the table diff with reference to original table schema'); + } + + $table = clone $fromTable; + + $columns = array(); + $oldColumnNames = array(); + $newColumnNames = array(); + $columnSql = array(); + + foreach ($table->getColumns() as $columnName => $column) { + $columnName = strtolower($columnName); + $columns[$columnName] = $column; + $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); + } + + foreach ($diff->removedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $columnName = strtolower($columnName); + if (isset($columns[$columnName])) { + unset($columns[$columnName]); + unset($oldColumnNames[$columnName]); + unset($newColumnNames[$columnName]); + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = strtolower($oldColumnName); + if (isset($columns[$oldColumnName])) { + unset($columns[$oldColumnName]); + } + + $columns[strtolower($column->getName())] = $column; + + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $column->getQuotedName($this); + } + } + + foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + if (isset($columns[$oldColumnName])) { + unset($columns[$oldColumnName]); + } + + $columns[strtolower($columnDiff->column->getName())] = $columnDiff->column; + + if (isset($newColumnNames[$oldColumnName])) { + $newColumnNames[$oldColumnName] = $columnDiff->column->getQuotedName($this); + } + } + + foreach ($diff->addedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columns[strtolower($columnName)] = $column; + } + + $sql = array(); + $tableSql = array(); + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + $newTableName = $diff->newName ?: $diff->name; + + $dataTable = new Table('__temp__'.$table->getName()); + + $newTable = new Table($table->getName(), $columns, $this->getPrimaryIndexInAlteredTable($diff), $this->getForeignKeysInAlteredTable($diff), 0, $table->getOptions()); + $newTable->addOption('alter', true); + + $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); + //$sql = array_merge($sql, $this->getCreateTableSQL($dataTable, 0)); + $sql[] = sprintf('CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', $dataTable->getQuotedName($this), implode(', ', $oldColumnNames), $table->getQuotedName($this)); + $sql[] = $this->getDropTableSQL($fromTable); + + $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); + $sql[] = sprintf('INSERT INTO %s (%s) SELECT %s FROM %s', $newTable->getQuotedName($this), implode(', ', $newColumnNames), implode(', ', $oldColumnNames), $dataTable->getQuotedName($this)); + $sql[] = $this->getDropTableSQL($dataTable); + + if ($diff->newName && $diff->newName != $diff->name) { + $renamedTable = new Table($diff->newName); + $sql[] = 'ALTER TABLE '.$newTable->getQuotedName($this).' RENAME TO '.$renamedTable->getQuotedName($this); + } + + $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + private function getSimpleAlterTableSQL(TableDiff $diff) + { + if ( ! empty($diff->renamedColumns) || ! empty($diff->addedForeignKeys) || ! empty($diff->addedIndexes) + || ! empty($diff->changedColumns) || ! empty($diff->changedForeignKeys) || ! empty($diff->changedIndexes) + || ! empty($diff->removedColumns) || ! empty($diff->removedForeignKeys) || ! empty($diff->removedIndexes) + ) { + return false; + } + + $table = new Table($diff->name); + + $sql = array(); + $tableSql = array(); + $columnSql = array(); + + foreach ($diff->addedColumns as $columnName => $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $field = array_merge(array('unique' => null, 'autoincrement' => null, 'default' => null), $column->toArray()); + $type = (string) $field['type']; + switch (true) { + case isset($field['columnDefinition']) || $field['autoincrement'] || $field['unique']: + case $type == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL(): + case $type == 'Date' && $field['default'] == $this->getCurrentDateSQL(): + case $type == 'Time' && $field['default'] == $this->getCurrentTimeSQL(): + return false; + } + + $field['name'] = $column->getQuotedName($this); + if (strtolower($field['type']) == 'string' && $field['length'] === null) { + $field['length'] = 255; + } + + $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' ADD COLUMN '.$this->getColumnDeclarationSQL($field['name'], $field); + } + + if ( ! $this->onSchemaAlterTable($diff, $tableSql)) { + if ($diff->newName !== false) { + $newTable = new Table($diff->newName); + $sql[] = 'ALTER TABLE '.$table->getQuotedName($this).' RENAME TO '.$newTable->getQuotedName($this); + } + } + + return array_merge($sql, $tableSql, $columnSql); + } + + private function getColumnNamesInAlteredTable(TableDiff $diff) + { + $columns = array(); + + foreach ($diff->fromTable->getColumns() as $columnName => $column) { + $columns[strtolower($columnName)] = $column->getName(); + } + + foreach ($diff->removedColumns as $columnName => $column) { + $columnName = strtolower($columnName); + if (isset($columns[$columnName])) { + unset($columns[$columnName]); + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + $columnName = $column->getName(); + $columns[strtolower($oldColumnName)] = $columnName; + $columns[strtolower($columnName)] = $columnName; + } + + foreach ($diff->changedColumns as $oldColumnName => $columnDiff) { + $columnName = $columnDiff->column->getName(); + $columns[strtolower($oldColumnName)] = $columnName; + $columns[strtolower($columnName)] = $columnName; + } + + foreach ($diff->addedColumns as $columnName => $column) { + $columns[strtolower($columnName)] = $columnName; + } + + return $columns; + } + + private function getIndexesInAlteredTable(TableDiff $diff) + { + $indexes = $diff->fromTable->getIndexes(); + $columnNames = $this->getColumnNamesInAlteredTable($diff); + + foreach ($indexes as $key => $index) { + $changed = false; + $indexColumns = array(); + foreach ($index->getColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if ( ! isset($columnNames[$normalizedColumnName])) { + unset($indexes[$key]); + continue 2; + } else { + $indexColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName !== $columnNames[$normalizedColumnName]) { + $changed = true; + } + } + } + + if ($changed) { + $indexes[$key] = new Index($index->getName(), $indexColumns, $index->isUnique(), $index->isPrimary(), $index->getFlags()); + } + } + + foreach ($diff->removedIndexes as $index) { + $indexName = strtolower($index->getName()); + if (strlen($indexName) && isset($indexes[$indexName])) { + unset($indexes[$indexName]); + } + } + + foreach (array_merge($diff->changedIndexes, $diff->addedIndexes) as $index) { + $indexName = strtolower($index->getName()); + if (strlen($indexName)) { + $indexes[$indexName] = $index; + } else { + $indexes[] = $index; + } + } + + return $indexes; + } + + private function getForeignKeysInAlteredTable(TableDiff $diff) + { + $foreignKeys = $diff->fromTable->getForeignKeys(); + $columnNames = $this->getColumnNamesInAlteredTable($diff); + + foreach ($foreignKeys as $key => $constraint) { + $changed = false; + $localColumns = array(); + foreach ($constraint->getLocalColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if ( ! isset($columnNames[$normalizedColumnName])) { + unset($foreignKeys[$key]); + continue 2; + } else { + $localColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName !== $columnNames[$normalizedColumnName]) { + $changed = true; + } + } + } + + if ($changed) { + $foreignKeys[$key] = new ForeignKeyConstraint($localColumns, $constraint->getForeignTableName(), $constraint->getForeignColumns(), $constraint->getName(), $constraint->getOptions()); + } + } + + foreach ($diff->removedForeignKeys as $constraint) { + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName) && isset($foreignKeys[$constraintName])) { + unset($foreignKeys[$constraintName]); + } + } + + foreach (array_merge($diff->changedForeignKeys, $diff->addedForeignKeys) as $constraint) { + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName)) { + $foreignKeys[$constraintName] = $constraint; + } else { + $foreignKeys[] = $constraint; + } + } + + return $foreignKeys; + } + + private function getPrimaryIndexInAlteredTable(TableDiff $diff) + { + $primaryIndex = array(); + + foreach ($this->getIndexesInAlteredTable($diff) as $index) { + if ($index->isPrimary()) { + $primaryIndex = array($index->getName() => $index); + } + } + + return $primaryIndex; + } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php index 4fc4b9c3..ef7a5a0e 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/ColumnDiff.php @@ -44,11 +44,17 @@ class ColumnDiff */ public $changedProperties = array(); - public function __construct($oldColumnName, Column $column, array $changedProperties = array()) + /** + * @var Column + */ + public $fromColumn; + + public function __construct($oldColumnName, Column $column, array $changedProperties = array(), Column $fromColumn = null) { $this->oldColumnName = $oldColumnName; $this->column = $column; $this->changedProperties = $changedProperties; + $this->fromColumn = $fromColumn; } public function hasChanged($propertyName) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php index b61a69a3..f8c4498f 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/Comparator.php @@ -58,6 +58,7 @@ static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) public function compare(Schema $fromSchema, Schema $toSchema) { $diff = new SchemaDiff(); + $diff->fromSchema = $fromSchema; $foreignKeysToTable = array(); @@ -179,6 +180,7 @@ public function diffTable(Table $table1, Table $table2) { $changes = 0; $tableDifferences = new TableDiff($table1->getName()); + $tableDifferences->fromTable = $table1; $table1Columns = $table1->getColumns(); $table2Columns = $table2->getColumns(); @@ -203,6 +205,7 @@ public function diffTable(Table $table1, Table $table2) $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); if (count($changedProperties) ) { $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); + $columnDiff->fromColumn = $column; $tableDifferences->changedColumns[$column->getName()] = $columnDiff; $changes++; } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php index ba0d9e6f..eff2b53e 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SchemaDiff.php @@ -34,6 +34,11 @@ */ class SchemaDiff { + /** + * @var Schema + */ + public $fromSchema; + /** * All added tables * @@ -81,12 +86,14 @@ class SchemaDiff * @param array(string=>Table) $newTables * @param array(string=>TableDiff) $changedTables * @param array(string=>bool) $removedTables + * @param Schema $fromSchema */ - public function __construct($newTables = array(), $changedTables = array(), $removedTables = array()) + public function __construct($newTables = array(), $changedTables = array(), $removedTables = array(), Schema $fromSchema = null) { $this->newTables = $newTables; $this->changedTables = $changedTables; $this->removedTables = $removedTables; + $this->fromSchema = $fromSchema; } /** diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php index f980f957..9a4f2ec1 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -19,6 +19,8 @@ namespace Doctrine\DBAL\Schema; +use Doctrine\DBAL\DBALException; + /** * SqliteSchemaManager * @@ -26,6 +28,7 @@ * @author Konsta Vesterinen * @author Lukas Smith (PEAR MDB2 library) * @author Jonathan H. Wage + * @author Martin Hasoň * @version $Revision$ * @since 2.0 */ @@ -61,6 +64,93 @@ public function createDatabase($database) $conn->close(); } + /** + * {@inheritdoc} + */ + public function renameTable($name, $newName) + { + $tableDiff = new TableDiff($name); + $tableDiff->fromTable = $this->listTableDetails($name); + $tableDiff->newName = $newName; + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->addedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->changedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function dropForeignKey($foreignKey, $table) + { + $tableDiff = $this->getTableDiffForAlterForeignKey($foreignKey, $table); + $tableDiff->removedForeignKeys[] = $foreignKey; + + $this->alterTable($tableDiff); + } + + /** + * {@inheritdoc} + */ + public function listTableForeignKeys($table, $database = null) + { + if (null === $database) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); + $tableForeignKeys = $this->_conn->fetchAll($sql); + + if ( ! empty($tableForeignKeys)) { + $createSql = $this->_conn->fetchAll("SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = 'table' AND name = '$table'"); + $createSql = isset($createSql[0]['sql']) ? $createSql[0]['sql'] : ''; + if (preg_match_all('# + (?:CONSTRAINT\s+([^\s]+)\s+)? + (?:FOREIGN\s+KEY[^\)]+\)\s*)? + REFERENCES\s+[^\s]+\s+(?:\([^\)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $createSql, $match)) { + + $names = array_reverse($match[1]); + $deferrable = array_reverse($match[2]); + $deferred = array_reverse($match[3]); + } else { + $names = $deferrable = $deferred = array(); + } + + foreach ($tableForeignKeys as $key => $value) { + $id = $value['id']; + $tableForeignKeys[$key]['constraint_name'] = isset($names[$id]) && '' != $names[$id] ? $names[$id] : $id; + $tableForeignKeys[$key]['deferrable'] = isset($deferrable[$id]) && 'deferrable' == strtolower($deferrable[$id]) ? true : false; + $tableForeignKeys[$key]['deferred'] = isset($deferred[$id]) && 'deferred' == strtolower($deferred[$id]) ? true : false; + } + } + + return $this->_getPortableTableForeignKeysList($tableForeignKeys); + } + protected function _getPortableTableDefinition($table) { return $table['name']; @@ -122,6 +212,31 @@ protected function _getPortableTableIndexDefinition($tableIndex) ); } + protected function _getPortableTableColumnList($table, $database, $tableColumns) + { + $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); + $autoincrementColumn = null; + $autoincrementCount = 0; + foreach ($tableColumns as $tableColumn) { + if ('1' == $tableColumn['pk']) { + $autoincrementCount++; + if (null === $autoincrementColumn && 'integer' == strtolower($tableColumn['type'])) { + $autoincrementColumn = $tableColumn['name']; + } + } + } + + if (1 == $autoincrementCount && null !== $autoincrementColumn) { + foreach ($list as $column) { + if ($autoincrementColumn == $column->getName()) { + $column->setAutoincrement(true); + } + } + } + + return $list; + } + protected function _getPortableTableColumnDefinition($tableColumn) { $e = explode('(', $tableColumn['type']); @@ -190,4 +305,67 @@ protected function _getPortableViewDefinition($view) { return new View($view['name'], $view['sql']); } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $key => $value) { + $value = array_change_key_case($value, CASE_LOWER); + $name = $value['constraint_name']; + if ( ! isset($list[$name])) { + if ( ! isset($value['on_delete']) || $value['on_delete'] == "RESTRICT") { + $value['on_delete'] = null; + } + if ( ! isset($value['on_update']) || $value['on_update'] == "RESTRICT") { + $value['on_update'] = null; + } + + $list[$name] = array( + 'name' => $name, + 'local' => array(), + 'foreign' => array(), + 'foreignTable' => $value['table'], + 'onDelete' => $value['on_delete'], + 'onUpdate' => $value['on_update'], + 'deferrable' => $value['deferrable'], + 'deferred'=> $value['deferred'], + ); + } + $list[$name]['local'][] = $value['from']; + $list[$name]['foreign'][] = $value['to']; + } + + $result = array(); + foreach($list as $constraint) { + $result[] = new ForeignKeyConstraint( + array_values($constraint['local']), $constraint['foreignTable'], + array_values($constraint['foreign']), $constraint['name'], + array( + 'onDelete' => $constraint['onDelete'], + 'onUpdate' => $constraint['onUpdate'], + 'deferrable' => $constraint['deferrable'], + 'deferred'=> $constraint['deferred'], + ) + ); + } + + return $result; + } + + private function getTableDiffForAlterForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + if ( ! $table instanceof Table) { + $tableDetails = $this->tryMethod('listTableDetails', $table); + if (false === $table) { + throw new \DBALException(sprintf('Sqlite schema manager requires to modify foreign keys table definition "%s".', $table)); + } + + $table = $tableDetails; + } + + $tableDiff = new TableDiff($table->getName()); + $tableDiff->fromTable = $table; + + return $tableDiff; + } } diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php index 257a3bd0..9ce475ba 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Schema/TableDiff.php @@ -111,6 +111,11 @@ class TableDiff */ public $removedForeignKeys = array(); + /** + * @var Table + */ + public $fromTable; + /** * Constructs an TableDiff object. * @@ -120,10 +125,11 @@ class TableDiff * @param array(string=>Index) $addedIndexes * @param array(string=>Index) $changedIndexes * @param array(string=>bool) $removedIndexes + * @param Table $fromTable */ public function __construct($tableName, $addedColumns = array(), $changedColumns = array(), $removedColumns = array(), $addedIndexes = array(), - $changedIndexes = array(), $removedIndexes = array()) + $changedIndexes = array(), $removedIndexes = array(), Table $fromTable = null) { $this->name = $tableName; $this->addedColumns = $addedColumns; @@ -132,5 +138,6 @@ public function __construct($tableName, $addedColumns = array(), $this->addedIndexes = $addedIndexes; $this->changedIndexes = $changedIndexes; $this->removedIndexes = $removedIndexes; + $this->fromTable = $fromTable; } } From 63f6a806b597f5ec8b2bcc2e4591d64af843b68c Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Fri, 21 Jun 2013 12:11:22 +0200 Subject: [PATCH 06/11] Don't add 'NOT NULL' to the 'ALTER TABLE' when that hasn't changed --- .../dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php index cd5c7744..cac6d9d9 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php @@ -569,7 +569,11 @@ public function getAlterTableSQL(TableDiff $diff) } $column = $columnDiff->column; - $fields[] = $column->getQuotedName($this). ' ' . $this->getColumnDeclarationSQL('', $column->toArray()); + $columnInfo = $column->toArray(); + if (!in_array('notnull', $columnDiff->changedProperties)) { + $columnInfo['notnull'] = false; + } + $fields[] = $column->getQuotedName($this). ' ' . $this->getColumnDeclarationSQL('', $columnInfo); if ($columnDiff->hasChanged('comment') && $comment = $this->getColumnComment($column)) { $commentsSQL[] = $this->getCommentOnColumnSQL($diff->name, $column->getName(), $comment); } From 7ae81563894b971e51cee7bdb0ac2da83ecc5149 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Fri, 21 Jun 2013 12:21:14 +0200 Subject: [PATCH 07/11] Fix quoting of identifiers in OraclePlatform --- .../DBAL/Platforms/OraclePlatform.php | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php index cac6d9d9..6a16ac31 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/OraclePlatform.php @@ -355,8 +355,6 @@ protected function _getCreateTableSQL($table, array $columns, array $options = a */ public function getListTableIndexesSQL($table, $currentDatabase = null) { - $table = strtoupper($table); - return "SELECT uind.index_name AS name, " . " uind.index_type AS type, " . " decode( uind.uniqueness, 'NONUNIQUE', 0, 'UNIQUE', 1 ) AS is_unique, " . @@ -392,7 +390,7 @@ public function getDropViewSQL($name) public function getCreateAutoincrementSql($name, $table, $start = 1) { - $table = strtoupper($table); + $table = $this->getOracleInternalName($table); $sql = array(); $indexName = $table . '_AI_PK'; @@ -408,11 +406,14 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) END IF; END;'; - $sequenceName = $table . '_SEQ'; + $sequenceNameNotQuoted = $table . '_SEQ'; + $sequenceName = $this->quoteIdentifier($sequenceNameNotQuoted); $sequence = new Sequence($sequenceName, $start); $sql[] = $this->getCreateSequenceSQL($sequence); $triggerName = $table . '_AI_PK'; + $triggerName = $this->quoteIdentifier($triggerName); + $table = $this->quoteIdentifier($table); $sql[] = 'CREATE TRIGGER ' . $triggerName . ' BEFORE INSERT ON ' . $table . ' @@ -427,7 +428,7 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) ELSE SELECT NVL(Last_Number, 0) INTO last_Sequence FROM User_Sequences - WHERE Sequence_Name = \'' . $sequenceName . '\'; + WHERE Sequence_Name = \'' . $sequenceNameNotQuoted . '\'; SELECT :NEW.' . $name . ' INTO last_InsertID FROM DUAL; WHILE (last_InsertID > last_Sequence) LOOP SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; @@ -440,7 +441,7 @@ public function getCreateAutoincrementSql($name, $table, $start = 1) public function getDropAutoincrementSql($table) { - $table = strtoupper($table); + $table = $this->getOracleInternalName($table); $trigger = $table . '_AI_PK'; $sql[] = 'DROP TRIGGER ' . $trigger; @@ -454,7 +455,7 @@ public function getDropAutoincrementSql($table) public function getListTableForeignKeysSQL($table) { - $table = strtoupper($table); + $table = $this->getOracleInternalName($table); return "SELECT alc.constraint_name, alc.DELETE_RULE, @@ -478,14 +479,12 @@ public function getListTableForeignKeysSQL($table) public function getListTableConstraintsSQL($table) { - $table = strtoupper($table); + $table = $this->getOracleInternalName($table); return 'SELECT * FROM user_constraints WHERE table_name = \'' . $table . '\''; } public function getListTableColumnsSQL($table, $database = null) { - $table = strtoupper($table); - $tabColumnsTableName = "user_tab_columns"; $ownerCondition = ''; @@ -823,4 +822,12 @@ public function getBlobTypeDeclarationSQL(array $field) { return 'BLOB'; } + + protected function getOracleInternalName($table) + { + if ($table[0] == '"') { + return str_replace(array('`', '"'), '', $table); + } + return strtoupper($table); + } } From 2b42bd6c1584c9a5b2b3d7218d429cf741042c64 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Wed, 17 Jul 2013 21:32:18 +0200 Subject: [PATCH 08/11] Add primary key to 'ALTER TABLE' in MySql --- doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php index 2cf573f0..8050e1f2 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -457,6 +457,12 @@ public function getAlterTableSQL(TableDiff $diff) . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnArray); } + if (isset($diff->addedIndexes['primary'])) { + $keyColumns = array_unique(array_values($diff->addedIndexes['primary']->getColumns())); + $queryParts[] = 'ADD PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + unset($diff->addedIndexes['primary']); + } + $sql = array(); $tableSql = array(); From 1d729298ee9464e03a4695f2df0e63b15d10874c Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Thu, 18 Jul 2013 20:19:32 +0200 Subject: [PATCH 09/11] When changing from a non-primary index to a primary index, the dropped index isnt named PRIMARY --- .../dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php index 8050e1f2..d77b9748 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -512,6 +512,15 @@ protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) } } } + foreach ($diff->changedIndexes as $changedKey => $changedIndex) { + if ($changedIndex->isPrimary() && $changedKey != 'PRIMARY') { + $index = $diff->changedIndexes[$changedKey]; + $index = new index($changedKey, $index->getColumns(), $index->isUnique(), false); + $diff->removedIndexes[$changedKey] = $index; + $diff->addedIndexes['PRIMARY'] = $diff->changedIndexes[$changedKey]; + unset($diff->changedIndexes[$changedKey]); + } + } $sql = array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); From 380f5a95315fde4a0eaefe69c9dd56c25a57464f Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Thu, 18 Jul 2013 20:23:36 +0200 Subject: [PATCH 10/11] Use quoted column name for alter table --- .../dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index 2f907a2a..c308b29d 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -367,7 +367,7 @@ public function getAlterTableSQL(TableDiff $diff) continue; } - $oldColumnName = $columnDiff->oldColumnName; + $oldColumnName = $columnDiff->fromColumn->getQuotedName($this); $column = $columnDiff->column; if ($columnDiff->hasChanged('type')) { From 25e8568d41a9b9a6d1662ccf33058822a890e7f5 Mon Sep 17 00:00:00 2001 From: Bart Visscher Date: Thu, 18 Jul 2013 20:24:18 +0200 Subject: [PATCH 11/11] Dropped indexes can have constraints, drop those first if they exist --- .../DBAL/Platforms/PostgreSqlPlatform.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php index c308b29d..63183021 100644 --- a/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php +++ b/doctrine/dbal/lib/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -435,6 +435,27 @@ public function getAlterTableSQL(TableDiff $diff) return array_merge($sql, $tableSql, $columnSql); } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $sql = array(); + $table = $diff->name; + foreach ($diff->changedIndexes as $changedKey => $changedIndex) { + + $sql[] = $this->getDropConstraintSQL('IF EXISTS '. $changedIndex->getName(), $table); + $sql[] = $this->getDropIndexSQL('IF EXISTS '. $changedIndex->getName(), $table); + $diff->addedIndexes[$changedKey] = $diff->changedIndexes[$changedKey]; + unset($diff->changedIndexes[$changedKey]); + } + + $sql = array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); + + return $sql; + } + /** * {@inheritDoc} */