-- ============================================================================= -- 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 --