How do we design schema for user settings table for postgresql? - sql

How do we design schema for user settings/preferences table in a sql database like postgresql?
I am interested to know the proper way to design the schema of users_setting table where users are able to modify their settings. This seems to be a 1-to-1 relationship because each row of users table corresponds to a single row in the users_setting table
so this is like a 1-to-1 table relation between users and users_setting. Is this the wrong way to do this? I have searched online and could not really find any useful example schemas where users manage their settings. So here i am asking this question. I am certain this will help many people also
Here is what my current design looks like
DROP TABLE if exists users cascade;
DROP TABLE IF EXISTS "users";
DROP SEQUENCE IF EXISTS users_id_seq;
CREATE SEQUENCE users_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1;
CREATE TABLE "public"."users" (
"id" bigint DEFAULT nextval('users_id_seq') NOT NULL,
"email" text NOT NULL,
"password" text NOT NULL,
"full_name" text NOT NULL,
"status" text NOT NULL,
"is_verified" boolean NOT NULL,
"role" text NOT NULL,
"created_at" timestamptz NOT NULL,
"updated_at" timestamptz NOT NULL,
"verified_at" timestamptz NOT NULL,
CONSTRAINT "users_email_key" UNIQUE ("email"),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
DROP TABLE if exists users_setting cascade;
DROP TABLE IF EXISTS "users_setting";
DROP SEQUENCE IF EXISTS users_setting_id_seq;
CREATE SEQUENCE users_setting_id_seq INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 CACHE 1;
CREATE TABLE "public"."users_setting" (
"id" bigint DEFAULT nextval('users_setting_id_seq') NOT NULL,
"default_currency" text NOT NULL,
"default_timezone" text NOT NULL,
"default_notification_method" text NOT NULL,
"default_source" text NOT NULL,
"default_cooldown" integer NOT NULL,
"updated_at" timestamptz NOT NULL,
"user_id" bigint,
CONSTRAINT "users_setting_pkey" PRIMARY KEY ("id")
) WITH (oids = false);
ALTER TABLE ONLY "public"."users_setting" ADD CONSTRAINT "users_setting_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "users"(id) NOT DEFERRABLE;
begin transaction;
INSERT INTO "users" ("id", "email", "password", "full_name", "status", "is_verified", "role", "created_at", "updated_at", "verified_at") VALUES
(1, 'users1#email.com', 'password', 'users1', 'active', '1', 'superuser', '2022-07-05 01:05:50.22384+00', '0001-01-01 00:00:00+00', '2022-07-11 14:10:26.615722+00'),
(2, 'users2#email.com', 'password', 'users2', 'active', '0', 'user', '2022-07-05 01:05:50.22384+00', '0001-01-01 00:00:00+00', '2022-07-11 14:10:26.615722+00');
INSERT INTO "users_setting" ("id", "default_currency", "default_timezone", "default_notification_method", "default_source", "default_cooldown", "updated_at", "user_id") VALUES
(1, 'usd', 'utc', 'email', 'google', 300, '2022-07-13 01:05:50.22384+00', 2),
(2, 'usd', 'utc', 'sms', 'yahoo', 600, '2022-07-14 01:05:50.22384+00', 2);
commit;
so lets say i want to return a single row where a users.email is users1#email.com for example, here is query i can run
select * from users, users_setting where users.id = users_setting.user_id AND users.email = 'users1#email.com';
id email password full_name status is_verified role created_at updated_at verified_at id default_currency default_timezone default_notification_method default_source default_cooldown updated_at user_id
1 users1#email.com password users1 active 1 superuser 2022-07-05 01:05:50.22384+00 0001-01-01 00:00:00+00 2022-07-11 14:10:26.615722+00 1 usd utc email google 300 2022-07-13 01:05:50.22384+00 1
i can have a single table for this but the table will get really long row-wise as i add more and more thing. user settings is just one, there are other tables similar to this. So will be great to know how to design a situation like this properly

In your case a JSON could do the job:
ALTER TABLE public.users ADD user_settings jsonb NULL;
Update of settings will be something like:
UPDATE users
SET user_settings = '{"default_currency": "usd", "default_timezone" : "utc"}'
WHERE id = 1;
And select:
select * from users WHERE id = 1;
You will find:
Also consider in Postgresql you can index a JSON, for example to query on a particular setting. Se here: https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING
Specific:
Still, with appropriate use of expression indexes, the above query can
use an index. If querying for particular items within the "tags" key
is common, defining an index like this may be worthwhile:
CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags'));

With this solution you can avoid JSON. Drawback is that setting_value cannot be tailored to exact type you need, compared to your first idea.
For example you can create:
CREATE TABLE public.user_setting (
user_id bigint NOT NULL,
setting_name text NOT NULL,
setting_value text NULL,
CONSTRAINT user_setting_pk PRIMARY KEY (user_id,setting_name)
);
ALTER TABLE public.user_setting ADD CONSTRAINT user_setting_fk FOREIGN KEY (user_id) REFERENCES public.users(id);
At this point I suggest you to have 2 query, one for users and one for settings:
SELECT *
FROM user_setting us
where user_id = 1;

Related

changing the name of a column in postgresql database

I'm trying to modify the name a column named "photo_url". I tried to simply changing the string name to "test" and killing the postgresql service and then re starting it again, but it doesn't seem to be working; it still loads up as "photo_url".
I'm not sure how to change the name if anyone could help me it would be greatly appreciated.
this is my table im using postgreSQL, and pgweb to view my database, i used dbdesigner to generate this schema
CREATE TABLE "users" (
"user_id" serial NOT NULL,
"name" TEXT NOT NULL,
"instrument" TEXT NOT NULL,
"country" TEXT NOT NULL,
"state" TEXT NOT NULL,
"city" TEXT NOT NULL,
"about" TEXT NOT NULL,
"email" TEXT NOT NULL UNIQUE,
"hashed_password" TEXT NOT NULL,
"photo_url" TEXT NOT NULL,
"created_at" timestamptz NOT NULL default now(),
CONSTRAINT "users_pk" PRIMARY KEY ("user_id")
) WITH (
OIDS=FALSE
);
If you've already created the table, you can use this query to rename the column
ALTER TABLE users RENAME COLUMN photo_url TO test;
otherwise simply recreate your table with the new column name.
More information on the ALTER TABLE command can be found in the PostgreSQL Docs.

Upsert (merge) for updating record if it exists and inserting otherwise

I am trying to write a DB2 query that allows me to either update a record if it already exists but if it does not exist it should be inserted. I wrote the following query that should accomplish this:
MERGE INTO OA1P.TLZ712A1 AS PC
USING (
SELECT * FROM OA1P.TLZ712A1
WHERE CALENDAR_ID=13 AND
"PACKAGE"='M2108'
) PC2
ON (PC.ID_PACKAGE_CALENDAR=PC2.ID_PACKAGE_CALENDAR)
WHEN MATCHED THEN
UPDATE SET ACT_DATE = '31.12.2021'
WHEN NOT MATCHED THEN
INSERT ("PACKAGE", ACT_DATE, CALENDAR_ID, PREPTA, MIXED) VALUES ('M2108', '31.12.2021', 13, 0, 0)
This query should attempt to check if a record already exists for the selection criteria. Updating a record seems to be working fine but I am not able to get the "WHEN NOT MATCHED" part to work and inserting a new record. Anyone able to provide some assistance?
The table is used to save the activation date of a certain software package. PACKAGE is the reference to the package table containing the name of the package (eg. "M2108"). CALENDAR_ID refers to a system where the software package will be activated. The actual date is stored in ACT_DATE.
Did not manage to get the DDL into SQLFiddle so I have to provide it here:
CREATE TABLE OA1P.TLZ712A1 (
ID_PACKAGE_CALENDAR INTEGER DEFAULT IDENTITY GENERATED BY DEFAULT NOT NULL,
CALENDAR_ID INTEGER,
"PACKAGE" VARCHAR(10) NOT NULL,
ACT_DATE DATE NOT NULL,
PREPTA SMALLINT DEFAULT 0 NOT NULL,
MIXED SMALLINT DEFAULT 0 NOT NULL,
"COMMENT" VARCHAR(60) NOT NULL,
LAST_MODIFIED_PID CHAR(7) NOT NULL,
ST_STARTID TIMESTAMP NOT NULL,
ST_FROM TIMESTAMP NOT NULL,
ST_TO TIMESTAMP NOT NULL,
CONSTRAINT TLZ712A1_PK PRIMARY KEY (ID_PACKAGE_CALENDAR),
CONSTRAINT CALENDAR FOREIGN KEY (CALENDAR_ID) REFERENCES OA1P.TLZ711A1(ID_CALENDAR) ON DELETE RESTRICT,
CONSTRAINT "PACKAGE" FOREIGN KEY ("PACKAGE") REFERENCES OA1P.TLZ716A1(NAME) ON DELETE RESTRICT
);
CREATE UNIQUE INDEX ILZ712A0 ON OA1P.TLZ712A1 (ID_PACKAGE_CALENDAR);
If your goal is to set ACT_DATE to 31.12.2021 if a row is found with PACKAGE = M2108 and CALENDAR_ID = 13 and if no row is found with these values then insert it, then this could be the answer
MERGE INTO OA1P.TLZ712A1 AS PC
USING (
VALUES ('M2108', 13, date '31.12.2021')
) PC2 ("PACKAGE", CALENDAR_ID, ACT_DATE)
ON (PC."PACKAGE", PC.CALENDAR_ID) = (PC2."PACKAGE", PC2.CALENDAR_ID)
WHEN MATCHED THEN
UPDATE SET ACT_DATE = PC2.ACT_DATE
WHEN NOT MATCHED THEN
INSERT ("PACKAGE", ACT_DATE, CALENDAR_ID, PREPTA, MIXED) VALUES (PC2."PACKAGE", PC2.ACT_DATE, PC2.CALENDAR_ID, 0, 0)

constraint c for table t does not exist on PostgreSQL even though it's there

I'm trying to run an INSERT query on TablePlus.
INSERT INTO minutes_clone (date, ticker, "lastTime", "openTime", date_time, group_type, "totalVolume", "totalPrice", "totalTrades")
VALUES('2021-07-02', 'YELP', '00:15:00', '00:00:00', '2021-07-02 00:00:00', 15, 0, 0, 0)
ON CONFLICT ON CONSTRAINT minutes_clone_stick_tickers_unique
DO UPDATE SET "lastTime" = '00:15:00', "openTime" = '00:00:00', date_time = '2021-07-02 00:00:00', "totalVolume" = 0, "totalPrice" = 0, "totalTrades" = 0
RETURNING id;
Instead of the query sending a success message, I'm getting an
ERROR: constraint "minutes_clone_stick_tickers_unique" for table "minutes_clone" does not exist.
Here is an image of my table structure.
to replicate:
CREATE TABLE "public"."minutes_clone" (
"ticker" varchar NOT NULL,
"totalTrades" int4 NOT NULL,
"totalPrice" numeric NOT NULL,
"totalVolume" int4,
"lastTime" time NOT NULL,
"openTime" time NOT NULL,
"date" date NOT NULL,
"group_type" int4 NOT NULL DEFAULT 1,
"date_time" timestamp,
"parent_id" int4,
"id" int4 NOT NULL DEFAULT nextval('id_seq'::regclass),
PRIMARY KEY ("id")
);
CREATE INDEX "minutes_clone_ticker_group_date_index" ON "public"."minutes_clone" USING BTREE ("ticker","group_type","date_time");
CREATE UNIQUE INDEX "minutes_clone_stick_tickers_unique" ON "public"."minutes_clone" USING BTREE ("date","ticker","openTime","group_type");
CREATE INDEX "minutes_clone_date_time_index" ON "public"."minutes_clone" USING BTREE ("date_time");
I've tried many things, like removing ON CONSTRAINT and dropping and re-adding the constraing but haven't been able to solve this issue. Any solutions?
You are mixing up indexes and constraints. That is understandable, because a unique constraint is always implemented by a unique index, but they are still not the same.
To make your statement work, you need a unique constraint on top of the index you currently have. You can create that with:
ALTER TABLE public.minutes_clone
ADD UNIQUE USING INDEX minutes_clone_stick_tickers_unique;

PostgreSQL Custom not null constraint

I want to add a NOT NULL contstraint to one of my table fields but only when another field has a certain value.
So I have a services table:
services:
- id
- professional_id
- is_master
I want to write a SQL constraint that whenever is_master is false professional_id cannot be null. I've tried things like:
CREATE TABLE "services" (
"id" text NOT NULL,
"professional_id" text REFERENCES professionals ON DELETE CASCADE ON UPDATE CASCADE,
EXCLUDE USING gist (
professional_id WITH =,
) WHERE (NOT is_master),
"is_master" boolean NOT NULL DEFAULT false,
PRIMARY KEY ("id")
);
What is the correct way to write this SQL?
You can do this with a check constraint. A simple one is:
constraint chk_services_master_professions
check (is_master or professional_id is not null)
Note: This version assumes that is_master is never NULL -- or that NULL is equivalent to "false".
check (not is_master and professional_id is not null or is_master)
You could use CHECK constraint:
CREATE TABLE "services" (
"id" text NOT NULL,
"professional_id" text
CHECK (CASE WHEN "is_master" IS FALSE AND "professional_id" IS NULL
THEN FALSE ELSE TRUE END), -- and rest of FK
"is_master" boolean NOT NULL DEFAULT false,
PRIMARY KEY ("id")
);
Rextester Demo
INSERT INTO services
VALUES (1,1, FALSE);
INSERT INTO services
VALUES (2,1, true);
INSERT INTO services
VALUES (3, null, true);
INSERT INTO services
VALUES (4, null, false);
23514: new row for relation "services" violates check constraint "services_check"

What options are available for applying a set level constraint in PostgreSQL?

I have a situation where I need to ensure that there is only one active record with the same object_id and user_id at any time. Here is a representative table:
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
object_id integer,
user_id integer,
active boolean default true,
created_at timestamptz default now()
);
By only one active record at a time, I mean you could have a sequence of inserts like the following:
insert into actions (object_id, user_id, active) values (1, 1, true);
insert into actions (object_id, user_id, active) values (1, 1, false);
but doing a subsequent
insert into actions (object_id, user_id, active) values (1, 1, true);
should fail because at this point in time, there already exists 1 active tuple with object_id = 1 and user_id = 1.
I'm using PostgreSQL 8.4.
I saw this post which looks interesting, but its Oracle specific.
I also saw this post but it requires more care regarding the transaction isolation level. I don't think it would work as-is in read committed mode.
My question is what other options are available to unsure this kind of constraint?
Edit: Removed the third insert in the first set. I think it was confusing the example. I also added the created_at time stamp to help with the context. To reiterate, there can be multiple (object_id, user_id, false) tuples, but only one (object_id, user_id, true) tuple.
Update: I accepted Craig's answer, but for others who may stumble upon something similar, here is another possible (though suboptimal) solution.
CREATE TABLE action_consistency (
object_id integer,
user_id integer,
count integer default 0,
primary key (object_id, user_id),
check (count >= 0 AND count <= 1)
);
CREATE OR REPLACE FUNCTION keep_action_consistency()
RETURNS TRIGGER AS
$BODY$
BEGIN
IF NEW.active THEN
UPDATE action_consistency
SET count = count + 1
WHERE object_id = NEW.object_id AND
user_id = NEW.user_id;
INSERT INTO action_consistency (object_id, user_id, count)
SELECT NEW.object_id, NEW.user_id, 1
WHERE NOT EXISTS (SELECT 1
FROM action_consistency
WHERE object_id = NEW.object_id AND
user_id = NEW.user_id);
ELSE
-- assuming insert will be active for simplicity
UPDATE action_consistency
SET count = count - 1
WHERE object_id = NEW.object_id AND
user_id = NEW.user_id;
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
CREATE TRIGGER ensure_action_consistency AFTER INSERT OR UPDATE ON actions
FOR EACH ROW EXECUTE PROCEDURE keep_action_consistency();
It requires the use of a tracking table. For what I hope are obvious reasons, this is not at all desirable. It means that you have an additional row each distinct (object_id, user_id) in actions.
Another reason why I accepted #Craig Ringer's answer is that there are foreign key references to actions.id in other tables that are also rendered inactive when a given action tuple changes state. This why the history table is less ideal in this scenario. Thank you for the comments and answers.
Given your specification that you want to limit only one entry to being active at a time, try:
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
object_id integer,
user_id integer,
active boolean default true,
created_at timestamptz default now()
);
CREATE UNIQUE INDEX actions_unique_active_y ON actions(object_id,user_id) WHERE (active = 't');
This is a partial unique index, a PostgreSQL specific feature - see partial indexes. It constrains the set such that only one (object_id,user_id) tuple may exist where active is true.
While that strictly answers your question as you explained further in comments, I think wildplasser's answer describes the more correct choice and best approach.
You can use UNIQUE constraint to ensure that the column contains the unique value...
Here, set of object_id and user_id have been made unique....
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
object_id integer,
user_id integer,
active boolean default true,
UNIQUE (object_id , user_id )
);
Check Out SQLFIDDLE
Similary, if you want to make set of object_id,user_id and active as UNIQUE, you can simply add the column name in the list of UNIQUE.
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
object_id integer,
user_id integer,
active boolean default true,
UNIQUE (object_id , user_id,active )
);
Check Out SQLFIDDLE
Original:
CREATE TABLE actions (
id SERIAL PRIMARY KEY,
object_id integer,
user_id integer,
active boolean default true
);
my version:
CREATE TABLE actions (
object_id integer NOT NULL REFERENCES objects (id),
user_id integer NOT NULL REFERENCES users(id),
PRIMARY KEY (user_id, object_id)
);
What are the differences:
omitted the surrogate key. It is useless, it enforces no constraint, and nobody will ever reference it
added a (composite) primary key, which happens to be the logical key
changed the two fields to NOT NULL, and made them into foreign keys (what would be the meaning of a row that would not exist in the users or objects table?
removed the boolean flag. What is the semantic difference between a {user_id,object_id} tuple that does not exist versus one that does exist but has it's "active" flag set to false? Why create three states when you only need two?