Use timestamp-based IDs

This change introduces timestamp-based (Snowflake/Cornflake/Waspflake)
identifier.
This commit is contained in:
Georg Gadinger 2020-07-04 20:12:53 +02:00
parent d627c4c261
commit efec568d43
3 changed files with 115 additions and 32 deletions

View File

@ -430,3 +430,32 @@ namespace :justask do
end
end
end
namespace :db do
namespace :schema do
task :create_timestampid_function do
conn = ActiveRecord::Base.connection
have_func = conn.execute("SELECT EXISTS(SELECT * FROM pg_proc WHERE proname = 'gen_timestamp_id');").values.first.first
next if have_func
statement = File.read(File.join(__dir__, 'db/migrate/20200704163504_use_timestamped_ids.rb')).match(/<~SQL(?<stmt>.+)SQL$/m)[:stmt]
conn.execute(statement)
end
task :create_id_sequences do
conn = ActiveRecord::Base.connection
# required for the timestampid function to work properly
%i[questions answers comments smiles comment_smiles users].each do |tbl|
conn.execute("CREATE SEQUENCE IF NOT EXISTS #{tbl}_id_seq")
end
end
end
# create timestampid before loading schema
Rake::Task['db:schema:load'].enhance ['db:schema:create_timestampid_function']
# create id_sequences after loading schema
Rake::Task['db:schema:load'].enhance do
Rake::Task['db:schema:create_id_sequences'].invoke
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'securerandom'
# This migration changes the IDs of several tables from serial to a
# timestamped/"snowflake" one.
#
# Instead of "snowflakes" we shall call those IDs Cornflakes or Waspflakes
# instead.
#
# Implementation somewhat lifted from Mastodon.
class UseTimestampedIds < ActiveRecord::Migration[5.2]
def up
# PL/pgSQL is just spicy pascal
# don't @ me
execute <<~SQL
CREATE or replace FUNCTION gen_timestamp_id(tblname text) RETURNS bigint AS $$
DECLARE
timepart bigint;
seqpart bigint;
BEGIN
timepart := (date_part('epoch', now()) * 1000)::bigint << 16;
seqpart := ('x' || substr(md5(tblname ||
'#{SecureRandom.hex(16)}' ||
timepart::text), 1, 4))::bit(16)::bigint;
RETURN timepart | ((seqpart + nextval(tblname || '_id_seq')) & 65535);
END;
$$ LANGUAGE plpgsql VOLATILE;
SQL
# we need to migrate related columns to bigints for this to work
{
question: %i[answers inboxes],
answer: %i[comments smiles subscriptions],
comment: %i[comment_smiles],
user: %i[announcements answers comment_smiles comments inboxes list_members lists moderation_comments moderation_votes questions reports services smiles subscriptions themes users_roles],
# polymorphic tables go brrr
recipient: %i[notifications],
source: %i[relationships],
target: %i[notifications relationships reports],
}.each do |ref, tbls|
tbls.each do |tbl|
say "Migrating #{tbl}.#{ref}_id to bigint"
change_column(tbl, :"#{ref}_id", :bigint)
end
end
%i[questions answers comments smiles comment_smiles users].each do |tbl|
say "Migrating #{tbl} to use timestamped ids"
change_column(tbl, :id, :bigint, default: -> { "gen_timestamp_id('#{tbl}'::text)" })
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_05_25_145144) do
ActiveRecord::Schema.define(version: 2020_07_04_163504) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -27,11 +27,11 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
t.index ["user_id"], name: "index_announcements_on_user_id"
end
create_table "answers", id: :serial, force: :cascade do |t|
create_table "answers", id: :bigint, default: -> { "gen_timestamp_id('answers'::text)" }, force: :cascade do |t|
t.text "content"
t.integer "question_id"
t.bigint "question_id"
t.integer "comment_count", default: 0, null: false
t.integer "user_id"
t.bigint "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "smile_count", default: 0, null: false
@ -39,9 +39,9 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
t.index ["user_id", "created_at"], name: "index_answers_on_user_id_and_created_at"
end
create_table "comment_smiles", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "comment_id"
create_table "comment_smiles", id: :bigint, default: -> { "gen_timestamp_id('comment_smiles'::text)" }, force: :cascade do |t|
t.bigint "user_id"
t.bigint "comment_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["comment_id"], name: "index_comment_smiles_on_comment_id"
@ -49,10 +49,10 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
t.index ["user_id"], name: "index_comment_smiles_on_user_id"
end
create_table "comments", id: :serial, force: :cascade do |t|
create_table "comments", id: :bigint, default: -> { "gen_timestamp_id('comments'::text)" }, force: :cascade do |t|
t.string "content"
t.integer "answer_id"
t.integer "user_id"
t.bigint "answer_id"
t.bigint "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "smile_count", default: 0, null: false
@ -61,8 +61,8 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
end
create_table "inboxes", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "question_id"
t.bigint "user_id"
t.bigint "question_id"
t.boolean "new"
t.datetime "created_at"
t.datetime "updated_at"
@ -71,14 +71,14 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "list_members", id: :serial, force: :cascade do |t|
t.integer "list_id", null: false
t.integer "user_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.index ["list_id", "user_id"], name: "index_list_members_on_list_id_and_user_id", unique: true
end
create_table "lists", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.bigint "user_id", null: false
t.string "name"
t.string "display_name"
t.boolean "private", default: true
@ -89,7 +89,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "moderation_comments", id: :serial, force: :cascade do |t|
t.integer "report_id"
t.integer "user_id"
t.bigint "user_id"
t.string "content"
t.datetime "created_at"
t.datetime "updated_at"
@ -98,7 +98,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "moderation_votes", id: :serial, force: :cascade do |t|
t.integer "report_id", null: false
t.integer "user_id", null: false
t.bigint "user_id", null: false
t.boolean "upvote", default: false, null: false
t.datetime "created_at"
t.datetime "updated_at"
@ -109,19 +109,19 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "notifications", id: :serial, force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
t.integer "recipient_id"
t.bigint "target_id"
t.bigint "recipient_id"
t.boolean "new"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "questions", id: :serial, force: :cascade do |t|
create_table "questions", id: :bigint, default: -> { "gen_timestamp_id('questions'::text)" }, force: :cascade do |t|
t.string "content"
t.boolean "author_is_anonymous"
t.string "author_name"
t.string "author_email"
t.integer "user_id"
t.bigint "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "answer_count", default: 0, null: false
@ -129,8 +129,8 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
end
create_table "relationships", id: :serial, force: :cascade do |t|
t.integer "source_id"
t.integer "target_id"
t.bigint "source_id"
t.bigint "target_id"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["source_id", "target_id"], name: "index_relationships_on_source_id_and_target_id", unique: true
@ -140,8 +140,8 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "reports", id: :serial, force: :cascade do |t|
t.string "type", null: false
t.integer "target_id", null: false
t.integer "user_id", null: false
t.bigint "target_id", null: false
t.bigint "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "deleted", default: false
@ -162,7 +162,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
create_table "services", id: :serial, force: :cascade do |t|
t.string "type", null: false
t.integer "user_id", null: false
t.bigint "user_id", null: false
t.string "uid"
t.string "access_token"
t.string "access_secret"
@ -172,9 +172,9 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
t.index ["user_id"], name: "index_services_on_user_id"
end
create_table "smiles", id: :serial, force: :cascade do |t|
t.integer "user_id"
t.integer "answer_id"
create_table "smiles", id: :bigint, default: -> { "gen_timestamp_id('smiles'::text)" }, force: :cascade do |t|
t.bigint "user_id"
t.bigint "answer_id"
t.datetime "created_at"
t.datetime "updated_at"
t.index ["answer_id"], name: "index_smiles_on_answer_id"
@ -183,8 +183,8 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
end
create_table "subscriptions", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "answer_id", null: false
t.bigint "user_id", null: false
t.bigint "answer_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "is_active", default: true
@ -192,7 +192,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
end
create_table "themes", id: :serial, force: :cascade do |t|
t.integer "user_id", null: false
t.bigint "user_id", null: false
t.integer "primary_color", default: 6174129
t.integer "primary_text", default: 16777215
t.integer "danger_color", default: 14431557
@ -219,7 +219,7 @@ ActiveRecord::Schema.define(version: 2020_05_25_145144) do
t.index ["user_id", "created_at"], name: "index_themes_on_user_id_and_created_at"
end
create_table "users", id: :serial, force: :cascade do |t|
create_table "users", id: :bigint, default: -> { "gen_timestamp_id('users'::text)" }, force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"