--
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;


--
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner: 
--

COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';


--
-- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: -
--

CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA public;


--
-- Name: EXTENSION pgcrypto; Type: COMMENT; Schema: -; Owner: 
--

COMMENT ON EXTENSION pgcrypto IS 'cryptographic functions';


--
-- Name: json_ancestor_array; Type: TYPE; Schema: public; Owner: postgres
--

CREATE TYPE public.json_ancestor_array AS (
	"Ancestors" character varying[]
);

--
-- Name: add_nodes(integer, character varying, jsonb); Type: PROCEDURE; Schema: public; Owner: postgres
--

CREATE PROCEDURE public.add_nodes(IN p_user_id integer, IN p_client_uuid character varying, IN p_nodes jsonb)
    LANGUAGE plpgsql
    AS $$

DECLARE
	node_data     jsonb;
	node_updated  timestamptz;
	db_updated    timestamptz;
	db_uuid       bpchar;
	db_client     bpchar;
	db_client_seq int;
	node_uuid     bpchar;
	parent_uuid_nullable   bpchar;

BEGIN
	RAISE NOTICE '--------------------------';
	FOR node_data IN SELECT * FROM jsonb_array_elements(p_nodes)
	LOOP
		node_uuid = (node_data->>'UUID')::bpchar;
		node_updated = (node_data->>'Updated')::timestamptz;

		IF node_data->>'ParentUUID' = '00000000-0000-0000-0000-000000000000' THEN
			parent_uuid_nullable = NULL;
		ELSE
			parent_uuid_nullable = node_data->>'ParentUUID';
		END IF;

		/* Retrieve the current modified timestamp for this node from the database. */
		SELECT
			uuid, updated, client, client_sequence
		INTO
			db_uuid, db_updated, db_client, db_client_seq
		FROM public."node"
		WHERE
			user_id = p_user_id AND
			uuid = node_uuid;

		/* Is the node not in database? It needs to be created. */
		IF db_uuid IS NULL THEN
			RAISE NOTICE '01 New node %', node_uuid;
			INSERT INTO public."node" (
				user_id, "uuid", parent_uuid, created, updated,
				"name", "content", markdown, "content_encrypted",
				client, client_sequence
			)
			VALUES(
				p_user_id,
				node_uuid,
				parent_uuid_nullable,
				(node_data->>'Created')::timestamptz,
				(node_data->>'Updated')::timestamptz,
				(node_data->>'Name')::varchar,
				(node_data->>'Content')::text,
				(node_data->>'Markdown')::bool,
				'', /* content_encrypted */
				p_client_uuid,
				(node_data->>'ClientSequence')::int
			);
			CONTINUE;
		END IF;


		/* The client could send a specific node again if it didn't receive the OK from this procedure before. */
		IF db_updated = node_updated AND db_client = p_client_uuid AND db_client_seq = (node_data->>'ClientSequence')::int THEN
			RAISE NOTICE '04, already recorded, %, %', db_client, db_client_seq;
			CONTINUE;
		END IF;

		/* Determine if the incoming node data is to go into history or replace the current node. */
		IF db_updated > node_updated THEN
			RAISE NOTICE '02 DB newer, % > % (%))', db_updated, node_updated, node_uuid;
			/* Incoming node is going straight to history since it is older than the current node. */
			INSERT INTO node_history(
				user_id, "uuid", parents, created, updated,
				"name", "content", markdown, "content_encrypted",
				client, client_sequence
			)
			VALUES(
				p_user_id,
				node_uuid,
				(jsonb_populate_record(null::json_ancestor_array, node_data))."Ancestors",
				(node_data->>'Created')::timestamptz,
				(node_data->>'Updated')::timestamptz,
				(node_data->>'Name')::varchar,
				(node_data->>'Content')::text,
				(node_data->>'Markdown')::bool,
				'', /* content_encrypted */
				p_client_uuid,
				(node_data->>'ClientSequence')::int
			)
			ON CONFLICT (client, client_sequence)
			DO NOTHING;
		ELSE
			RAISE NOTICE '03 Client newer, % > % (%, %)', node_updated, db_updated, node_uuid, (node_data->>'ClientSequence');
			/* Incoming node is newer and will replace the current node.
			 *
			 * The current node is copied to the node_history table and then modified in place
			 * with the incoming data. */
			INSERT INTO node_history(
				user_id, "uuid", parents,
				created, updated, "name", "content", markdown, "content_encrypted",
				client, client_sequence
			)
			SELECT
				user_id,
				"uuid",
				(
					WITH RECURSIVE nodes AS (
						SELECT
							uuid,
							COALESCE(parent_uuid, '') AS parent_uuid,
							name,
							0 AS depth
						FROM node
						WHERE
							uuid = node_uuid

						UNION
						
						SELECT
							n.uuid,
							COALESCE(n.parent_uuid, '') AS parent_uuid,
							n.name,
							nr.depth+1 AS depth
						FROM node n
						INNER JOIN nodes nr ON n.uuid = nr.parent_uuid
					)
					SELECT ARRAY (
						SELECT name
						FROM nodes
						ORDER BY depth DESC
						OFFSET 1 /* discard itself */
					)
				),
				created,
				updated,
				name,
				content,
				markdown,
				content_encrypted,
				client,
				client_sequence
			FROM public."node"
			WHERE
				user_id = p_user_id AND
				uuid = node_uuid
			ON CONFLICT (client, client_sequence)
			DO NOTHING;

			/* Current node in database is updated with incoming data. */
			UPDATE public."node"
			SET
				updated = (node_data->>'Updated')::timestamptz,
				updated_seq = nextval('node_updates'),
				name = (node_data->>'Name')::varchar,
				content = (node_data->>'Content')::text,
				markdown = (node_data->>'Markdown')::bool,
				client = p_client_uuid,
				client_sequence = (node_data->>'ClientSequence')::int
			WHERE
				user_id = p_user_id AND
				uuid = node_uuid;
		END IF;

	END LOOP;
