@hot-updater/postgres 0.27.1 → 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.
- package/dist/index.cjs +527 -528
- package/dist/index.d.cts +4 -3
- package/dist/{index.d.ts → index.d.mts} +4 -3
- package/dist/{index.js → index.mjs} +515 -513
- package/package.json +14 -7
- package/sql/bundles.sql +7 -2
- package/sql/get_update_info.spec.ts +48 -12
- package/sql/get_update_info_by_app_version.sql +61 -25
- package/sql/get_update_info_by_fingerprint_hash.sql +61 -27
- package/sql/hash_user_id.sql +30 -0
- package/sql/is_device_eligible.sql +236 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hot-updater/postgres",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
5
5
|
"description": "React Native OTA solution for self-hosted",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
|
-
"module": "./dist/index.
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"import": "./dist/index.
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
11
|
"require": "./dist/index.cjs"
|
|
12
12
|
},
|
|
13
13
|
"./package.json": "./package.json"
|
|
@@ -31,16 +31,23 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"kysely": "^0.28.5",
|
|
33
33
|
"pg": "^8.13.1",
|
|
34
|
-
"@hot-updater/core": "0.
|
|
35
|
-
"@hot-updater/plugin-core": "0.
|
|
34
|
+
"@hot-updater/core": "0.29.0",
|
|
35
|
+
"@hot-updater/plugin-core": "0.29.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@electric-sql/pglite": "^0.2.15",
|
|
39
39
|
"@types/pg": "^8.11.10",
|
|
40
40
|
"camelcase-keys": "^9.1.3",
|
|
41
41
|
"pg-minify": "^1.6.5",
|
|
42
|
-
"@hot-updater/js": "0.
|
|
43
|
-
"@hot-updater/test-utils": "0.
|
|
42
|
+
"@hot-updater/js": "0.29.0",
|
|
43
|
+
"@hot-updater/test-utils": "0.29.0"
|
|
44
|
+
},
|
|
45
|
+
"inlinedDependencies": {
|
|
46
|
+
"camelcase": "8.0.0",
|
|
47
|
+
"camelcase-keys": "9.1.3",
|
|
48
|
+
"map-obj": "5.0.0",
|
|
49
|
+
"pg-minify": "1.6.5",
|
|
50
|
+
"quick-lru": "6.1.2"
|
|
44
51
|
},
|
|
45
52
|
"scripts": {
|
|
46
53
|
"build": "tsdown",
|
package/sql/bundles.sql
CHANGED
|
@@ -17,9 +17,14 @@ CREATE TABLE bundles (
|
|
|
17
17
|
CONSTRAINT check_version_or_fingerprint CHECK (
|
|
18
18
|
(target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
|
|
19
19
|
),
|
|
20
|
-
metadata jsonb DEFAULT '{}'::jsonb
|
|
20
|
+
metadata jsonb DEFAULT '{}'::jsonb,
|
|
21
|
+
rollout_cohort_count INTEGER DEFAULT 1000
|
|
22
|
+
CHECK (rollout_cohort_count >= 0 AND rollout_cohort_count <= 1000),
|
|
23
|
+
target_cohorts TEXT[]
|
|
21
24
|
);
|
|
22
25
|
|
|
23
26
|
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
24
27
|
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
25
|
-
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
28
|
+
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
29
|
+
CREATE INDEX bundles_rollout_idx ON bundles(rollout_cohort_count);
|
|
30
|
+
CREATE INDEX bundles_target_cohorts_idx ON bundles USING GIN (target_cohorts);
|
|
@@ -12,23 +12,43 @@ import { afterAll, beforeEach, describe } from "vitest";
|
|
|
12
12
|
import { prepareSql } from "./prepareSql";
|
|
13
13
|
|
|
14
14
|
const createInsertBundleQuery = (bundle: Bundle) => {
|
|
15
|
+
const rolloutCohortCount = bundle.rolloutCohortCount ?? 1000;
|
|
16
|
+
const targetCohorts = bundle.targetCohorts
|
|
17
|
+
? `ARRAY[${bundle.targetCohorts.map((id) => `'${id}'`).join(",")}]::TEXT[]`
|
|
18
|
+
: "NULL";
|
|
19
|
+
|
|
15
20
|
return `
|
|
16
21
|
INSERT INTO bundles (
|
|
17
22
|
id, file_hash, platform, target_app_version,
|
|
18
|
-
should_force_update, enabled, git_commit_hash, message, channel, storage_uri, fingerprint_hash
|
|
23
|
+
should_force_update, enabled, git_commit_hash, message, channel, storage_uri, fingerprint_hash,
|
|
24
|
+
rollout_cohort_count, target_cohorts
|
|
19
25
|
) VALUES (
|
|
20
26
|
'${bundle.id}',
|
|
21
27
|
'${bundle.fileHash}',
|
|
22
28
|
'${bundle.platform}',
|
|
23
|
-
'${bundle.targetAppVersion}',
|
|
29
|
+
${bundle.targetAppVersion ? `'${bundle.targetAppVersion}'` : "null"},
|
|
24
30
|
${bundle.shouldForceUpdate},
|
|
25
31
|
${bundle.enabled},
|
|
26
32
|
${bundle.gitCommitHash ? `'${bundle.gitCommitHash}'` : "null"},
|
|
27
33
|
${bundle.message ? `'${bundle.message}'` : "null"},
|
|
28
34
|
'${bundle.channel}',
|
|
29
35
|
'${bundle.storageUri}',
|
|
30
|
-
'${bundle.fingerprintHash}'
|
|
31
|
-
|
|
36
|
+
${bundle.fingerprintHash ? `'${bundle.fingerprintHash}'` : "null"},
|
|
37
|
+
${rolloutCohortCount},
|
|
38
|
+
${targetCohorts}
|
|
39
|
+
) ON CONFLICT (id) DO UPDATE SET
|
|
40
|
+
file_hash = EXCLUDED.file_hash,
|
|
41
|
+
platform = EXCLUDED.platform,
|
|
42
|
+
target_app_version = EXCLUDED.target_app_version,
|
|
43
|
+
should_force_update = EXCLUDED.should_force_update,
|
|
44
|
+
enabled = EXCLUDED.enabled,
|
|
45
|
+
git_commit_hash = EXCLUDED.git_commit_hash,
|
|
46
|
+
message = EXCLUDED.message,
|
|
47
|
+
channel = EXCLUDED.channel,
|
|
48
|
+
storage_uri = EXCLUDED.storage_uri,
|
|
49
|
+
fingerprint_hash = EXCLUDED.fingerprint_hash,
|
|
50
|
+
rollout_cohort_count = EXCLUDED.rollout_cohort_count,
|
|
51
|
+
target_cohorts = EXCLUDED.target_cohorts;
|
|
32
52
|
`;
|
|
33
53
|
};
|
|
34
54
|
|
|
@@ -49,11 +69,15 @@ const createGetUpdateInfo =
|
|
|
49
69
|
|
|
50
70
|
if (_updateStrategy === "fingerprint") {
|
|
51
71
|
const fingerprintHash = args.fingerprintHash;
|
|
72
|
+
const cohort = args.cohort;
|
|
73
|
+
const cohortSql = cohort ? `'${cohort}'` : "NULL";
|
|
52
74
|
const result = await db.query<{
|
|
53
75
|
id: string;
|
|
54
76
|
should_force_update: boolean;
|
|
55
77
|
message: string;
|
|
56
78
|
status: string;
|
|
79
|
+
storage_uri: string | null;
|
|
80
|
+
file_hash: string | null;
|
|
57
81
|
}>(
|
|
58
82
|
`
|
|
59
83
|
SELECT * FROM get_update_info_by_fingerprint_hash(
|
|
@@ -61,14 +85,18 @@ const createGetUpdateInfo =
|
|
|
61
85
|
'${bundleId}',
|
|
62
86
|
'${minBundleId}',
|
|
63
87
|
'${channel}',
|
|
64
|
-
'${fingerprintHash}'
|
|
88
|
+
'${fingerprintHash}',
|
|
89
|
+
${cohortSql}
|
|
65
90
|
);
|
|
66
91
|
`,
|
|
67
92
|
);
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
if (!result.rows[0]) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const row = result.rows[0];
|
|
99
|
+
return camelcaseKeys(row) as UpdateInfo;
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
const appVersion = args.appVersion;
|
|
@@ -85,11 +113,15 @@ const createGetUpdateInfo =
|
|
|
85
113
|
appVersion,
|
|
86
114
|
);
|
|
87
115
|
|
|
116
|
+
const cohort = args.cohort;
|
|
117
|
+
const cohortSql = cohort ? `'${cohort}'` : "NULL";
|
|
88
118
|
const result = await db.query<{
|
|
89
119
|
id: string;
|
|
90
120
|
should_force_update: boolean;
|
|
91
121
|
message: string;
|
|
92
122
|
status: string;
|
|
123
|
+
storage_uri: string | null;
|
|
124
|
+
file_hash: string | null;
|
|
93
125
|
}>(
|
|
94
126
|
`
|
|
95
127
|
SELECT * FROM get_update_info_by_app_version(
|
|
@@ -98,14 +130,18 @@ const createGetUpdateInfo =
|
|
|
98
130
|
'${bundleId}',
|
|
99
131
|
'${minBundleId ?? NIL_UUID}',
|
|
100
132
|
'${channel}',
|
|
101
|
-
ARRAY[${targetAppVersionList.map((v) => `'${v}'`).join(",")}]::text[]
|
|
133
|
+
ARRAY[${targetAppVersionList.map((v) => `'${v}'`).join(",")}]::text[],
|
|
134
|
+
${cohortSql}
|
|
102
135
|
);
|
|
103
136
|
`,
|
|
104
137
|
);
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
if (!result.rows[0]) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const row = result.rows[0];
|
|
144
|
+
return camelcaseKeys(row) as UpdateInfo;
|
|
109
145
|
};
|
|
110
146
|
|
|
111
147
|
const createInsertBundleQuerys = (bundles: Bundle[]) => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
-- HotUpdater.
|
|
1
|
+
-- HotUpdater.get_update_info_by_app_version
|
|
2
|
+
|
|
3
|
+
DROP FUNCTION IF EXISTS get_update_info_by_app_version;
|
|
2
4
|
|
|
3
5
|
CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
|
|
4
6
|
app_platform platforms,
|
|
@@ -6,7 +8,8 @@ CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
|
|
|
6
8
|
bundle_id uuid,
|
|
7
9
|
min_bundle_id uuid,
|
|
8
10
|
target_channel text,
|
|
9
|
-
target_app_version_list text[]
|
|
11
|
+
target_app_version_list text[],
|
|
12
|
+
cohort TEXT DEFAULT NULL
|
|
10
13
|
)
|
|
11
14
|
RETURNS TABLE (
|
|
12
15
|
id uuid,
|
|
@@ -23,45 +26,78 @@ DECLARE
|
|
|
23
26
|
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
24
27
|
BEGIN
|
|
25
28
|
RETURN QUERY
|
|
26
|
-
WITH
|
|
29
|
+
WITH candidate_bundles AS (
|
|
27
30
|
SELECT
|
|
28
31
|
b.id,
|
|
29
32
|
b.should_force_update,
|
|
30
33
|
b.message,
|
|
31
|
-
'UPDATE' AS status,
|
|
32
34
|
b.storage_uri,
|
|
33
|
-
b.file_hash
|
|
35
|
+
b.file_hash,
|
|
36
|
+
b.rollout_cohort_count,
|
|
37
|
+
b.target_cohorts
|
|
34
38
|
FROM bundles b
|
|
35
39
|
WHERE b.enabled = TRUE
|
|
36
40
|
AND b.platform = app_platform
|
|
37
|
-
AND b.id >=
|
|
38
|
-
AND b.id > min_bundle_id
|
|
41
|
+
AND b.id >= min_bundle_id
|
|
39
42
|
AND b.target_app_version IN (SELECT unnest(target_app_version_list))
|
|
40
43
|
AND b.channel = target_channel
|
|
41
|
-
|
|
44
|
+
),
|
|
45
|
+
current_candidate AS (
|
|
46
|
+
SELECT
|
|
47
|
+
cb.id,
|
|
48
|
+
is_cohort_eligible(
|
|
49
|
+
cb.id,
|
|
50
|
+
cohort,
|
|
51
|
+
cb.rollout_cohort_count,
|
|
52
|
+
cb.target_cohorts
|
|
53
|
+
) AS is_eligible
|
|
54
|
+
FROM candidate_bundles cb
|
|
55
|
+
WHERE cb.id = bundle_id
|
|
56
|
+
LIMIT 1
|
|
57
|
+
),
|
|
58
|
+
eligible_update_candidate AS (
|
|
59
|
+
SELECT
|
|
60
|
+
cb.id,
|
|
61
|
+
cb.should_force_update,
|
|
62
|
+
cb.message,
|
|
63
|
+
'UPDATE' AS status,
|
|
64
|
+
cb.storage_uri,
|
|
65
|
+
cb.file_hash
|
|
66
|
+
FROM candidate_bundles cb
|
|
67
|
+
WHERE cb.id > bundle_id
|
|
68
|
+
AND is_cohort_eligible(
|
|
69
|
+
cb.id,
|
|
70
|
+
cohort,
|
|
71
|
+
cb.rollout_cohort_count,
|
|
72
|
+
cb.target_cohorts
|
|
73
|
+
)
|
|
74
|
+
ORDER BY cb.id DESC
|
|
42
75
|
LIMIT 1
|
|
43
76
|
),
|
|
44
77
|
rollback_candidate AS (
|
|
45
78
|
SELECT
|
|
46
|
-
|
|
79
|
+
cb.id,
|
|
47
80
|
TRUE AS should_force_update,
|
|
48
|
-
|
|
81
|
+
cb.message,
|
|
49
82
|
'ROLLBACK' AS status,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
FROM
|
|
53
|
-
WHERE
|
|
54
|
-
AND
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
cb.storage_uri,
|
|
84
|
+
cb.file_hash
|
|
85
|
+
FROM candidate_bundles cb
|
|
86
|
+
WHERE cb.id < bundle_id
|
|
87
|
+
AND NOT EXISTS (
|
|
88
|
+
SELECT 1
|
|
89
|
+
FROM current_candidate
|
|
90
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
91
|
+
)
|
|
92
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
93
|
+
ORDER BY cb.id DESC
|
|
58
94
|
LIMIT 1
|
|
59
95
|
),
|
|
60
96
|
final_result AS (
|
|
61
|
-
SELECT * FROM
|
|
97
|
+
SELECT * FROM eligible_update_candidate
|
|
62
98
|
UNION ALL
|
|
63
99
|
SELECT * FROM rollback_candidate
|
|
64
|
-
WHERE NOT EXISTS (SELECT 1 FROM
|
|
100
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
65
101
|
)
|
|
66
102
|
SELECT *
|
|
67
103
|
FROM final_result
|
|
@@ -81,10 +117,10 @@ BEGIN
|
|
|
81
117
|
AND bundle_id > min_bundle_id
|
|
82
118
|
AND NOT EXISTS (
|
|
83
119
|
SELECT 1
|
|
84
|
-
FROM
|
|
85
|
-
WHERE
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
120
|
+
FROM current_candidate
|
|
121
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
122
|
+
)
|
|
123
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
124
|
+
AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
|
|
89
125
|
END;
|
|
90
126
|
$$;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
-- HotUpdater.
|
|
1
|
+
-- HotUpdater.get_update_info_by_fingerprint_hash
|
|
2
|
+
|
|
3
|
+
DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
|
|
2
4
|
|
|
3
5
|
CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
|
|
4
6
|
app_platform platforms,
|
|
5
7
|
bundle_id uuid,
|
|
6
8
|
min_bundle_id uuid,
|
|
7
9
|
target_channel text,
|
|
8
|
-
target_fingerprint_hash text
|
|
10
|
+
target_fingerprint_hash text,
|
|
11
|
+
cohort TEXT DEFAULT NULL
|
|
9
12
|
)
|
|
10
13
|
RETURNS TABLE (
|
|
11
14
|
id uuid,
|
|
@@ -22,47 +25,78 @@ DECLARE
|
|
|
22
25
|
NIL_UUID CONSTANT uuid := '00000000-0000-0000-0000-000000000000';
|
|
23
26
|
BEGIN
|
|
24
27
|
RETURN QUERY
|
|
25
|
-
WITH
|
|
28
|
+
WITH candidate_bundles AS (
|
|
26
29
|
SELECT
|
|
27
30
|
b.id,
|
|
28
31
|
b.should_force_update,
|
|
29
32
|
b.message,
|
|
30
|
-
'UPDATE' AS status,
|
|
31
33
|
b.storage_uri,
|
|
32
|
-
b.file_hash
|
|
34
|
+
b.file_hash,
|
|
35
|
+
b.rollout_cohort_count,
|
|
36
|
+
b.target_cohorts
|
|
33
37
|
FROM bundles b
|
|
34
38
|
WHERE b.enabled = TRUE
|
|
35
39
|
AND b.platform = app_platform
|
|
36
|
-
AND b.id >=
|
|
37
|
-
AND b.id > min_bundle_id
|
|
40
|
+
AND b.id >= min_bundle_id
|
|
38
41
|
AND b.channel = target_channel
|
|
39
42
|
AND b.fingerprint_hash = target_fingerprint_hash
|
|
40
|
-
|
|
43
|
+
),
|
|
44
|
+
current_candidate AS (
|
|
45
|
+
SELECT
|
|
46
|
+
cb.id,
|
|
47
|
+
is_cohort_eligible(
|
|
48
|
+
cb.id,
|
|
49
|
+
cohort,
|
|
50
|
+
cb.rollout_cohort_count,
|
|
51
|
+
cb.target_cohorts
|
|
52
|
+
) AS is_eligible
|
|
53
|
+
FROM candidate_bundles cb
|
|
54
|
+
WHERE cb.id = bundle_id
|
|
55
|
+
LIMIT 1
|
|
56
|
+
),
|
|
57
|
+
eligible_update_candidate AS (
|
|
58
|
+
SELECT
|
|
59
|
+
cb.id,
|
|
60
|
+
cb.should_force_update,
|
|
61
|
+
cb.message,
|
|
62
|
+
'UPDATE' AS status,
|
|
63
|
+
cb.storage_uri,
|
|
64
|
+
cb.file_hash
|
|
65
|
+
FROM candidate_bundles cb
|
|
66
|
+
WHERE cb.id > bundle_id
|
|
67
|
+
AND is_cohort_eligible(
|
|
68
|
+
cb.id,
|
|
69
|
+
cohort,
|
|
70
|
+
cb.rollout_cohort_count,
|
|
71
|
+
cb.target_cohorts
|
|
72
|
+
)
|
|
73
|
+
ORDER BY cb.id DESC
|
|
41
74
|
LIMIT 1
|
|
42
75
|
),
|
|
43
76
|
rollback_candidate AS (
|
|
44
77
|
SELECT
|
|
45
|
-
|
|
78
|
+
cb.id,
|
|
46
79
|
TRUE AS should_force_update,
|
|
47
|
-
|
|
80
|
+
cb.message,
|
|
48
81
|
'ROLLBACK' AS status,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
FROM
|
|
52
|
-
WHERE
|
|
53
|
-
AND
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
cb.storage_uri,
|
|
83
|
+
cb.file_hash
|
|
84
|
+
FROM candidate_bundles cb
|
|
85
|
+
WHERE cb.id < bundle_id
|
|
86
|
+
AND NOT EXISTS (
|
|
87
|
+
SELECT 1
|
|
88
|
+
FROM current_candidate
|
|
89
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
90
|
+
)
|
|
91
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
92
|
+
ORDER BY cb.id DESC
|
|
59
93
|
LIMIT 1
|
|
60
94
|
),
|
|
61
95
|
final_result AS (
|
|
62
|
-
SELECT * FROM
|
|
96
|
+
SELECT * FROM eligible_update_candidate
|
|
63
97
|
UNION ALL
|
|
64
98
|
SELECT * FROM rollback_candidate
|
|
65
|
-
WHERE NOT EXISTS (SELECT 1 FROM
|
|
99
|
+
WHERE NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
66
100
|
)
|
|
67
101
|
SELECT *
|
|
68
102
|
FROM final_result
|
|
@@ -82,10 +116,10 @@ BEGIN
|
|
|
82
116
|
AND bundle_id > min_bundle_id
|
|
83
117
|
AND NOT EXISTS (
|
|
84
118
|
SELECT 1
|
|
85
|
-
FROM
|
|
86
|
-
WHERE
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
119
|
+
FROM current_candidate
|
|
120
|
+
WHERE current_candidate.is_eligible = TRUE
|
|
121
|
+
)
|
|
122
|
+
AND NOT EXISTS (SELECT 1 FROM eligible_update_candidate)
|
|
123
|
+
AND NOT EXISTS (SELECT 1 FROM rollback_candidate);
|
|
90
124
|
END;
|
|
91
125
|
$$;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- Deterministic hash function matching JavaScript implementation
|
|
2
|
+
-- Returns hash value in range [0, 99]
|
|
3
|
+
CREATE OR REPLACE FUNCTION hash_user_id(user_id TEXT)
|
|
4
|
+
RETURNS INTEGER
|
|
5
|
+
LANGUAGE plpgsql
|
|
6
|
+
IMMUTABLE
|
|
7
|
+
AS $$
|
|
8
|
+
DECLARE
|
|
9
|
+
hash BIGINT := 0;
|
|
10
|
+
char_code INTEGER;
|
|
11
|
+
i INTEGER;
|
|
12
|
+
normalized BIGINT;
|
|
13
|
+
BEGIN
|
|
14
|
+
-- Replicate JavaScript hash algorithm
|
|
15
|
+
FOR i IN 1..length(user_id) LOOP
|
|
16
|
+
char_code := ascii(substring(user_id from i for 1));
|
|
17
|
+
hash := (hash * 31) + char_code;
|
|
18
|
+
|
|
19
|
+
-- Simulate JavaScript's |= 0 by wrapping into a signed 32-bit range.
|
|
20
|
+
normalized := mod(hash + 2147483648, 4294967296);
|
|
21
|
+
IF normalized < 0 THEN
|
|
22
|
+
normalized := normalized + 4294967296;
|
|
23
|
+
END IF;
|
|
24
|
+
hash := normalized - 2147483648;
|
|
25
|
+
END LOOP;
|
|
26
|
+
|
|
27
|
+
-- Return absolute value modulo 100
|
|
28
|
+
RETURN abs(hash % 100);
|
|
29
|
+
END;
|
|
30
|
+
$$;
|