From 38f9cfd48e27284ae538602eec3951639d1cc325 Mon Sep 17 00:00:00 2001 From: sjdonado Date: Thu, 20 Mar 2025 07:34:33 +0100 Subject: [PATCH] fix: replace uuid columns with rowid aliases --- app/controllers/link.cr | 4 +- ...convert_all_tables_text_ids_to_integer.sql | 102 ++++++++++++++++++ db/seed.sql | 79 ++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 db/migrations/20250319192003_convert_all_tables_text_ids_to_integer.sql create mode 100644 db/seed.sql diff --git a/app/controllers/link.cr b/app/controllers/link.cr index 98c91aa..b5c0bb7 100644 --- a/app/controllers/link.cr +++ b/app/controllers/link.cr @@ -42,7 +42,9 @@ module App::Controllers slug = @env.params.url["slug"] link_data = nil - Database.raw_query("SELECT id, url FROM links WHERE slug = (?) LIMIT 1", slug) do |result| + # LIMIT 1 degrades performance on unique field searches + # slug autoindex has better perormance than the covering index + Database.raw_query("SELECT id, url FROM links WHERE slug = (?)", slug) do |result| if result.move_next link_data = {result.read(String), result.read(String)} end diff --git a/db/migrations/20250319192003_convert_all_tables_text_ids_to_integer.sql b/db/migrations/20250319192003_convert_all_tables_text_ids_to_integer.sql new file mode 100644 index 0000000..545d46f --- /dev/null +++ b/db/migrations/20250319192003_convert_all_tables_text_ids_to_integer.sql @@ -0,0 +1,102 @@ +-- +micrate Up +-- SQL in section 'Up' is executed when this migration is applied +-- 1. Create new users table with INTEGER PK +CREATE TABLE users_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(100) NOT NULL, + api_key VARCHAR(64) UNIQUE NOT NULL, + created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL +); + +-- Create a mapping table to track old and new user IDs +CREATE TEMPORARY TABLE user_id_map ( + old_id TEXT, + new_id INTEGER +); + +-- Insert users data and capture the mappings +INSERT INTO users_new (name, api_key, created_at, updated_at) +SELECT name, api_key, created_at, updated_at FROM users; + +INSERT INTO user_id_map +SELECT u.id, u_new.id +FROM users u +JOIN users_new u_new ON u_new.api_key = u.api_key; + +-- 2. Create new links table with INTEGER PK +CREATE TABLE links_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + slug VARCHAR(8) UNIQUE NOT NULL, + url TEXT NOT NULL, + created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (user_id) REFERENCES users_new(id) ON DELETE CASCADE +); + +-- Create a mapping table for links +CREATE TEMPORARY TABLE link_id_map ( + old_id TEXT, + new_id INTEGER +); + +-- Insert links data with new user_id foreign keys +INSERT INTO links_new (user_id, slug, url, created_at, updated_at) +SELECT + (SELECT new_id FROM user_id_map WHERE old_id = l.user_id), + l.slug, + l.url, + l.created_at, + l.updated_at +FROM links l; + +-- Create the mapping for links +INSERT INTO link_id_map +SELECT l.id, l_new.id +FROM links l +JOIN links_new l_new ON l_new.slug = l.slug AND l_new.url = l.url; + +-- 3. Create new clicks table with INTEGER PK +CREATE TABLE clicks_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + link_id INTEGER NOT NULL, + user_agent TEXT, + browser TEXT, + os TEXT, + referer TEXT, + country TEXT, + created_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (link_id) REFERENCES links_new(id) ON DELETE CASCADE +); + +-- Insert clicks data with new link_id foreign keys +INSERT INTO clicks_new (link_id, user_agent, browser, os, referer, country, created_at, updated_at) +SELECT + (SELECT new_id FROM link_id_map WHERE old_id = c.link_id), + c.user_agent, + c.browser, + c.os, + c.referer, + c.country, + c.created_at, + c.updated_at +FROM clicks c; + +-- 4. Drop old tables and rename new tables +DROP TABLE clicks; +DROP TABLE links; +DROP TABLE users; + +ALTER TABLE clicks_new RENAME TO clicks; +ALTER TABLE links_new RENAME TO links; +ALTER TABLE users_new RENAME TO users; + +-- 5. Drop unused indexes +DROP INDEX IF EXISTS index_users_api_key; +DROP INDEX IF EXISTS idx_links_slug; +DROP INDEX IF EXISTS idx_links_slug_optimized; + +-- +micrate Down +-- SQL section 'Down' is executed when this migration is rolled back diff --git a/db/seed.sql b/db/seed.sql new file mode 100644 index 0000000..cf027f3 --- /dev/null +++ b/db/seed.sql @@ -0,0 +1,79 @@ +-- Create 10 users +INSERT INTO users (name, api_key) +VALUES +('User 1', 'secure_api_key_1'), +('User 2', 'secure_api_key_2'), +('User 3', 'secure_api_key_3'), +('User 4', 'secure_api_key_4'), +('User 5', 'secure_api_key_5'), +('User 6', 'secure_api_key_6'), +('User 7', 'secure_api_key_7'), +('User 8', 'secure_api_key_8'), +('User 9', 'secure_api_key_9'), +('User 10', 'secure_api_key_10'); + +-- Create 1,000 links (100 per user) +WITH RECURSIVE link_numbers(n) AS ( + SELECT 1 + UNION ALL + SELECT n+1 FROM link_numbers + LIMIT 1000 +) +INSERT INTO links (user_id, slug, url) +SELECT + ((n-1) % 10) + 1, -- User ID (1-10) + 'slug' || n, -- Unique slug + 'https://sjdonado.com/page/' || n +FROM link_numbers; + +-- Create 1,000,000 clicks (1,000 per link) +WITH RECURSIVE link_numbers(link_id) AS ( + SELECT id FROM links +), +click_batch(link_id, n) AS ( + SELECT link_id, 1 FROM link_numbers + UNION ALL + SELECT link_id, n+1 FROM click_batch WHERE n < 1000 +) +INSERT INTO clicks (link_id, user_agent, browser, os, referer, country) +SELECT + link_id, + CASE (n % 5) + WHEN 0 THEN 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' + WHEN 1 THEN 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15)' + WHEN 2 THEN 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)' + WHEN 3 THEN 'Mozilla/5.0 (X11; Linux x86_64)' + ELSE 'Mozilla/5.0 (Android 11; Mobile)' + END, + CASE (n % 3) + WHEN 0 THEN 'Chrome' + WHEN 1 THEN 'Firefox' + ELSE 'Safari' + END, + CASE (n % 4) + WHEN 0 THEN 'Windows' + WHEN 1 THEN 'macOS' + WHEN 2 THEN 'iOS' + ELSE 'Android' + END, + CASE (n % 6) + WHEN 0 THEN 'https://sjdonado.com' + WHEN 1 THEN 'https://donado.co' + WHEN 2 THEN 'https://idonthavespotify.donado.co' + WHEN 3 THEN 'https://spookyplanning.com' + WHEN 4 THEN 'https://github.com/sjdonado' + ELSE NULL + END, + CASE (n % 10) + WHEN 0 THEN 'US' + WHEN 1 THEN 'UK' + WHEN 2 THEN 'Canada' + WHEN 3 THEN 'Germany' + WHEN 4 THEN 'France' + WHEN 5 THEN 'Japan' + WHEN 6 THEN 'Australia' + WHEN 7 THEN 'Brazil' + WHEN 8 THEN 'India' + ELSE 'China' + END +FROM click_batch;