88use KitLoong \MigrationsGenerator \Tests \TestCase ;
99use Mockery \MockInterface ;
1010use PHPUnit \Framework \Attributes \DataProvider ;
11+ use ReflectionClass ;
1112
1213class PgSQLColumnTest extends TestCase
1314{
@@ -35,6 +36,38 @@ public function testSpatialTypeName(string $type): void
3536 $ this ->assertSame (4326 , $ column ->getSpatialSrID ());
3637 }
3738
39+ /**
40+ * @param string[] $expectedValues
41+ */
42+ #[DataProvider('parseEnumValuesFromConstraintProvider ' )]
43+ public function testParseEnumValuesFromConstraint (string $ constraintDefinition , array $ expectedValues ): void
44+ {
45+ $ this ->mock (PgSQLRepository::class, static function (MockInterface $ mock ): void {
46+ $ mock ->shouldReceive ('getCheckConstraintDefinition ' )->andReturn ('' );
47+ });
48+
49+ $ column = new PgSQLColumn ('test_table ' , [
50+ 'name ' => 'status ' ,
51+ 'type_name ' => 'varchar ' ,
52+ 'type ' => 'character varying(255) ' ,
53+ 'collation ' => null ,
54+ 'nullable ' => false ,
55+ 'default ' => null ,
56+ 'auto_increment ' => false ,
57+ 'comment ' => null ,
58+ 'generation ' => null ,
59+ ]);
60+
61+ // Use reflection to access the private method
62+ $ reflection = new ReflectionClass ($ column );
63+ $ method = $ reflection ->getMethod ('parseEnumValuesFromConstraint ' );
64+ $ method ->setAccessible (true );
65+
66+ $ result = $ method ->invoke ($ column , $ constraintDefinition );
67+
68+ $ this ->assertSame ($ expectedValues , $ result );
69+ }
70+
3871 /**
3972 * @return array<string, string[]>
4073 */
@@ -45,4 +78,104 @@ public static function spatialTypeNameProvider(): array
4578 'without dot ' => ['geography(Point,4326) ' ],
4679 ];
4780 }
81+
82+ /**
83+ * @return array<string, array{string, string[]}>
84+ */
85+ public static function parseEnumValuesFromConstraintProvider (): array
86+ {
87+ return [
88+ // Pattern 1: ANY with ARRAY pattern (most common in PostgreSQL)
89+ 'ANY ARRAY with character varying ' => [
90+ "CHECK ((status)::text = ANY ((ARRAY['active'::character varying, 'inactive'::character varying, 'pending'::character varying])::text[])) " ,
91+ ['active ' , 'inactive ' , 'pending ' ],
92+ ],
93+ 'ANY ARRAY with text ' => [
94+ "CHECK ((status)::text = ANY ((ARRAY['draft'::text, 'published'::text])::text[])) " ,
95+ ['draft ' , 'published ' ],
96+ ],
97+ 'ANY ARRAY with mixed types ' => [
98+ "CHECK ((priority)::text = ANY ((ARRAY['low'::character varying, 'medium'::text, 'high'::character varying])::text[])) " ,
99+ ['low ' , 'medium ' , 'high ' ],
100+ ],
101+ 'ANY ARRAY case insensitive ' => [
102+ "check ((status)::text = any ((array['Active'::character varying, 'INACTIVE'::character varying])::text[])) " ,
103+ ['Active ' , 'INACTIVE ' ],
104+ ],
105+ 'ANY ARRAY with extra spaces ' => [
106+ "CHECK ( ( status )::text = ANY ( ( ARRAY[ 'option1'::character varying , 'option2'::character varying ] )::text[] ) ) " ,
107+ ['option1 ' , 'option2 ' ],
108+ ],
109+
110+ // Pattern 2: Multiple OR conditions
111+ 'OR conditions basic ' => [
112+ "CHECK (((status)::text = 'active'::text) OR ((status)::text = 'inactive'::text)) " ,
113+ ['active ' , 'inactive ' ],
114+ ],
115+ 'OR conditions with more values ' => [
116+ "CHECK (((status)::text = 'draft'::text) OR ((status)::text = 'review'::text) OR ((status)::text = 'published'::text)) " ,
117+ ['draft ' , 'review ' , 'published ' ],
118+ ],
119+ 'OR conditions case insensitive ' => [
120+ "check (((status)::text = 'ACTIVE'::text) or ((status)::text = 'inactive'::text)) " ,
121+ ['ACTIVE ' , 'inactive ' ],
122+ ],
123+ 'OR conditions with character varying ' => [
124+ "CHECK (((status)::character varying = 'yes'::character varying) OR ((status)::character varying = 'no'::character varying)) " ,
125+ ['yes ' , 'no ' ],
126+ ],
127+ 'OR conditions with duplicates ' => [
128+ "CHECK (((status)::text = 'active'::text) OR ((status)::text = 'inactive'::text) OR ((status)::text = 'active'::text)) " ,
129+ ['active ' , 'inactive ' ], // Should remove duplicates
130+ ],
131+
132+ // Pattern 3: Simple IN clause
133+ 'IN clause basic ' => [
134+ "CHECK (status IN ('active', 'inactive', 'pending')) " ,
135+ ['active ' , 'inactive ' , 'pending ' ],
136+ ],
137+ 'IN clause case insensitive ' => [
138+ "check (status in ('YES', 'no', 'Maybe')) " ,
139+ ['YES ' , 'no ' , 'Maybe ' ],
140+ ],
141+ 'IN clause with extra spaces ' => [
142+ "CHECK ( status IN ( 'option1' , 'option2' , 'option3' ) ) " ,
143+ ['option1 ' , 'option2 ' , 'option3 ' ],
144+ ],
145+ 'IN clause single value ' => [
146+ "CHECK (status IN ('single')) " ,
147+ ['single ' ],
148+ ],
149+
150+ // Edge cases and invalid patterns
151+ "CHECK (((string IS NULL) OR ((string)::text ~ '^O\.[0-9]+$'::text))) " => [
152+ '' ,
153+ [],
154+ ],
155+ 'empty constraint ' => [
156+ '' ,
157+ [],
158+ ],
159+ 'non-enum constraint ' => [
160+ "CHECK (age > 18) " ,
161+ [],
162+ ],
163+ 'malformed ARRAY pattern ' => [
164+ "CHECK ((status)::text = ANY (ARRAY[missing quotes])) " ,
165+ [],
166+ ],
167+ 'different column name in OR ' => [
168+ "CHECK (((other_column)::text = 'value1'::text) OR ((other_column)::text = 'value2'::text)) " ,
169+ [],
170+ ],
171+ 'different column name in IN ' => [
172+ "CHECK (other_column IN ('value1', 'value2')) " ,
173+ [],
174+ ],
175+ 'no values in ARRAY ' => [
176+ "CHECK ((status)::text = ANY ((ARRAY[])::text[])) " ,
177+ [],
178+ ],
179+ ];
180+ }
48181}
0 commit comments