From c399f75b6036064d16efd1d3ec5e47f395058cd7 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Tue, 25 Nov 2025 23:00:36 -0500 Subject: 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 --- Omni/Task/Core.hs | 67 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 26 deletions(-) (limited to 'Omni/Task/Core.hs') diff --git a/Omni/Task/Core.hs b/Omni/Task/Core.hs index af982d8..5b1551c 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 = -- cgit v1.2.3