create-protobase 0.23.13 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/dist/ask-preset.js +25 -0
  2. package/dist/create-project.js +7 -6
  3. package/dist/index.js +19 -9
  4. package/dist/next-steps.js +8 -2
  5. package/dist/presets.js +17 -0
  6. package/dist/templates/erp/.build-test-abf2a11f/protobase.config.ts +6 -0
  7. package/dist/templates/erp/.build-test-abf2a11f/resources.ts +2 -0
  8. package/dist/templates/erp/.build-test-b878b26f/config/index.ts +2 -0
  9. package/dist/templates/erp/.build-test-b878b26f/config/things/data.ts +2 -0
  10. package/dist/templates/erp/.build-test-b878b26f/config/things/ui.ts +2 -0
  11. package/dist/templates/erp/.env.example +14 -0
  12. package/dist/templates/erp/AGENTS.md +24 -0
  13. package/dist/templates/erp/auth/auth.ts +68 -0
  14. package/dist/templates/erp/components/orders-per-day.tsx +21 -0
  15. package/dist/templates/erp/config/bottom-bar.ts +9 -0
  16. package/dist/templates/erp/config/categories/data.ts +20 -0
  17. package/dist/templates/erp/config/categories/ui.ts +11 -0
  18. package/dist/templates/erp/config/companies/data.ts +23 -0
  19. package/dist/templates/erp/config/companies/ui.ts +22 -0
  20. package/dist/templates/erp/config/company_tags/data.ts +16 -0
  21. package/dist/templates/erp/config/company_tags/ui.ts +7 -0
  22. package/dist/templates/erp/config/countries/data.ts +13 -0
  23. package/dist/templates/erp/config/countries/ui.ts +6 -0
  24. package/dist/templates/erp/config/currencies/data.ts +14 -0
  25. package/dist/templates/erp/config/currencies/ui.ts +6 -0
  26. package/dist/templates/erp/config/emp_master/data.ts +19 -0
  27. package/dist/templates/erp/config/emp_master/ui.ts +7 -0
  28. package/dist/templates/erp/config/index.ts +42 -0
  29. package/dist/templates/erp/config/invoice_lines/data.ts +23 -0
  30. package/dist/templates/erp/config/invoice_lines/ui.ts +7 -0
  31. package/dist/templates/erp/config/invoices/data.ts +26 -0
  32. package/dist/templates/erp/config/invoices/ui.ts +38 -0
  33. package/dist/templates/erp/config/jobs.test.ts +23 -0
  34. package/dist/templates/erp/config/jobs.ts +29 -0
  35. package/dist/templates/erp/config/locations/data.ts +18 -0
  36. package/dist/templates/erp/config/locations/ui.ts +6 -0
  37. package/dist/templates/erp/config/order_lines/data.ts +20 -0
  38. package/dist/templates/erp/config/order_lines/ui.ts +7 -0
  39. package/dist/templates/erp/config/orders/data.ts +27 -0
  40. package/dist/templates/erp/config/orders/ui.ts +41 -0
  41. package/dist/templates/erp/config/organizations/data.ts +17 -0
  42. package/dist/templates/erp/config/organizations/ui.ts +6 -0
  43. package/dist/templates/erp/config/overview/page.tsx +80 -0
  44. package/dist/templates/erp/config/pages.test.ts +14 -0
  45. package/dist/templates/erp/config/people/data.ts +21 -0
  46. package/dist/templates/erp/config/people/ui.ts +6 -0
  47. package/dist/templates/erp/config/products/data.ts +25 -0
  48. package/dist/templates/erp/config/products/ui.ts +6 -0
  49. package/dist/templates/erp/config/roles.test.ts +56 -0
  50. package/dist/templates/erp/config/roles.ts +54 -0
  51. package/dist/templates/erp/config/shell.test.ts +24 -0
  52. package/dist/templates/erp/config/stock_levels/data.ts +16 -0
  53. package/dist/templates/erp/config/stock_levels/ui.ts +6 -0
  54. package/dist/templates/erp/config/stock_moves/data.ts +22 -0
  55. package/dist/templates/erp/config/stock_moves/ui.ts +25 -0
  56. package/dist/templates/erp/config/tags/data.ts +17 -0
  57. package/dist/templates/erp/config/tags/ui.ts +6 -0
  58. package/dist/templates/erp/config/user-menu.ts +9 -0
  59. package/dist/templates/erp/config/users/data.ts +19 -0
  60. package/dist/templates/erp/config/users/ui.ts +9 -0
  61. package/dist/templates/erp/config/warehouses/data.ts +19 -0
  62. package/dist/templates/erp/config/warehouses/ui.ts +6 -0
  63. package/dist/templates/erp/data.ts +3 -0
  64. package/dist/templates/erp/db/connection.ts +7 -0
  65. package/dist/templates/erp/db/drop.ts +9 -0
  66. package/dist/templates/erp/db/kysely.ts +13 -0
  67. package/dist/templates/erp/db/migrations/001_core.sql +48 -0
  68. package/dist/templates/erp/db/migrations/002_crm.sql +59 -0
  69. package/dist/templates/erp/db/migrations/003_catalog.sql +38 -0
  70. package/dist/templates/erp/db/migrations/004_sales.sql +86 -0
  71. package/dist/templates/erp/db/migrations/005_inventory.sql +65 -0
  72. package/dist/templates/erp/db/migrations/006_hr_legacy.sql +16 -0
  73. package/dist/templates/erp/db/migrations/007_filter_indexes.sql +4 -0
  74. package/dist/templates/erp/db/migrations/008_calculated_totals.sql +98 -0
  75. package/dist/templates/erp/db/migrations/011_auth_organizations.sql +22 -0
  76. package/dist/templates/erp/db/migrations/012_jobs.sql +531 -0
  77. package/dist/templates/erp/db/migrations/013_jobs.sql +98 -0
  78. package/dist/templates/erp/db/migrations/016_jobs.sql +99 -0
  79. package/dist/templates/erp/db/migrations/017_filter_indexes.sql +5 -0
  80. package/dist/templates/erp/db/migrations/018_sort_indexes.sql +4 -0
  81. package/dist/templates/erp/db/migrations/019_auth_users.sql +18 -0
  82. package/dist/templates/erp/functions/unpaid-orders.ts +13 -0
  83. package/dist/templates/erp/package.json +38 -0
  84. package/dist/templates/erp/protobase.config.ts +22 -0
  85. package/dist/templates/erp/protobase.ui.tsx +14 -0
  86. package/dist/templates/erp/seed/auth-organizations.ts +11 -0
  87. package/dist/templates/erp/seed/base-organization.test.ts +17 -0
  88. package/dist/templates/erp/seed/base-organization.ts +10 -0
  89. package/dist/templates/erp/seed/base.ts +22 -0
  90. package/dist/templates/erp/seed/builders/categories.ts +18 -0
  91. package/dist/templates/erp/seed/builders/company.ts +61 -0
  92. package/dist/templates/erp/seed/builders/invoice.ts +31 -0
  93. package/dist/templates/erp/seed/builders/order.ts +66 -0
  94. package/dist/templates/erp/seed/builders/product.ts +46 -0
  95. package/dist/templates/erp/seed/calendar.ts +60 -0
  96. package/dist/templates/erp/seed/cli.ts +54 -0
  97. package/dist/templates/erp/seed/copy.ts +22 -0
  98. package/dist/templates/erp/seed/data/catalogTree.ts +72 -0
  99. package/dist/templates/erp/seed/data/layout.ts +19 -0
  100. package/dist/templates/erp/seed/data/names.ts +44 -0
  101. package/dist/templates/erp/seed/data/reference.ts +53 -0
  102. package/dist/templates/erp/seed/format.ts +24 -0
  103. package/dist/templates/erp/seed/maintenance.ts +63 -0
  104. package/dist/templates/erp/seed/rng.ts +36 -0
  105. package/dist/templates/erp/seed/scales.ts +9 -0
  106. package/dist/templates/erp/seed/steps/catalog.ts +34 -0
  107. package/dist/templates/erp/seed/steps/crm.ts +62 -0
  108. package/dist/templates/erp/seed/steps/hr.ts +26 -0
  109. package/dist/templates/erp/seed/steps/ledger.ts +88 -0
  110. package/dist/templates/erp/seed/steps/reference.ts +24 -0
  111. package/dist/templates/erp/seed/steps/sales.ts +58 -0
  112. package/dist/templates/erp/seed/steps/warehouses.ts +42 -0
  113. package/dist/templates/erp/seed/world.ts +86 -0
  114. package/dist/templates/erp/vitest.config.ts +4 -0
  115. package/dist/templates/real-estate/.env.example +9 -0
  116. package/dist/templates/real-estate/AGENTS.md +24 -0
  117. package/dist/templates/real-estate/_.gitignore +3 -0
  118. package/dist/templates/real-estate/_bunfig.toml +4 -0
  119. package/dist/templates/real-estate/auth/auth.ts +36 -0
  120. package/dist/templates/real-estate/config/amenities/data.ts +12 -0
  121. package/dist/templates/real-estate/config/amenities/ui.ts +7 -0
  122. package/dist/templates/real-estate/config/arrears/data.ts +19 -0
  123. package/dist/templates/real-estate/config/arrears/ui.ts +21 -0
  124. package/dist/templates/real-estate/config/deposits/data.ts +21 -0
  125. package/dist/templates/real-estate/config/deposits/ui.ts +17 -0
  126. package/dist/templates/real-estate/config/index.ts +41 -0
  127. package/dist/templates/real-estate/config/inspections/data.ts +22 -0
  128. package/dist/templates/real-estate/config/inspections/ui.ts +15 -0
  129. package/dist/templates/real-estate/config/lease_tenants/data.ts +16 -0
  130. package/dist/templates/real-estate/config/lease_tenants/ui.ts +8 -0
  131. package/dist/templates/real-estate/config/leases/data.ts +29 -0
  132. package/dist/templates/real-estate/config/leases/ui.ts +33 -0
  133. package/dist/templates/real-estate/config/organizations/data.ts +17 -0
  134. package/dist/templates/real-estate/config/organizations/ui.ts +7 -0
  135. package/dist/templates/real-estate/config/overview/page.tsx +57 -0
  136. package/dist/templates/real-estate/config/owners/data.ts +22 -0
  137. package/dist/templates/real-estate/config/owners/ui.ts +17 -0
  138. package/dist/templates/real-estate/config/pages.test.ts +49 -0
  139. package/dist/templates/real-estate/config/payments/data.ts +20 -0
  140. package/dist/templates/real-estate/config/payments/ui.ts +14 -0
  141. package/dist/templates/real-estate/config/properties/data.ts +26 -0
  142. package/dist/templates/real-estate/config/properties/ui.ts +21 -0
  143. package/dist/templates/real-estate/config/rent_charges/data.ts +22 -0
  144. package/dist/templates/real-estate/config/rent_charges/ui.ts +27 -0
  145. package/dist/templates/real-estate/config/roles.test.ts +49 -0
  146. package/dist/templates/real-estate/config/roles.ts +45 -0
  147. package/dist/templates/real-estate/config/shell.test.ts +18 -0
  148. package/dist/templates/real-estate/config/tenants/data.ts +23 -0
  149. package/dist/templates/real-estate/config/tenants/ui.ts +27 -0
  150. package/dist/templates/real-estate/config/tickets/data.ts +33 -0
  151. package/dist/templates/real-estate/config/tickets/ui.ts +58 -0
  152. package/dist/templates/real-estate/config/unit_amenities/data.ts +15 -0
  153. package/dist/templates/real-estate/config/unit_amenities/ui.ts +8 -0
  154. package/dist/templates/real-estate/config/units/data.ts +24 -0
  155. package/dist/templates/real-estate/config/units/ui.ts +41 -0
  156. package/dist/templates/real-estate/config/user-menu.ts +9 -0
  157. package/dist/templates/real-estate/config/users/data.ts +19 -0
  158. package/dist/templates/real-estate/config/users/ui.ts +11 -0
  159. package/dist/templates/real-estate/config/valuations/data.ts +19 -0
  160. package/dist/templates/real-estate/config/valuations/ui.ts +13 -0
  161. package/dist/templates/real-estate/config/vendors/data.ts +21 -0
  162. package/dist/templates/real-estate/config/vendors/ui.ts +18 -0
  163. package/dist/templates/real-estate/config/work_orders/data.ts +22 -0
  164. package/dist/templates/real-estate/config/work_orders/ui.ts +18 -0
  165. package/dist/templates/real-estate/db/connection.ts +7 -0
  166. package/dist/templates/real-estate/db/drop.ts +9 -0
  167. package/dist/templates/real-estate/db/kysely.ts +13 -0
  168. package/dist/templates/real-estate/db/migrate.ts +30 -0
  169. package/dist/templates/real-estate/db/migrations/001_core.sql +33 -0
  170. package/dist/templates/real-estate/db/migrations/002_portfolio.sql +97 -0
  171. package/dist/templates/real-estate/db/migrations/003_leasing.sql +78 -0
  172. package/dist/templates/real-estate/db/migrations/004_billing.sql +88 -0
  173. package/dist/templates/real-estate/db/migrations/005_maintenance.sql +105 -0
  174. package/dist/templates/real-estate/db/migrations/006_outstanding.sql +14 -0
  175. package/dist/templates/real-estate/db/migrations/007_auth.sql +36 -0
  176. package/dist/templates/real-estate/db/migrations/008_auth_snake_case.sql +109 -0
  177. package/dist/templates/real-estate/db/migrations/009_auth.sql +6 -0
  178. package/dist/templates/real-estate/db/migrations/010_auth.sql +4 -0
  179. package/dist/templates/real-estate/db/migrations/011_filter_indexes.sql +8 -0
  180. package/dist/templates/real-estate/db/migrations/012_sort_indexes.sql +2 -0
  181. package/dist/templates/real-estate/db/migrations/013_auth_users.sql +17 -0
  182. package/dist/templates/real-estate/node-types.d.ts +1 -0
  183. package/dist/templates/real-estate/package.json +38 -0
  184. package/dist/templates/real-estate/protobase.config.ts +5 -0
  185. package/dist/templates/real-estate/protobase.ui.tsx +16 -0
  186. package/dist/templates/real-estate/seed/base-organization.test.ts +17 -0
  187. package/dist/templates/real-estate/seed/base-organization.ts +10 -0
  188. package/dist/templates/real-estate/seed/base.ts +18 -0
  189. package/dist/templates/real-estate/seed/builders/allocation.ts +47 -0
  190. package/dist/templates/real-estate/seed/builders/ledger.ts +52 -0
  191. package/dist/templates/real-estate/seed/builders/property.ts +98 -0
  192. package/dist/templates/real-estate/seed/builders/tenancy.ts +99 -0
  193. package/dist/templates/real-estate/seed/builders/ticket.ts +115 -0
  194. package/dist/templates/real-estate/seed/calendar.ts +77 -0
  195. package/dist/templates/real-estate/seed/cli.ts +50 -0
  196. package/dist/templates/real-estate/seed/copy.ts +22 -0
  197. package/dist/templates/real-estate/seed/data/names.ts +31 -0
  198. package/dist/templates/real-estate/seed/data/reference.ts +63 -0
  199. package/dist/templates/real-estate/seed/format.ts +24 -0
  200. package/dist/templates/real-estate/seed/iban.ts +15 -0
  201. package/dist/templates/real-estate/seed/maintenance.ts +63 -0
  202. package/dist/templates/real-estate/seed/rng.ts +36 -0
  203. package/dist/templates/real-estate/seed/scales.ts +9 -0
  204. package/dist/templates/real-estate/seed/steps/billing.ts +44 -0
  205. package/dist/templates/real-estate/seed/steps/leasing.ts +60 -0
  206. package/dist/templates/real-estate/seed/steps/maintenance.ts +77 -0
  207. package/dist/templates/real-estate/seed/steps/portfolio.ts +97 -0
  208. package/dist/templates/real-estate/seed/steps/reference.ts +21 -0
  209. package/dist/templates/real-estate/seed/world.ts +92 -0
  210. package/dist/templates/real-estate/tsconfig.json +16 -0
  211. package/dist/templates/real-estate/vitest.config.ts +4 -0
  212. package/dist/templates/scratch/_.gitignore +3 -0
  213. package/dist/templates/scratch/_bunfig.toml +4 -0
  214. package/dist/templates/scratch/db/migrate.ts +30 -0
  215. package/dist/templates/scratch/db/migrations/001_auth.sql +36 -0
  216. package/dist/templates/scratch/db/migrations/002_auth_snake_case.sql +109 -0
  217. package/dist/templates/scratch/db/migrations/003_auth.sql +6 -0
  218. package/dist/templates/scratch/db/migrations/004_auth.sql +4 -0
  219. package/dist/templates/scratch/node-types.d.ts +1 -0
  220. package/dist/{template → templates/scratch}/package.json +5 -5
  221. package/dist/templates/scratch/tsconfig.json +16 -0
  222. package/package.json +2 -2
  223. /package/dist/{template → templates/erp}/_.gitignore +0 -0
  224. /package/dist/{template → templates/erp}/_bunfig.toml +0 -0
  225. /package/dist/{template → templates/erp}/db/migrate.ts +0 -0
  226. /package/dist/{template/db/migrations/001_auth.sql → templates/erp/db/migrations/009_auth.sql} +0 -0
  227. /package/dist/{template/db/migrations/002_auth_snake_case.sql → templates/erp/db/migrations/010_auth_snake_case.sql} +0 -0
  228. /package/dist/{template/db/migrations/003_auth.sql → templates/erp/db/migrations/014_auth.sql} +0 -0
  229. /package/dist/{template/db/migrations/004_auth.sql → templates/erp/db/migrations/015_auth.sql} +0 -0
  230. /package/dist/{template → templates/erp}/node-types.d.ts +0 -0
  231. /package/dist/{template → templates/erp}/tsconfig.json +0 -0
  232. /package/dist/{template → templates/scratch}/.env.example +0 -0
  233. /package/dist/{template → templates/scratch}/AGENTS.md +0 -0
  234. /package/dist/{template → templates/scratch}/auth/auth.ts +0 -0
  235. /package/dist/{template → templates/scratch}/config/index.ts +0 -0
  236. /package/dist/{template → templates/scratch}/config/user-menu.ts +0 -0
  237. /package/dist/{template → templates/scratch}/db/connection.ts +0 -0
  238. /package/dist/{template → templates/scratch}/db/migrations/.gitkeep +0 -0
  239. /package/dist/{template → templates/scratch}/protobase.config.ts +0 -0
  240. /package/dist/{template → templates/scratch}/vitest.config.ts +0 -0
