← Back to task

Commit c399f75b

commit c399f75b6036064d16efd1d3ec5e47f395058cd7
Author: Ben Sima <ben@bensima.com>
Date:   Tue Nov 25 23:00:36 2025

    task: use sqids for uniform-length IDs
    
    8-char lowercase IDs using sqids with sequential counter.
    
    Task-Id: t-1o2g8gu9y2z Amp-Thread-ID:
    https://ampcode.com/threads/T-7d88c849-530f-4703-9f90-cbc86d608e3c
    Co-authored-by: Amp <amp@ampcode.com>

diff --git a/Omni/Bild/Deps/Haskell.nix b/Omni/Bild/Deps/Haskell.nix
index d7147996..7e3650a5 100644
--- a/Omni/Bild/Deps/Haskell.nix
+++ b/Omni/Bild/Deps/Haskell.nix
@@ -50,6 +50,7 @@
   "servant-lucid"
   "servant-server"
   "split"
+  "sqids"
   "sqlite-simple"
   "stm"
   "tasty"
diff --git a/Omni/Bild/Haskell.nix b/Omni/Bild/Haskell.nix
index 7e969dab..e55dee9f 100644
--- a/Omni/Bild/Haskell.nix
+++ b/Omni/Bild/Haskell.nix
@@ -26,6 +26,7 @@ in rec {
                     servant-auth = doJailbreak sup.servant-auth;
                     servant-auth-server = dontCheck sup.servant-auth-server;
                     shellcheck = doJailbreak sup.shellcheck;
+                    sqids = dontCheck sup.sqids;
                     string-qq = doJailbreak sup.string-qq;
                     syb-with-class = doJailbreak sup.syb-with-class;
                     th-abstraction = doJailbreak sup.th-abstraction;
diff --git a/Omni/Task.hs b/Omni/Task.hs
index a724e824..7ad05480 100644
--- a/Omni/Task.hs
+++ b/Omni/Task.hs
@@ -5,6 +5,7 @@
 
 -- : out task
 -- : dep sqlite-simple
+-- : dep sqids
 -- : modified by benign worker
 module Omni.Task where
 
diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs
index af982d83..5b1551c4 100644
--- a/Omni/Task/Core.hs
+++ b/Omni/Task/Core.hs
@@ -12,8 +12,7 @@ import qualified Data.ByteString.Lazy.Char8 as BLC
 import qualified Data.List as List
 import qualified Data.Text as T
 import qualified Data.Text.IO as TIO
-import Data.Time (UTCTime, diffTimeToPicoseconds, getCurrentTime, utctDay, utctDayTime)
-import Data.Time.Calendar (toModifiedJulianDay)
+import Data.Time (UTCTime, getCurrentTime)
 import qualified Database.SQLite.Simple as SQL
 import qualified Database.SQLite.Simple.FromField as SQL
 import qualified Database.SQLite.Simple.Ok as SQLOk
@@ -22,6 +21,7 @@ import GHC.Generics ()
 import System.Directory (createDirectoryIfMissing, doesFileExist)
 import System.Environment (lookupEnv)
 import System.IO.Unsafe (unsafePerformIO)
+import qualified Web.Sqids as Sqids
 
 -- Core data types
 data Task = Task
@@ -242,18 +242,48 @@ initTaskDb = do
       \ created_at TIMESTAMP NOT NULL, \
       \ updated_at TIMESTAMP NOT NULL \
       \)"
+    SQL.execute_
+      conn
+      "CREATE TABLE IF NOT EXISTS id_counter (\
+      \ id INTEGER PRIMARY KEY CHECK (id = 1), \
+      \ counter INTEGER NOT NULL DEFAULT 0 \
+      \)"
+    SQL.execute_
+      conn
+      "INSERT OR IGNORE INTO id_counter (id, counter) VALUES (1, 0)"
+
+-- Sqids configuration: lowercase alphabet only, minimum length 8
+sqidsOptions :: Sqids.SqidsOptions
+sqidsOptions =
+  Sqids.defaultSqidsOptions
+    { Sqids.alphabet = "abcdefghijklmnopqrstuvwxyz0123456789",
+      Sqids.minLength = 8,
+      Sqids.blocklist = []
+    }
 
--- Generate a short ID using base36 encoding of timestamp (lowercase)
+-- Generate a short ID using sqids with sequential counter
 generateId :: IO Text
 generateId = do
-  now <- getCurrentTime
-  let day = utctDay now
-      dayTime = utctDayTime now
-      mjd = toModifiedJulianDay day
-      micros = diffTimeToPicoseconds dayTime `div` 1000000
-      totalMicros = (mjd * 100000000000) + micros
-      encoded = toBase36 totalMicros
-  pure <| "t-" <> T.pack encoded
+  counter <- getNextCounter
+  let encoded = case Sqids.runSqids sqidsOptions (Sqids.encode [counter]) of
+        Left _ -> "00000000"
+        Right sqid -> sqid
+  pure <| "t-" <> encoded
+
+-- Get the next counter value (atomically increments)
+getNextCounter :: IO Int
+getNextCounter =
+  withDb <| \conn -> do
+    SQL.execute_
+      conn
+      "CREATE TABLE IF NOT EXISTS id_counter (\
+      \ id INTEGER PRIMARY KEY CHECK (id = 1), \
+      \ counter INTEGER NOT NULL DEFAULT 0 \
+      \)"
+    SQL.execute_ conn "INSERT OR IGNORE INTO id_counter (id, counter) VALUES (1, 0)"
+    SQL.execute_ conn "UPDATE id_counter SET counter = counter + 1 WHERE id = 1"
+    [SQL.Only c] <- SQL.query_ conn "SELECT counter FROM id_counter WHERE id = 1" :: IO [SQL.Only Int]
+    pure c
 
 -- Generate a child ID based on parent ID
 generateChildId :: Text -> IO Text
@@ -279,21 +309,6 @@ getSuffix parent childId =
             else Nothing
     else Nothing
 
--- Convert number to base36 (0-9, a-z)
-toBase36 :: Integer -> String
-toBase36 0 = "0"
-toBase36 n = reverse <| go n
-  where
-    alphabet = ['0' .. '9'] ++ ['a' .. 'z']
-    go 0 = []
-    go x =
-      let (q, r) = x `divMod` 36
-          idx = fromIntegral r
-          char = case drop idx alphabet of
-            (c : _) -> c
-            [] -> '0'
-       in char : go q
-
 -- Load all tasks from DB
 loadTasks :: IO [Task]
 loadTasks =