@hot-updater/postgres 0.28.0 → 0.29.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.
@@ -0,0 +1,236 @@
1
+ -- HotUpdater.is_cohort_eligible
2
+ -- Cohort eligibility helpers matching @hot-updater/core rollout.ts
3
+
4
+ CREATE OR REPLACE FUNCTION positive_mod(
5
+ value INTEGER,
6
+ modulus INTEGER
7
+ )
8
+ RETURNS INTEGER
9
+ LANGUAGE plpgsql
10
+ IMMUTABLE
11
+ AS $$
12
+ BEGIN
13
+ RETURN ((value % modulus) + modulus) % modulus;
14
+ END;
15
+ $$;
16
+
17
+ CREATE OR REPLACE FUNCTION hash_rollout_value(input TEXT)
18
+ RETURNS INTEGER
19
+ LANGUAGE plpgsql
20
+ IMMUTABLE
21
+ AS $$
22
+ DECLARE
23
+ hash_value NUMERIC := 0;
24
+ char_code INTEGER;
25
+ i INTEGER;
26
+ BEGIN
27
+ FOR i IN 1..length(input) LOOP
28
+ char_code := ascii(substring(input from i for 1));
29
+ hash_value := mod((hash_value * 31) + char_code, 4294967296);
30
+ END LOOP;
31
+
32
+ IF hash_value >= 2147483648 THEN
33
+ hash_value := hash_value - 4294967296;
34
+ END IF;
35
+
36
+ RETURN hash_value::INTEGER;
37
+ END;
38
+ $$;
39
+
40
+ CREATE OR REPLACE FUNCTION normalize_cohort_value(cohort TEXT)
41
+ RETURNS TEXT
42
+ LANGUAGE plpgsql
43
+ IMMUTABLE
44
+ AS $$
45
+ DECLARE
46
+ normalized TEXT;
47
+ cohort_value INTEGER;
48
+ BEGIN
49
+ IF cohort IS NULL THEN
50
+ RETURN NULL;
51
+ END IF;
52
+
53
+ normalized := lower(btrim(cohort));
54
+
55
+ IF normalized ~ '^[0-9]+$' THEN
56
+ cohort_value := normalized::INTEGER;
57
+ IF cohort_value BETWEEN 1 AND 1000 THEN
58
+ RETURN cohort_value::TEXT;
59
+ END IF;
60
+ END IF;
61
+
62
+ RETURN normalized;
63
+ END;
64
+ $$;
65
+
66
+ CREATE OR REPLACE FUNCTION gcd_int(a INTEGER, b INTEGER)
67
+ RETURNS INTEGER
68
+ LANGUAGE plpgsql
69
+ IMMUTABLE
70
+ AS $$
71
+ DECLARE
72
+ x INTEGER := abs(a);
73
+ y INTEGER := abs(b);
74
+ next_value INTEGER;
75
+ BEGIN
76
+ WHILE y <> 0 LOOP
77
+ next_value := x % y;
78
+ x := y;
79
+ y := next_value;
80
+ END LOOP;
81
+
82
+ RETURN x;
83
+ END;
84
+ $$;
85
+
86
+ CREATE OR REPLACE FUNCTION get_rollout_multiplier(bundle_id UUID)
87
+ RETURNS INTEGER
88
+ LANGUAGE plpgsql
89
+ IMMUTABLE
90
+ AS $$
91
+ DECLARE
92
+ candidate INTEGER := positive_mod(
93
+ hash_rollout_value(bundle_id::TEXT || ':multiplier'),
94
+ 997
95
+ );
96
+ BEGIN
97
+ IF candidate = 0 THEN
98
+ candidate := 1;
99
+ END IF;
100
+
101
+ WHILE gcd_int(candidate, 1000) <> 1 LOOP
102
+ candidate := positive_mod(candidate + 1, 1000);
103
+ IF candidate = 0 THEN
104
+ candidate := 1;
105
+ END IF;
106
+ END LOOP;
107
+
108
+ RETURN candidate;
109
+ END;
110
+ $$;
111
+
112
+ CREATE OR REPLACE FUNCTION get_rollout_offset(bundle_id UUID)
113
+ RETURNS INTEGER
114
+ LANGUAGE plpgsql
115
+ IMMUTABLE
116
+ AS $$
117
+ BEGIN
118
+ RETURN positive_mod(hash_rollout_value(bundle_id::TEXT || ':offset'), 1000);
119
+ END;
120
+ $$;
121
+
122
+ CREATE OR REPLACE FUNCTION get_modular_inverse(value INTEGER, modulus INTEGER)
123
+ RETURNS INTEGER
124
+ LANGUAGE plpgsql
125
+ IMMUTABLE
126
+ AS $$
127
+ DECLARE
128
+ candidate INTEGER;
129
+ BEGIN
130
+ FOR candidate IN 1..(modulus - 1) LOOP
131
+ IF positive_mod(value * candidate, modulus) = 1 THEN
132
+ RETURN candidate;
133
+ END IF;
134
+ END LOOP;
135
+
136
+ RAISE EXCEPTION 'No modular inverse for % mod %', value, modulus;
137
+ END;
138
+ $$;
139
+
140
+ CREATE OR REPLACE FUNCTION is_numeric_cohort(cohort TEXT)
141
+ RETURNS BOOLEAN
142
+ LANGUAGE plpgsql
143
+ IMMUTABLE
144
+ AS $$
145
+ DECLARE
146
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
147
+ cohort_value INTEGER;
148
+ BEGIN
149
+ IF normalized_cohort IS NULL OR normalized_cohort !~ '^[0-9]+$' THEN
150
+ RETURN FALSE;
151
+ END IF;
152
+
153
+ cohort_value := normalized_cohort::INTEGER;
154
+ RETURN cohort_value BETWEEN 1 AND 1000;
155
+ END;
156
+ $$;
157
+
158
+ CREATE OR REPLACE FUNCTION get_numeric_cohort_rollout_position(
159
+ bundle_id UUID,
160
+ cohort TEXT
161
+ )
162
+ RETURNS INTEGER
163
+ LANGUAGE plpgsql
164
+ IMMUTABLE
165
+ AS $$
166
+ DECLARE
167
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
168
+ cohort_value INTEGER;
169
+ multiplier INTEGER;
170
+ offset_value INTEGER;
171
+ inverse_multiplier INTEGER;
172
+ BEGIN
173
+ IF NOT is_numeric_cohort(normalized_cohort) THEN
174
+ RAISE EXCEPTION 'Invalid numeric cohort: %', cohort;
175
+ END IF;
176
+
177
+ cohort_value := normalized_cohort::INTEGER - 1;
178
+ multiplier := get_rollout_multiplier(bundle_id);
179
+ offset_value := get_rollout_offset(bundle_id);
180
+ inverse_multiplier := get_modular_inverse(multiplier, 1000);
181
+
182
+ RETURN positive_mod(
183
+ inverse_multiplier * (cohort_value - offset_value),
184
+ 1000
185
+ );
186
+ END;
187
+ $$;
188
+
189
+ CREATE OR REPLACE FUNCTION is_cohort_eligible(
190
+ bundle_id UUID,
191
+ cohort TEXT,
192
+ rollout_cohort_count INTEGER,
193
+ target_cohorts TEXT[]
194
+ )
195
+ RETURNS BOOLEAN
196
+ LANGUAGE plpgsql
197
+ IMMUTABLE
198
+ AS $$
199
+ DECLARE
200
+ normalized_cohort TEXT := normalize_cohort_value(cohort);
201
+ normalized_rollout_count INTEGER := COALESCE(rollout_cohort_count, 1000);
202
+ normalized_target_cohorts TEXT[];
203
+ BEGIN
204
+ IF target_cohorts IS NOT NULL THEN
205
+ normalized_target_cohorts := ARRAY(
206
+ SELECT normalize_cohort_value(value)
207
+ FROM unnest(target_cohorts) AS value
208
+ );
209
+ END IF;
210
+
211
+ IF normalized_target_cohorts IS NOT NULL
212
+ AND array_length(normalized_target_cohorts, 1) > 0 THEN
213
+ RETURN normalized_cohort IS NOT NULL
214
+ AND normalized_cohort = ANY(normalized_target_cohorts);
215
+ END IF;
216
+
217
+ IF normalized_rollout_count <= 0 THEN
218
+ RETURN FALSE;
219
+ END IF;
220
+
221
+ IF normalized_cohort IS NULL THEN
222
+ RETURN normalized_rollout_count >= 1000;
223
+ END IF;
224
+
225
+ IF NOT is_numeric_cohort(normalized_cohort) THEN
226
+ RETURN FALSE;
227
+ END IF;
228
+
229
+ IF normalized_rollout_count >= 1000 THEN
230
+ RETURN TRUE;
231
+ END IF;
232
+
233
+ RETURN get_numeric_cohort_rollout_position(bundle_id, normalized_cohort)
234
+ < normalized_rollout_count;
235
+ END;
236
+ $$;