migration-test.vala 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954
  1. using Invercargill.DataStructures;
  2. using InvercargillSql;
  3. using InvercargillSql.Migrations;
  4. using InvercargillSql.Dialects;
  5. using InvercargillSql.Orm;
  6. // ============================================================================
  7. // TEST MIGRATION CLASSES
  8. // ============================================================================
  9. // --- Auth namespace migrations ---
  10. /**
  11. * Auth V1: Create users table - base migration with no dependencies
  12. */
  13. public class Test_Auth_V1 : Migration {
  14. public override string migration_namespace { get { return "auth"; } }
  15. public override uint64 serial { get { return 1; } }
  16. public override string name { get { return "CreateUsers"; } }
  17. public override void up(MigrationBuilder b) throws SqlError {
  18. b.create_table("users", t => {
  19. t.column<int64?>("id").primary_key().auto_increment();
  20. t.column<string>("email").not_null().unique();
  21. t.column<string>("password_hash").not_null();
  22. });
  23. }
  24. public override void down(MigrationBuilder b) throws SqlError {
  25. b.drop_table("users");
  26. }
  27. }
  28. /**
  29. * Auth V2: Add roles table - depends on auth:1 (within-namespace)
  30. */
  31. public class Test_Auth_V2 : Migration {
  32. public override string migration_namespace { get { return "auth"; } }
  33. public override uint64 serial { get { return 2; } }
  34. public override string name { get { return "AddRoles"; } }
  35. public override string[] dependencies { owned get { return {"auth:1"}; } }
  36. public override void up(MigrationBuilder b) throws SqlError {
  37. b.create_table("roles", t => {
  38. t.column<int64?>("id").primary_key().auto_increment();
  39. t.column<string>("name").not_null().unique();
  40. });
  41. b.alter_table("users", t => {
  42. t.add_column<int64?>("role_id").references("roles", "id");
  43. });
  44. }
  45. public override void down(MigrationBuilder b) throws SqlError {
  46. b.alter_table("users", t => {
  47. t.drop_column("role_id");
  48. });
  49. b.drop_table("roles");
  50. }
  51. }
  52. /**
  53. * Auth V3: Add sessions table - depends on auth:2
  54. */
  55. public class Test_Auth_V3 : Migration {
  56. public override string migration_namespace { get { return "auth"; } }
  57. public override uint64 serial { get { return 3; } }
  58. public override string name { get { return "AddSessions"; } }
  59. public override string[] dependencies { owned get { return {"auth:2"}; } }
  60. public override void up(MigrationBuilder b) throws SqlError {
  61. b.create_table("sessions", t => {
  62. t.column<int64?>("id").primary_key().auto_increment();
  63. t.column<int64?>("user_id").not_null().references("users", "id");
  64. t.column<string>("token").not_null();
  65. });
  66. }
  67. public override void down(MigrationBuilder b) throws SqlError {
  68. b.drop_table("sessions");
  69. }
  70. }
  71. // --- App namespace migrations ---
  72. /**
  73. * App V1: Create orders table - depends on "auth" namespace (any migration)
  74. */
  75. public class Test_App_V1 : Migration {
  76. public override string migration_namespace { get { return "app"; } }
  77. public override uint64 serial { get { return 1; } }
  78. public override string name { get { return "CreateOrders"; } }
  79. public override string[] dependencies { owned get { return {"auth"}; } }
  80. public override void up(MigrationBuilder b) throws SqlError {
  81. b.create_table("orders", t => {
  82. t.column<int64?>("id").primary_key().auto_increment();
  83. t.column<int64?>("user_id").not_null().references("users", "id");
  84. t.column<double?>("total").not_null();
  85. t.column<string>("status").not_null();
  86. t.index("idx_orders_user_id").on_column("user_id");
  87. });
  88. }
  89. public override void down(MigrationBuilder b) throws SqlError {
  90. b.drop_table("orders");
  91. }
  92. }
  93. /**
  94. * App V2: Create order items - depends on auth:1 and app:1
  95. */
  96. public class Test_App_V2 : Migration {
  97. public override string migration_namespace { get { return "app"; } }
  98. public override uint64 serial { get { return 2; } }
  99. public override string name { get { return "CreateOrderItems"; } }
  100. public override string[] dependencies { owned get { return {"auth:1", "app:1"}; } }
  101. public override void up(MigrationBuilder b) throws SqlError {
  102. b.create_table("order_items", t => {
  103. t.column<int64?>("id").primary_key().auto_increment();
  104. t.column<int64?>("order_id").not_null().references("orders", "id");
  105. t.column<string>("product_name").not_null();
  106. t.column<int>("quantity").not_null();
  107. t.column<double?>("price").not_null();
  108. });
  109. }
  110. public override void down(MigrationBuilder b) throws SqlError {
  111. b.drop_table("order_items");
  112. }
  113. }
  114. // --- Logging namespace migrations ---
  115. /**
  116. * Logging V1: Create audit log - no dependencies
  117. */
  118. public class Test_Logging_V1 : Migration {
  119. public override string migration_namespace { get { return "logging"; } }
  120. public override uint64 serial { get { return 1; } }
  121. public override string name { get { return "CreateAuditLog"; } }
  122. // No dependencies
  123. public override void up(MigrationBuilder b) throws SqlError {
  124. b.create_table("audit_log", t => {
  125. t.column<int64?>("id").primary_key().auto_increment();
  126. t.column<string>("action").not_null();
  127. t.column<int64?>("user_id");
  128. t.column<string>("details");
  129. });
  130. }
  131. public override void down(MigrationBuilder b) throws SqlError {
  132. b.drop_table("audit_log");
  133. }
  134. }
  135. /**
  136. * Logging V2: Add timestamps - depends on logging:1
  137. */
  138. public class Test_Logging_V2 : Migration {
  139. public override string migration_namespace { get { return "logging"; } }
  140. public override uint64 serial { get { return 2; } }
  141. public override string name { get { return "AddTimestamps"; } }
  142. public override string[] dependencies { owned get { return {"logging:1"}; } }
  143. public override void up(MigrationBuilder b) throws SqlError {
  144. b.alter_table("audit_log", t => {
  145. t.add_column<string>("created_at").not_null();
  146. });
  147. }
  148. public override void down(MigrationBuilder b) throws SqlError {
  149. b.alter_table("audit_log", t => {
  150. t.drop_column("created_at");
  151. });
  152. }
  153. }
  154. // --- Circular dependency test migrations ---
  155. /**
  156. * Circular A: depends on Circular B
  157. */
  158. public class Test_Circular_A : Migration {
  159. public override string migration_namespace { get { return "circular"; } }
  160. public override uint64 serial { get { return 1; } }
  161. public override string name { get { return "CircularA"; } }
  162. public override string[] dependencies { owned get { return {"circular:2"}; } }
  163. public override void up(MigrationBuilder b) throws SqlError {
  164. b.create_table("circular_a", t => {
  165. t.column<int64?>("id").primary_key().auto_increment();
  166. });
  167. }
  168. public override void down(MigrationBuilder b) throws SqlError {
  169. b.drop_table("circular_a");
  170. }
  171. }
  172. /**
  173. * Circular B: depends on Circular A - creates a cycle!
  174. */
  175. public class Test_Circular_B : Migration {
  176. public override string migration_namespace { get { return "circular"; } }
  177. public override uint64 serial { get { return 2; } }
  178. public override string name { get { return "CircularB"; } }
  179. public override string[] dependencies { owned get { return {"circular:1"}; } }
  180. public override void up(MigrationBuilder b) throws SqlError {
  181. b.create_table("circular_b", t => {
  182. t.column<int64?>("id").primary_key().auto_increment();
  183. });
  184. }
  185. public override void down(MigrationBuilder b) throws SqlError {
  186. b.drop_table("circular_b");
  187. }
  188. }
  189. /**
  190. * Three-way cycle: A -> B -> C -> A
  191. */
  192. public class Test_Cycle_A : Migration {
  193. public override string migration_namespace { get { return "cycle"; } }
  194. public override uint64 serial { get { return 1; } }
  195. public override string name { get { return "CycleA"; } }
  196. public override string[] dependencies { owned get { return {"cycle:3"}; } } // depends on C
  197. public override void up(MigrationBuilder b) throws SqlError {
  198. b.create_table("cycle_a", t => {
  199. t.column<int64?>("id").primary_key().auto_increment();
  200. });
  201. }
  202. public override void down(MigrationBuilder b) throws SqlError {
  203. b.drop_table("cycle_a");
  204. }
  205. }
  206. /**
  207. * Three-way cycle: B
  208. */
  209. public class Test_Cycle_B : Migration {
  210. public override string migration_namespace { get { return "cycle"; } }
  211. public override uint64 serial { get { return 2; } }
  212. public override string name { get { return "CycleB"; } }
  213. public override string[] dependencies { owned get { return {"cycle:1"}; } } // depends on A
  214. public override void up(MigrationBuilder b) throws SqlError {
  215. b.create_table("cycle_b", t => {
  216. t.column<int64?>("id").primary_key().auto_increment();
  217. });
  218. }
  219. public override void down(MigrationBuilder b) throws SqlError {
  220. b.drop_table("cycle_b");
  221. }
  222. }
  223. /**
  224. * Three-way cycle: C
  225. */
  226. public class Test_Cycle_C : Migration {
  227. public override string migration_namespace { get { return "cycle"; } }
  228. public override uint64 serial { get { return 3; } }
  229. public override string name { get { return "CycleC"; } }
  230. public override string[] dependencies { owned get { return {"cycle:2"}; } } // depends on B
  231. public override void up(MigrationBuilder b) throws SqlError {
  232. b.create_table("cycle_c", t => {
  233. t.column<int64?>("id").primary_key().auto_increment();
  234. });
  235. }
  236. public override void down(MigrationBuilder b) throws SqlError {
  237. b.drop_table("cycle_c");
  238. }
  239. }
  240. /**
  241. * Self-dependency: depends on itself
  242. */
  243. public class Test_Self_Dependency : Migration {
  244. public override string migration_namespace { get { return "selfdep"; } }
  245. public override uint64 serial { get { return 1; } }
  246. public override string name { get { return "SelfDependency"; } }
  247. public override string[] dependencies { owned get { return {"selfdep:1"}; } } // depends on itself!
  248. public override void up(MigrationBuilder b) throws SqlError {
  249. b.create_table("self_dep", t => {
  250. t.column<int64?>("id").primary_key().auto_increment();
  251. });
  252. }
  253. public override void down(MigrationBuilder b) throws SqlError {
  254. b.drop_table("self_dep");
  255. }
  256. }
  257. // --- Missing dependency test migrations ---
  258. /**
  259. * Missing namespace dependency
  260. */
  261. public class Test_Missing_Namespace : Migration {
  262. public override string migration_namespace { get { return "missing"; } }
  263. public override uint64 serial { get { return 1; } }
  264. public override string name { get { return "MissingNamespace"; } }
  265. public override string[] dependencies { owned get { return {"nonexistent"}; } } // namespace doesn't exist
  266. public override void up(MigrationBuilder b) throws SqlError {
  267. b.create_table("missing_test", t => {
  268. t.column<int64?>("id").primary_key().auto_increment();
  269. });
  270. }
  271. public override void down(MigrationBuilder b) throws SqlError {
  272. b.drop_table("missing_test");
  273. }
  274. }
  275. /**
  276. * Missing serial dependency
  277. */
  278. public class Test_Missing_Serial : Migration {
  279. public override string migration_namespace { get { return "missing_serial"; } }
  280. public override uint64 serial { get { return 1; } }
  281. public override string name { get { return "MissingSerial"; } }
  282. public override string[] dependencies { owned get { return {"auth:999"}; } } // serial 999 doesn't exist
  283. public override void up(MigrationBuilder b) throws SqlError {
  284. b.create_table("missing_serial_test", t => {
  285. t.column<int64?>("id").primary_key().auto_increment();
  286. });
  287. }
  288. public override void down(MigrationBuilder b) throws SqlError {
  289. b.drop_table("missing_serial_test");
  290. }
  291. }
  292. // --- FK + indexed regression test migrations ---
  293. /**
  294. * Chained FK + indexed: the exact shape from the bug report.
  295. * The index must actually be created on the FK column.
  296. */
  297. public class Test_FkIndexed_V1 : Migration {
  298. public override string migration_namespace { get { return "fkidx"; } }
  299. public override uint64 serial { get { return 1; } }
  300. public override string name { get { return "CreateRepos"; } }
  301. public override void up(MigrationBuilder b) throws SqlError {
  302. b.create_table("repos", t => {
  303. t.column<int64?>("id").primary_key().auto_increment();
  304. t.column<string>("name").not_null();
  305. });
  306. }
  307. public override void down(MigrationBuilder b) throws SqlError {
  308. b.drop_table("repos");
  309. }
  310. }
  311. public class Test_FkIndexed_V2 : Migration {
  312. public override string migration_namespace { get { return "fkidx"; } }
  313. public override uint64 serial { get { return 2; } }
  314. public override string name { get { return "CreateOrgRepos"; } }
  315. public override string[] dependencies { owned get { return {"fkidx:1"}; } }
  316. public override void up(MigrationBuilder b) throws SqlError {
  317. b.create_table("org_repos", t => {
  318. t.column<int64?>("id").primary_key().auto_increment();
  319. t.column<int64?>("repo_id")
  320. .not_null()
  321. .references("repos", "id")
  322. .on_delete_cascade()
  323. .indexed();
  324. t.column<string>("slug").not_null();
  325. });
  326. }
  327. public override void down(MigrationBuilder b) throws SqlError {
  328. b.drop_table("org_repos");
  329. }
  330. }
  331. // ============================================================================
  332. // TEST MAIN
  333. // ============================================================================
  334. public int main(string[] args) {
  335. Test.init(ref args);
  336. // ========================================
  337. // 1. Dependency Parsing Tests
  338. // ========================================
  339. Test.add_func("/migrations/dependency/parse_namespace", test_parse_namespace);
  340. Test.add_func("/migrations/dependency/parse_namespace_serial", test_parse_namespace_serial);
  341. Test.add_func("/migrations/dependency/parse_serial_zero", test_parse_serial_zero);
  342. Test.add_func("/migrations/dependency/parse_empty_namespace", test_parse_empty_namespace);
  343. Test.add_func("/migrations/dependency/parse_empty_serial", test_parse_empty_serial);
  344. Test.add_func("/migrations/dependency/parse_invalid_serial", test_parse_invalid_serial);
  345. Test.add_func("/migrations/dependency/parse_multiple_colons", test_parse_multiple_colons);
  346. Test.add_func("/migrations/dependency/to_string", test_dependency_to_string);
  347. Test.add_func("/migrations/dependency/equality", test_dependency_equality);
  348. // ========================================
  349. // 2. Single Namespace Migration Tests
  350. // ========================================
  351. Test.add_func("/migrations/single/register", test_single_namespace_register);
  352. Test.add_func("/migrations/single/migrate_to_latest", test_single_namespace_migrate_to_latest);
  353. Test.add_func("/migrations/single/get_applied", test_single_namespace_get_applied);
  354. Test.add_func("/migrations/single/get_pending", test_single_namespace_get_pending);
  355. Test.add_func("/migrations/single/rollback", test_single_namespace_rollback);
  356. Test.add_func("/migrations/single/get_current_serial", test_single_namespace_get_current_serial);
  357. // ========================================
  358. // 3. Multi-Namespace Migration Tests
  359. // ========================================
  360. Test.add_func("/migrations/multi/register_multiple_namespaces", test_multi_register_namespaces);
  361. Test.add_func("/migrations/multi/migrate_to_latest_all", test_multi_migrate_to_latest_all);
  362. Test.add_func("/migrations/multi/migrate_specific_namespace", test_multi_migrate_specific_namespace);
  363. Test.add_func("/migrations/multi/filter_applied_by_namespace", test_multi_filter_applied_by_namespace);
  364. Test.add_func("/migrations/multi/filter_pending_by_namespace", test_multi_filter_pending_by_namespace);
  365. // ========================================
  366. // 4. Cross-Namespace Dependency Tests
  367. // ========================================
  368. Test.add_func("/migrations/cross_namespace/namespace_dependency", test_cross_namespace_dependency);
  369. Test.add_func("/migrations/cross_namespace/specific_serial_dependency", test_cross_namespace_specific_serial);
  370. Test.add_func("/migrations/cross_namespace/multiple_dependencies", test_cross_namespace_multiple_deps);
  371. Test.add_func("/migrations/cross_namespace/dependency_order", test_cross_namespace_order);
  372. Test.add_func("/migrations/cross_namespace/missing_namespace_error", test_cross_namespace_missing_namespace);
  373. Test.add_func("/migrations/cross_namespace/missing_serial_error", test_cross_namespace_missing_serial);
  374. // ========================================
  375. // 5. Circular Dependency Detection Tests
  376. // ========================================
  377. Test.add_func("/migrations/circular/two_migration_cycle", test_circular_two_migration);
  378. Test.add_func("/migrations/circular/three_migration_cycle", test_circular_three_migration);
  379. Test.add_func("/migrations/circular/self_dependency", test_circular_self_dependency);
  380. Test.add_func("/migrations/circular/error_message_includes_path", test_circular_error_message);
  381. // ========================================
  382. // 6. Time-Based Rollback Tests
  383. // ========================================
  384. Test.add_func("/migrations/rollback/to_specific_migration", test_rollback_to_specific);
  385. Test.add_func("/migrations/rollback/last_n_migrations", test_rollback_last_n);
  386. Test.add_func("/migrations/rollback/all_migrations", test_rollback_all);
  387. Test.add_func("/migrations/rollback/across_namespaces", test_rollback_across_namespaces);
  388. // ========================================
  389. // 7. Error Message Tests
  390. // ========================================
  391. Test.add_func("/migrations/errors/circular_dependency_message", test_error_circular_message);
  392. Test.add_func("/migrations/errors/unsatisfied_dependency_message", test_error_unsatisfied_message);
  393. Test.add_func("/migrations/errors/invalid_dependency_syntax", test_error_invalid_syntax);
  394. // ========================================
  395. // 8. SQL Generation Tests (retained from original)
  396. // ========================================
  397. Test.add_func("/migrations/sql/create_table", test_create_table_sql);
  398. Test.add_func("/migrations/sql/drop_table", test_drop_table_sql);
  399. Test.add_func("/migrations/sql/create_index", test_create_index_sql);
  400. Test.add_func("/migrations/sql/add_column", test_add_column_sql);
  401. Test.add_func("/migrations/sql/drop_column", test_drop_column_sql);
  402. Test.add_func("/migrations/sql/rename_column", test_rename_column_sql);
  403. // ========================================
  404. // 9. Foreign Key Tests (retained from original)
  405. // ========================================
  406. Test.add_func("/migrations/fk/auto_name", test_fk_creation_with_auto_generated_name);
  407. Test.add_func("/migrations/fk/explicit_name", test_fk_creation_with_explicit_name);
  408. Test.add_func("/migrations/fk/on_delete_actions", test_fk_on_delete_actions);
  409. Test.add_func("/migrations/fk/on_update_actions", test_fk_on_update_actions);
  410. Test.add_func("/migrations/fk/alter_table", test_fk_in_alter_table_add_column);
  411. // ========================================
  412. // 10. Indexed Column Tests (retained from original)
  413. // ========================================
  414. Test.add_func("/migrations/indexed/auto_name", test_indexed_with_auto_generated_name);
  415. Test.add_func("/migrations/indexed/custom_name", test_indexed_with_custom_name);
  416. Test.add_func("/migrations/indexed/unique", test_unique_indexed_creates_unique_index);
  417. Test.add_func("/migrations/indexed/drop", test_drop_index_on);
  418. // ========================================
  419. // 11. FK + Indexed Regression Tests
  420. // ========================================
  421. Test.add_func("/migrations/fk_indexed/e2e_index_created", test_fk_indexed_creates_index_in_database);
  422. Test.add_func("/migrations/fk_indexed/operation_model", test_fk_indexed_emits_index_operation);
  423. Test.add_func("/migrations/fk_indexed/custom_name", test_fk_indexed_with_custom_name);
  424. Test.add_func("/migrations/fk_indexed/not_null_propagates", test_fk_chain_not_null_propagates);
  425. Test.add_func("/migrations/fk_indexed/unique_propagates", test_fk_chain_unique_propagates);
  426. Test.add_func("/migrations/fk_indexed/type_overrides_propagate", test_fk_chain_type_overrides_propagate);
  427. Test.add_func("/migrations/fk_indexed/alter_table_index_created", test_fk_indexed_in_alter_table_add_column);
  428. Test.add_func("/migrations/fk_indexed/double_fk_not_required", test_fk_chain_double_references_returns_fk_builder);
  429. return Test.run();
  430. }
  431. // ============================================================================
  432. // 1. DEPENDENCY PARSING TESTS
  433. // ============================================================================
  434. void test_parse_namespace() {
  435. try {
  436. var dep = Dependency.parse("auth");
  437. assert(dep.namespace == "auth");
  438. assert(dep.serial == null);
  439. } catch (SqlError e) {
  440. assert_not_reached();
  441. }
  442. }
  443. void test_parse_namespace_serial() {
  444. try {
  445. var dep = Dependency.parse("auth:2");
  446. assert(dep.namespace == "auth");
  447. assert(dep.serial != null);
  448. assert(dep.serial == 2);
  449. } catch (SqlError e) {
  450. assert_not_reached();
  451. }
  452. }
  453. void test_parse_serial_zero() {
  454. try {
  455. var dep = Dependency.parse("auth:0");
  456. assert(dep.namespace == "auth");
  457. assert(dep.serial != null);
  458. assert(dep.serial == 0); // Serial 0 is valid, not null
  459. } catch (SqlError e) {
  460. assert_not_reached();
  461. }
  462. }
  463. void test_parse_empty_namespace() {
  464. try {
  465. Dependency.parse(":1");
  466. assert_not_reached(); // Should have thrown
  467. } catch (SqlError e) {
  468. assert("namespace cannot be empty" in e.message || "Invalid dependency syntax" in e.message);
  469. }
  470. }
  471. void test_parse_empty_serial() {
  472. try {
  473. Dependency.parse("auth:");
  474. assert_not_reached(); // Should have thrown
  475. } catch (SqlError e) {
  476. assert("Invalid dependency syntax" in e.message);
  477. }
  478. }
  479. void test_parse_invalid_serial() {
  480. try {
  481. Dependency.parse("auth:abc");
  482. assert_not_reached(); // Should have thrown
  483. } catch (SqlError e) {
  484. assert("serial must be a number" in e.message || "Invalid dependency syntax" in e.message);
  485. }
  486. }
  487. void test_parse_multiple_colons() {
  488. try {
  489. Dependency.parse("auth:1:extra");
  490. assert_not_reached(); // Should have thrown
  491. } catch (SqlError e) {
  492. assert("Invalid dependency syntax" in e.message);
  493. }
  494. }
  495. void test_dependency_to_string() {
  496. try {
  497. var dep1 = Dependency.parse("auth");
  498. assert(dep1.to_string() == "auth");
  499. var dep2 = Dependency.parse("auth:2");
  500. assert(dep2.to_string() == "auth:2");
  501. var dep3 = Dependency.parse("logging:0");
  502. assert(dep3.to_string() == "logging:0");
  503. } catch (SqlError e) {
  504. assert_not_reached();
  505. }
  506. }
  507. void test_dependency_equality() {
  508. try {
  509. var dep1 = Dependency.parse("auth");
  510. var dep2 = Dependency.parse("auth");
  511. assert(dep1.equals_dependency(dep2));
  512. var dep3 = Dependency.parse("auth:1");
  513. var dep4 = Dependency.parse("auth:1");
  514. assert(dep3.equals_dependency(dep4));
  515. var dep5 = Dependency.parse("auth:1");
  516. var dep6 = Dependency.parse("auth:2");
  517. assert(!dep5.equals_dependency(dep6));
  518. var dep7 = Dependency.parse("auth");
  519. var dep8 = Dependency.parse("auth:1");
  520. assert(!dep7.equals_dependency(dep8));
  521. } catch (SqlError e) {
  522. assert_not_reached();
  523. }
  524. }
  525. // ============================================================================
  526. // 2. SINGLE NAMESPACE MIGRATION TESTS
  527. // ============================================================================
  528. void test_single_namespace_register() {
  529. try {
  530. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  531. var dialect = new SqliteDialect();
  532. var runner = new MigrationRunner(conn, dialect);
  533. runner.register_migration(new Test_Auth_V1());
  534. runner.register_migration(new Test_Auth_V2());
  535. // Current serial should be 0 (nothing applied yet)
  536. var current = runner.get_current_serial("auth");
  537. assert(current == 0);
  538. conn.close();
  539. } catch (SqlError e) {
  540. assert_not_reached();
  541. }
  542. }
  543. void test_single_namespace_migrate_to_latest() {
  544. try {
  545. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  546. var dialect = new SqliteDialect();
  547. var runner = new MigrationRunner(conn, dialect);
  548. runner.register_migration(new Test_Auth_V1());
  549. runner.register_migration(new Test_Auth_V2());
  550. runner.migrate_to_latest();
  551. // Both migrations should be applied
  552. var current = runner.get_current_serial("auth");
  553. assert(current == 2);
  554. // Verify tables exist
  555. var cmd = conn.create_command("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
  556. assert(cmd.execute_query().any());
  557. cmd = conn.create_command("SELECT name FROM sqlite_master WHERE type='table' AND name='roles'");
  558. assert(cmd.execute_query().any());
  559. conn.close();
  560. } catch (SqlError e) {
  561. stderr.printf("ERROR: %s\n", e.message);
  562. assert_not_reached();
  563. }
  564. }
  565. void test_single_namespace_get_applied() {
  566. try {
  567. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  568. var dialect = new SqliteDialect();
  569. var runner = new MigrationRunner(conn, dialect);
  570. runner.register_migration(new Test_Auth_V1());
  571. runner.register_migration(new Test_Auth_V2());
  572. runner.register_migration(new Test_Auth_V3());
  573. // Before migration: no applied
  574. var applied = runner.get_applied_migrations("auth");
  575. assert(applied.length == 0);
  576. runner.migrate_to_latest();
  577. // After migration: 3 applied in auth namespace
  578. applied = runner.get_applied_migrations("auth");
  579. assert(applied.length == 3);
  580. // Verify order (by application_order)
  581. assert(applied[0].serial == 1);
  582. assert(applied[1].serial == 2);
  583. assert(applied[2].serial == 3);
  584. conn.close();
  585. } catch (SqlError e) {
  586. assert_not_reached();
  587. }
  588. }
  589. void test_single_namespace_get_pending() {
  590. try {
  591. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  592. var dialect = new SqliteDialect();
  593. var runner = new MigrationRunner(conn, dialect);
  594. runner.register_migration(new Test_Auth_V1());
  595. runner.register_migration(new Test_Auth_V2());
  596. runner.register_migration(new Test_Auth_V3());
  597. // Before migration: all pending
  598. var pending = runner.get_pending_migrations("auth");
  599. assert(pending.length == 3);
  600. // Apply first migration only via migrate_to
  601. runner.migrate_to("auth", 1);
  602. // After: 2 pending (V2 and V3)
  603. pending = runner.get_pending_migrations("auth");
  604. assert(pending.length == 2);
  605. conn.close();
  606. } catch (SqlError e) {
  607. assert_not_reached();
  608. }
  609. }
  610. void test_single_namespace_rollback() {
  611. try {
  612. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  613. var dialect = new SqliteDialect();
  614. var runner = new MigrationRunner(conn, dialect);
  615. runner.register_migration(new Test_Auth_V1());
  616. runner.register_migration(new Test_Auth_V2());
  617. runner.register_migration(new Test_Auth_V3());
  618. runner.migrate_to_latest();
  619. assert(runner.get_current_serial("auth") == 3);
  620. // Rollback one step
  621. runner.rollback(1);
  622. assert(runner.get_current_serial("auth") == 2);
  623. // Rollback two more steps
  624. runner.rollback(2);
  625. assert(runner.get_current_serial("auth") == 0);
  626. conn.close();
  627. } catch (SqlError e) {
  628. assert_not_reached();
  629. }
  630. }
  631. void test_single_namespace_get_current_serial() {
  632. try {
  633. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  634. var dialect = new SqliteDialect();
  635. var runner = new MigrationRunner(conn, dialect);
  636. runner.register_migration(new Test_Auth_V1());
  637. runner.register_migration(new Test_Auth_V2());
  638. // No migrations applied
  639. assert(runner.get_current_serial("auth") == 0);
  640. // Apply first
  641. runner.migrate_to("auth", 1);
  642. assert(runner.get_current_serial("auth") == 1);
  643. // Apply second
  644. runner.migrate_to("auth", 2);
  645. assert(runner.get_current_serial("auth") == 2);
  646. // Unknown namespace returns 0
  647. assert(runner.get_current_serial("unknown") == 0);
  648. conn.close();
  649. } catch (SqlError e) {
  650. assert_not_reached();
  651. }
  652. }
  653. // ============================================================================
  654. // 3. MULTI-NAMESPACE MIGRATION TESTS
  655. // ============================================================================
  656. void test_multi_register_namespaces() {
  657. try {
  658. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  659. var dialect = new SqliteDialect();
  660. var runner = new MigrationRunner(conn, dialect);
  661. // Register migrations from multiple namespaces
  662. runner.register_migration(new Test_Auth_V1());
  663. runner.register_migration(new Test_Auth_V2());
  664. runner.register_migration(new Test_App_V1());
  665. runner.register_migration(new Test_App_V2());
  666. runner.register_migration(new Test_Logging_V1());
  667. // All should start at 0
  668. assert(runner.get_current_serial("auth") == 0);
  669. assert(runner.get_current_serial("app") == 0);
  670. assert(runner.get_current_serial("logging") == 0);
  671. conn.close();
  672. } catch (SqlError e) {
  673. assert_not_reached();
  674. }
  675. }
  676. void test_multi_migrate_to_latest_all() {
  677. try {
  678. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  679. var dialect = new SqliteDialect();
  680. var runner = new MigrationRunner(conn, dialect);
  681. // Register all migrations
  682. runner.register_migration(new Test_Auth_V1());
  683. runner.register_migration(new Test_Auth_V2());
  684. runner.register_migration(new Test_App_V1());
  685. runner.register_migration(new Test_App_V2());
  686. runner.register_migration(new Test_Logging_V1());
  687. runner.register_migration(new Test_Logging_V2());
  688. runner.migrate_to_latest();
  689. // All namespaces should be at their latest
  690. assert(runner.get_current_serial("auth") == 2);
  691. assert(runner.get_current_serial("app") == 2);
  692. assert(runner.get_current_serial("logging") == 2);
  693. // Verify all tables exist
  694. string[] expected_tables = {"users", "roles", "orders", "order_items", "audit_log"};
  695. foreach (var table in expected_tables) {
  696. var cmd = conn.create_command(
  697. "SELECT name FROM sqlite_master WHERE type='table' AND name='%s'".printf(table)
  698. );
  699. assert(cmd.execute_query().any());
  700. }
  701. conn.close();
  702. } catch (SqlError e) {
  703. assert_not_reached();
  704. }
  705. }
  706. void test_multi_migrate_specific_namespace() {
  707. try {
  708. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  709. var dialect = new SqliteDialect();
  710. var runner = new MigrationRunner(conn, dialect);
  711. runner.register_migration(new Test_Auth_V1());
  712. runner.register_migration(new Test_Auth_V2());
  713. runner.register_migration(new Test_App_V1()); // depends on "auth"
  714. runner.register_migration(new Test_Logging_V1()); // no dependencies
  715. // Migrate only the auth namespace
  716. runner.migrate_to_latest_for_namespace("auth");
  717. // Auth should be migrated
  718. assert(runner.get_current_serial("auth") == 2);
  719. // App and logging should not be migrated (even though logging has no deps)
  720. assert(runner.get_current_serial("app") == 0);
  721. assert(runner.get_current_serial("logging") == 0);
  722. conn.close();
  723. } catch (SqlError e) {
  724. assert_not_reached();
  725. }
  726. }
  727. void test_multi_filter_applied_by_namespace() {
  728. try {
  729. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  730. var dialect = new SqliteDialect();
  731. var runner = new MigrationRunner(conn, dialect);
  732. runner.register_migration(new Test_Auth_V1());
  733. runner.register_migration(new Test_Auth_V2());
  734. runner.register_migration(new Test_App_V1());
  735. runner.register_migration(new Test_Logging_V1());
  736. runner.migrate_to_latest();
  737. // Get applied for auth only
  738. var auth_applied = runner.get_applied_migrations("auth");
  739. assert(auth_applied.length == 2);
  740. foreach (var record in auth_applied) {
  741. assert(record.migration_namespace == "auth");
  742. }
  743. // Get applied for app only
  744. var app_applied = runner.get_applied_migrations("app");
  745. assert(app_applied.length == 1);
  746. foreach (var record in app_applied) {
  747. assert(record.migration_namespace == "app");
  748. }
  749. // Get all applied
  750. var all_applied = runner.get_applied_migrations();
  751. assert(all_applied.length == 4);
  752. conn.close();
  753. } catch (SqlError e) {
  754. assert_not_reached();
  755. }
  756. }
  757. void test_multi_filter_pending_by_namespace() {
  758. try {
  759. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  760. var dialect = new SqliteDialect();
  761. var runner = new MigrationRunner(conn, dialect);
  762. runner.register_migration(new Test_Auth_V1());
  763. runner.register_migration(new Test_Auth_V2());
  764. runner.register_migration(new Test_Auth_V3());
  765. runner.register_migration(new Test_App_V1());
  766. runner.register_migration(new Test_Logging_V1());
  767. // Apply only auth:1 and auth:2
  768. runner.migrate_to("auth", 2);
  769. // Pending for auth should be just V3
  770. var auth_pending = runner.get_pending_migrations("auth");
  771. assert(auth_pending.length == 1);
  772. assert(auth_pending[0].serial == 3);
  773. // Pending for app should be V1
  774. var app_pending = runner.get_pending_migrations("app");
  775. assert(app_pending.length == 1);
  776. // Pending for logging should be V1
  777. var log_pending = runner.get_pending_migrations("logging");
  778. assert(log_pending.length == 1);
  779. conn.close();
  780. } catch (SqlError e) {
  781. assert_not_reached();
  782. }
  783. }
  784. // ============================================================================
  785. // 4. CROSS-NAMESPACE DEPENDENCY TESTS
  786. // ============================================================================
  787. void test_cross_namespace_dependency() {
  788. try {
  789. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  790. var dialect = new SqliteDialect();
  791. var runner = new MigrationRunner(conn, dialect);
  792. // App V1 depends on "auth" namespace (any migration)
  793. runner.register_migration(new Test_Auth_V1());
  794. runner.register_migration(new Test_App_V1());
  795. runner.migrate_to_latest();
  796. // Both should be applied
  797. assert(runner.get_current_serial("auth") == 1);
  798. assert(runner.get_current_serial("app") == 1);
  799. // Verify auth was applied BEFORE app by checking application_order
  800. var all_applied = runner.get_applied_migrations();
  801. int64? auth_order = null;
  802. int64? app_order = null;
  803. foreach (var record in all_applied) {
  804. if (record.migration_namespace == "auth" && record.serial == 1) {
  805. auth_order = record.application_order;
  806. }
  807. if (record.migration_namespace == "app" && record.serial == 1) {
  808. app_order = record.application_order;
  809. }
  810. }
  811. assert(auth_order != null);
  812. assert(app_order != null);
  813. assert(auth_order < app_order); // Auth must be applied before app
  814. conn.close();
  815. } catch (SqlError e) {
  816. assert_not_reached();
  817. }
  818. }
  819. void test_cross_namespace_specific_serial() {
  820. try {
  821. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  822. var dialect = new SqliteDialect();
  823. var runner = new MigrationRunner(conn, dialect);
  824. // App V2 depends on auth:1 specifically
  825. runner.register_migration(new Test_Auth_V1());
  826. runner.register_migration(new Test_Auth_V2());
  827. runner.register_migration(new Test_App_V2()); // depends on auth:1, app:1
  828. runner.register_migration(new Test_App_V1()); // depends on auth
  829. runner.migrate_to_latest();
  830. // All should be applied
  831. assert(runner.get_current_serial("auth") == 2);
  832. assert(runner.get_current_serial("app") == 2);
  833. conn.close();
  834. } catch (SqlError e) {
  835. stderr.printf("ERROR in specific_serial: %s\n", e.message);
  836. assert_not_reached();
  837. }
  838. }
  839. void test_cross_namespace_multiple_deps() {
  840. try {
  841. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  842. var dialect = new SqliteDialect();
  843. var runner = new MigrationRunner(conn, dialect);
  844. // App V2 has multiple dependencies: auth:1 and app:1
  845. runner.register_migration(new Test_Auth_V1());
  846. runner.register_migration(new Test_App_V1());
  847. runner.register_migration(new Test_App_V2());
  848. runner.migrate_to_latest();
  849. // Verify all dependencies were applied before App V2
  850. var all_applied = runner.get_applied_migrations();
  851. int64? auth1_order = null;
  852. int64? app1_order = null;
  853. int64? app2_order = null;
  854. foreach (var record in all_applied) {
  855. if (record.migration_namespace == "auth" && record.serial == 1) auth1_order = record.application_order;
  856. if (record.migration_namespace == "app" && record.serial == 1) app1_order = record.application_order;
  857. if (record.migration_namespace == "app" && record.serial == 2) app2_order = record.application_order;
  858. }
  859. assert(auth1_order != null);
  860. assert(app1_order != null);
  861. assert(app2_order != null);
  862. // App V2 must come after both auth:1 and app:1
  863. assert(app2_order > auth1_order);
  864. assert(app2_order > app1_order);
  865. conn.close();
  866. } catch (SqlError e) {
  867. assert_not_reached();
  868. }
  869. }
  870. void test_cross_namespace_order() {
  871. try {
  872. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  873. var dialect = new SqliteDialect();
  874. var runner = new MigrationRunner(conn, dialect);
  875. // Register in random order
  876. runner.register_migration(new Test_App_V2()); // depends on auth:1, app:1
  877. runner.register_migration(new Test_Logging_V1()); // no deps
  878. runner.register_migration(new Test_Auth_V2()); // depends on auth:1
  879. runner.register_migration(new Test_App_V1()); // depends on auth
  880. runner.register_migration(new Test_Auth_V1()); // no deps
  881. runner.migrate_to_latest();
  882. // Verify correct order regardless of registration order
  883. var all_applied = runner.get_applied_migrations();
  884. // Build a map of application orders
  885. var orders = new Dictionary<string, int64?>();
  886. foreach (var record in all_applied) {
  887. string key = "%s:%s".printf(record.migration_namespace, record.serial.to_string());
  888. orders.set(key, record.application_order);
  889. }
  890. // Verify ordering constraints
  891. // auth:1 must come before auth:2
  892. assert(orders.get("auth:1") < orders.get("auth:2"));
  893. // auth:1 must come before app:1 (app:1 depends on "auth")
  894. assert(orders.get("auth:1") < orders.get("app:1"));
  895. // app:1 must come before app:2
  896. assert(orders.get("app:1") < orders.get("app:2"));
  897. // auth:1 must come before app:2 (app:2 depends on auth:1)
  898. assert(orders.get("auth:1") < orders.get("app:2"));
  899. conn.close();
  900. } catch (SqlError e) {
  901. assert_not_reached();
  902. }
  903. }
  904. void test_cross_namespace_missing_namespace() {
  905. try {
  906. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  907. var dialect = new SqliteDialect();
  908. var runner = new MigrationRunner(conn, dialect);
  909. // Register migration that depends on non-existent namespace
  910. runner.register_migration(new Test_Missing_Namespace());
  911. runner.validate_dependencies();
  912. assert_not_reached(); // Should have thrown
  913. } catch (SqlError e) {
  914. assert("requires namespace" in e.message || "Unsatisfied dependency" in e.message);
  915. }
  916. }
  917. void test_cross_namespace_missing_serial() {
  918. try {
  919. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  920. var dialect = new SqliteDialect();
  921. var runner = new MigrationRunner(conn, dialect);
  922. // Register auth:1 and migration that depends on auth:999
  923. runner.register_migration(new Test_Auth_V1());
  924. runner.register_migration(new Test_Missing_Serial());
  925. runner.validate_dependencies();
  926. assert_not_reached(); // Should have thrown
  927. } catch (SqlError e) {
  928. assert("serial" in e.message || "Unsatisfied dependency" in e.message);
  929. }
  930. }
  931. // ============================================================================
  932. // 5. CIRCULAR DEPENDENCY DETECTION TESTS
  933. // ============================================================================
  934. void test_circular_two_migration() {
  935. try {
  936. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  937. var dialect = new SqliteDialect();
  938. var runner = new MigrationRunner(conn, dialect);
  939. // Circular A depends on B, B depends on A
  940. runner.register_migration(new Test_Circular_A());
  941. runner.register_migration(new Test_Circular_B());
  942. runner.migrate_to_latest();
  943. assert_not_reached(); // Should have thrown
  944. } catch (SqlError e) {
  945. assert("Circular dependency" in e.message);
  946. }
  947. }
  948. void test_circular_three_migration() {
  949. try {
  950. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  951. var dialect = new SqliteDialect();
  952. var runner = new MigrationRunner(conn, dialect);
  953. // Three-way cycle: A -> B -> C -> A
  954. runner.register_migration(new Test_Cycle_A());
  955. runner.register_migration(new Test_Cycle_B());
  956. runner.register_migration(new Test_Cycle_C());
  957. runner.migrate_to_latest();
  958. assert_not_reached(); // Should have thrown
  959. } catch (SqlError e) {
  960. assert("Circular dependency" in e.message);
  961. }
  962. }
  963. void test_circular_self_dependency() {
  964. try {
  965. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  966. var dialect = new SqliteDialect();
  967. var runner = new MigrationRunner(conn, dialect);
  968. // Self-dependency
  969. runner.register_migration(new Test_Self_Dependency());
  970. runner.migrate_to_latest();
  971. assert_not_reached(); // Should have thrown
  972. } catch (SqlError e) {
  973. assert("Circular dependency" in e.message);
  974. }
  975. }
  976. void test_circular_error_message() {
  977. try {
  978. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  979. var dialect = new SqliteDialect();
  980. var runner = new MigrationRunner(conn, dialect);
  981. runner.register_migration(new Test_Circular_A());
  982. runner.register_migration(new Test_Circular_B());
  983. runner.migrate_to_latest();
  984. assert_not_reached();
  985. } catch (SqlError e) {
  986. // Error message should include the cycle path
  987. assert("Circular dependency" in e.message);
  988. // Should show the cycle with arrow notation
  989. assert("→" in e.message || "->" in e.message || "circular" in e.message.down());
  990. }
  991. }
  992. // ============================================================================
  993. // 6. TIME-BASED ROLLBACK TESTS
  994. // ============================================================================
  995. void test_rollback_to_specific() {
  996. try {
  997. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  998. var dialect = new SqliteDialect();
  999. var runner = new MigrationRunner(conn, dialect);
  1000. runner.register_migration(new Test_Auth_V1());
  1001. runner.register_migration(new Test_Auth_V2());
  1002. runner.register_migration(new Test_Auth_V3());
  1003. runner.migrate_to_latest();
  1004. assert(runner.get_current_serial("auth") == 3);
  1005. // Rollback to auth:1
  1006. runner.rollback_to("auth", 1);
  1007. assert(runner.get_current_serial("auth") == 1);
  1008. // V2 and V3 should be rolled back (tables dropped)
  1009. // Only users table should exist
  1010. var cmd = conn.create_command("SELECT name FROM sqlite_master WHERE type='table' AND name='roles'");
  1011. assert(!cmd.execute_query().any());
  1012. conn.close();
  1013. } catch (SqlError e) {
  1014. assert_not_reached();
  1015. }
  1016. }
  1017. void test_rollback_last_n() {
  1018. try {
  1019. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  1020. var dialect = new SqliteDialect();
  1021. var runner = new MigrationRunner(conn, dialect);
  1022. runner.register_migration(new Test_Auth_V1());
  1023. runner.register_migration(new Test_Auth_V2());
  1024. runner.register_migration(new Test_Auth_V3());
  1025. runner.migrate_to_latest();
  1026. // Rollback last 2 migrations
  1027. runner.rollback(2);
  1028. assert(runner.get_current_serial("auth") == 1);
  1029. conn.close();
  1030. } catch (SqlError e) {
  1031. assert_not_reached();
  1032. }
  1033. }
  1034. void test_rollback_all() {
  1035. try {
  1036. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  1037. var dialect = new SqliteDialect();
  1038. var runner = new MigrationRunner(conn, dialect);
  1039. runner.register_migration(new Test_Auth_V1());
  1040. runner.register_migration(new Test_Auth_V2());
  1041. runner.register_migration(new Test_App_V1());
  1042. runner.register_migration(new Test_Logging_V1());
  1043. runner.migrate_to_latest();
  1044. // Rollback all
  1045. runner.rollback_all();
  1046. assert(runner.get_current_serial("auth") == 0);
  1047. assert(runner.get_current_serial("app") == 0);
  1048. assert(runner.get_current_serial("logging") == 0);
  1049. // No migrations should be applied
  1050. var applied = runner.get_applied_migrations();
  1051. assert(applied.length == 0);
  1052. conn.close();
  1053. } catch (SqlError e) {
  1054. assert_not_reached();
  1055. }
  1056. }
  1057. void test_rollback_across_namespaces() {
  1058. try {
  1059. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  1060. var dialect = new SqliteDialect();
  1061. var runner = new MigrationRunner(conn, dialect);
  1062. // Register migrations that will apply in a specific order
  1063. runner.register_migration(new Test_Auth_V1()); // 1st
  1064. runner.register_migration(new Test_Auth_V2()); // 2nd (depends on auth:1)
  1065. runner.register_migration(new Test_App_V1()); // 3rd (depends on auth)
  1066. runner.register_migration(new Test_Logging_V1()); // 4th (no deps)
  1067. runner.migrate_to_latest();
  1068. // Verify all are applied
  1069. var applied = runner.get_applied_migrations();
  1070. assert(applied.length == 4);
  1071. // Rollback to auth:1 should roll back logging:1, app:1, auth:2 in that order
  1072. runner.rollback_to("auth", 1);
  1073. // Only auth:1 should remain
  1074. applied = runner.get_applied_migrations();
  1075. assert(applied.length == 1);
  1076. assert(applied[0].migration_namespace == "auth");
  1077. assert(applied[0].serial == 1);
  1078. conn.close();
  1079. } catch (SqlError e) {
  1080. assert_not_reached();
  1081. }
  1082. }
  1083. // ============================================================================
  1084. // 7. ERROR MESSAGE TESTS
  1085. // ============================================================================
  1086. void test_error_circular_message() {
  1087. try {
  1088. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  1089. var dialect = new SqliteDialect();
  1090. var runner = new MigrationRunner(conn, dialect);
  1091. runner.register_migration(new Test_Circular_A());
  1092. runner.register_migration(new Test_Circular_B());
  1093. runner.validate_dependencies();
  1094. assert_not_reached();
  1095. } catch (SqlError e) {
  1096. // Verify error message format
  1097. assert("Circular dependency detected" in e.message);
  1098. }
  1099. }
  1100. void test_error_unsatisfied_message() {
  1101. try {
  1102. var conn = ConnectionFactory.create_and_open("sqlite::memory:");
  1103. var dialect = new SqliteDialect();
  1104. var runner = new MigrationRunner(conn, dialect);
  1105. runner.register_migration(new Test_Auth_V1());
  1106. runner.register_migration(new Test_Missing_Serial()); // depends on auth:999
  1107. runner.validate_dependencies();
  1108. assert_not_reached();
  1109. } catch (SqlError e) {
  1110. // Verify error message contains useful information
  1111. assert("Unsatisfied dependency" in e.message);
  1112. assert("999" in e.message); // The missing serial
  1113. assert("auth" in e.message); // The namespace
  1114. }
  1115. }
  1116. void test_error_invalid_syntax() {
  1117. try {
  1118. Dependency.parse("auth:abc:extra");
  1119. assert_not_reached();
  1120. } catch (SqlError e) {
  1121. assert("Invalid dependency syntax" in e.message);
  1122. }
  1123. }
  1124. // ============================================================================
  1125. // 8. SQL GENERATION TESTS (retained from original)
  1126. // ============================================================================
  1127. void test_create_table_sql() {
  1128. var dialect = new SqliteDialect();
  1129. var op = new CreateTableOperation() { table_name = "test" };
  1130. op.columns.add(new ColumnDefinition() {
  1131. name = "id",
  1132. column_type = ColumnType.INT_64,
  1133. is_primary_key = true,
  1134. auto_increment = true
  1135. });
  1136. op.columns.add(new ColumnDefinition() {
  1137. name = "name",
  1138. column_type = ColumnType.TEXT,
  1139. is_required = true
  1140. });
  1141. var sql = dialect.create_table_sql(op);
  1142. assert("CREATE TABLE test" in sql);
  1143. assert("id" in sql);
  1144. assert("INTEGER" in sql);
  1145. assert("PRIMARY KEY" in sql);
  1146. assert("name" in sql);
  1147. assert("TEXT" in sql);
  1148. assert("NOT NULL" in sql);
  1149. }
  1150. void test_drop_table_sql() {
  1151. var dialect = new SqliteDialect();
  1152. var op = new DropTableOperation() { table_name = "test" };
  1153. var sql = dialect.drop_table_sql(op);
  1154. assert(sql == "DROP TABLE IF EXISTS test");
  1155. }
  1156. void test_create_index_sql() {
  1157. var dialect = new SqliteDialect();
  1158. var op = new CreateIndexOperation() {
  1159. index_name = "idx_test",
  1160. table_name = "test",
  1161. is_unique = false
  1162. };
  1163. op.columns.add("name");
  1164. var sql = dialect.create_index_sql(op);
  1165. assert("CREATE INDEX idx_test ON test (name)" == sql);
  1166. }
  1167. void test_add_column_sql() {
  1168. var dialect = new SqliteDialect();
  1169. var op = new AddColumnOperation() {
  1170. table_name = "users",
  1171. column = new ColumnDefinition() {
  1172. name = "age",
  1173. column_type = ColumnType.INT_32
  1174. }
  1175. };
  1176. var sql = dialect.add_column_sql(op);
  1177. assert("ALTER TABLE users ADD COLUMN age" in sql);
  1178. assert("INTEGER" in sql);
  1179. }
  1180. void test_drop_column_sql() {
  1181. var dialect = new SqliteDialect();
  1182. var op = new DropColumnOperation() {
  1183. table_name = "users",
  1184. column_name = "age"
  1185. };
  1186. var sql = dialect.drop_column_sql(op);
  1187. assert("ALTER TABLE users DROP COLUMN age" == sql);
  1188. }
  1189. void test_rename_column_sql() {
  1190. var dialect = new SqliteDialect();
  1191. var op = new RenameColumnOperation() {
  1192. table_name = "users",
  1193. old_name = "old_name",
  1194. new_name = "new_name"
  1195. };
  1196. var sql = dialect.rename_column_sql(op);
  1197. assert("ALTER TABLE users RENAME COLUMN old_name TO new_name" == sql);
  1198. }
  1199. // ============================================================================
  1200. // 9. FOREIGN KEY TESTS (retained from original)
  1201. // ============================================================================
  1202. void test_fk_creation_with_auto_generated_name() {
  1203. var dialect = new SqliteDialect();
  1204. var builder = new MigrationBuilder(dialect);
  1205. builder.create_table("orders", t => {
  1206. t.column<int64?>("id").primary_key().auto_increment();
  1207. t.column<int64?>("user_id")
  1208. .not_null()
  1209. .references("users", "id");
  1210. });
  1211. var ops = builder.get_operations();
  1212. assert(ops.length == 1);
  1213. var table_op = ops[0] as CreateTableOperation;
  1214. assert(table_op != null);
  1215. assert(table_op.constraints.length == 1);
  1216. var constraint = table_op.constraints.get(0);
  1217. assert(constraint.constraint_type == "FOREIGN KEY");
  1218. assert(constraint.name == "fk_orders_user_id");
  1219. assert(constraint.reference_table == "users");
  1220. assert(constraint.reference_columns.get(0) == "id");
  1221. assert(constraint.columns.get(0) == "user_id");
  1222. var sql = dialect.create_table_sql(table_op);
  1223. assert("FOREIGN KEY" in sql);
  1224. assert("REFERENCES users (id)" in sql);
  1225. assert("fk_orders_user_id" in sql);
  1226. }
  1227. void test_fk_creation_with_explicit_name() {
  1228. var dialect = new SqliteDialect();
  1229. var builder = new MigrationBuilder(dialect);
  1230. builder.create_table("orders", t => {
  1231. t.column<int64?>("id").primary_key().auto_increment();
  1232. t.column<int64?>("user_id")
  1233. .not_null()
  1234. .references("users", "id")
  1235. .name("custom_fk_orders_users");
  1236. });
  1237. var ops = builder.get_operations();
  1238. var table_op = ops[0] as CreateTableOperation;
  1239. assert(table_op != null);
  1240. var constraint = table_op.constraints.get(0);
  1241. assert(constraint.name == "custom_fk_orders_users");
  1242. var sql = dialect.create_table_sql(table_op);
  1243. assert("custom_fk_orders_users" in sql);
  1244. }
  1245. void test_fk_on_delete_actions() {
  1246. var dialect = new SqliteDialect();
  1247. // Test CASCADE
  1248. var builder = new MigrationBuilder(dialect);
  1249. builder.create_table("orders", t => {
  1250. t.column<int64?>("user_id")
  1251. .references("users", "id")
  1252. .on_delete_cascade();
  1253. });
  1254. var ops = builder.get_operations();
  1255. var table_op = ops[0] as CreateTableOperation;
  1256. var constraint = table_op.constraints.get(0);
  1257. assert(constraint.on_delete_action == ReferentialAction.CASCADE);
  1258. var sql = dialect.create_table_sql(table_op);
  1259. assert("ON DELETE CASCADE" in sql);
  1260. // Test SET NULL
  1261. builder = new MigrationBuilder(dialect);
  1262. builder.create_table("orders", t => {
  1263. t.column<int64?>("user_id")
  1264. .references("users", "id")
  1265. .on_delete_set_null();
  1266. });
  1267. ops = builder.get_operations();
  1268. table_op = ops[0] as CreateTableOperation;
  1269. constraint = table_op.constraints.get(0);
  1270. assert(constraint.on_delete_action == ReferentialAction.SET_NULL);
  1271. sql = dialect.create_table_sql(table_op);
  1272. assert("ON DELETE SET NULL" in sql);
  1273. // Test RESTRICT
  1274. builder = new MigrationBuilder(dialect);
  1275. builder.create_table("orders", t => {
  1276. t.column<int64?>("user_id")
  1277. .references("users", "id")
  1278. .on_delete_restrict();
  1279. });
  1280. ops = builder.get_operations();
  1281. table_op = ops[0] as CreateTableOperation;
  1282. constraint = table_op.constraints.get(0);
  1283. assert(constraint.on_delete_action == ReferentialAction.RESTRICT);
  1284. sql = dialect.create_table_sql(table_op);
  1285. assert("ON DELETE RESTRICT" in sql);
  1286. }
  1287. void test_fk_on_update_actions() {
  1288. var dialect = new SqliteDialect();
  1289. // Test CASCADE
  1290. var builder = new MigrationBuilder(dialect);
  1291. builder.create_table("orders", t => {
  1292. t.column<int64?>("user_id")
  1293. .references("users", "id")
  1294. .on_update_cascade();
  1295. });
  1296. var ops = builder.get_operations();
  1297. var table_op = ops[0] as CreateTableOperation;
  1298. var constraint = table_op.constraints.get(0);
  1299. assert(constraint.on_update_action == ReferentialAction.CASCADE);
  1300. var sql = dialect.create_table_sql(table_op);
  1301. assert("ON UPDATE CASCADE" in sql);
  1302. // Test SET NULL
  1303. builder = new MigrationBuilder(dialect);
  1304. builder.create_table("orders", t => {
  1305. t.column<int64?>("user_id")
  1306. .references("users", "id")
  1307. .on_update_set_null();
  1308. });
  1309. ops = builder.get_operations();
  1310. table_op = ops[0] as CreateTableOperation;
  1311. constraint = table_op.constraints.get(0);
  1312. assert(constraint.on_update_action == ReferentialAction.SET_NULL);
  1313. sql = dialect.create_table_sql(table_op);
  1314. assert("ON UPDATE SET NULL" in sql);
  1315. }
  1316. void test_fk_in_alter_table_add_column() {
  1317. var dialect = new SqliteDialect();
  1318. var builder = new MigrationBuilder(dialect);
  1319. builder.alter_table("orders", t => {
  1320. t.add_column<int64?>("product_id")
  1321. .not_null()
  1322. .references("products", "id")
  1323. .on_delete_restrict();
  1324. });
  1325. var ops = builder.get_operations();
  1326. assert(ops.length == 1);
  1327. var add_col_op = ops[0] as AddColumnOperation;
  1328. assert(add_col_op != null);
  1329. assert(add_col_op.foreign_key_constraint != null);
  1330. var fk = add_col_op.foreign_key_constraint;
  1331. assert(fk.constraint_type == "FOREIGN KEY");
  1332. assert(fk.reference_table == "products");
  1333. assert(fk.reference_columns.get(0) == "id");
  1334. assert(fk.on_delete_action == ReferentialAction.RESTRICT);
  1335. var sql = dialect.add_column_sql(add_col_op);
  1336. assert("REFERENCES products (id)" in sql);
  1337. assert("ON DELETE RESTRICT" in sql);
  1338. }
  1339. // ============================================================================
  1340. // 10. INDEXED COLUMN TESTS (retained from original)
  1341. // ============================================================================
  1342. void test_indexed_with_auto_generated_name() {
  1343. var dialect = new SqliteDialect();
  1344. var builder = new MigrationBuilder(dialect);
  1345. builder.create_table("users", t => {
  1346. t.column<int64?>("id").primary_key().auto_increment();
  1347. t.column<string>("email")
  1348. .not_null()
  1349. .indexed();
  1350. });
  1351. var ops = builder.get_operations();
  1352. assert(ops.length == 2);
  1353. var table_op = ops[0] as CreateTableOperation;
  1354. assert(table_op != null);
  1355. var idx_op = ops[1] as CreateIndexOperation;
  1356. assert(idx_op != null);
  1357. assert(idx_op.index_name == "idx_users_email");
  1358. assert(idx_op.table_name == "users");
  1359. assert(idx_op.columns.length == 1);
  1360. assert(idx_op.columns.get(0) == "email");
  1361. assert(idx_op.is_unique == false);
  1362. }
  1363. void test_indexed_with_custom_name() {
  1364. var dialect = new SqliteDialect();
  1365. var builder = new MigrationBuilder(dialect);
  1366. builder.create_table("users", t => {
  1367. t.column<int64?>("id").primary_key().auto_increment();
  1368. t.column<string>("email")
  1369. .not_null()
  1370. .indexed("idx_users_email_address");
  1371. });
  1372. var ops = builder.get_operations();
  1373. assert(ops.length == 2);
  1374. var idx_op = ops[1] as CreateIndexOperation;
  1375. assert(idx_op != null);
  1376. assert(idx_op.index_name == "idx_users_email_address");
  1377. }
  1378. void test_unique_indexed_creates_unique_index() {
  1379. var dialect = new SqliteDialect();
  1380. var builder = new MigrationBuilder(dialect);
  1381. builder.create_table("users", t => {
  1382. t.column<int64?>("id").primary_key().auto_increment();
  1383. t.column<string>("email")
  1384. .not_null()
  1385. .unique()
  1386. .indexed();
  1387. });
  1388. var ops = builder.get_operations();
  1389. assert(ops.length == 2);
  1390. var idx_op = ops[1] as CreateIndexOperation;
  1391. assert(idx_op != null);
  1392. assert(idx_op.is_unique == true);
  1393. var sql = dialect.create_index_sql(idx_op);
  1394. assert("CREATE UNIQUE INDEX" in sql);
  1395. }
  1396. void test_drop_index_on() {
  1397. var dialect = new SqliteDialect();
  1398. var builder = new MigrationBuilder(dialect);
  1399. builder.alter_table("users", t => {
  1400. t.drop_index_on("email");
  1401. });
  1402. var ops = builder.get_operations();
  1403. assert(ops.length == 1);
  1404. var drop_op = ops[0] as DropIndexOperation;
  1405. assert(drop_op != null);
  1406. assert(drop_op.index_name == "idx_users_email");
  1407. assert(drop_op.table_name == "users");
  1408. }
  1409. // ============================================================================
  1410. // 11. FK + INDEXED REGRESSION TESTS
  1411. //
  1412. // Regression guard for the dropped-flag bug class: ForeignKeyBuilder wraps a
  1413. // MigrationColumnBuilder, and any column method called after .references()
  1414. // (indexed(), not_null(), unique(), type_*(), ...) used to mutate the
  1415. // wrapper's own throwaway state instead of the wrapped column builder that
  1416. // TableBuilder/AlterTableBuilder read at build time.
  1417. // ============================================================================
  1418. /**
  1419. * End-to-end: the chained FK+indexed form must produce a real index in an
  1420. * actual SQLite database file, verified via sqlite_master (what
  1421. * `sqlite3 db .schema` reads).
  1422. */
  1423. void test_fk_indexed_creates_index_in_database() {
  1424. string db_path = GLib.Path.build_filename(
  1425. GLib.Environment.get_tmp_dir(),
  1426. "invercargill-sql-fk-indexed-test.db"
  1427. );
  1428. GLib.FileUtils.remove(db_path); // ignore missing
  1429. try {
  1430. var conn = ConnectionFactory.create_and_open(@"sqlite://$db_path");
  1431. var dialect = new SqliteDialect();
  1432. var runner = new MigrationRunner(conn, dialect);
  1433. runner.register_migration(new Test_FkIndexed_V1());
  1434. runner.register_migration(new Test_FkIndexed_V2());
  1435. runner.migrate_to_latest();
  1436. assert(runner.get_current_serial("fkidx") == 2);
  1437. // The FK constraint itself must still be there
  1438. var fk_cmd = conn.create_command(
  1439. "SELECT sql FROM sqlite_master WHERE type='table' AND name='org_repos'"
  1440. );
  1441. var create_sql = fk_cmd.execute_scalar();
  1442. assert(create_sql != null);
  1443. string table_sql = create_sql.as_string_or_null() ?? "";
  1444. assert("FOREIGN KEY" in table_sql);
  1445. assert("REFERENCES repos (id)" in table_sql);
  1446. assert("ON DELETE CASCADE" in table_sql);
  1447. // THE regression: a CREATE INDEX for the FK column must exist
  1448. var idx_cmd = conn.create_command(
  1449. "SELECT COUNT(*) FROM sqlite_master WHERE type='index' AND tbl_name='org_repos' AND name='idx_org_repos_repo_id'"
  1450. );
  1451. var count_elem = idx_cmd.execute_scalar();
  1452. assert(count_elem != null);
  1453. assert(count_elem.as_int64_or_null() == 1);
  1454. conn.close();
  1455. } catch (SqlError e) {
  1456. stderr.printf("ERROR: %s\n", e.message);
  1457. assert_not_reached();
  1458. } finally {
  1459. GLib.FileUtils.remove(db_path);
  1460. }
  1461. }
  1462. /**
  1463. * Unit level: the chained form must emit a CreateIndexOperation alongside
  1464. * the CreateTableOperation, with the auto-generated idx_{table}_{column} name.
  1465. */
  1466. void test_fk_indexed_emits_index_operation() {
  1467. var dialect = new SqliteDialect();
  1468. var builder = new MigrationBuilder(dialect);
  1469. builder.create_table("org_repos", t => {
  1470. t.column<int64?>("id").primary_key().auto_increment();
  1471. t.column<int64?>("repo_id")
  1472. .not_null()
  1473. .references("repos", "id")
  1474. .on_delete_cascade()
  1475. .indexed();
  1476. });
  1477. var ops = builder.get_operations();
  1478. assert(ops.length == 2);
  1479. var table_op = ops[0] as CreateTableOperation;
  1480. assert(table_op != null);
  1481. // FK constraint still collected from the inner builder chain
  1482. assert(table_op.constraints.length == 1);
  1483. var constraint = table_op.constraints.get(0);
  1484. assert(constraint.constraint_type == "FOREIGN KEY");
  1485. assert(constraint.name == "fk_org_repos_repo_id");
  1486. assert(constraint.reference_table == "repos");
  1487. assert(constraint.on_delete_action == ReferentialAction.CASCADE);
  1488. // The index operation must ALSO be emitted
  1489. var idx_op = ops[1] as CreateIndexOperation;
  1490. assert(idx_op != null);
  1491. assert(idx_op.index_name == "idx_org_repos_repo_id");
  1492. assert(idx_op.table_name == "org_repos");
  1493. assert(idx_op.columns.length == 1);
  1494. assert(idx_op.columns.get(0) == "repo_id");
  1495. assert(idx_op.is_unique == false);
  1496. var sql = dialect.create_index_sql(idx_op);
  1497. assert("CREATE INDEX idx_org_repos_repo_id ON org_repos (repo_id)" == sql);
  1498. }
  1499. /**
  1500. * Custom index names given after .references() must survive the wrapper.
  1501. */
  1502. void test_fk_indexed_with_custom_name() {
  1503. var dialect = new SqliteDialect();
  1504. var builder = new MigrationBuilder(dialect);
  1505. builder.create_table("org_repos", t => {
  1506. t.column<int64?>("repo_id")
  1507. .references("repos", "id")
  1508. .indexed("idx_org_repos_repo_fk");
  1509. });
  1510. var ops = builder.get_operations();
  1511. assert(ops.length == 2);
  1512. var idx_op = ops[1] as CreateIndexOperation;
  1513. assert(idx_op != null);
  1514. assert(idx_op.index_name == "idx_org_repos_repo_fk");
  1515. }
  1516. /**
  1517. * not_null() called after .references() must reach the real column.
  1518. */
  1519. void test_fk_chain_not_null_propagates() {
  1520. var dialect = new SqliteDialect();
  1521. var builder = new MigrationBuilder(dialect);
  1522. builder.create_table("org_repos", t => {
  1523. t.column<int64?>("repo_id")
  1524. .references("repos", "id")
  1525. .not_null();
  1526. });
  1527. var ops = builder.get_operations();
  1528. var table_op = ops[0] as CreateTableOperation;
  1529. assert(table_op != null);
  1530. assert(table_op.columns.length == 1);
  1531. assert(table_op.columns.get(0).is_required == true);
  1532. var sql = dialect.create_table_sql(table_op);
  1533. assert("NOT NULL" in sql);
  1534. }
  1535. /**
  1536. * unique() called after .references() must reach the real column.
  1537. */
  1538. void test_fk_chain_unique_propagates() {
  1539. var dialect = new SqliteDialect();
  1540. var builder = new MigrationBuilder(dialect);
  1541. builder.create_table("org_repos", t => {
  1542. t.column<int64?>("repo_id")
  1543. .references("repos", "id")
  1544. .unique()
  1545. .indexed();
  1546. });
  1547. var ops = builder.get_operations();
  1548. assert(ops.length == 2);
  1549. var table_op = ops[0] as CreateTableOperation;
  1550. assert(table_op.columns.get(0).is_unique == true);
  1551. var idx_op = ops[1] as CreateIndexOperation;
  1552. assert(idx_op != null);
  1553. assert(idx_op.is_unique == true);
  1554. var sql = dialect.create_index_sql(idx_op);
  1555. assert("CREATE UNIQUE INDEX" in sql);
  1556. }
  1557. /**
  1558. * Column type overrides called after .references() must reach the real column
  1559. * (type_text() overriding the generic int64 inference).
  1560. */
  1561. void test_fk_chain_type_overrides_propagate() {
  1562. var dialect = new SqliteDialect();
  1563. var builder = new MigrationBuilder(dialect);
  1564. builder.create_table("org_repos", t => {
  1565. t.column<int64?>("repo_id")
  1566. .references("repos", "id")
  1567. .type_text();
  1568. });
  1569. var ops = builder.get_operations();
  1570. var table_op = ops[0] as CreateTableOperation;
  1571. assert(table_op.columns.get(0).column_type == ColumnType.TEXT);
  1572. var sql = dialect.create_table_sql(table_op);
  1573. assert("TEXT" in sql);
  1574. }
  1575. /**
  1576. * ALTER TABLE form: add_column with the chained FK+indexed must emit the
  1577. * index operation too.
  1578. */
  1579. void test_fk_indexed_in_alter_table_add_column() {
  1580. var dialect = new SqliteDialect();
  1581. var builder = new MigrationBuilder(dialect);
  1582. builder.alter_table("org_repos", t => {
  1583. t.add_column<int64?>("repo_id")
  1584. .references("repos", "id")
  1585. .indexed();
  1586. });
  1587. var ops = builder.get_operations();
  1588. assert(ops.length == 2);
  1589. var add_col_op = ops[0] as AddColumnOperation;
  1590. assert(add_col_op != null);
  1591. assert(add_col_op.foreign_key_constraint != null);
  1592. assert(add_col_op.foreign_key_constraint.reference_table == "repos");
  1593. var idx_op = ops[1] as CreateIndexOperation;
  1594. assert(idx_op != null);
  1595. assert(idx_op.index_name == "idx_org_repos_repo_id");
  1596. assert(idx_op.columns.get(0) == "repo_id");
  1597. }
  1598. /**
  1599. * Chaining a second .references() after FK configuration must still return a
  1600. * ForeignKeyBuilder (documented behaviour) and register on the inner builder.
  1601. */
  1602. void test_fk_chain_double_references_returns_fk_builder() {
  1603. var dialect = new SqliteDialect();
  1604. var builder = new MigrationBuilder(dialect);
  1605. builder.create_table("org_repos", t => {
  1606. var fk = t.column<int64?>("repo_id")
  1607. .references("repos", "id")
  1608. .on_delete_cascade()
  1609. .references("other_table", "id");
  1610. assert(fk is ForeignKeyBuilder);
  1611. });
  1612. // The inner column builder must expose the LAST fk configured
  1613. var ops = builder.get_operations();
  1614. var table_op = ops[0] as CreateTableOperation;
  1615. assert(table_op != null);
  1616. assert(table_op.constraints.length == 1);
  1617. assert(table_op.constraints.get(0).reference_table == "other_table");
  1618. }