diff --git a/WebFiori/Database/Attributes/AttributeTableBuilder.php b/WebFiori/Database/Attributes/AttributeTableBuilder.php index a66e29a..d0c08c2 100644 --- a/WebFiori/Database/Attributes/AttributeTableBuilder.php +++ b/WebFiori/Database/Attributes/AttributeTableBuilder.php @@ -42,7 +42,7 @@ public static function build(string $entityClass, string $dbType = 'mysql'): Tab } $columnConfig = $columnAttrs[0]->newInstance(); - $columnKey = self::propertyToKey($property->getName()); + $columnKey = $columnConfig->name ?? self::propertyToKey($property->getName()); $columns[$columnKey] = self::columnConfigToArray($columnConfig); foreach ($property->getAttributes(ForeignKey::class) as $fkAttr) { diff --git a/WebFiori/Database/Column.php b/WebFiori/Database/Column.php index d02d806..633d959 100644 --- a/WebFiori/Database/Column.php +++ b/WebFiori/Database/Column.php @@ -602,7 +602,7 @@ public function setScale(int $scale) : bool { * */ public function setSize(int $size) : bool { - if ($size >= 0) { + if ($size >= 0 || $size == -1) { $this->size = $size; return true; diff --git a/WebFiori/Database/MsSql/MSSQLColumn.php b/WebFiori/Database/MsSql/MSSQLColumn.php index 4fadd1b..d0119d1 100644 --- a/WebFiori/Database/MsSql/MSSQLColumn.php +++ b/WebFiori/Database/MsSql/MSSQLColumn.php @@ -496,6 +496,12 @@ public function setSize(int $size) : bool { return parent::setSize($size); } + $maxTypes = ['varchar', 'nvarchar', 'varbinary']; + + if ($size == -1 && in_array($this->getDatatype(), $maxTypes)) { + return parent::setSize($size); + } + return false; } /** @@ -615,7 +621,8 @@ private function firstColPartString() { if ($colDataType == 'varchar' || $colDataType == 'nvarchar' || $colDataType == 'char' || $colDataType == 'nchar' || $colDataType == 'binary' || $colDataType == 'varbinary') { - $retVal .= $colDataTypeSq.'('.$this->getSize().') '; + $size = $this->getSize() == -1 ? 'max' : $this->getSize(); + $retVal .= $colDataTypeSq.'('.$size.') '; } else if (in_array($colDataType, Column::BOOL_TYPES)) { $retVal .= '[bit] '; } else if ($colDataType == 'decimal') { diff --git a/tests/WebFiori/Tests/Database/BugVerification/BugVerificationTest.php b/tests/WebFiori/Tests/Database/BugVerification/BugVerificationTest.php new file mode 100644 index 0000000..d8b4b5b --- /dev/null +++ b/tests/WebFiori/Tests/Database/BugVerification/BugVerificationTest.php @@ -0,0 +1,176 @@ +assertEquals(-1, $col->getSize(), + 'Issue #163: varbinary column with size -1 should retain -1 for MAX'); + } + + /** + * Issue #163: varchar(max) should also work. + */ + public function testIssue163_VarcharMaxSize() { + $col = new MSSQLColumn('content', 'varchar', -1); + $this->assertEquals(-1, $col->getSize(), + 'Issue #163: varchar column with size -1 should retain -1 for MAX'); + } + + /** + * Issue #163: nvarchar(max) should also work. + */ + public function testIssue163_NvarcharMaxSize() { + $col = new MSSQLColumn('content', 'nvarchar', -1); + $this->assertEquals(-1, $col->getSize(), + 'Issue #163: nvarchar column with size -1 should retain -1 for MAX'); + } + + /** + * Issue #163: The SQL output should say "max" not "1" or "-1". + */ + public function testIssue163_SqlOutputContainsMax() { + $col = new MSSQLColumn('data', 'varbinary', -1); + $sql = $col->asString(); + $this->assertStringContainsStringIgnoringCase('max', $sql, + 'Issue #163: SQL output should contain "max" for size -1, got: ' . $sql); + $this->assertStringNotContainsString('(-1)', $sql, + 'Issue #163: SQL output should not contain literal "(-1)"'); + } + + /** + * Issue #159: Column attribute 'name' parameter is ignored during + * property-level table creation. The actual DB column name should use + * the explicit name from the attribute, not the propertyToKey() result. + */ + public function testIssue159_ColumnNameFromAttribute() { + $table = AttributeTableBuilder::build(Issue159Entity::class, 'mysql'); + + // The attribute says name: 'created_at' + $col = $table->getColByName('created_at'); + $this->assertNotNull($col, + 'Issue #159: Column with name "created_at" should exist (from attribute name parameter)'); + + $col2 = $table->getColByName('user_name'); + $this->assertNotNull($col2, + 'Issue #159: Column with name "user_name" should exist (from attribute name parameter)'); + } + + /** + * Issue #159: Verify on MSSQL as well. + */ + public function testIssue159_ColumnNameFromAttributeMSSQL() { + $table = AttributeTableBuilder::build(Issue159Entity::class, 'mssql'); + + $col = $table->getColByName('created_at'); + $this->assertNotNull($col, + 'Issue #159 (MSSQL): Column with name "created_at" should exist'); + } + + /** + * Issue #153: AttributeTableBuilder does not create UNIQUE constraints + * for MSSQL columns when using class-level Column attributes. + */ + public function testIssue153_UniqueConstraintMSSQL() { + $table = AttributeTableBuilder::build(Issue153Entity::class, 'mssql'); + + $uniqueCols = $table->getUniqueCols(); + $this->assertNotEmpty($uniqueCols, + 'Issue #153: Table should have at least one unique column'); + + $emailCol = $table->getColByName('email'); + $this->assertNotNull($emailCol, 'email column should exist'); + $this->assertTrue($emailCol->isUnique(), + 'Issue #153: email column should be marked as unique'); + } + + /** + * Issue #153: The generated CREATE TABLE SQL should contain a UNIQUE constraint. + */ + public function testIssue153_CreateTableSqlContainsUnique() { + $table = AttributeTableBuilder::build(Issue153Entity::class, 'mssql'); + $sql = $table->toSQL(); + + $this->assertStringContainsStringIgnoringCase('unique', $sql, + 'Issue #153: CREATE TABLE SQL should contain UNIQUE constraint. Got: ' . $sql); + } + + /** + * Issue #153: Integration test — inserting duplicate values should fail. + */ + public function testIssue153_UniqueConstraintEnforcedOnMSSQL() { + try { + $conn = new ConnectionInfo('mssql', 'sa', getenv('SA_SQL_SERVER_PASSWORD') ?: 'YourStr0ng!Pass', 'testing_db', 'localhost', 1433, [ + 'TrustServerCertificate' => 'true' + ]); + $db = new Database($conn); + $table = AttributeTableBuilder::build(Issue153Entity::class, 'mssql'); + $db->addTable($table); + $db->table('issue153_test')->createTable()->execute(); + + // First insert should succeed + $db->table('issue153_test')->insert(['email' => 'test@example.com'])->execute(); + + // Second insert with same email should fail + $this->expectException(\WebFiori\Database\DatabaseException::class); + $db->table('issue153_test')->insert(['email' => 'test@example.com'])->execute(); + } catch (\WebFiori\Database\DatabaseException $ex) { + if (str_contains($ex->getMessage(), 'Unable to connect')) { + $this->markTestSkipped('MSSQL connection failed: ' . $ex->getMessage()); + } + // Re-throw if it's the expected unique violation + throw $ex; + } finally { + try { + if (isset($db)) { + $db->table('issue153_test')->drop()->execute(); + } + } catch (\Exception $e) { + } + } + } +}