END
$$;

--
-- Name: node_update_timestamp(); Type: FUNCTION; Schema: public; Owner: postgres
--

CREATE FUNCTION public.node_update_timestamp() RETURNS trigger
    LANGUAGE plpgsql
    AS $$
BEGIN
	IF NEW.updated = OLD.updated THEN
		UPDATE node
		SET
			updated = NOW(),
			updated_seq = nextval('node_updates')
		WHERE
			id=NEW.id;
	END IF;
	RETURN NEW;
END;
$$;


--
-- Name: password_hash(character, bytea); Type: FUNCTION; Schema: public; Owner: postgres
--

CREATE FUNCTION public.password_hash(salt_hex character, pass bytea) RETURNS character
    LANGUAGE plpgsql
    AS $$
BEGIN
	RETURN (
		SELECT
			salt_hex ||
			encode(
				sha256(
					decode(salt_hex, 'hex') || /* salt in binary */
					pass                       /* password */
				),
				'hex'
			)
	);
END;
$$;


--
-- Name: client; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public.client (
    id integer NOT NULL,
    user_id integer NOT NULL,
    client_uuid uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
    created timestamp with time zone DEFAULT now() NOT NULL,
    description character varying DEFAULT ''::character varying NOT NULL
);


--
-- Name: client_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.client_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: client_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--

ALTER SEQUENCE public.client_id_seq OWNED BY public.client.id;


--
-- Name: node_updates; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.node_updates
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: node; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public.node (
    id integer NOT NULL,
    user_id integer NOT NULL,
    "uuid" uuid DEFAULT gen_random_uuid() NOT NULL,
    parent_uuid uuid,
    created timestamp with time zone DEFAULT now() NOT NULL,
    updated timestamp with time zone DEFAULT now() NOT NULL,
    deleted timestamp with time zone,
    created_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL,
    updated_seq bigint DEFAULT nextval('public.node_updates'::regclass) NOT NULL,
    deleted_seq bigint,
    name character varying(256) DEFAULT ''::character varying NOT NULL,
    content text DEFAULT ''::text NOT NULL,
    content_encrypted text DEFAULT ''::text NOT NULL,
    markdown boolean DEFAULT false NOT NULL,
    history boolean DEFAULT false NOT NULL,
    client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
    client_sequence integer,
    CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0))
);


