M0012_ProviderPushSubscriptions.vala 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. using InvercargillSql.Migrations;
  2. using Sqlite;
  3. namespace ClankerSaddle.Data.Migrations {
  4. /**
  5. * Moves Web Push subscriptions from our hand-rolled
  6. * `push_subscriptions` table (plan 11 task E) into Spry's
  7. * `spry_push_subscriptions` store (goal 47: the app adopted
  8. * `Spry.Push` for storage, crypto and delivery — our own RFC
  9. * 8291/8292 implementation never worked and is gone).
  10. *
  11. * Column mapping is one-to-one (`created_at` → `created`); the
  12. * copy is INSERT OR IGNORE because the Spry table enforces
  13. * UNIQUE(endpoint) — a database that already carried both
  14. * shapes keeps its newer rows. The copy is ONE-WAY: `down()`
  15. * recreates the old table per M0001's shape, empty, for
  16. * operators who must roll back (the subscriptions themselves
  17. * are re-created by browsers on their next visit — pushing to
  18. * stale endpoints never worked anyway).
  19. *
  20. * ORDERING: the copy may only run after Spry's own
  21. * M0003_PushSubscriptions ("spry-authentication" namespace)
  22. * has created `spry_push_subscriptions`. The runner's Kahn
  23. * queue is namespace-first, so a bare clanker-saddle migration
  24. * lands BEFORE the spry namespace on a fresh database
  25. * (empirically: clanker-saddle:1, spry:1, clanker-saddle:12,
  26. * spry:2..5). The `dependencies` declaration below pins this
  27. * migration after the WHOLE spry-authentication namespace —
  28. * verified empirically on a scratch database: with it, the
  29. * order becomes clanker-saddle:1, spry:1..5,
  30. * clanker-saddle:12. On an already-migrated database both
  31. * namespaces are applied, the copy finds both tables and the
  32. * dependency costs nothing.
  33. *
  34. * IDEMPOTENCE: same contract as M0002–M0011 — up() runs before
  35. * the apply transaction opens (the builder only queues), so it
  36. * introspects the schema through a separate read-only handle
  37. * and copies only when the old table still exists. The DROP is
  38. * likewise conditional (guarded in the SQL itself, since the
  39. * builder has no drop-if-exists).
  40. */
  41. public class M0012_ProviderPushSubscriptions : Migration {
  42. public override string name { get { return "Move push subscriptions to Spry"; } }
  43. public override uint64 serial { get { return 12; } }
  44. public override string migration_namespace { get { return "clanker-saddle"; } }
  45. public override owned string[] dependencies {
  46. owned get { return { "spry-authentication" }; }
  47. }
  48. // The runner never hands up() a connection, so the schema is
  49. // read through a read-only handle on the SPRY_DB_PATH main.vala
  50. // connects to.
  51. private string db_path() {
  52. var path = Environment.get_variable("SPRY_DB_PATH") ?? "";
  53. if (path.strip().length == 0) {
  54. return "./clanker-saddle.sqlite";
  55. }
  56. return path;
  57. }
  58. private bool table_exists(string table) {
  59. Database db;
  60. int rc = Database.open_v2(db_path(), out db, Sqlite.OPEN_READONLY);
  61. if (rc != Sqlite.OK) {
  62. return false;
  63. }
  64. Statement statement;
  65. rc = db.prepare_v2(
  66. @"select 1 from sqlite_master where type = 'table' and name = '$table'",
  67. -1, out statement);
  68. if (rc != Sqlite.OK) {
  69. return false;
  70. }
  71. return statement.step() == Sqlite.ROW;
  72. }
  73. public override void up(MigrationBuilder b) {
  74. if (!table_exists("push_subscriptions")) {
  75. // Fresh database (or already moved): nothing to copy.
  76. return;
  77. }
  78. b.execute_sql(@"
  79. insert or ignore into spry_push_subscriptions
  80. (user_id, endpoint, p256dh, auth, created)
  81. select user_id, endpoint, p256dh, auth, created_at
  82. from push_subscriptions");
  83. b.execute_sql("drop table if exists push_subscriptions");
  84. }
  85. public override void down(MigrationBuilder b) {
  86. b.create_table("push_subscriptions", (t) => {
  87. t.column<int64?>("id")
  88. .auto_increment()
  89. .primary_key();
  90. t.column<int64?>("user_id")
  91. .not_null()
  92. .indexed();
  93. t.column<string>("endpoint")
  94. .not_null()
  95. .unique();
  96. t.column<string>("p256dh")
  97. .not_null();
  98. t.column<string>("auth")
  99. .not_null();
  100. t.column<DateTime?>("created_at");
  101. });
  102. }
  103. }
  104. }