878 lines
34 KiB
PL/PgSQL
878 lines
34 KiB
PL/PgSQL
-- =============================================================================
|
|
-- AgenciaPsi — Functions — storage schema
|
|
-- =============================================================================
|
|
|
|
CREATE FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) RETURNS void
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
INSERT INTO "storage"."objects" ("bucket_id", "name", "owner", "metadata") VALUES (bucketid, name, owner, metadata);
|
|
-- hack to rollback the successful insert
|
|
RAISE sqlstate 'PT200' using
|
|
message = 'ROLLBACK',
|
|
detail = 'rollback successful insert';
|
|
END
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.can_insert_object(bucketid text, name text, owner uuid, metadata jsonb) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: enforce_bucket_name_length(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.enforce_bucket_name_length() RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
begin
|
|
if length(new.name) > 100 then
|
|
raise exception 'bucket name "%" is too long (% characters). Max is 100.', new.name, length(new.name);
|
|
end if;
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.enforce_bucket_name_length() OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: extension(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.extension(name text) RETURNS text
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
_parts text[];
|
|
_filename text;
|
|
BEGIN
|
|
select string_to_array(name, '/') into _parts;
|
|
select _parts[array_length(_parts,1)] into _filename;
|
|
-- @todo return the last part instead of 2
|
|
return reverse(split_part(reverse(_filename), '.', 1));
|
|
END
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.extension(name text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: filename(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.filename(name text) RETURNS text
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
_parts text[];
|
|
BEGIN
|
|
select string_to_array(name, '/') into _parts;
|
|
return _parts[array_length(_parts,1)];
|
|
END
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.filename(name text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: foldername(text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.foldername(name text) RETURNS text[]
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
_parts text[];
|
|
BEGIN
|
|
select string_to_array(name, '/') into _parts;
|
|
return _parts[1:array_length(_parts,1)-1];
|
|
END
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.foldername(name text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: get_common_prefix(text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) RETURNS text
|
|
LANGUAGE sql IMMUTABLE
|
|
AS $$
|
|
SELECT CASE
|
|
WHEN position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)) > 0
|
|
THEN left(p_key, length(p_prefix) + position(p_delimiter IN substring(p_key FROM length(p_prefix) + 1)))
|
|
ELSE NULL
|
|
END;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.get_common_prefix(p_key text, p_prefix text, p_delimiter text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: get_size_by_bucket(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.get_size_by_bucket() RETURNS TABLE(size bigint, bucket_id text)
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
return query
|
|
select sum((metadata->>'size')::int) as size, obj.bucket_id
|
|
from "storage".objects as obj
|
|
group by obj.bucket_id;
|
|
END
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.get_size_by_bucket() OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: list_multipart_uploads_with_delimiter(text, text, text, integer, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, next_key_token text DEFAULT ''::text, next_upload_token text DEFAULT ''::text) RETURNS TABLE(key text, id text, created_at timestamp with time zone)
|
|
LANGUAGE plpgsql
|
|
AS $_$
|
|
BEGIN
|
|
RETURN QUERY EXECUTE
|
|
'SELECT DISTINCT ON(key COLLATE "C") * from (
|
|
SELECT
|
|
CASE
|
|
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
|
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1)))
|
|
ELSE
|
|
key
|
|
END AS key, id, created_at
|
|
FROM
|
|
storage.s3_multipart_uploads
|
|
WHERE
|
|
bucket_id = $5 AND
|
|
key ILIKE $1 || ''%'' AND
|
|
CASE
|
|
WHEN $4 != '''' AND $6 = '''' THEN
|
|
CASE
|
|
WHEN position($2 IN substring(key from length($1) + 1)) > 0 THEN
|
|
substring(key from 1 for length($1) + position($2 IN substring(key from length($1) + 1))) COLLATE "C" > $4
|
|
ELSE
|
|
key COLLATE "C" > $4
|
|
END
|
|
ELSE
|
|
true
|
|
END AND
|
|
CASE
|
|
WHEN $6 != '''' THEN
|
|
id COLLATE "C" > $6
|
|
ELSE
|
|
true
|
|
END
|
|
ORDER BY
|
|
key COLLATE "C" ASC, created_at ASC) as e order by key COLLATE "C" LIMIT $3'
|
|
USING prefix_param, delimiter_param, max_keys, next_key_token, bucket_id, next_upload_token;
|
|
END;
|
|
$_$;
|
|
|
|
|
|
ALTER FUNCTION storage.list_multipart_uploads_with_delimiter(bucket_id text, prefix_param text, delimiter_param text, max_keys integer, next_key_token text, next_upload_token text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: list_objects_with_delimiter(text, text, text, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer DEFAULT 100, start_after text DEFAULT ''::text, next_token text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, metadata jsonb, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone)
|
|
LANGUAGE plpgsql STABLE
|
|
AS $_$
|
|
DECLARE
|
|
v_peek_name TEXT;
|
|
v_current RECORD;
|
|
v_common_prefix TEXT;
|
|
|
|
-- Configuration
|
|
v_is_asc BOOLEAN;
|
|
v_prefix TEXT;
|
|
v_start TEXT;
|
|
v_upper_bound TEXT;
|
|
v_file_batch_size INT;
|
|
|
|
-- Seek state
|
|
v_next_seek TEXT;
|
|
v_count INT := 0;
|
|
|
|
-- Dynamic SQL for batch query only
|
|
v_batch_query TEXT;
|
|
|
|
BEGIN
|
|
-- ========================================================================
|
|
-- INITIALIZATION
|
|
-- ========================================================================
|
|
v_is_asc := lower(coalesce(sort_order, 'asc')) = 'asc';
|
|
v_prefix := coalesce(prefix_param, '');
|
|
v_start := CASE WHEN coalesce(next_token, '') <> '' THEN next_token ELSE coalesce(start_after, '') END;
|
|
v_file_batch_size := LEAST(GREATEST(max_keys * 2, 100), 1000);
|
|
|
|
-- Calculate upper bound for prefix filtering (bytewise, using COLLATE "C")
|
|
IF v_prefix = '' THEN
|
|
v_upper_bound := NULL;
|
|
ELSIF right(v_prefix, 1) = delimiter_param THEN
|
|
v_upper_bound := left(v_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
|
ELSE
|
|
v_upper_bound := left(v_prefix, -1) || chr(ascii(right(v_prefix, 1)) + 1);
|
|
END IF;
|
|
|
|
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
|
IF v_is_asc THEN
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
|
'AND o.name COLLATE "C" < $3 ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
|
ELSE
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" >= $2 ' ||
|
|
'ORDER BY o.name COLLATE "C" ASC LIMIT $4';
|
|
END IF;
|
|
ELSE
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
|
'AND o.name COLLATE "C" >= $3 ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
|
ELSE
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND o.name COLLATE "C" < $2 ' ||
|
|
'ORDER BY o.name COLLATE "C" DESC LIMIT $4';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- ========================================================================
|
|
-- SEEK INITIALIZATION: Determine starting position
|
|
-- ========================================================================
|
|
IF v_start = '' THEN
|
|
IF v_is_asc THEN
|
|
v_next_seek := v_prefix;
|
|
ELSE
|
|
-- DESC without cursor: find the last item in range
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix AND o.name COLLATE "C" < v_upper_bound
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
ELSIF v_prefix <> '' THEN
|
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_prefix
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_next_seek FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
END IF;
|
|
|
|
IF v_next_seek IS NOT NULL THEN
|
|
v_next_seek := v_next_seek || delimiter_param;
|
|
ELSE
|
|
RETURN;
|
|
END IF;
|
|
END IF;
|
|
ELSE
|
|
-- Cursor provided: determine if it refers to a folder or leaf
|
|
IF EXISTS (
|
|
SELECT 1 FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id
|
|
AND o.name COLLATE "C" LIKE v_start || delimiter_param || '%'
|
|
LIMIT 1
|
|
) THEN
|
|
-- Cursor refers to a folder
|
|
IF v_is_asc THEN
|
|
v_next_seek := v_start || chr(ascii(delimiter_param) + 1);
|
|
ELSE
|
|
v_next_seek := v_start || delimiter_param;
|
|
END IF;
|
|
ELSE
|
|
-- Cursor refers to a leaf object
|
|
IF v_is_asc THEN
|
|
v_next_seek := v_start || delimiter_param;
|
|
ELSE
|
|
v_next_seek := v_start;
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- ========================================================================
|
|
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
|
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
|
-- ========================================================================
|
|
LOOP
|
|
EXIT WHEN v_count >= max_keys;
|
|
|
|
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
|
IF v_is_asc THEN
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek AND o.name COLLATE "C" < v_upper_bound
|
|
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" >= v_next_seek
|
|
ORDER BY o.name COLLATE "C" ASC LIMIT 1;
|
|
END IF;
|
|
ELSE
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
ELSIF v_prefix <> '' THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek AND o.name COLLATE "C" >= v_prefix
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = _bucket_id AND o.name COLLATE "C" < v_next_seek
|
|
ORDER BY o.name COLLATE "C" DESC LIMIT 1;
|
|
END IF;
|
|
END IF;
|
|
|
|
EXIT WHEN v_peek_name IS NULL;
|
|
|
|
-- STEP 2: Check if this is a FOLDER or FILE
|
|
v_common_prefix := storage.get_common_prefix(v_peek_name, v_prefix, delimiter_param);
|
|
|
|
IF v_common_prefix IS NOT NULL THEN
|
|
-- FOLDER: Emit and skip to next folder (no heap access needed)
|
|
name := rtrim(v_common_prefix, delimiter_param);
|
|
id := NULL;
|
|
updated_at := NULL;
|
|
created_at := NULL;
|
|
last_accessed_at := NULL;
|
|
metadata := NULL;
|
|
RETURN NEXT;
|
|
v_count := v_count + 1;
|
|
|
|
-- Advance seek past the folder range
|
|
IF v_is_asc THEN
|
|
v_next_seek := left(v_common_prefix, -1) || chr(ascii(delimiter_param) + 1);
|
|
ELSE
|
|
v_next_seek := v_common_prefix;
|
|
END IF;
|
|
ELSE
|
|
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
|
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
|
-- For DESC: prefix is the inclusive lower limit (>= condition)
|
|
FOR v_current IN EXECUTE v_batch_query USING _bucket_id, v_next_seek,
|
|
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix) ELSE v_prefix END, v_file_batch_size
|
|
LOOP
|
|
v_common_prefix := storage.get_common_prefix(v_current.name, v_prefix, delimiter_param);
|
|
|
|
IF v_common_prefix IS NOT NULL THEN
|
|
-- Hit a folder: exit batch, let peek handle it
|
|
v_next_seek := v_current.name;
|
|
EXIT;
|
|
END IF;
|
|
|
|
-- Emit file
|
|
name := v_current.name;
|
|
id := v_current.id;
|
|
updated_at := v_current.updated_at;
|
|
created_at := v_current.created_at;
|
|
last_accessed_at := v_current.last_accessed_at;
|
|
metadata := v_current.metadata;
|
|
RETURN NEXT;
|
|
v_count := v_count + 1;
|
|
|
|
-- Advance seek past this file
|
|
IF v_is_asc THEN
|
|
v_next_seek := v_current.name || delimiter_param;
|
|
ELSE
|
|
v_next_seek := v_current.name;
|
|
END IF;
|
|
|
|
EXIT WHEN v_count >= max_keys;
|
|
END LOOP;
|
|
END IF;
|
|
END LOOP;
|
|
END;
|
|
$_$;
|
|
|
|
|
|
ALTER FUNCTION storage.list_objects_with_delimiter(_bucket_id text, prefix_param text, delimiter_param text, max_keys integer, start_after text, next_token text, sort_order text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: operation(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.operation() RETURNS text
|
|
LANGUAGE plpgsql STABLE
|
|
AS $$
|
|
BEGIN
|
|
RETURN current_setting('storage.operation', true);
|
|
END;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.operation() OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: protect_delete(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.protect_delete() RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
-- Check if storage.allow_delete_query is set to 'true'
|
|
IF COALESCE(current_setting('storage.allow_delete_query', true), 'false') != 'true' THEN
|
|
RAISE EXCEPTION 'Direct deletion from storage tables is not allowed. Use the Storage API instead.'
|
|
USING HINT = 'This prevents accidental data loss from orphaned objects.',
|
|
ERRCODE = '42501';
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.protect_delete() OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: search(text, text, integer, integer, integer, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.search(prefix text, bucketname text, limits integer DEFAULT 100, levels integer DEFAULT 1, offsets integer DEFAULT 0, search text DEFAULT ''::text, sortcolumn text DEFAULT 'name'::text, sortorder text DEFAULT 'asc'::text) RETURNS TABLE(name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
|
LANGUAGE plpgsql STABLE
|
|
AS $_$
|
|
DECLARE
|
|
v_peek_name TEXT;
|
|
v_current RECORD;
|
|
v_common_prefix TEXT;
|
|
v_delimiter CONSTANT TEXT := '/';
|
|
|
|
-- Configuration
|
|
v_limit INT;
|
|
v_prefix TEXT;
|
|
v_prefix_lower TEXT;
|
|
v_is_asc BOOLEAN;
|
|
v_order_by TEXT;
|
|
v_sort_order TEXT;
|
|
v_upper_bound TEXT;
|
|
v_file_batch_size INT;
|
|
|
|
-- Dynamic SQL for batch query only
|
|
v_batch_query TEXT;
|
|
|
|
-- Seek state
|
|
v_next_seek TEXT;
|
|
v_count INT := 0;
|
|
v_skipped INT := 0;
|
|
BEGIN
|
|
-- ========================================================================
|
|
-- INITIALIZATION
|
|
-- ========================================================================
|
|
v_limit := LEAST(coalesce(limits, 100), 1500);
|
|
v_prefix := coalesce(prefix, '') || coalesce(search, '');
|
|
v_prefix_lower := lower(v_prefix);
|
|
v_is_asc := lower(coalesce(sortorder, 'asc')) = 'asc';
|
|
v_file_batch_size := LEAST(GREATEST(v_limit * 2, 100), 1000);
|
|
|
|
-- Validate sort column
|
|
CASE lower(coalesce(sortcolumn, 'name'))
|
|
WHEN 'name' THEN v_order_by := 'name';
|
|
WHEN 'updated_at' THEN v_order_by := 'updated_at';
|
|
WHEN 'created_at' THEN v_order_by := 'created_at';
|
|
WHEN 'last_accessed_at' THEN v_order_by := 'last_accessed_at';
|
|
ELSE v_order_by := 'name';
|
|
END CASE;
|
|
|
|
v_sort_order := CASE WHEN v_is_asc THEN 'asc' ELSE 'desc' END;
|
|
|
|
-- ========================================================================
|
|
-- NON-NAME SORTING: Use path_tokens approach (unchanged)
|
|
-- ========================================================================
|
|
IF v_order_by != 'name' THEN
|
|
RETURN QUERY EXECUTE format(
|
|
$sql$
|
|
WITH folders AS (
|
|
SELECT path_tokens[$1] AS folder
|
|
FROM storage.objects
|
|
WHERE objects.name ILIKE $2 || '%%'
|
|
AND bucket_id = $3
|
|
AND array_length(objects.path_tokens, 1) <> $1
|
|
GROUP BY folder
|
|
ORDER BY folder %s
|
|
)
|
|
(SELECT folder AS "name",
|
|
NULL::uuid AS id,
|
|
NULL::timestamptz AS updated_at,
|
|
NULL::timestamptz AS created_at,
|
|
NULL::timestamptz AS last_accessed_at,
|
|
NULL::jsonb AS metadata FROM folders)
|
|
UNION ALL
|
|
(SELECT path_tokens[$1] AS "name",
|
|
id, updated_at, created_at, last_accessed_at, metadata
|
|
FROM storage.objects
|
|
WHERE objects.name ILIKE $2 || '%%'
|
|
AND bucket_id = $3
|
|
AND array_length(objects.path_tokens, 1) = $1
|
|
ORDER BY %I %s)
|
|
LIMIT $4 OFFSET $5
|
|
$sql$, v_sort_order, v_order_by, v_sort_order
|
|
) USING levels, v_prefix, bucketname, v_limit, offsets;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- ========================================================================
|
|
-- NAME SORTING: Hybrid skip-scan with batch optimization
|
|
-- ========================================================================
|
|
|
|
-- Calculate upper bound for prefix filtering
|
|
IF v_prefix_lower = '' THEN
|
|
v_upper_bound := NULL;
|
|
ELSIF right(v_prefix_lower, 1) = v_delimiter THEN
|
|
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(v_delimiter) + 1);
|
|
ELSE
|
|
v_upper_bound := left(v_prefix_lower, -1) || chr(ascii(right(v_prefix_lower, 1)) + 1);
|
|
END IF;
|
|
|
|
-- Build batch query (dynamic SQL - called infrequently, amortized over many rows)
|
|
IF v_is_asc THEN
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
|
'AND lower(o.name) COLLATE "C" < $3 ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
|
ELSE
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" >= $2 ' ||
|
|
'ORDER BY lower(o.name) COLLATE "C" ASC LIMIT $4';
|
|
END IF;
|
|
ELSE
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
|
'AND lower(o.name) COLLATE "C" >= $3 ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
|
ELSE
|
|
v_batch_query := 'SELECT o.name, o.id, o.updated_at, o.created_at, o.last_accessed_at, o.metadata ' ||
|
|
'FROM storage.objects o WHERE o.bucket_id = $1 AND lower(o.name) COLLATE "C" < $2 ' ||
|
|
'ORDER BY lower(o.name) COLLATE "C" DESC LIMIT $4';
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Initialize seek position
|
|
IF v_is_asc THEN
|
|
v_next_seek := v_prefix_lower;
|
|
ELSE
|
|
-- DESC: find the last item in range first (static SQL)
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower AND lower(o.name) COLLATE "C" < v_upper_bound
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
ELSIF v_prefix_lower <> '' THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
END IF;
|
|
|
|
IF v_peek_name IS NOT NULL THEN
|
|
v_next_seek := lower(v_peek_name) || v_delimiter;
|
|
ELSE
|
|
RETURN;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- ========================================================================
|
|
-- MAIN LOOP: Hybrid peek-then-batch algorithm
|
|
-- Uses STATIC SQL for peek (hot path) and DYNAMIC SQL for batch
|
|
-- ========================================================================
|
|
LOOP
|
|
EXIT WHEN v_count >= v_limit;
|
|
|
|
-- STEP 1: PEEK using STATIC SQL (plan cached, very fast)
|
|
IF v_is_asc THEN
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek AND lower(o.name) COLLATE "C" < v_upper_bound
|
|
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" >= v_next_seek
|
|
ORDER BY lower(o.name) COLLATE "C" ASC LIMIT 1;
|
|
END IF;
|
|
ELSE
|
|
IF v_upper_bound IS NOT NULL THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
ELSIF v_prefix_lower <> '' THEN
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek AND lower(o.name) COLLATE "C" >= v_prefix_lower
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
ELSE
|
|
SELECT o.name INTO v_peek_name FROM storage.objects o
|
|
WHERE o.bucket_id = bucketname AND lower(o.name) COLLATE "C" < v_next_seek
|
|
ORDER BY lower(o.name) COLLATE "C" DESC LIMIT 1;
|
|
END IF;
|
|
END IF;
|
|
|
|
EXIT WHEN v_peek_name IS NULL;
|
|
|
|
-- STEP 2: Check if this is a FOLDER or FILE
|
|
v_common_prefix := storage.get_common_prefix(lower(v_peek_name), v_prefix_lower, v_delimiter);
|
|
|
|
IF v_common_prefix IS NOT NULL THEN
|
|
-- FOLDER: Handle offset, emit if needed, skip to next folder
|
|
IF v_skipped < offsets THEN
|
|
v_skipped := v_skipped + 1;
|
|
ELSE
|
|
name := split_part(rtrim(v_common_prefix, v_delimiter), v_delimiter, levels);
|
|
id := NULL;
|
|
updated_at := NULL;
|
|
created_at := NULL;
|
|
last_accessed_at := NULL;
|
|
metadata := NULL;
|
|
RETURN NEXT;
|
|
v_count := v_count + 1;
|
|
END IF;
|
|
|
|
-- Advance seek past the folder range
|
|
IF v_is_asc THEN
|
|
v_next_seek := lower(left(v_common_prefix, -1)) || chr(ascii(v_delimiter) + 1);
|
|
ELSE
|
|
v_next_seek := lower(v_common_prefix);
|
|
END IF;
|
|
ELSE
|
|
-- FILE: Batch fetch using DYNAMIC SQL (overhead amortized over many rows)
|
|
-- For ASC: upper_bound is the exclusive upper limit (< condition)
|
|
-- For DESC: prefix_lower is the inclusive lower limit (>= condition)
|
|
FOR v_current IN EXECUTE v_batch_query
|
|
USING bucketname, v_next_seek,
|
|
CASE WHEN v_is_asc THEN COALESCE(v_upper_bound, v_prefix_lower) ELSE v_prefix_lower END, v_file_batch_size
|
|
LOOP
|
|
v_common_prefix := storage.get_common_prefix(lower(v_current.name), v_prefix_lower, v_delimiter);
|
|
|
|
IF v_common_prefix IS NOT NULL THEN
|
|
-- Hit a folder: exit batch, let peek handle it
|
|
v_next_seek := lower(v_current.name);
|
|
EXIT;
|
|
END IF;
|
|
|
|
-- Handle offset skipping
|
|
IF v_skipped < offsets THEN
|
|
v_skipped := v_skipped + 1;
|
|
ELSE
|
|
-- Emit file
|
|
name := split_part(v_current.name, v_delimiter, levels);
|
|
id := v_current.id;
|
|
updated_at := v_current.updated_at;
|
|
created_at := v_current.created_at;
|
|
last_accessed_at := v_current.last_accessed_at;
|
|
metadata := v_current.metadata;
|
|
RETURN NEXT;
|
|
v_count := v_count + 1;
|
|
END IF;
|
|
|
|
-- Advance seek past this file
|
|
IF v_is_asc THEN
|
|
v_next_seek := lower(v_current.name) || v_delimiter;
|
|
ELSE
|
|
v_next_seek := lower(v_current.name);
|
|
END IF;
|
|
|
|
EXIT WHEN v_count >= v_limit;
|
|
END LOOP;
|
|
END IF;
|
|
END LOOP;
|
|
END;
|
|
$_$;
|
|
|
|
|
|
ALTER FUNCTION storage.search(prefix text, bucketname text, limits integer, levels integer, offsets integer, search text, sortcolumn text, sortorder text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: search_by_timestamp(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
|
LANGUAGE plpgsql STABLE
|
|
AS $_$
|
|
DECLARE
|
|
v_cursor_op text;
|
|
v_query text;
|
|
v_prefix text;
|
|
BEGIN
|
|
v_prefix := coalesce(p_prefix, '');
|
|
|
|
IF p_sort_order = 'asc' THEN
|
|
v_cursor_op := '>';
|
|
ELSE
|
|
v_cursor_op := '<';
|
|
END IF;
|
|
|
|
v_query := format($sql$
|
|
WITH raw_objects AS (
|
|
SELECT
|
|
o.name AS obj_name,
|
|
o.id AS obj_id,
|
|
o.updated_at AS obj_updated_at,
|
|
o.created_at AS obj_created_at,
|
|
o.last_accessed_at AS obj_last_accessed_at,
|
|
o.metadata AS obj_metadata,
|
|
storage.get_common_prefix(o.name, $1, '/') AS common_prefix
|
|
FROM storage.objects o
|
|
WHERE o.bucket_id = $2
|
|
AND o.name COLLATE "C" LIKE $1 || '%%'
|
|
),
|
|
-- Aggregate common prefixes (folders)
|
|
-- Both created_at and updated_at use MIN(obj_created_at) to match the old prefixes table behavior
|
|
aggregated_prefixes AS (
|
|
SELECT
|
|
rtrim(common_prefix, '/') AS name,
|
|
NULL::uuid AS id,
|
|
MIN(obj_created_at) AS updated_at,
|
|
MIN(obj_created_at) AS created_at,
|
|
NULL::timestamptz AS last_accessed_at,
|
|
NULL::jsonb AS metadata,
|
|
TRUE AS is_prefix
|
|
FROM raw_objects
|
|
WHERE common_prefix IS NOT NULL
|
|
GROUP BY common_prefix
|
|
),
|
|
leaf_objects AS (
|
|
SELECT
|
|
obj_name AS name,
|
|
obj_id AS id,
|
|
obj_updated_at AS updated_at,
|
|
obj_created_at AS created_at,
|
|
obj_last_accessed_at AS last_accessed_at,
|
|
obj_metadata AS metadata,
|
|
FALSE AS is_prefix
|
|
FROM raw_objects
|
|
WHERE common_prefix IS NULL
|
|
),
|
|
combined AS (
|
|
SELECT * FROM aggregated_prefixes
|
|
UNION ALL
|
|
SELECT * FROM leaf_objects
|
|
),
|
|
filtered AS (
|
|
SELECT *
|
|
FROM combined
|
|
WHERE (
|
|
$5 = ''
|
|
OR ROW(
|
|
date_trunc('milliseconds', %I),
|
|
name COLLATE "C"
|
|
) %s ROW(
|
|
COALESCE(NULLIF($6, '')::timestamptz, 'epoch'::timestamptz),
|
|
$5
|
|
)
|
|
)
|
|
)
|
|
SELECT
|
|
split_part(name, '/', $3) AS key,
|
|
name,
|
|
id,
|
|
updated_at,
|
|
created_at,
|
|
last_accessed_at,
|
|
metadata
|
|
FROM filtered
|
|
ORDER BY
|
|
COALESCE(date_trunc('milliseconds', %I), 'epoch'::timestamptz) %s,
|
|
name COLLATE "C" %s
|
|
LIMIT $4
|
|
$sql$,
|
|
p_sort_column,
|
|
v_cursor_op,
|
|
p_sort_column,
|
|
p_sort_order,
|
|
p_sort_order
|
|
);
|
|
|
|
RETURN QUERY EXECUTE v_query
|
|
USING v_prefix, p_bucket_id, p_level, p_limit, p_start_after, p_sort_column_after;
|
|
END;
|
|
$_$;
|
|
|
|
|
|
ALTER FUNCTION storage.search_by_timestamp(p_prefix text, p_bucket_id text, p_limit integer, p_level integer, p_start_after text, p_sort_order text, p_sort_column text, p_sort_column_after text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: search_v2(text, text, integer, integer, text, text, text, text); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer DEFAULT 100, levels integer DEFAULT 1, start_after text DEFAULT ''::text, sort_order text DEFAULT 'asc'::text, sort_column text DEFAULT 'name'::text, sort_column_after text DEFAULT ''::text) RETURNS TABLE(key text, name text, id uuid, updated_at timestamp with time zone, created_at timestamp with time zone, last_accessed_at timestamp with time zone, metadata jsonb)
|
|
LANGUAGE plpgsql STABLE
|
|
AS $$
|
|
DECLARE
|
|
v_sort_col text;
|
|
v_sort_ord text;
|
|
v_limit int;
|
|
BEGIN
|
|
-- Cap limit to maximum of 1500 records
|
|
v_limit := LEAST(coalesce(limits, 100), 1500);
|
|
|
|
-- Validate and normalize sort_order
|
|
v_sort_ord := lower(coalesce(sort_order, 'asc'));
|
|
IF v_sort_ord NOT IN ('asc', 'desc') THEN
|
|
v_sort_ord := 'asc';
|
|
END IF;
|
|
|
|
-- Validate and normalize sort_column
|
|
v_sort_col := lower(coalesce(sort_column, 'name'));
|
|
IF v_sort_col NOT IN ('name', 'updated_at', 'created_at') THEN
|
|
v_sort_col := 'name';
|
|
END IF;
|
|
|
|
-- Route to appropriate implementation
|
|
IF v_sort_col = 'name' THEN
|
|
-- Use list_objects_with_delimiter for name sorting (most efficient: O(k * log n))
|
|
RETURN QUERY
|
|
SELECT
|
|
split_part(l.name, '/', levels) AS key,
|
|
l.name AS name,
|
|
l.id,
|
|
l.updated_at,
|
|
l.created_at,
|
|
l.last_accessed_at,
|
|
l.metadata
|
|
FROM storage.list_objects_with_delimiter(
|
|
bucket_name,
|
|
coalesce(prefix, ''),
|
|
'/',
|
|
v_limit,
|
|
start_after,
|
|
'',
|
|
v_sort_ord
|
|
) l;
|
|
ELSE
|
|
-- Use aggregation approach for timestamp sorting
|
|
-- Not efficient for large datasets but supports correct pagination
|
|
RETURN QUERY SELECT * FROM storage.search_by_timestamp(
|
|
prefix, bucket_name, v_limit, levels, start_after,
|
|
v_sort_ord, v_sort_col, sort_column_after
|
|
);
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.search_v2(prefix text, bucket_name text, limits integer, levels integer, start_after text, sort_order text, sort_column text, sort_column_after text) OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: update_updated_at_column(); Type: FUNCTION; Schema: storage; Owner: supabase_storage_admin
|
|
--
|
|
|
|
CREATE FUNCTION storage.update_updated_at_column() RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
|
|
ALTER FUNCTION storage.update_updated_at_column() OWNER TO supabase_storage_admin;
|
|
|
|
--
|
|
-- Name: http_request(); Type: FUNCTION; Schema: supabase_functions; Owner: supabase_functions_admin
|
|
--
|
|
|