--
-- Name: node_history; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public.node_history (
    id integer NOT NULL,
    user_id integer NOT NULL,
    "uuid" uuid NOT NULL,
    parents character varying[],
    created timestamp with time zone NOT NULL,
    updated timestamp with time zone NOT NULL,
    name character varying(256) NOT NULL,
    content text NOT NULL,
    content_encrypted text NOT NULL,
    markdown boolean DEFAULT false NOT NULL,
    client uuid DEFAULT '00000000-0000-0000-0000-000000000000'::uuid NOT NULL,
    client_sequence integer
);


--
-- Name: node_history_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.node_history_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: node_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--

ALTER SEQUENCE public.node_history_id_seq OWNED BY public.node_history.id;


--
-- Name: node_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.node_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: node_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--

ALTER SEQUENCE public.node_id_seq OWNED BY public.node.id;


--
-- Name: test_data; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.test_data
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: user; Type: TABLE; Schema: public; Owner: postgres
--

CREATE TABLE public."user" (
    id integer NOT NULL,
    username character varying NOT NULL,
    name character varying NOT NULL,
    password character varying NOT NULL,
    last_login timestamp without time zone DEFAULT now() NOT NULL,
    timezone character varying DEFAULT 'UTC'::character varying NOT NULL
);


--
-- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres
--

CREATE SEQUENCE public.user_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;


--
-- Name: user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres
--

ALTER SEQUENCE public.user_id_seq OWNED BY public."user".id;


--
-- Name: client id; Type: DEFAULT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.client ALTER COLUMN id SET DEFAULT nextval('public.client_id_seq'::regclass);


--
-- Name: node id; Type: DEFAULT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node ALTER COLUMN id SET DEFAULT nextval('public.node_id_seq'::regclass);


--
-- Name: node_history id; Type: DEFAULT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node_history ALTER COLUMN id SET DEFAULT nextval('public.node_history_id_seq'::regclass);


--
-- Name: user id; Type: DEFAULT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public."user" ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass);


--
-- Name: client client_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.client
    ADD CONSTRAINT client_pk PRIMARY KEY (id);


--
-- Name: node_history node_history_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node_history
    ADD CONSTRAINT node_history_pk PRIMARY KEY (id);


--
-- Name: node node_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node
    ADD CONSTRAINT node_pk PRIMARY KEY (id);


--
-- Name: user user_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public."user"
    ADD CONSTRAINT user_pk PRIMARY KEY (id);


--
-- Name: client_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE UNIQUE INDEX client_uuid_idx ON public.client USING btree (client_uuid);


--
-- Name: node_history_client_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE UNIQUE INDEX node_history_client_idx ON public.node_history USING btree (client, client_sequence);


--
-- Name: node_history_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX node_history_idx ON public.node USING btree (history);


--
-- Name: node_history_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX node_history_uuid_idx ON public.node USING btree (uuid);


--
-- Name: node_search_index; Type: INDEX; Schema: public; Owner: postgres
--

CREATE INDEX node_search_index ON public.node USING gin (name public.gin_trgm_ops, content public.gin_trgm_ops);


--
-- Name: node_uuid_idx; Type: INDEX; Schema: public; Owner: postgres
--

CREATE UNIQUE INDEX node_uuid_idx ON public.node USING btree (uuid);


--
-- Name: node node_update; Type: TRIGGER; Schema: public; Owner: postgres
--

CREATE TRIGGER node_update AFTER UPDATE ON public.node FOR EACH ROW EXECUTE FUNCTION public.node_update_timestamp();


--
-- Name: node_history node_history_user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node_history
    ADD CONSTRAINT node_history_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT;


--
-- Name: node node_node_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node
    ADD CONSTRAINT node_node_fk FOREIGN KEY (parent_uuid) REFERENCES public.node(uuid) ON UPDATE SET NULL ON DELETE SET NULL;


--
-- Name: node user_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--

ALTER TABLE ONLY public.node
    ADD CONSTRAINT user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON UPDATE RESTRICT ON DELETE RESTRICT;