@@ -0,0 +1,531 @@
1
+ -- The jobs schema this version of Protobase needs, written by `protobase migration`.
2
+ -- Applied with the project's other migrations; edit or remove statements the project handles differently.
3
+
4
+ SELECT pg_advisory_xact_lock(('x' || encode(sha224((current_database() || '.pgboss.jobs')::bytea), 'hex'))::bit(64)::bigint);
5
+ CREATE SCHEMA IF NOT EXISTS jobs;
6
+
7
+ CREATE TYPE jobs.job_state AS ENUM (
8
+ 'created',
9
+ 'retry',
10
+ 'active',
11
+ 'completed',
12
+ 'cancelled',
13
+ 'failed'
14
+ )
15
+ ;
16
+
17
+ CREATE FUNCTION jobs.job_now()
18
+ RETURNS timestamp with time zone AS
19
+ $$
20
+ SELECT pg_catalog.now();
21
+ $$
22
+ LANGUAGE sql STABLE;
23
+ ;
24
+
25
+ CREATE TABLE jobs.version (
26
+ version int primary key,
27
+ cron_on timestamp with time zone,
28
+ bam_on timestamp with time zone,
29
+ flow_on timestamp with time zone,
30
+ reindex_on timestamp with time zone,
31
+ monitor_backoff_on timestamp with time zone
32
+ )
33
+ ;
34
+
35
+ CREATE TABLE jobs.queue (
36
+ name text NOT NULL,
37
+ policy text NOT NULL,
38
+ retry_limit int NOT NULL,
39
+ retry_delay int NOT NULL,
40
+ retry_backoff bool NOT NULL,
41
+ retry_delay_max int,
42
+ expire_seconds int NOT NULL,
43
+ retention_seconds int NOT NULL,
44
+ deletion_seconds int NOT NULL,
45
+ dead_letter text REFERENCES jobs.queue (name) CHECK (dead_letter IS DISTINCT FROM name),
46
+ partition bool NOT NULL,
47
+ table_name text NOT NULL,
48
+ deferred_count int NOT NULL default 0,
49
+ blocked_count int NOT NULL default 0,
50
+ queued_count int NOT NULL default 0,
51
+ ready_count int NOT NULL default 0,
52
+ warning_queued int NOT NULL default 0,
53
+ active_count int NOT NULL default 0,
54
+ failed_count int NOT NULL default 0,
55
+ total_count int NOT NULL default 0,
56
+ created_delta int NOT NULL default 0,
57
+ completed_delta int NOT NULL default 0,
58
+ failed_delta int NOT NULL default 0,
59
+ delta_on timestamp with time zone,
60
+ delta_seconds int,
61
+ wait_bins int[],
62
+ run_bins int[],
63
+ ready_oldest_seconds int,
64
+ ready_history int[] NOT NULL default '{}',
65
+ heartbeat_seconds int,
66
+ notify bool NOT NULL DEFAULT false,
67
+ singletons_active text[],
68
+ monitor_claim_on timestamp with time zone,
69
+ monitor_on timestamp with time zone,
70
+ maintain_on timestamp with time zone,
71
+ created_on timestamp with time zone not null default now(),
72
+ updated_on timestamp with time zone not null default now(),
73
+ PRIMARY KEY (name)
74
+ )
75
+ ;
76
+
77
+ CREATE TABLE jobs.schedule (
78
+ name text REFERENCES jobs.queue ON DELETE CASCADE,
79
+ key text not null DEFAULT '',
80
+ kind text not null DEFAULT 'cron' CHECK (kind IN ('cron', 'rrule')),
81
+ cron text not null,
82
+ timezone text DEFAULT 'UTC',
83
+ data jsonb,
84
+ options jsonb,
85
+ created_on timestamp with time zone not null default now(),
86
+ updated_on timestamp with time zone not null default now(),
87
+ last_job_id uuid,
88
+ PRIMARY KEY (name, key)
89
+ )
90
+ ;
91
+
92
+ CREATE TABLE jobs.subscription (
93
+ event text not null,
94
+ name text not null REFERENCES jobs.queue ON DELETE CASCADE,
95
+ created_on timestamp with time zone not null default now(),
96
+ updated_on timestamp with time zone not null default now(),
97
+ PRIMARY KEY(event, name)
98
+ )
99
+ ;
100
+
101
+ CREATE TABLE jobs.bam (
102
+ id uuid PRIMARY KEY default gen_random_uuid(),
103
+ name text NOT NULL,
104
+ version int NOT NULL,
105
+ status text NOT NULL DEFAULT 'pending',
106
+ queue text,
107
+ table_name text NOT NULL,
108
+ command text NOT NULL,
109
+ error text,
110
+ created_on timestamp with time zone NOT NULL DEFAULT clock_timestamp(),
111
+ started_on timestamp with time zone,
112
+ completed_on timestamp with time zone
113
+ )
114
+ ;
115
+
116
+ CREATE FUNCTION jobs.job_table_format(command text, table_name text)
117
+ RETURNS text AS
118
+ $$
119
+ SELECT format(
120
+ regexp_replace(
121
+ regexp_replace(command, '\.job\y', '.%1$I', 'g'),
122
+ '\yjob_i(\d+)', '%1$s_i\1', 'g'
123
+ ),
124
+ table_name
125
+ );
126
+ $$
127
+ LANGUAGE sql IMMUTABLE;
128
+ ;
129
+
130
+ CREATE FUNCTION jobs.job_table_run(command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
131
+ RETURNS VOID AS
132
+ $$
133
+ DECLARE
134
+ tbl RECORD;
135
+ BEGIN
136
+ IF queue_name IS NOT NULL THEN
137
+ SELECT table_name INTO tbl_name FROM jobs.queue WHERE name = queue_name;
138
+ END IF;
139
+
140
+ IF tbl_name IS NOT NULL THEN
141
+ EXECUTE jobs.job_table_format(command, tbl_name);
142
+ RETURN;
143
+ END IF;
144
+
145
+ EXECUTE jobs.job_table_format(command, 'job_common');
146
+
147
+ FOR tbl IN SELECT table_name FROM jobs.queue WHERE partition = true
148
+ LOOP
149
+ EXECUTE jobs.job_table_format(command, tbl.table_name);
150
+ END LOOP;
151
+ END;
152
+ $$
153
+ LANGUAGE plpgsql;
154
+ ;
155
+
156
+ CREATE FUNCTION jobs.job_table_run_async(command_name text, version int, command text, tbl_name text DEFAULT NULL, queue_name text DEFAULT NULL)
157
+ RETURNS VOID AS
158
+ $$
159
+ BEGIN
160
+ IF queue_name IS NOT NULL THEN
161
+ SELECT table_name INTO tbl_name FROM jobs.queue WHERE name = queue_name;
162
+ END IF;
163
+
164
+ IF tbl_name IS NOT NULL THEN
165
+ INSERT INTO jobs.bam (name, version, status, queue, table_name, command)
166
+ VALUES (
167
+ command_name,
168
+ version,
169
+ 'pending',
170
+ queue_name,
171
+ tbl_name,
172
+ jobs.job_table_format(command, tbl_name)
173
+ );
174
+ RETURN;
175
+ END IF;
176
+
177
+ INSERT INTO jobs.bam (name, version, status, queue, table_name, command)
178
+ SELECT
179
+ command_name,
180
+ version,
181
+ 'pending',
182
+ NULL,
183
+ 'job_common',
184
+ jobs.job_table_format(command, 'job_common')
185
+ UNION ALL
186
+ SELECT
187
+ command_name,
188
+ version,
189
+ 'pending',
190
+ queue.name,
191
+ queue.table_name,
192
+ jobs.job_table_format(command, queue.table_name)
193
+ FROM jobs.queue
194
+ WHERE partition = true;
195
+ END;
196
+ $$
197
+ LANGUAGE plpgsql;
198
+ ;
199
+
200
+ CREATE TABLE jobs.job (
201
+ id uuid not null default gen_random_uuid(),
202
+ name text not null,
203
+ priority integer not null default(0),
204
+ data jsonb,
205
+ state jobs.job_state not null default 'created',
206
+ retry_limit integer not null default 2,
207
+ retry_count integer not null default 0,
208
+ retry_delay integer not null default 0,
209
+ retry_backoff boolean not null default false,
210
+ retry_delay_max integer,
211
+ expire_seconds int not null default 900,
212
+ deletion_seconds int not null default 604800,
213
+ singleton_key text,
214
+ singleton_on timestamp without time zone,
215
+ group_id text,
216
+ group_tier text,
217
+ start_after timestamp with time zone not null default now(),
218
+ created_on timestamp with time zone not null default now(),
219
+ started_on timestamp with time zone,
220
+ completed_on timestamp with time zone,
221
+ keep_until timestamp with time zone NOT NULL default now() + interval '1209600',
222
+ output jsonb,
223
+ dead_letter text,
224
+ policy text,
225
+ heartbeat_on timestamp with time zone,
226
+ heartbeat_seconds int,
227
+ blocked boolean not null default false,
228
+ blocking boolean not null default false,
229
+ pending_dependencies int not null default 0,
230
+ source_name text,
231
+ source_id uuid,
232
+ source_created_on timestamp with time zone,
233
+ source_retry_count int,
234
+ source_output jsonb,
235
+ source_root_id uuid,
236
+ trace_context jsonb,
237
+ upsert_by_key bool
238
+ ) PARTITION BY LIST (name)
239
+ ;
240
+ ALTER TABLE jobs.job ADD PRIMARY KEY (name, id);
241
+
242
+ CREATE TABLE jobs.job_common (LIKE jobs.job INCLUDING GENERATED INCLUDING DEFAULTS);
243
+
244
+ SELECT jobs.job_table_run($cmd$ALTER TABLE jobs.job ADD PRIMARY KEY (name, id)$cmd$, 'job_common');
245
+ SELECT jobs.job_table_run($cmd$ALTER TABLE jobs.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES jobs.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, 'job_common');
246
+ SELECT jobs.job_table_run($cmd$ALTER TABLE jobs.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES jobs.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, 'job_common');
247
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i1 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, 'job_common');
248
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i2 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, 'job_common');
249
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i3 ON jobs.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, 'job_common');
250
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i6 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, 'job_common');
251
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i8 ON jobs.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, 'job_common');
252
+ SELECT jobs.job_table_run($cmd$CREATE INDEX job_i10 ON jobs.job (name, singleton_key, state DESC, created_on, id) INCLUDE (start_after) WHERE state < 'active' AND NOT blocked AND policy = 'key_strict_fifo'$cmd$, 'job_common');
253
+ SELECT jobs.job_table_run($cmd$ALTER TABLE jobs.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, 'job_common');
254
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i4 ON jobs.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, 'job_common');
255
+ SELECT jobs.job_table_run($cmd$CREATE INDEX job_i11 ON jobs.job (name, priority DESC, created_on, start_after) WHERE state < 'active' AND NOT blocked$cmd$, 'job_common');
256
+ SELECT jobs.job_table_run($cmd$CREATE INDEX job_i7 ON jobs.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, 'job_common');
257
+ SELECT jobs.job_table_run($cmd$CREATE INDEX job_i9 ON jobs.job (name, id) WHERE blocking AND state = 'completed'$cmd$, 'job_common');
258
+ SELECT jobs.job_table_run($cmd$CREATE INDEX job_i12 ON jobs.job (source_root_id) WHERE source_root_id IS NOT NULL$cmd$, 'job_common');
259
+ SELECT jobs.job_table_run($cmd$CREATE UNIQUE INDEX job_i13 ON jobs.job (name, singleton_key) WHERE state = 'created' AND upsert_by_key$cmd$, 'job_common');
260
+
261
+ ALTER TABLE jobs.job ATTACH PARTITION jobs.job_common DEFAULT;
262
+ ;
263
+
264
+ CREATE TABLE jobs.warning (
265
+ id uuid PRIMARY KEY default gen_random_uuid(),
266
+ type text NOT NULL,
267
+ message text NOT NULL,
268
+ data jsonb,
269
+ created_on timestamp with time zone NOT NULL DEFAULT now()
270
+ )
271
+ ;
272
+ CREATE INDEX warning_i1 ON jobs.warning (created_on DESC);
273
+
274
+ CREATE TABLE jobs.queue_stats (
275
+ id uuid NOT NULL DEFAULT gen_random_uuid(),
276
+ name text NOT NULL,
277
+ deferred_count int NOT NULL DEFAULT 0,
278
+ queued_count int NOT NULL DEFAULT 0,
279
+ ready_count int NOT NULL DEFAULT 0,
280
+ active_count int NOT NULL DEFAULT 0,
281
+ failed_count int NOT NULL DEFAULT 0,
282
+ total_count int NOT NULL DEFAULT 0,
283
+ created_delta int,
284
+ completed_delta int,
285
+ failed_delta int,
286
+ delta_seconds int,
287
+ delta_on timestamptz,
288
+ wait_bins int[],
289
+ run_bins int[],
290
+ ready_oldest_seconds int,
291
+ captured_on timestamptz NOT NULL DEFAULT now(),
292
+ PRIMARY KEY (id, captured_on)
293
+ ) PARTITION BY RANGE (captured_on)
294
+ ;
295
+ CREATE INDEX queue_stats_i1 ON jobs.queue_stats (name, captured_on DESC) INCLUDE (deferred_count, queued_count, ready_count, active_count, failed_count, total_count);
296
+
297
+ DO $$
298
+ DECLARE
299
+ d date;
300
+ i int;
301
+ part_name text;
302
+ BEGIN
303
+ FOR i IN 0..1 LOOP
304
+ d := (jobs.job_now() AT TIME ZONE 'UTC')::date + i;
305
+ part_name := 'queue_stats_' || to_char(d, 'YYYYMMDD');
306
+ IF NOT EXISTS (
307
+ SELECT 1 FROM pg_class c
308
+ JOIN pg_namespace n ON n.oid = c.relnamespace
309
+ WHERE n.nspname = 'jobs' AND c.relname = part_name
310
+ ) THEN
311
+ EXECUTE format(
312
+ 'CREATE TABLE jobs.%I PARTITION OF jobs.queue_stats FOR VALUES FROM (%L) TO (%L)',
313
+ part_name,
314
+ to_char(d, 'YYYY-MM-DD') || ' 00:00:00+00',
315
+ to_char(d + 1, 'YYYY-MM-DD') || ' 00:00:00+00'
316
+ );
317
+ END IF;
318
+ END LOOP;
319
+ END;
320
+ $$
321
+ ;
322
+
323
+ CREATE TABLE jobs.job_dependency (
324
+ child_name text NOT NULL,
325
+ child_id uuid NOT NULL,
326
+ parent_name text NOT NULL,
327
+ parent_id uuid NOT NULL,
328
+ PRIMARY KEY (child_name, child_id, parent_name, parent_id)
329
+ )
330
+ ;
331
+ CREATE INDEX IF NOT EXISTS job_dep_parent_idx ON jobs.job_dependency (parent_name, parent_id);
332
+
333
+ CREATE TABLE jobs.instance (
334
+ id uuid PRIMARY KEY,
335
+ name text,
336
+ host text NOT NULL,
337
+ pid int NOT NULL,
338
+ version text NOT NULL,
339
+ node_version text NOT NULL,
340
+ application_name text,
341
+ heartbeat_seconds int NOT NULL,
342
+ supervise bool NOT NULL,
343
+ schedule bool NOT NULL,
344
+ migrate bool NOT NULL,
345
+ persist_queue_stats bool NOT NULL,
346
+ persist_warnings bool NOT NULL,
347
+ pool_max int,
348
+ pool_total int,
349
+ pool_idle int,
350
+ pool_waiting int,
351
+ workers jsonb NOT NULL DEFAULT '[]'::jsonb,
352
+ metrics jsonb,
353
+ config jsonb NOT NULL DEFAULT '{}'::jsonb,
354
+ crash_restarts int NOT NULL DEFAULT 0,
355
+ crash_restarts_since timestamptz,
356
+ started_on timestamptz NOT NULL DEFAULT now(),
357
+ heartbeat_on timestamptz NOT NULL DEFAULT now(),
358
+ stopped_on timestamptz
359
+ )
360
+ ;
361
+
362
+ CREATE FUNCTION jobs.create_queue(queue_name text, options jsonb)
363
+ RETURNS VOID AS
364
+ $$
365
+ DECLARE
366
+ tablename varchar := CASE WHEN options->>'partition' = 'true'
367
+ THEN 'j' || encode(sha224(queue_name::bytea), 'hex')
368
+ ELSE 'job_common'
369
+ END;
370
+ queue_created_on timestamptz;
371
+ BEGIN
372
+
373
+ WITH q as (
374
+ INSERT INTO jobs.queue (
375
+ name,
376
+ policy,
377
+ retry_limit,
378
+ retry_delay,
379
+ retry_backoff,
380
+ retry_delay_max,
381
+ expire_seconds,
382
+ retention_seconds,
383
+ deletion_seconds,
384
+ warning_queued,
385
+ dead_letter,
386
+ partition,
387
+ table_name,
388
+ heartbeat_seconds,
389
+ notify,
390
+ created_on,
391
+ updated_on
392
+ )
393
+ VALUES (
394
+ queue_name,
395
+ options->>'policy',
396
+ COALESCE((options->>'retryLimit')::int, 2),
397
+ COALESCE((options->>'retryDelay')::int, 0),
398
+ COALESCE((options->>'retryBackoff')::bool, false),
399
+ (options->>'retryDelayMax')::int,
400
+ COALESCE((options->>'expireInSeconds')::int, 900),
401
+ COALESCE((options->>'retentionSeconds')::int, 1209600),
402
+ COALESCE((options->>'deleteAfterSeconds')::int, 604800),
403
+ COALESCE((options->>'warningQueueSize')::int, 0),
404
+ options->>'deadLetter',
405
+ COALESCE((options->>'partition')::bool, false),
406
+ tablename,
407
+ (options->>'heartbeatSeconds')::int,
408
+ COALESCE((options->>'notify')::bool, false),
409
+ jobs.job_now(),
410
+ jobs.job_now()
411
+ )
412
+ ON CONFLICT DO NOTHING
413
+ RETURNING created_on
414
+ )
415
+ SELECT created_on into queue_created_on from q;
416
+
417
+ IF queue_created_on IS NULL OR options->>'partition' IS DISTINCT FROM 'true' THEN
418
+ RETURN;
419
+ END IF;
420
+
421
+ EXECUTE format('CREATE TABLE jobs.%I (LIKE jobs.job INCLUDING DEFAULTS)', tablename);
422
+
423
+ EXECUTE jobs.job_table_format($cmd$ALTER TABLE jobs.job ADD PRIMARY KEY (name, id)$cmd$, tablename);
424
+ EXECUTE jobs.job_table_format($cmd$ALTER TABLE jobs.job ADD CONSTRAINT q_fkey FOREIGN KEY (name) REFERENCES jobs.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
425
+ EXECUTE jobs.job_table_format($cmd$ALTER TABLE jobs.job ADD CONSTRAINT dlq_fkey FOREIGN KEY (dead_letter) REFERENCES jobs.queue (name) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED$cmd$, tablename);
426
+
427
+ EXECUTE jobs.job_table_format($cmd$CREATE INDEX job_i11 ON jobs.job (name, priority DESC, created_on, start_after) WHERE state < 'active' AND NOT blocked$cmd$, tablename);
428
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i4 ON jobs.job (name, singleton_on, COALESCE(singleton_key, '')) WHERE state <> 'cancelled' AND singleton_on IS NOT NULL$cmd$, tablename);
429
+ EXECUTE jobs.job_table_format($cmd$CREATE INDEX job_i7 ON jobs.job (name, group_id) WHERE state = 'active' AND group_id IS NOT NULL$cmd$, tablename);
430
+ EXECUTE jobs.job_table_format($cmd$CREATE INDEX job_i9 ON jobs.job (name, id) WHERE blocking AND state = 'completed'$cmd$, tablename);
431
+ EXECUTE jobs.job_table_format($cmd$CREATE INDEX job_i12 ON jobs.job (source_root_id) WHERE source_root_id IS NOT NULL$cmd$, tablename);
432
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i13 ON jobs.job (name, singleton_key) WHERE state = 'created' AND upsert_by_key$cmd$, tablename);
433
+
434
+ IF options->>'policy' = 'short' THEN
435
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i1 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state = 'created' AND policy = 'short'$cmd$, tablename);
436
+ ELSIF options->>'policy' = 'singleton' THEN
437
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i2 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state = 'active' AND policy = 'singleton'$cmd$, tablename);
438
+ ELSIF options->>'policy' = 'stately' THEN
439
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i3 ON jobs.job (name, state, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'stately'$cmd$, tablename);
440
+ ELSIF options->>'policy' = 'exclusive' THEN
441
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i6 ON jobs.job (name, COALESCE(singleton_key, '')) WHERE state <= 'active' AND policy = 'exclusive'$cmd$, tablename);
442
+ ELSIF options->>'policy' = 'key_strict_fifo' THEN
443
+ EXECUTE jobs.job_table_format($cmd$CREATE UNIQUE INDEX job_i8 ON jobs.job (name, singleton_key) WHERE state IN ('active', 'retry', 'failed') AND policy = 'key_strict_fifo'$cmd$, tablename);
444
+ EXECUTE jobs.job_table_format($cmd$CREATE INDEX job_i10 ON jobs.job (name, singleton_key, state DESC, created_on, id) INCLUDE (start_after) WHERE state < 'active' AND NOT blocked AND policy = 'key_strict_fifo'$cmd$, tablename);
445
+ EXECUTE jobs.job_table_format($cmd$ALTER TABLE jobs.job ADD CONSTRAINT job_key_strict_fifo_singleton_key_check CHECK (NOT (policy = 'key_strict_fifo' AND singleton_key IS NULL))$cmd$, tablename);
446
+ END IF;
447
+
448
+ EXECUTE format('ALTER TABLE jobs.%I ADD CONSTRAINT cjc CHECK (name=%L)', tablename, queue_name);
449
+ EXECUTE format('ALTER TABLE jobs.job ATTACH PARTITION jobs.%I FOR VALUES IN (%L)', tablename, queue_name);
450
+ END;
451
+ $$
452
+ LANGUAGE plpgsql;
453
+ ;
454
+
455
+ CREATE FUNCTION jobs.delete_queue(queue_name text)
456
+ RETURNS VOID AS
457
+ $$
458
+ DECLARE
459
+ v_table varchar;
460
+ v_partition bool;
461
+ BEGIN
462
+
463
+ SELECT table_name, partition
464
+ FROM jobs.queue
465
+ WHERE name = queue_name
466
+ INTO v_table, v_partition;
467
+
468
+ IF v_partition THEN
469
+ EXECUTE format('DROP TABLE IF EXISTS jobs.%I', v_table);
470
+ ELSE
471
+ EXECUTE format('DELETE FROM jobs.%I WHERE name = %L', v_table, queue_name);
472
+ END IF;
473
+
474
+ DELETE FROM jobs.queue WHERE name = queue_name;
475
+ END;
476
+ $$
477
+ LANGUAGE plpgsql;
478
+ ;
479
+ INSERT INTO jobs.version(version) VALUES ('45');
480
+
481
+ create or replace function jobs.enqueue(
482
+ job_name text,
483
+ payload jsonb default null,
484
+ run_at timestamptz default null,
485
+ key text default null,
486
+ group_key text default null,
487
+ priority integer default 0
488
+ ) returns uuid
489
+ language plpgsql as $$
490
+ declare
491
+ queue jobs.queue;
492
+ start timestamptz := coalesce(run_at, jobs.job_now());
493
+ job_id uuid;
494
+ begin
495
+ if key is not null and group_key is not null then
496
+ raise exception 'Job "%": an ordered job takes a group_key and no key', job_name;
497
+ end if;
498
+ perform jobs.create_queue(job_name, jsonb_build_object('policy', case when group_key is null then 'standard' else 'key_strict_fifo' end));
499
+ select * into queue from jobs.queue where name = job_name;
500
+ if (queue.policy = 'key_strict_fifo') <> (group_key is not null) then
501
+ raise exception 'Job "%" %', job_name, case when group_key is null then 'is ordered: pass its group_key' else 'is not ordered: it takes no group_key' end;
502
+ end if;
503
+ if key is not null then
504
+ select id into job_id from jobs.job
505
+ where name = job_name and singleton_key = key and state < 'active'
506
+ order by created_on desc limit 1;
507
+ if found then
508
+ return job_id;
509
+ end if;
510
+ end if;
511
+ execute format(
512
+ 'insert into jobs.%I (name, data, priority, start_after, singleton_key, retry_limit, retry_delay, retry_backoff,
513
+ retry_delay_max, expire_seconds, deletion_seconds, keep_until, policy, dead_letter, heartbeat_seconds, upsert_by_key)
514
+ values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $4 + $12 * interval ''1s'', $13, $14, $15, $16)
515
+ on conflict do nothing
516
+ returning id',
517
+ queue.table_name)
518
+ using job_name, payload, priority, start, coalesce(key, group_key), queue.retry_limit, queue.retry_delay,
519
+ queue.retry_backoff, queue.retry_delay_max, queue.expire_seconds, queue.deletion_seconds, queue.retention_seconds,
520
+ queue.policy, queue.dead_letter, queue.heartbeat_seconds, key is not null
521
+ into job_id;
522
+ -- A concurrent enqueue of the same key inserted first: its job is the one waiting.
523
+ if job_id is null and key is not null then
524
+ select id into job_id from jobs.job
525
+ where name = job_name and singleton_key = key and state < 'active'
526
+ order by created_on desc limit 1;
527
+ end if;
528
+ return job_id;
529
+ end
530
+ $$;
531
+ comment on function jobs.enqueue(text, jsonb, timestamptz, text, text, integer) is 'Protobase jobs 1';
@@ -0,0 +1,98 @@
1
+ -- The jobs schema this version of Protobase needs, written by `protobase migration`.
2
+ -- Applied with the project's other migrations; edit or remove statements the project handles differently.
3
+
4
+ create or replace function jobs.enqueue(
5
+ job_name text,
6
+ payload jsonb default null,
7
+ run_at timestamptz default null,
8
+ key text default null,
9
+ group_key text default null,
10
+ priority integer default 0
11
+ ) returns uuid
12
+ language plpgsql as $$
13
+ declare
14
+ queue jobs.queue;
15
+ start timestamptz := coalesce(run_at, jobs.job_now());
16
+ job_id uuid;
17
+ begin
18
+ if key is not null and group_key is not null then
19
+ raise exception 'Job "%": an ordered job takes a group_key and no key', job_name;
20
+ end if;
21
+ perform jobs.create_queue(job_name, jsonb_build_object('policy', case when group_key is null then 'standard' else 'key_strict_fifo' end));
22
+ select * into queue from jobs.queue where name = job_name;
23
+ if (queue.policy = 'key_strict_fifo') <> (group_key is not null) then
24
+ raise exception 'Job "%" %', job_name, case when group_key is null then 'is ordered: pass its group_key' else 'is not ordered: it takes no group_key' end;
25
+ end if;
26
+ if key is not null then
27
+ select id into job_id from jobs.job
28
+ where name = job_name and singleton_key = key and state < 'active'
29
+ order by created_on desc limit 1;
30
+ if found then
31
+ return job_id;
32
+ end if;
33
+ end if;
34
+ execute format(
35
+ 'insert into jobs.%I (name, data, priority, start_after, singleton_key, retry_limit, retry_delay, retry_backoff,
36
+ retry_delay_max, expire_seconds, deletion_seconds, keep_until, policy, dead_letter, heartbeat_seconds, upsert_by_key)
37
+ values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $4 + $12 * interval ''1s'', $13, $14, $15, $16)
38
+ on conflict do nothing
39
+ returning id',
40
+ queue.table_name)
41
+ using job_name, payload, priority, start, coalesce(key, group_key), queue.retry_limit, queue.retry_delay,
42
+ queue.retry_backoff, queue.retry_delay_max, queue.expire_seconds, queue.deletion_seconds, queue.retention_seconds,
43
+ queue.policy, queue.dead_letter, queue.heartbeat_seconds, key is not null
44
+ into job_id;
45
+ -- A concurrent enqueue of the same key inserted first: its job is the one waiting.
46
+ if job_id is null and key is not null then
47
+ select id into job_id from jobs.job
48
+ where name = job_name and singleton_key = key and state < 'active'
49
+ order by created_on desc limit 1;
50
+ end if;
51
+ return job_id;
52
+ end
53
+ $$;
54
+ comment on function jobs.enqueue(text, jsonb, timestamptz, text, text, integer) is 'Protobase jobs 2';
55
+
56
+ create or replace function jobs.next_due() returns timestamptz
57
+ language sql stable as $$
58
+ select min(due) from (
59
+ select j.start_after as due from jobs.job j
60
+ where j.state < 'active' and not j.blocked
61
+ and not (j.policy = 'key_strict_fifo' and exists (
62
+ select 1 from jobs.job h where h.name = j.name and h.singleton_key = j.singleton_key and h.state = 'failed'))
63
+ union all
64
+ select started_on + expire_seconds * interval '1s' from jobs.job where state = 'active'
65
+ ) jobs
66
+ $$;
67
+ comment on function jobs.next_due() is 'Protobase jobs 2';
68
+
69
+ create index if not exists job_failed_i on jobs.job (completed_on) where state = 'failed';
70
+ drop view if exists jobs.failed;
71
+ create view jobs.failed as
72
+ select id, name, data as payload, retry_count + 1 as attempts,
73
+ coalesce(output ->> 'message', output -> 'value' ->> 'message', output ->> 'value') as error,
74
+ output, completed_on as failed_at, created_on as created_at, singleton_key as key, false as retry
75
+ from jobs.job where state = 'failed';
76
+ create or replace function jobs.failed_retry() returns trigger
77
+ language plpgsql as $$
78
+ begin
79
+ if new.retry then
80
+ update jobs.job
81
+ set state = 'retry', retry_limit = retry_limit + 1, completed_on = null,
82
+ start_after = greatest(start_after, jobs.job_now()), keep_until = greatest(keep_until, jobs.job_now() + interval '14 days')
83
+ where name = old.name and id = old.id and state = 'failed';
84
+ end if;
85
+ return new;
86
+ end
87
+ $$;
88
+ comment on function jobs.failed_retry() is 'Protobase jobs 2';
89
+ create or replace function jobs.failed_delete() returns trigger
90
+ language plpgsql as $$
91
+ begin
92
+ delete from jobs.job where name = old.name and id = old.id and state = 'failed';
93
+ return old;
94
+ end
95
+ $$;
96
+ comment on function jobs.failed_delete() is 'Protobase jobs 2';
97
+ create trigger failed_retry instead of update on jobs.failed for each row execute function jobs.failed_retry();
98
+ create trigger failed_delete instead of delete on jobs.failed for each row execute function jobs.failed_delete();