@supacloud/lite 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSES/POSTGRESQL-17-COPYRIGHT.txt +23 -0
  3. package/LICENSES/THESEUS-POSTGRESQL-LICENSE.txt +7 -0
  4. package/THIRD_PARTY_NOTICES.md +11 -1
  5. package/dist/cli.js +2569 -334
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2432 -264
  9. package/dist/project-runtime.d.ts +5 -0
  10. package/dist/project-runtime.d.ts.map +1 -1
  11. package/dist/runtime/db/data-dir-lock.d.ts +2 -0
  12. package/dist/runtime/db/data-dir-lock.d.ts.map +1 -1
  13. package/dist/runtime/db/database.d.ts.map +1 -1
  14. package/dist/runtime/db/emulated.d.ts +2 -1
  15. package/dist/runtime/db/emulated.d.ts.map +1 -1
  16. package/dist/runtime/db/pglite-engine.d.ts.map +1 -1
  17. package/dist/runtime/node/fs-driver.d.ts +2 -0
  18. package/dist/runtime/node/fs-driver.d.ts.map +1 -1
  19. package/dist/runtime/node/native/engine.d.ts +23 -0
  20. package/dist/runtime/node/native/engine.d.ts.map +1 -0
  21. package/dist/runtime/node/native/wire-engine.d.ts +9 -0
  22. package/dist/runtime/node/native/wire-engine.d.ts.map +1 -0
  23. package/dist/runtime/node/native/wire.d.ts +56 -0
  24. package/dist/runtime/node/native/wire.d.ts.map +1 -0
  25. package/dist/runtime/rest/handler.d.ts.map +1 -1
  26. package/dist/runtime/storage/handler.d.ts +5 -0
  27. package/dist/runtime/storage/handler.d.ts.map +1 -1
  28. package/dist/runtime/storage/image-transform-cache.d.ts +13 -0
  29. package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -0
  30. package/dist/runtime/storage/image-transform.d.ts +1 -1
  31. package/dist/runtime/storage/image-transform.d.ts.map +1 -1
  32. package/dist/runtime/storage/s3-driver.d.ts +2 -0
  33. package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
  34. package/dist/runtime/types.d.ts +2 -0
  35. package/dist/runtime/types.d.ts.map +1 -1
  36. package/dist/snapshot.d.ts +5 -1
  37. package/dist/snapshot.d.ts.map +1 -1
  38. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -3,13 +3,13 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli.ts
6
- import { mkdir as mkdir7, rm as rm5, writeFile as writeFile6 } from "fs/promises";
7
- import { dirname as dirname6, join as join9, resolve as resolve4 } from "path";
6
+ import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
7
+ import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.7.3",
12
- description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
11
+ version: "0.8.0",
12
+ description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
15
15
  bin: {
@@ -45,6 +45,10 @@ var package_default = {
45
45
  dev: "bun run src/cli.ts start",
46
46
  start: "bun run src/cli.ts start",
47
47
  test: "bun test --timeout 20000",
48
+ "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts --timeout 180000",
49
+ parity: "bun run parity/harness.ts",
50
+ "parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
51
+ "check:native": "bun run test:native && bun run parity:native",
48
52
  "test:package": "bun run scripts/package-smoke.ts",
49
53
  "test:standalone": "bun run scripts/standalone-smoke.ts",
50
54
  prepack: "bun run build",
@@ -3545,41 +3549,1158 @@ $pgmq$;
3545
3549
  revoke all on all functions in schema pgmq from public, anon, authenticated;
3546
3550
  grant execute on all functions in schema pgmq to service_role;
3547
3551
 
3548
- create schema if not exists pgmq_public;
3549
- grant usage on schema pgmq_public to anon, authenticated, service_role;
3550
-
3551
- create or replace function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer default 0)
3552
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3553
- select * from pgmq.send(queue_name, message, sleep_seconds);
3554
- $pgmq_public$;
3555
-
3556
- create or replace function pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer default 0)
3557
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3558
- select * from pgmq.send_batch(queue_name, messages, sleep_seconds);
3559
- $pgmq_public$;
3560
-
3561
- create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3562
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3563
- select * from pgmq.read(queue_name, sleep_seconds, n);
3564
- $pgmq_public$;
3565
-
3566
- create or replace function pgmq_public.pop(queue_name text)
3567
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3568
- select * from pgmq.pop(queue_name);
3569
- $pgmq_public$;
3570
-
3571
- create or replace function pgmq_public.archive(queue_name text, message_id bigint)
3572
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3573
- select pgmq.archive(queue_name, message_id);
3574
- $pgmq_public$;
3575
-
3576
- create or replace function pgmq_public."delete"(queue_name text, message_id bigint)
3577
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3578
- select pgmq.delete(queue_name, message_id);
3579
- $pgmq_public$;
3580
-
3581
- revoke all on all functions in schema pgmq_public from public;
3582
- grant execute on all functions in schema pgmq_public to anon, authenticated, service_role;
3552
+ -- supacloud:sql-module:pgmq-public:start
3553
+ DO $pgmq_extension$
3554
+ BEGIN
3555
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3556
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3557
+ END IF;
3558
+ END
3559
+ $pgmq_extension$;
3560
+
3561
+ CREATE SCHEMA IF NOT EXISTS pgmq_public;
3562
+ GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
3563
+
3564
+ CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
3565
+ RETURNS text
3566
+ LANGUAGE plpgsql
3567
+ IMMUTABLE
3568
+ SET search_path = ''
3569
+ AS $$
3570
+ DECLARE
3571
+ normalized_queue_name text := lower(btrim(queue_name));
3572
+ BEGIN
3573
+ IF normalized_queue_name IS NULL
3574
+ OR left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN
3575
+ RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';
3576
+ END IF;
3577
+ RETURN normalized_queue_name;
3578
+ END;
3579
+ $$;
3580
+
3581
+ CREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)
3582
+ RETURNS SETOF bigint
3583
+ LANGUAGE sql
3584
+ VOLATILE
3585
+ SECURITY DEFINER
3586
+ SET search_path = ''
3587
+ AS $$ SELECT * FROM pgmq.send(pgmq_public.require_public_queue(queue_name), message, sleep_seconds); $$;
3588
+
3589
+ CREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)
3590
+ RETURNS SETOF bigint
3591
+ LANGUAGE sql
3592
+ VOLATILE
3593
+ SECURITY DEFINER
3594
+ SET search_path = ''
3595
+ AS $$ SELECT * FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), messages, sleep_seconds); $$;
3596
+
3597
+ CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3598
+ RETURNS SETOF pgmq.message_record
3599
+ LANGUAGE sql
3600
+ VOLATILE
3601
+ SECURITY DEFINER
3602
+ SET search_path = ''
3603
+ AS $$ SELECT * FROM pgmq.read(pgmq_public.require_public_queue(queue_name), sleep_seconds, n); $$;
3604
+
3605
+ CREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)
3606
+ RETURNS SETOF pgmq.message_record
3607
+ LANGUAGE sql
3608
+ VOLATILE
3609
+ SECURITY DEFINER
3610
+ SET search_path = ''
3611
+ AS $$ SELECT * FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)); $$;
3612
+
3613
+ CREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)
3614
+ RETURNS boolean
3615
+ LANGUAGE sql
3616
+ VOLATILE
3617
+ SECURITY DEFINER
3618
+ SET search_path = ''
3619
+ AS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), message_id); $$;
3620
+
3621
+ CREATE OR REPLACE FUNCTION pgmq_public."delete"(queue_name text, message_id bigint)
3622
+ RETURNS boolean
3623
+ LANGUAGE sql
3624
+ VOLATILE
3625
+ SECURITY DEFINER
3626
+ SET search_path = ''
3627
+ AS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), message_id); $$;
3628
+
3629
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;
3630
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;
3631
+ -- supacloud:sql-module:pgmq-public:end
3632
+ `;
3633
+ var WORKFLOWS_SQL = `
3634
+ -- supacloud:sql-module:workflows-public:start
3635
+ DO $pgmq_extension$
3636
+ BEGIN
3637
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3638
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3639
+ END IF;
3640
+ END
3641
+ $pgmq_extension$;
3642
+ CREATE SCHEMA IF NOT EXISTS supacloud_workflows;
3643
+ REVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;
3644
+ GRANT USAGE ON SCHEMA supacloud_workflows TO service_role;
3645
+
3646
+ SELECT pgmq.create('supacloud_internal_workflows');
3647
+
3648
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.runs (
3649
+ id uuid PRIMARY KEY,
3650
+ workflow_name text NOT NULL
3651
+ CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3652
+ workflow_version text NOT NULL
3653
+ CHECK (char_length(workflow_version) BETWEEN 1 AND 80),
3654
+ status text NOT NULL DEFAULT 'queued'
3655
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
3656
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3657
+ CHECK (jsonb_typeof(input) = 'object'),
3658
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3659
+ CHECK (jsonb_typeof(output) = 'object'),
3660
+ error_message text NOT NULL DEFAULT ''
3661
+ CHECK (char_length(error_message) <= 4000),
3662
+ row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),
3663
+ created_at timestamptz NOT NULL DEFAULT now(),
3664
+ started_at timestamptz,
3665
+ completed_at timestamptz,
3666
+ updated_at timestamptz NOT NULL DEFAULT now()
3667
+ );
3668
+
3669
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.steps (
3670
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3671
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3672
+ step_key text NOT NULL
3673
+ CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3674
+ status text NOT NULL DEFAULT 'queued'
3675
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),
3676
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3677
+ CHECK (jsonb_typeof(input) = 'object'),
3678
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3679
+ CHECK (jsonb_typeof(output) = 'object'),
3680
+ error_message text NOT NULL DEFAULT ''
3681
+ CHECK (char_length(error_message) <= 4000),
3682
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
3683
+ max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),
3684
+ retry_delay_seconds integer NOT NULL DEFAULT 0
3685
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400),
3686
+ queue_message_id bigint NOT NULL UNIQUE,
3687
+ claimed_by text,
3688
+ claimed_at timestamptz,
3689
+ completed_at timestamptz,
3690
+ next_step_key text,
3691
+ created_at timestamptz NOT NULL DEFAULT now(),
3692
+ updated_at timestamptz NOT NULL DEFAULT now(),
3693
+ UNIQUE (run_id, step_key)
3694
+ );
3695
+
3696
+ ALTER TABLE supacloud_workflows.steps
3697
+ ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0
3698
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400);
3699
+
3700
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx
3701
+ ON supacloud_workflows.steps (run_id)
3702
+ WHERE status IN ('queued', 'running');
3703
+
3704
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx
3705
+ ON supacloud_workflows.runs (status, updated_at DESC, id);
3706
+
3707
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx
3708
+ ON supacloud_workflows.steps (run_id, created_at, id);
3709
+
3710
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.events (
3711
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
3712
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3713
+ step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,
3714
+ event_type text NOT NULL
3715
+ CHECK (event_type IN (
3716
+ 'run_started', 'step_claimed', 'step_retried', 'step_completed',
3717
+ 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'
3718
+ )),
3719
+ attempt integer CHECK (attempt IS NULL OR attempt > 0),
3720
+ details jsonb NOT NULL DEFAULT '{}'::jsonb
3721
+ CHECK (jsonb_typeof(details) = 'object'),
3722
+ created_at timestamptz NOT NULL DEFAULT now()
3723
+ );
3724
+
3725
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx
3726
+ ON supacloud_workflows.events (run_id, id);
3727
+
3728
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx
3729
+ ON supacloud_workflows.events (step_id, attempt)
3730
+ WHERE event_type IN ('step_retried', 'step_dead_lettered')
3731
+ AND details ->> 'operation' = 'retry';
3732
+
3733
+ CREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(
3734
+ p_run_id uuid,
3735
+ p_idempotent boolean DEFAULT false
3736
+ ) RETURNS jsonb
3737
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
3738
+ SELECT jsonb_build_object(
3739
+ 'runId', run.id,
3740
+ 'workflowName', run.workflow_name,
3741
+ 'workflowVersion', run.workflow_version,
3742
+ 'status', run.status,
3743
+ 'input', run.input,
3744
+ 'output', run.output,
3745
+ 'errorMessage', run.error_message,
3746
+ 'rowVersion', run.row_version::text,
3747
+ 'createdAt', run.created_at,
3748
+ 'startedAt', run.started_at,
3749
+ 'completedAt', run.completed_at,
3750
+ 'updatedAt', run.updated_at,
3751
+ 'idempotent', p_idempotent,
3752
+ 'steps', coalesce((
3753
+ SELECT jsonb_agg(jsonb_build_object(
3754
+ 'stepId', step.id,
3755
+ 'stepKey', step.step_key,
3756
+ 'status', step.status,
3757
+ 'input', step.input,
3758
+ 'output', step.output,
3759
+ 'errorMessage', step.error_message,
3760
+ 'attempts', step.attempts,
3761
+ 'maxAttempts', step.max_attempts,
3762
+ 'retryDelaySeconds', step.retry_delay_seconds,
3763
+ 'queueMessageId', step.queue_message_id::text,
3764
+ 'claimedBy', step.claimed_by,
3765
+ 'claimedAt', step.claimed_at,
3766
+ 'completedAt', step.completed_at,
3767
+ 'nextStepKey', step.next_step_key,
3768
+ 'createdAt', step.created_at,
3769
+ 'updatedAt', step.updated_at
3770
+ ) ORDER BY step.created_at, step.id)
3771
+ FROM supacloud_workflows.steps step
3772
+ WHERE step.run_id = run.id
3773
+ ), '[]'::jsonb)
3774
+ )
3775
+ FROM supacloud_workflows.runs run
3776
+ WHERE run.id = p_run_id
3777
+ $$;
3778
+
3779
+ CREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(
3780
+ p_run_id uuid,
3781
+ p_step_key text,
3782
+ p_input jsonb,
3783
+ p_max_attempts integer
3784
+ ) RETURNS supacloud_workflows.steps
3785
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3786
+ DECLARE
3787
+ normalized_step_key text := nullif(btrim(p_step_key), '');
3788
+ step_id uuid := gen_random_uuid();
3789
+ message_id bigint;
3790
+ created_step supacloud_workflows.steps%ROWTYPE;
3791
+ BEGIN
3792
+ IF normalized_step_key IS NULL
3793
+ OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3794
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3795
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3796
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';
3797
+ END IF;
3798
+
3799
+ IF NOT EXISTS (
3800
+ SELECT 1 FROM supacloud_workflows.runs run
3801
+ WHERE run.id = p_run_id AND run.status IN ('queued', 'running')
3802
+ ) THEN
3803
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
3804
+ END IF;
3805
+
3806
+ SELECT queued_id INTO message_id
3807
+ FROM pgmq.send(
3808
+ 'supacloud_internal_workflows',
3809
+ jsonb_build_object('run_id', p_run_id, 'step_id', step_id),
3810
+ 0
3811
+ ) AS queued_id;
3812
+
3813
+ INSERT INTO supacloud_workflows.steps (
3814
+ id, run_id, step_key, input, max_attempts, queue_message_id
3815
+ ) VALUES (
3816
+ step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
3817
+ ) RETURNING * INTO created_step;
3818
+
3819
+ RETURN created_step;
3820
+ END;
3821
+ $$;
3822
+
3823
+ -- Clean-code exception: private transitions keep typed PostgreSQL arguments and
3824
+ -- the complete lock/queue/ledger/event mutation in one transaction. The public
3825
+ -- contract already uses one JSON request; revisit if a private routine gains a
3826
+ -- second caller or any transition can be decomposed without weakening atomicity.
3827
+ CREATE OR REPLACE FUNCTION supacloud_workflows.start_run(
3828
+ p_run_id uuid,
3829
+ p_workflow_name text,
3830
+ p_workflow_version text,
3831
+ p_first_step_key text,
3832
+ p_input jsonb,
3833
+ p_max_attempts integer
3834
+ ) RETURNS jsonb
3835
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3836
+ DECLARE
3837
+ normalized_name text := nullif(btrim(p_workflow_name), '');
3838
+ normalized_version text := nullif(btrim(p_workflow_version), '');
3839
+ normalized_first_step_key text := nullif(btrim(p_first_step_key), '');
3840
+ existing_run supacloud_workflows.runs%ROWTYPE;
3841
+ existing_step supacloud_workflows.steps%ROWTYPE;
3842
+ first_step supacloud_workflows.steps%ROWTYPE;
3843
+ BEGIN
3844
+ IF p_run_id IS NULL
3845
+ OR normalized_name IS NULL
3846
+ OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3847
+ OR normalized_version IS NULL
3848
+ OR char_length(normalized_version) > 80
3849
+ OR normalized_first_step_key IS NULL
3850
+ OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3851
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3852
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3853
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
3854
+ END IF;
3855
+
3856
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
3857
+ SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
3858
+ IF FOUND THEN
3859
+ SELECT * INTO existing_step
3860
+ FROM supacloud_workflows.steps
3861
+ WHERE run_id = p_run_id
3862
+ ORDER BY created_at, id
3863
+ LIMIT 1;
3864
+ IF NOT FOUND THEN
3865
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3866
+ END IF;
3867
+ IF existing_run.workflow_name <> normalized_name
3868
+ OR existing_run.workflow_version <> normalized_version
3869
+ OR existing_run.input <> p_input
3870
+ OR existing_step.step_key <> normalized_first_step_key
3871
+ OR existing_step.input <> p_input
3872
+ OR existing_step.max_attempts <> p_max_attempts THEN
3873
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3874
+ END IF;
3875
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
3876
+ END IF;
3877
+
3878
+ INSERT INTO supacloud_workflows.runs (
3879
+ id, workflow_name, workflow_version, input
3880
+ ) VALUES (
3881
+ p_run_id, normalized_name, normalized_version, p_input
3882
+ );
3883
+ first_step := supacloud_workflows.enqueue_step(
3884
+ p_run_id, normalized_first_step_key, p_input, p_max_attempts
3885
+ );
3886
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)
3887
+ VALUES (p_run_id, first_step.id, 'run_started');
3888
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
3889
+ END;
3890
+ $$;
3891
+
3892
+ CREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(
3893
+ p_worker_id text,
3894
+ p_visibility_timeout_seconds integer
3895
+ ) RETURNS jsonb
3896
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3897
+ DECLARE
3898
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
3899
+ queued_message pgmq.message_record;
3900
+ message_run_id text;
3901
+ message_step_id text;
3902
+ candidate_run_id uuid;
3903
+ claimed_step supacloud_workflows.steps%ROWTYPE;
3904
+ claimed_run supacloud_workflows.runs%ROWTYPE;
3905
+ BEGIN
3906
+ IF normalized_worker_id IS NULL
3907
+ OR char_length(normalized_worker_id) > 200
3908
+ OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN
3909
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
3910
+ END IF;
3911
+
3912
+ SELECT * INTO queued_message
3913
+ FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)
3914
+ LIMIT 1;
3915
+ IF NOT FOUND THEN RETURN NULL; END IF;
3916
+
3917
+ message_run_id := queued_message.message ->> 'run_id';
3918
+ message_step_id := queued_message.message ->> 'step_id';
3919
+ IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'
3920
+ OR message_run_id IS NULL
3921
+ OR message_step_id IS NULL
3922
+ OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
3923
+ OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
3924
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3925
+ RETURN jsonb_build_object(
3926
+ 'status', 'discarded',
3927
+ 'reason', 'invalid_message',
3928
+ 'messageId', queued_message.msg_id::text
3929
+ );
3930
+ END IF;
3931
+
3932
+ SELECT step.run_id INTO candidate_run_id
3933
+ FROM supacloud_workflows.steps step
3934
+ WHERE step.id::text = lower(message_step_id)
3935
+ AND step.run_id::text = lower(message_run_id)
3936
+ AND step.queue_message_id = queued_message.msg_id;
3937
+ IF NOT FOUND THEN
3938
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3939
+ RETURN jsonb_build_object(
3940
+ 'status', 'discarded',
3941
+ 'reason', 'orphaned_message',
3942
+ 'messageId', queued_message.msg_id::text
3943
+ );
3944
+ END IF;
3945
+
3946
+ IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
3947
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
3948
+ END IF;
3949
+ SELECT * INTO claimed_step
3950
+ FROM supacloud_workflows.steps
3951
+ WHERE id::text = lower(message_step_id)
3952
+ AND run_id = candidate_run_id
3953
+ AND queue_message_id = queued_message.msg_id
3954
+ FOR UPDATE;
3955
+ IF NOT FOUND THEN
3956
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3957
+ RETURN jsonb_build_object(
3958
+ 'status', 'discarded',
3959
+ 'reason', 'orphaned_message',
3960
+ 'messageId', queued_message.msg_id::text
3961
+ );
3962
+ END IF;
3963
+
3964
+ SELECT * INTO claimed_run
3965
+ FROM supacloud_workflows.runs
3966
+ WHERE id = claimed_step.run_id
3967
+ FOR UPDATE;
3968
+ IF claimed_run.status NOT IN ('queued', 'running')
3969
+ OR claimed_step.status NOT IN ('queued', 'running') THEN
3970
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3971
+ RETURN jsonb_build_object(
3972
+ 'status', 'discarded',
3973
+ 'reason', 'step_not_claimable',
3974
+ 'runId', claimed_step.run_id,
3975
+ 'stepId', claimed_step.id,
3976
+ 'messageId', queued_message.msg_id::text
3977
+ );
3978
+ END IF;
3979
+
3980
+ IF queued_message.read_ct > claimed_step.max_attempts THEN
3981
+ UPDATE supacloud_workflows.steps
3982
+ SET status = 'dead_lettered', attempts = queued_message.read_ct,
3983
+ error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()
3984
+ WHERE id = claimed_step.id;
3985
+ UPDATE supacloud_workflows.runs
3986
+ SET status = 'failed', error_message = 'maximum attempts exceeded',
3987
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
3988
+ WHERE id = claimed_step.run_id;
3989
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3990
+ INSERT INTO supacloud_workflows.events (
3991
+ run_id, step_id, event_type, attempt, details
3992
+ ) VALUES (
3993
+ claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,
3994
+ jsonb_build_object('errorMessage', 'maximum attempts exceeded')
3995
+ );
3996
+ RETURN jsonb_build_object(
3997
+ 'status', 'dead_lettered',
3998
+ 'runId', claimed_step.run_id,
3999
+ 'stepId', claimed_step.id,
4000
+ 'stepKey', claimed_step.step_key,
4001
+ 'messageId', queued_message.msg_id::text,
4002
+ 'attempt', queued_message.read_ct,
4003
+ 'maxAttempts', claimed_step.max_attempts
4004
+ );
4005
+ END IF;
4006
+
4007
+ UPDATE supacloud_workflows.steps
4008
+ SET status = 'running', attempts = queued_message.read_ct,
4009
+ retry_delay_seconds = 0, claimed_by = normalized_worker_id,
4010
+ claimed_at = now(), updated_at = now()
4011
+ WHERE id = claimed_step.id;
4012
+ UPDATE supacloud_workflows.runs
4013
+ SET status = 'running', started_at = coalesce(started_at, now()),
4014
+ updated_at = now(), row_version = row_version + 1
4015
+ WHERE id = claimed_step.run_id;
4016
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)
4017
+ VALUES (
4018
+ claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,
4019
+ jsonb_build_object('workerId', normalized_worker_id)
4020
+ );
4021
+
4022
+ RETURN jsonb_build_object(
4023
+ 'status', 'claimed',
4024
+ 'runId', claimed_step.run_id,
4025
+ 'workflowName', claimed_run.workflow_name,
4026
+ 'workflowVersion', claimed_run.workflow_version,
4027
+ 'stepId', claimed_step.id,
4028
+ 'stepKey', claimed_step.step_key,
4029
+ 'input', claimed_step.input,
4030
+ 'messageId', queued_message.msg_id::text,
4031
+ 'attempt', queued_message.read_ct,
4032
+ 'maxAttempts', claimed_step.max_attempts,
4033
+ 'workerId', normalized_worker_id
4034
+ );
4035
+ END;
4036
+ $$;
4037
+
4038
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(
4039
+ p_step_id uuid
4040
+ ) RETURNS supacloud_workflows.steps
4041
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4042
+ DECLARE
4043
+ candidate_run_id uuid;
4044
+ active_step supacloud_workflows.steps%ROWTYPE;
4045
+ BEGIN
4046
+ SELECT step.run_id INTO candidate_run_id
4047
+ FROM supacloud_workflows.steps step
4048
+ WHERE step.id = p_step_id;
4049
+ IF NOT FOUND THEN
4050
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4051
+ END IF;
4052
+ PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
4053
+ SELECT * INTO active_step
4054
+ FROM supacloud_workflows.steps
4055
+ WHERE id = p_step_id
4056
+ FOR UPDATE;
4057
+ IF NOT FOUND THEN
4058
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4059
+ END IF;
4060
+ RETURN active_step;
4061
+ END;
4062
+ $$;
4063
+
4064
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(
4065
+ p_step_id uuid,
4066
+ p_message_id bigint,
4067
+ p_attempt integer,
4068
+ p_worker_id text
4069
+ ) RETURNS supacloud_workflows.steps
4070
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4071
+ DECLARE
4072
+ active_step supacloud_workflows.steps%ROWTYPE;
4073
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4074
+ BEGIN
4075
+ active_step := supacloud_workflows.lock_step(p_step_id);
4076
+ IF normalized_worker_id IS NULL
4077
+ OR p_message_id IS NULL
4078
+ OR p_attempt IS NULL
4079
+ OR active_step.queue_message_id IS DISTINCT FROM p_message_id
4080
+ OR active_step.attempts IS DISTINCT FROM p_attempt
4081
+ OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN
4082
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4083
+ END IF;
4084
+ RETURN active_step;
4085
+ END;
4086
+ $$;
4087
+
4088
+ CREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(
4089
+ p_step_id uuid,
4090
+ p_message_id bigint,
4091
+ p_attempt integer,
4092
+ p_worker_id text,
4093
+ p_output jsonb,
4094
+ p_next_step_key text,
4095
+ p_next_input jsonb,
4096
+ p_next_max_attempts integer
4097
+ ) RETURNS jsonb
4098
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4099
+ DECLARE
4100
+ current_step supacloud_workflows.steps%ROWTYPE;
4101
+ next_step supacloud_workflows.steps%ROWTYPE;
4102
+ normalized_next_step_key text := nullif(btrim(p_next_step_key), '');
4103
+ archived boolean;
4104
+ BEGIN
4105
+ IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'
4106
+ OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'
4107
+ OR normalized_next_step_key IS NULL
4108
+ OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4109
+ OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN
4110
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4111
+ END IF;
4112
+ current_step := supacloud_workflows.lock_step_attempt(
4113
+ p_step_id, p_message_id, p_attempt, p_worker_id
4114
+ );
4115
+
4116
+ IF current_step.status = 'completed' THEN
4117
+ SELECT * INTO next_step
4118
+ FROM supacloud_workflows.steps
4119
+ WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
4120
+ IF NOT FOUND
4121
+ OR current_step.output IS DISTINCT FROM p_output
4122
+ OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
4123
+ OR next_step.input IS DISTINCT FROM p_next_input
4124
+ OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN
4125
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4126
+ END IF;
4127
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4128
+ END IF;
4129
+ IF current_step.status <> 'running' THEN
4130
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4131
+ END IF;
4132
+
4133
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4134
+ IF archived IS DISTINCT FROM true THEN
4135
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4136
+ END IF;
4137
+ UPDATE supacloud_workflows.steps
4138
+ SET status = 'completed', output = p_output, error_message = '',
4139
+ completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()
4140
+ WHERE id = current_step.id;
4141
+ INSERT INTO supacloud_workflows.events (
4142
+ run_id, step_id, event_type, attempt, details
4143
+ ) VALUES (
4144
+ current_step.run_id, current_step.id, 'step_completed', p_attempt,
4145
+ jsonb_build_object('nextStepKey', normalized_next_step_key)
4146
+ );
4147
+ next_step := supacloud_workflows.enqueue_step(
4148
+ current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts
4149
+ );
4150
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4151
+ END;
4152
+ $$;
4153
+
4154
+ CREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(
4155
+ p_step_id uuid,
4156
+ p_message_id bigint,
4157
+ p_attempt integer,
4158
+ p_worker_id text,
4159
+ p_step_output jsonb,
4160
+ p_run_output jsonb
4161
+ ) RETURNS jsonb
4162
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4163
+ DECLARE
4164
+ current_step supacloud_workflows.steps%ROWTYPE;
4165
+ current_run supacloud_workflows.runs%ROWTYPE;
4166
+ archived boolean;
4167
+ BEGIN
4168
+ IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'
4169
+ OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN
4170
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4171
+ END IF;
4172
+ current_step := supacloud_workflows.lock_step_attempt(
4173
+ p_step_id, p_message_id, p_attempt, p_worker_id
4174
+ );
4175
+ SELECT * INTO current_run
4176
+ FROM supacloud_workflows.runs
4177
+ WHERE id = current_step.run_id
4178
+ FOR UPDATE;
4179
+
4180
+ IF current_step.status = 'completed' THEN
4181
+ IF current_step.next_step_key IS NOT NULL
4182
+ OR current_step.output IS DISTINCT FROM p_step_output
4183
+ OR current_run.status IS DISTINCT FROM 'completed'
4184
+ OR current_run.output IS DISTINCT FROM p_run_output THEN
4185
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4186
+ END IF;
4187
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4188
+ END IF;
4189
+ IF current_step.status <> 'running' THEN
4190
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4191
+ END IF;
4192
+ IF current_run.status <> 'running' THEN
4193
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4194
+ END IF;
4195
+
4196
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4197
+ IF archived IS DISTINCT FROM true THEN
4198
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4199
+ END IF;
4200
+ UPDATE supacloud_workflows.steps
4201
+ SET status = 'completed', output = p_step_output, error_message = '',
4202
+ completed_at = now(), updated_at = now()
4203
+ WHERE id = current_step.id;
4204
+ UPDATE supacloud_workflows.runs
4205
+ SET status = 'completed', output = p_run_output, error_message = '',
4206
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4207
+ WHERE id = current_step.run_id AND status = 'running';
4208
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4209
+ VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);
4210
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4211
+ VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);
4212
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4213
+ END;
4214
+ $$;
4215
+
4216
+ CREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(
4217
+ p_step_id uuid,
4218
+ p_message_id bigint,
4219
+ p_attempt integer,
4220
+ p_worker_id text,
4221
+ p_error_message text,
4222
+ p_delay_seconds integer
4223
+ ) RETURNS jsonb
4224
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4225
+ DECLARE
4226
+ current_step supacloud_workflows.steps%ROWTYPE;
4227
+ normalized_error text := nullif(btrim(p_error_message), '');
4228
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4229
+ retry_receipt jsonb;
4230
+ queue_message_updated boolean;
4231
+ archived boolean;
4232
+ BEGIN
4233
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000
4234
+ OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN
4235
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4236
+ END IF;
4237
+ current_step := supacloud_workflows.lock_step(p_step_id);
4238
+ SELECT event.details INTO retry_receipt
4239
+ FROM supacloud_workflows.events event
4240
+ WHERE event.step_id = current_step.id
4241
+ AND event.attempt = p_attempt
4242
+ AND event.event_type IN ('step_retried', 'step_dead_lettered')
4243
+ AND event.details ->> 'operation' = 'retry'
4244
+ ORDER BY event.id DESC
4245
+ LIMIT 1;
4246
+ IF FOUND THEN
4247
+ IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text
4248
+ OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id
4249
+ OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error
4250
+ OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN
4251
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4252
+ END IF;
4253
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4254
+ END IF;
4255
+ IF normalized_worker_id IS NULL
4256
+ OR p_message_id IS NULL
4257
+ OR p_attempt IS NULL
4258
+ OR current_step.queue_message_id IS DISTINCT FROM p_message_id
4259
+ OR current_step.attempts IS DISTINCT FROM p_attempt
4260
+ OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id
4261
+ OR current_step.status <> 'running' THEN
4262
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4263
+ END IF;
4264
+
4265
+ IF current_step.attempts >= current_step.max_attempts THEN
4266
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4267
+ IF archived IS DISTINCT FROM true THEN
4268
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4269
+ END IF;
4270
+ UPDATE supacloud_workflows.steps
4271
+ SET status = 'dead_lettered', error_message = normalized_error,
4272
+ completed_at = now(), updated_at = now()
4273
+ WHERE id = current_step.id;
4274
+ UPDATE supacloud_workflows.runs
4275
+ SET status = 'failed', error_message = normalized_error,
4276
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4277
+ WHERE id = current_step.run_id AND status = 'running';
4278
+ IF NOT FOUND THEN
4279
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4280
+ END IF;
4281
+ INSERT INTO supacloud_workflows.events (
4282
+ run_id, step_id, event_type, attempt, details
4283
+ ) VALUES (
4284
+ current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt,
4285
+ jsonb_build_object(
4286
+ 'operation', 'retry',
4287
+ 'messageId', p_message_id::text,
4288
+ 'workerId', normalized_worker_id,
4289
+ 'errorMessage', normalized_error,
4290
+ 'delaySeconds', p_delay_seconds
4291
+ )
4292
+ );
4293
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4294
+ END IF;
4295
+
4296
+ SELECT EXISTS (
4297
+ SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)
4298
+ ) INTO queue_message_updated;
4299
+ IF queue_message_updated IS DISTINCT FROM true THEN
4300
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4301
+ END IF;
4302
+ UPDATE supacloud_workflows.steps
4303
+ SET status = 'queued', error_message = normalized_error,
4304
+ retry_delay_seconds = p_delay_seconds, updated_at = now()
4305
+ WHERE id = current_step.id;
4306
+ INSERT INTO supacloud_workflows.events (
4307
+ run_id, step_id, event_type, attempt, details
4308
+ ) VALUES (
4309
+ current_step.run_id, current_step.id, 'step_retried', p_attempt,
4310
+ jsonb_build_object(
4311
+ 'operation', 'retry',
4312
+ 'messageId', p_message_id::text,
4313
+ 'workerId', normalized_worker_id,
4314
+ 'errorMessage', normalized_error,
4315
+ 'delaySeconds', p_delay_seconds
4316
+ )
4317
+ );
4318
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4319
+ END;
4320
+ $$;
4321
+
4322
+ CREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(
4323
+ p_step_id uuid,
4324
+ p_message_id bigint,
4325
+ p_attempt integer,
4326
+ p_worker_id text,
4327
+ p_error_message text
4328
+ ) RETURNS jsonb
4329
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4330
+ DECLARE
4331
+ current_step supacloud_workflows.steps%ROWTYPE;
4332
+ current_run supacloud_workflows.runs%ROWTYPE;
4333
+ normalized_error text := nullif(btrim(p_error_message), '');
4334
+ archived boolean;
4335
+ BEGIN
4336
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN
4337
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4338
+ END IF;
4339
+ current_step := supacloud_workflows.lock_step_attempt(
4340
+ p_step_id, p_message_id, p_attempt, p_worker_id
4341
+ );
4342
+ SELECT * INTO current_run
4343
+ FROM supacloud_workflows.runs
4344
+ WHERE id = current_step.run_id
4345
+ FOR UPDATE;
4346
+
4347
+ IF current_step.status = 'failed' THEN
4348
+ IF current_step.error_message IS DISTINCT FROM normalized_error
4349
+ OR current_run.status IS DISTINCT FROM 'failed'
4350
+ OR current_run.error_message IS DISTINCT FROM normalized_error THEN
4351
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4352
+ END IF;
4353
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4354
+ END IF;
4355
+ IF current_step.status <> 'running' THEN
4356
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4357
+ END IF;
4358
+ IF current_run.status <> 'running' THEN
4359
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4360
+ END IF;
4361
+
4362
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4363
+ IF archived IS DISTINCT FROM true THEN
4364
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4365
+ END IF;
4366
+ UPDATE supacloud_workflows.steps
4367
+ SET status = 'failed', error_message = normalized_error,
4368
+ completed_at = now(), updated_at = now()
4369
+ WHERE id = current_step.id;
4370
+ UPDATE supacloud_workflows.runs
4371
+ SET status = 'failed', error_message = normalized_error,
4372
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4373
+ WHERE id = current_step.run_id AND status = 'running';
4374
+ INSERT INTO supacloud_workflows.events (
4375
+ run_id, step_id, event_type, attempt, details
4376
+ ) VALUES (
4377
+ current_step.run_id, current_step.id, 'step_failed', p_attempt,
4378
+ jsonb_build_object('errorMessage', normalized_error)
4379
+ );
4380
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4381
+ END;
4382
+ $$;
4383
+
4384
+ CREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(
4385
+ p_run_id uuid,
4386
+ p_reason text
4387
+ ) RETURNS jsonb
4388
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4389
+ DECLARE
4390
+ normalized_reason text := nullif(btrim(p_reason), '');
4391
+ locked_run supacloud_workflows.runs%ROWTYPE;
4392
+ active_step supacloud_workflows.steps%ROWTYPE;
4393
+ BEGIN
4394
+ IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN
4395
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4396
+ END IF;
4397
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
4398
+ SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
4399
+ IF NOT FOUND THEN
4400
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
4401
+ END IF;
4402
+ IF locked_run.status = 'cancelled' THEN
4403
+ IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN
4404
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4405
+ END IF;
4406
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
4407
+ END IF;
4408
+ IF locked_run.status NOT IN ('queued', 'running') THEN
4409
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4410
+ END IF;
4411
+ SELECT * INTO active_step
4412
+ FROM supacloud_workflows.steps
4413
+ WHERE run_id = p_run_id AND status IN ('queued', 'running')
4414
+ FOR UPDATE;
4415
+ IF FOUND THEN
4416
+ PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
4417
+ UPDATE supacloud_workflows.steps
4418
+ SET status = 'cancelled', error_message = normalized_reason,
4419
+ completed_at = now(), updated_at = now()
4420
+ WHERE id = active_step.id;
4421
+ END IF;
4422
+ UPDATE supacloud_workflows.runs
4423
+ SET status = 'cancelled', error_message = normalized_reason,
4424
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4425
+ WHERE id = p_run_id;
4426
+ INSERT INTO supacloud_workflows.events (
4427
+ run_id, step_id, event_type, details
4428
+ ) VALUES (
4429
+ p_run_id, active_step.id, 'run_cancelled',
4430
+ jsonb_build_object('reason', normalized_reason)
4431
+ );
4432
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
4433
+ END;
4434
+ $$;
4435
+
4436
+ CREATE OR REPLACE FUNCTION supacloud_workflows.run_events(
4437
+ p_run_id uuid,
4438
+ p_after_event_id bigint,
4439
+ p_limit integer
4440
+ ) RETURNS jsonb
4441
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
4442
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
4443
+ 'eventId', page.id::text,
4444
+ 'runId', page.run_id,
4445
+ 'stepId', page.step_id,
4446
+ 'eventType', page.event_type,
4447
+ 'attempt', page.attempt,
4448
+ 'details', page.details,
4449
+ 'createdAt', page.created_at
4450
+ ) ORDER BY page.id), '[]'::jsonb)
4451
+ FROM (
4452
+ SELECT event.*
4453
+ FROM supacloud_workflows.events event
4454
+ WHERE event.run_id = p_run_id AND event.id > p_after_event_id
4455
+ ORDER BY event.id
4456
+ LIMIT p_limit
4457
+ ) page
4458
+ $$;
4459
+
4460
+ CREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(
4461
+ request jsonb,
4462
+ key text
4463
+ ) RETURNS uuid
4464
+ LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$
4465
+ DECLARE
4466
+ uuid_text text;
4467
+ BEGIN
4468
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4469
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4470
+ END IF;
4471
+ uuid_text := request ->> key;
4472
+ IF uuid_text IS NULL
4473
+ OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
4474
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4475
+ END IF;
4476
+ RETURN uuid_text::uuid;
4477
+ END;
4478
+ $$;
4479
+
4480
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)
4481
+ RETURNS jsonb
4482
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4483
+ DECLARE
4484
+ max_attempts integer;
4485
+ BEGIN
4486
+ max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
4487
+ RETURN supacloud_workflows.start_run(
4488
+ supacloud_workflows.request_uuid(request, 'runId'),
4489
+ request ->> 'workflowName',
4490
+ request ->> 'workflowVersion',
4491
+ request ->> 'firstStepKey',
4492
+ coalesce(request -> 'input', '{}'::jsonb),
4493
+ max_attempts
4494
+ );
4495
+ EXCEPTION
4496
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4497
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
4498
+ END;
4499
+ $$;
4500
+
4501
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)
4502
+ RETURNS jsonb
4503
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4504
+ DECLARE
4505
+ visibility_timeout_seconds integer;
4506
+ BEGIN
4507
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4508
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4509
+ END IF;
4510
+ visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);
4511
+ RETURN supacloud_workflows.claim_step(
4512
+ request ->> 'workerId', visibility_timeout_seconds
4513
+ );
4514
+ EXCEPTION
4515
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4516
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4517
+ END;
4518
+ $$;
4519
+
4520
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)
4521
+ RETURNS jsonb
4522
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4523
+ DECLARE
4524
+ message_id bigint;
4525
+ attempt integer;
4526
+ next_max_attempts integer;
4527
+ BEGIN
4528
+ message_id := (request ->> 'messageId')::bigint;
4529
+ attempt := (request ->> 'attempt')::integer;
4530
+ next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);
4531
+ IF message_id <= 0 OR attempt <= 0 THEN
4532
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4533
+ END IF;
4534
+ RETURN supacloud_workflows.advance_step(
4535
+ supacloud_workflows.request_uuid(request, 'stepId'),
4536
+ message_id,
4537
+ attempt,
4538
+ request ->> 'workerId',
4539
+ coalesce(request -> 'output', '{}'::jsonb),
4540
+ request ->> 'nextStepKey',
4541
+ coalesce(request -> 'nextInput', '{}'::jsonb),
4542
+ next_max_attempts
4543
+ );
4544
+ EXCEPTION
4545
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4546
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4547
+ END;
4548
+ $$;
4549
+
4550
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)
4551
+ RETURNS jsonb
4552
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4553
+ DECLARE
4554
+ message_id bigint;
4555
+ attempt integer;
4556
+ BEGIN
4557
+ message_id := (request ->> 'messageId')::bigint;
4558
+ attempt := (request ->> 'attempt')::integer;
4559
+ IF message_id <= 0 OR attempt <= 0 THEN
4560
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4561
+ END IF;
4562
+ RETURN supacloud_workflows.complete_run(
4563
+ supacloud_workflows.request_uuid(request, 'stepId'),
4564
+ message_id,
4565
+ attempt,
4566
+ request ->> 'workerId',
4567
+ coalesce(request -> 'stepOutput', '{}'::jsonb),
4568
+ coalesce(request -> 'runOutput', '{}'::jsonb)
4569
+ );
4570
+ EXCEPTION
4571
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4572
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4573
+ END;
4574
+ $$;
4575
+
4576
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)
4577
+ RETURNS jsonb
4578
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4579
+ DECLARE
4580
+ message_id bigint;
4581
+ attempt integer;
4582
+ delay_seconds integer;
4583
+ BEGIN
4584
+ message_id := (request ->> 'messageId')::bigint;
4585
+ attempt := (request ->> 'attempt')::integer;
4586
+ delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);
4587
+ IF message_id <= 0 OR attempt <= 0 THEN
4588
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4589
+ END IF;
4590
+ RETURN supacloud_workflows.retry_step(
4591
+ supacloud_workflows.request_uuid(request, 'stepId'),
4592
+ message_id,
4593
+ attempt,
4594
+ request ->> 'workerId',
4595
+ request ->> 'errorMessage',
4596
+ delay_seconds
4597
+ );
4598
+ EXCEPTION
4599
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4600
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4601
+ END;
4602
+ $$;
4603
+
4604
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)
4605
+ RETURNS jsonb
4606
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4607
+ DECLARE
4608
+ message_id bigint;
4609
+ attempt integer;
4610
+ BEGIN
4611
+ message_id := (request ->> 'messageId')::bigint;
4612
+ attempt := (request ->> 'attempt')::integer;
4613
+ IF message_id <= 0 OR attempt <= 0 THEN
4614
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4615
+ END IF;
4616
+ RETURN supacloud_workflows.fail_step(
4617
+ supacloud_workflows.request_uuid(request, 'stepId'),
4618
+ message_id,
4619
+ attempt,
4620
+ request ->> 'workerId',
4621
+ request ->> 'errorMessage'
4622
+ );
4623
+ EXCEPTION
4624
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4625
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4626
+ END;
4627
+ $$;
4628
+
4629
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)
4630
+ RETURNS jsonb
4631
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4632
+ BEGIN
4633
+ RETURN supacloud_workflows.cancel_run(
4634
+ supacloud_workflows.request_uuid(request, 'runId'),
4635
+ request ->> 'reason'
4636
+ );
4637
+ EXCEPTION
4638
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4639
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4640
+ END;
4641
+ $$;
4642
+
4643
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)
4644
+ RETURNS jsonb
4645
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4646
+ BEGIN
4647
+ RETURN supacloud_workflows.snapshot(
4648
+ supacloud_workflows.request_uuid(request, 'runId'), false
4649
+ );
4650
+ EXCEPTION
4651
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4652
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';
4653
+ END;
4654
+ $$;
4655
+
4656
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)
4657
+ RETURNS jsonb
4658
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4659
+ DECLARE
4660
+ after_event_id bigint;
4661
+ event_limit integer;
4662
+ BEGIN
4663
+ after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);
4664
+ event_limit := coalesce((request ->> 'limit')::integer, 100);
4665
+ IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN
4666
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4667
+ END IF;
4668
+ RETURN supacloud_workflows.run_events(
4669
+ supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit
4670
+ );
4671
+ EXCEPTION
4672
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4673
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4674
+ END;
4675
+ $$;
4676
+
4677
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows
4678
+ FROM PUBLIC, anon, authenticated, service_role;
4679
+ REVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows
4680
+ FROM PUBLIC, anon, authenticated, service_role;
4681
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows
4682
+ FROM PUBLIC, anon, authenticated, service_role;
4683
+
4684
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;
4685
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;
4686
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;
4687
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;
4688
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;
4689
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;
4690
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;
4691
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;
4692
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;
4693
+
4694
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;
4695
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;
4696
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;
4697
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;
4698
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;
4699
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;
4700
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;
4701
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
4702
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
4703
+ -- supacloud:sql-module:workflows-public:end
3583
4704
  `;
3584
4705
  var CRON_SQL = `
3585
4706
  create schema if not exists cron;
@@ -3906,12 +5027,32 @@ function pickTag(text, base) {
3906
5027
  return tag;
3907
5028
  }
3908
5029
 
3909
- // src/runtime/db/pglite-engine.ts
3910
- import { mkdir, open, unlink as unlink2 } from "fs/promises";
3911
- import { dirname, resolve } from "path";
3912
-
3913
5030
  // src/runtime/db/data-dir-lock.ts
3914
- import { readFile, unlink } from "fs/promises";
5031
+ import { mkdir, open, readFile, unlink } from "fs/promises";
5032
+ import { dirname, resolve } from "path";
5033
+ async function acquireDataDirLock(dataDir, engineName = "database") {
5034
+ if (!dataDir || dataDir.includes("://"))
5035
+ return async () => {};
5036
+ const absoluteDataDir = resolve(dataDir);
5037
+ const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
5038
+ await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
5039
+ const nonce = crypto.randomUUID();
5040
+ const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce, engineName);
5041
+ let released = false;
5042
+ return async () => {
5043
+ if (released)
5044
+ return;
5045
+ released = true;
5046
+ await handle.close();
5047
+ const owner = await readDataDirLockOwner(lockPath);
5048
+ if (owner?.nonce !== nonce)
5049
+ return;
5050
+ await unlink(lockPath).catch((error) => {
5051
+ if (error.code !== "ENOENT")
5052
+ throw error;
5053
+ });
5054
+ };
5055
+ }
3915
5056
  async function recoverStaleDataDirLock(lockPath) {
3916
5057
  const lockState = await inspectDataDirLock(lockPath);
3917
5058
  if (lockState.kind !== "stale")
@@ -3929,6 +5070,50 @@ async function readDataDirLockOwner(lockPath) {
3929
5070
  const lockState = await inspectDataDirLock(lockPath);
3930
5071
  return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
3931
5072
  }
5073
+ async function assertDataDirUnlocked(dataDir) {
5074
+ if (!dataDir)
5075
+ return;
5076
+ const lockPath = `${resolve(dataDir)}.supacloud-lite.lock`;
5077
+ const lockState = await recoverStaleDataDirLock(lockPath);
5078
+ if (lockState.kind === "missing")
5079
+ return;
5080
+ if (lockState.kind === "active") {
5081
+ throw new Error(`database data directory is already in use: ${resolve(dataDir)} (pid ${lockState.owner.pid})`);
5082
+ }
5083
+ throw unreadableLockError(lockPath, "database");
5084
+ }
5085
+ async function createDataDirLock(absoluteDataDir, lockPath, nonce, engineName) {
5086
+ for (let attempt = 0;attempt < 3; attempt++) {
5087
+ try {
5088
+ return await writeDataDirLock(lockPath, nonce);
5089
+ } catch (error) {
5090
+ if (error.code !== "EEXIST")
5091
+ throw error;
5092
+ const lockState = await recoverStaleDataDirLock(lockPath);
5093
+ if (lockState.kind === "active") {
5094
+ throw new Error(`${engineName} data directory is already in use: ${absoluteDataDir} (pid ${lockState.owner.pid})`);
5095
+ }
5096
+ if (lockState.kind === "unreadable")
5097
+ throw unreadableLockError(lockPath, engineName);
5098
+ }
5099
+ }
5100
+ throw unreadableLockError(lockPath, engineName);
5101
+ }
5102
+ async function writeDataDirLock(lockPath, nonce) {
5103
+ const handle = await open(lockPath, "wx", 384);
5104
+ try {
5105
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
5106
+ `);
5107
+ return handle;
5108
+ } catch (error) {
5109
+ await handle.close().catch(() => {});
5110
+ await unlink(lockPath).catch(() => {});
5111
+ throw error;
5112
+ }
5113
+ }
5114
+ function unreadableLockError(lockPath, engineName) {
5115
+ return new Error(`${engineName} data directory has an unreadable lock: ${lockPath}. ` + "Confirm no SupaCloud Lite process is using it, then remove the lock manually.");
5116
+ }
3932
5117
  async function inspectDataDirLock(lockPath) {
3933
5118
  let contents;
3934
5119
  try {
@@ -3982,7 +5167,7 @@ begin
3982
5167
  end $$;
3983
5168
  `;
3984
5169
  async function createPgliteEngine(dataDir) {
3985
- const releaseLock = await acquireDataDirLock(dataDir);
5170
+ const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
3986
5171
  let PGlite, extensions;
3987
5172
  const standaloneAssets = getStandaloneAssets();
3988
5173
  let cleanupStandaloneBundles = async () => {};
@@ -4106,63 +5291,6 @@ async function removePreparedBundles(cleanup) {
4106
5291
  console.error("Unable to remove temporary PGlite extension bundles:", error);
4107
5292
  }
4108
5293
  }
4109
- async function acquireDataDirLock(dataDir) {
4110
- if (!dataDir || dataDir.includes("://"))
4111
- return async () => {};
4112
- const absoluteDataDir = resolve(dataDir);
4113
- const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
4114
- await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
4115
- const nonce = crypto.randomUUID();
4116
- const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
4117
- let released = false;
4118
- return async () => {
4119
- if (released)
4120
- return;
4121
- released = true;
4122
- await handle?.close();
4123
- const owner = await readDataDirLockOwner(lockPath);
4124
- if (owner?.nonce !== nonce)
4125
- return;
4126
- await unlink2(lockPath).catch((error) => {
4127
- if (error.code !== "ENOENT")
4128
- throw error;
4129
- });
4130
- };
4131
- }
4132
- async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
4133
- for (let attempt = 0;attempt < 3; attempt++) {
4134
- try {
4135
- return await writeDataDirLock(lockPath, nonce);
4136
- } catch (error) {
4137
- if (error.code !== "EEXIST")
4138
- throw error;
4139
- const lockState = await recoverStaleDataDirLock(lockPath);
4140
- if (lockState.kind === "active")
4141
- throw lockInUseError(absoluteDataDir, lockState.owner.pid);
4142
- if (lockState.kind === "unreadable")
4143
- throw unreadableLockError(lockPath);
4144
- }
4145
- }
4146
- throw unreadableLockError(lockPath);
4147
- }
4148
- async function writeDataDirLock(lockPath, nonce) {
4149
- const handle = await open(lockPath, "wx", 384);
4150
- try {
4151
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
4152
- `);
4153
- return handle;
4154
- } catch (error) {
4155
- await handle.close().catch(() => {});
4156
- await unlink2(lockPath).catch(() => {});
4157
- throw error;
4158
- }
4159
- }
4160
- function lockInUseError(dataDir, pid) {
4161
- return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
4162
- }
4163
- function unreadableLockError(lockPath) {
4164
- return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
4165
- }
4166
5294
 
4167
5295
  // src/runtime/db/database.ts
4168
5296
  var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
@@ -4180,20 +5308,30 @@ class Database {
4180
5308
  }
4181
5309
  static async create(dataDirOrEngine, opts) {
4182
5310
  const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
4183
- if (engine.minimalBootstrap) {
4184
- await engine.exec(MINIMAL_BOOTSTRAP_SQL);
4185
- } else {
4186
- await engine.exec(BOOTSTRAP_SQL);
4187
- await engine.exec(PGMQ_SQL);
4188
- await engine.exec(CRON_SQL);
4189
- await engine.exec(NET_SQL);
4190
- await engine.exec(EXT_COMPAT_SQL);
4191
- await engine.exec(VAULT_SQL);
4192
- if (opts?.vaultKey) {
4193
- await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5311
+ try {
5312
+ if (engine.minimalBootstrap) {
5313
+ await engine.exec(MINIMAL_BOOTSTRAP_SQL);
5314
+ } else {
5315
+ await engine.exec(BOOTSTRAP_SQL);
5316
+ await engine.exec(PGMQ_SQL);
5317
+ await engine.exec(WORKFLOWS_SQL);
5318
+ await engine.exec(CRON_SQL);
5319
+ await engine.exec(NET_SQL);
5320
+ await engine.exec(EXT_COMPAT_SQL);
5321
+ await engine.exec(VAULT_SQL);
5322
+ if (opts?.vaultKey) {
5323
+ await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5324
+ }
5325
+ }
5326
+ return new Database(engine);
5327
+ } catch (error) {
5328
+ try {
5329
+ await engine.close();
5330
+ } catch (cleanupError) {
5331
+ throw new AggregateError([error, cleanupError], "database bootstrap and cleanup failed");
4194
5332
  }
5333
+ throw error;
4195
5334
  }
4196
- return new Database(engine);
4197
5335
  }
4198
5336
  query(sql, params) {
4199
5337
  return this.engine.query(sql, params);
@@ -5932,6 +7070,25 @@ function parsePrefer(header) {
5932
7070
  }
5933
7071
  return prefer;
5934
7072
  }
7073
+ function applyRequestRange(query, request) {
7074
+ const range = request.headers.get("range");
7075
+ if (!range || query.limits.has("") || query.offsets.has(""))
7076
+ return;
7077
+ const unit = request.headers.get("range-unit");
7078
+ if (unit !== null && unit.toLowerCase() !== "items")
7079
+ throw new ParseError(`unsupported range unit: ${unit}`);
7080
+ const match = range.match(/^(\d+)-(\d*)$/);
7081
+ if (!match)
7082
+ throw new ParseError(`invalid range: ${range}`);
7083
+ const start = Number(match[1]);
7084
+ const end = match[2] ? Number(match[2]) : undefined;
7085
+ if (!Number.isSafeInteger(start) || end !== undefined && (!Number.isSafeInteger(end) || end < start)) {
7086
+ throw new ParseError(`invalid range: ${range}`);
7087
+ }
7088
+ query.offsets.set("", start);
7089
+ if (end !== undefined)
7090
+ query.limits.set("", end - start + 1);
7091
+ }
5935
7092
  var OBJECT_MEDIA = "application/vnd.pgrst.object+json";
5936
7093
  var CSV_MEDIA = "text/csv";
5937
7094
  var PLAN_MEDIA = "application/vnd.pgrst.plan";
@@ -6008,6 +7165,8 @@ class RestHandler {
6008
7165
  const wantsObject = accept.includes(OBJECT_MEDIA);
6009
7166
  const wantsCsv = accept.includes(CSV_MEDIA);
6010
7167
  const q = parseQuery(url.searchParams);
7168
+ if (method === "GET" || method === "HEAD")
7169
+ applyRequestRange(q, req);
6011
7170
  if (this.maxRows !== undefined && (method === "GET" || method === "HEAD")) {
6012
7171
  const requested = q.limits.get("");
6013
7172
  q.limits.set("", requested === undefined ? this.maxRows : Math.min(requested, this.maxRows));
@@ -6050,10 +7209,11 @@ class RestHandler {
6050
7209
  }
6051
7210
  return { rows: res.rows[0].body, count: count2 };
6052
7211
  });
7212
+ const offset = q.offsets.get("") ?? 0;
6053
7213
  return this.dataResponse(rows, {
6054
- status: 200,
7214
+ status: count !== null && (offset > 0 || rows.length < count) ? 206 : 200,
6055
7215
  count,
6056
- offset: q.offsets.get("") ?? 0,
7216
+ offset,
6057
7217
  wantsObject,
6058
7218
  wantsCsv,
6059
7219
  head: method === "HEAD"
@@ -6484,8 +7644,10 @@ function applyResize(image, metadata, options) {
6484
7644
  const proportionalWidth = Math.max(1, Math.round(metadata.width * options.height / metadata.height));
6485
7645
  image.resize(proportionalWidth);
6486
7646
  }
6487
- async function transformImage(bytes, options) {
6488
- if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7647
+ async function transformImage(source, options, knownSourceSize) {
7648
+ const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
7649
+ const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
7650
+ if (sourceSize > MAX_TRANSFORM_BYTES) {
6489
7651
  return {
6490
7652
  ok: false,
6491
7653
  status: 413,
@@ -6493,9 +7655,10 @@ async function transformImage(bytes, options) {
6493
7655
  message: "The source image exceeds the 25MB transformation limit"
6494
7656
  };
6495
7657
  }
7658
+ const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
6496
7659
  let metadata;
6497
7660
  try {
6498
- metadata = await new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS }).metadata();
7661
+ metadata = await image.metadata();
6499
7662
  } catch (error) {
6500
7663
  return mapImageError(error);
6501
7664
  }
@@ -6512,7 +7675,6 @@ async function transformImage(bytes, options) {
6512
7675
  message: `The source format ${outputFormat} is not supported by this runtime`
6513
7676
  };
6514
7677
  }
6515
- const image = new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS });
6516
7678
  applyResize(image, metadata, options);
6517
7679
  if (options.format === "jpeg" || options.format === "origin" && outputFormat === "jpeg" && options.quality !== undefined) {
6518
7680
  image.jpeg(options.quality === undefined ? undefined : { quality: options.quality });
@@ -6522,12 +7684,127 @@ async function transformImage(bytes, options) {
6522
7684
  image.webp(options.quality === undefined ? undefined : { quality: options.quality });
6523
7685
  }
6524
7686
  try {
6525
- return { ok: true, bytes: await image.bytes(), contentType };
7687
+ const bytes = await image.bytes();
7688
+ if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7689
+ return {
7690
+ ok: false,
7691
+ status: 413,
7692
+ error: "ImageTooLarge",
7693
+ message: "The transformed image exceeds the 25MB transformation limit"
7694
+ };
7695
+ }
7696
+ return { ok: true, bytes, contentType };
6526
7697
  } catch (error) {
6527
7698
  return mapImageError(error);
6528
7699
  }
6529
7700
  }
6530
7701
 
7702
+ // src/runtime/storage/image-transform-cache.ts
7703
+ var DEFAULT_MAX_CACHE_BYTES = 64 * 1024 * 1024;
7704
+ var DEFAULT_MAX_CACHE_ENTRIES = 128;
7705
+ var OBJECT_VERSION_PREFIX = "v2-";
7706
+
7707
+ class Semaphore {
7708
+ limit;
7709
+ active = 0;
7710
+ waiters = [];
7711
+ constructor(limit) {
7712
+ this.limit = limit;
7713
+ }
7714
+ async run(operation) {
7715
+ await this.acquire();
7716
+ try {
7717
+ return await operation();
7718
+ } finally {
7719
+ this.release();
7720
+ }
7721
+ }
7722
+ async acquire() {
7723
+ if (this.active < this.limit) {
7724
+ this.active += 1;
7725
+ return;
7726
+ }
7727
+ await new Promise((resolve2) => this.waiters.push(resolve2));
7728
+ }
7729
+ release() {
7730
+ const next = this.waiters.shift();
7731
+ if (next) {
7732
+ next();
7733
+ return;
7734
+ }
7735
+ this.active -= 1;
7736
+ }
7737
+ }
7738
+ var globalImageTransformSemaphore = new Semaphore(1);
7739
+ function imageTransformCacheKey(version, options) {
7740
+ if (!version?.startsWith(OBJECT_VERSION_PREFIX))
7741
+ return null;
7742
+ return [
7743
+ version,
7744
+ options.width ?? "",
7745
+ options.height ?? "",
7746
+ options.resize,
7747
+ options.quality ?? "",
7748
+ options.format
7749
+ ].join("\x00");
7750
+ }
7751
+
7752
+ class ImageTransformCache {
7753
+ maxBytes;
7754
+ maxEntries;
7755
+ entries = new Map;
7756
+ inFlight = new Map;
7757
+ cachedBytes = 0;
7758
+ constructor(maxBytes = DEFAULT_MAX_CACHE_BYTES, maxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
7759
+ this.maxBytes = maxBytes;
7760
+ this.maxEntries = maxEntries;
7761
+ }
7762
+ async getOrTransform(key, operation) {
7763
+ if (key === null)
7764
+ return globalImageTransformSemaphore.run(operation);
7765
+ const cached = this.entries.get(key);
7766
+ if (cached) {
7767
+ this.entries.delete(key);
7768
+ this.entries.set(key, cached);
7769
+ return cached.transform;
7770
+ }
7771
+ const pending = this.inFlight.get(key);
7772
+ if (pending)
7773
+ return pending;
7774
+ const transform = globalImageTransformSemaphore.run(operation);
7775
+ this.inFlight.set(key, transform);
7776
+ try {
7777
+ const transformResult = await transform;
7778
+ if (transformResult.ok)
7779
+ this.store(key, transformResult);
7780
+ return transformResult;
7781
+ } finally {
7782
+ if (this.inFlight.get(key) === transform)
7783
+ this.inFlight.delete(key);
7784
+ }
7785
+ }
7786
+ store(key, transform) {
7787
+ const size = transform.bytes.byteLength;
7788
+ if (size > this.maxBytes || this.maxEntries === 0)
7789
+ return;
7790
+ const previous = this.entries.get(key);
7791
+ if (previous) {
7792
+ this.cachedBytes -= previous.size;
7793
+ this.entries.delete(key);
7794
+ }
7795
+ this.entries.set(key, { transform, size });
7796
+ this.cachedBytes += size;
7797
+ while (this.entries.size > this.maxEntries || this.cachedBytes > this.maxBytes) {
7798
+ const oldestKey = this.entries.keys().next().value;
7799
+ if (oldestKey === undefined)
7800
+ break;
7801
+ const oldest = this.entries.get(oldestKey);
7802
+ this.entries.delete(oldestKey);
7803
+ this.cachedBytes -= oldest.size;
7804
+ }
7805
+ }
7806
+ }
7807
+
6531
7808
  // src/runtime/storage/handler.ts
6532
7809
  var MAX_SIGNED_URL_EXPIRY = 7 * 24 * 60 * 60;
6533
7810
  function clampExpiry(expiresIn) {
@@ -6606,7 +7883,7 @@ var MAX_COMPLETED_TUS_UPLOADS = 64;
6606
7883
  var COMPLETED_TUS_RETENTION_MS = 60 * 60 * 1000;
6607
7884
  var PREFLIGHT_ROLLBACK = Symbol("storage-preflight-rollback");
6608
7885
  var INTERNAL_STORAGE_BUCKET = ".supacloud-lite";
6609
- var OBJECT_VERSION_PREFIX = "v2-";
7886
+ var OBJECT_VERSION_PREFIX2 = "v2-";
6610
7887
 
6611
7888
  class StorageHandler {
6612
7889
  db;
@@ -6614,6 +7891,7 @@ class StorageHandler {
6614
7891
  config;
6615
7892
  tusUploads = new Map;
6616
7893
  mutationTail = Promise.resolve();
7894
+ imageTransforms = new ImageTransformCache;
6617
7895
  constructor(db, driver, config) {
6618
7896
  this.db = db;
6619
7897
  this.driver = driver;
@@ -6656,15 +7934,21 @@ class StorageHandler {
6656
7934
  const bucket2 = parts[3];
6657
7935
  const key2 = parts.slice(4).join("/");
6658
7936
  if (kind === "public" && (method === "GET" || method === "HEAD")) {
6659
- const source = await this.downloadPublic(bucket2, key2, false);
7937
+ const source = await this.loadPublicObject(bucket2, key2);
7938
+ if (source instanceof Response)
7939
+ return source;
6660
7940
  return await this.transformImageResponse(source, url, method === "HEAD");
6661
7941
  }
6662
7942
  if (kind === "authenticated" && (method === "GET" || method === "HEAD")) {
6663
- const source = await this.download(ctx, bucket2, key2, false);
7943
+ const source = await this.loadAuthenticatedObject(ctx, bucket2, key2);
7944
+ if (source instanceof Response)
7945
+ return source;
6664
7946
  return await this.transformImageResponse(source, url, method === "HEAD");
6665
7947
  }
6666
7948
  if (kind === "sign" && method === "GET") {
6667
- const source = await this.redeemSignedUrl(url, bucket2, key2);
7949
+ const source = await this.loadSignedObject(url, bucket2, key2);
7950
+ if (source instanceof Response)
7951
+ return source;
6668
7952
  return await this.transformImageResponse(source, url, false);
6669
7953
  }
6670
7954
  return storageError(404, "not_found", `unknown render endpoint: ${rest}`);
@@ -6877,21 +8161,31 @@ class StorageHandler {
6877
8161
  throw e;
6878
8162
  }
6879
8163
  }
6880
- async transformImageResponse(source, url, head) {
6881
- if (!source.ok)
6882
- return source;
8164
+ async transformImageResponse(row, url, head) {
6883
8165
  const parsed = parseImageTransform(url.searchParams);
6884
8166
  if (!parsed.ok)
6885
8167
  return storageError(parsed.status, parsed.error, parsed.message);
6886
- const result = await transformImage(new Uint8Array(await source.arrayBuffer()), parsed.value);
8168
+ let result;
8169
+ try {
8170
+ result = await this.imageTransforms.getOrTransform(imageTransformCacheKey(row.version, parsed.value), async () => {
8171
+ const source = await this.readObjectSource(row);
8172
+ if (source === null)
8173
+ throw new StorageObjectMissingError;
8174
+ return transformImage(source, parsed.value, objectSize(row));
8175
+ });
8176
+ } catch (error) {
8177
+ if (error instanceof StorageObjectMissingError) {
8178
+ return storageError(404, "not_found", "Object not found");
8179
+ }
8180
+ throw error;
8181
+ }
6887
8182
  if (!result.ok)
6888
8183
  return storageError(result.status, result.error, result.message);
6889
- const headers = new Headers(source.headers);
8184
+ const headers = objectHeaders(row, result.bytes.length);
6890
8185
  headers.delete("content-disposition");
6891
8186
  headers.delete("etag");
6892
8187
  headers.set("content-type", result.contentType);
6893
- headers.set("content-length", String(result.bytes.length));
6894
- return new Response(head ? null : result.bytes, { status: source.status, headers });
8188
+ return new Response(head ? null : result.bytes, { status: 200, headers });
6895
8189
  }
6896
8190
  async persistObject(ctx, bucketId, key, bytes, contentType, cacheControl, upsert) {
6897
8191
  const metadata = objectMetadata(bytes.length, contentType, cacheControl);
@@ -7161,13 +8455,25 @@ class StorageHandler {
7161
8455
  return null;
7162
8456
  }
7163
8457
  async download(ctx, bucketId, key, head) {
8458
+ const row = await this.loadAuthenticatedObject(ctx, bucketId, key);
8459
+ if (row instanceof Response)
8460
+ return row;
8461
+ return this.serveObject(row, head);
8462
+ }
8463
+ async loadAuthenticatedObject(ctx, bucketId, key) {
7164
8464
  const res = await this.db.withContext(ctx, (q) => q(`select * from storage.objects where bucket_id = $1 and name = $2`, [bucketId, key]));
7165
8465
  const row = res.rows[0];
7166
8466
  if (!row)
7167
8467
  return storageError(404, "not_found", "Object not found");
7168
- return this.serveObject(row, head);
8468
+ return row;
7169
8469
  }
7170
8470
  async downloadPublic(bucketId, key, head) {
8471
+ const row = await this.loadPublicObject(bucketId, key);
8472
+ if (row instanceof Response)
8473
+ return row;
8474
+ return this.serveObject(row, head);
8475
+ }
8476
+ async loadPublicObject(bucketId, key) {
7171
8477
  const bucket = await this.loadBucket(bucketId);
7172
8478
  if (!bucket?.public)
7173
8479
  return storageError(400, "not_found", "Bucket is not public");
@@ -7178,24 +8484,16 @@ class StorageHandler {
7178
8484
  const row = res.rows[0];
7179
8485
  if (!row)
7180
8486
  return storageError(404, "not_found", "Object not found");
7181
- return this.serveObject(row, head);
8487
+ return row;
7182
8488
  }
7183
8489
  async serveObject(row, head) {
7184
8490
  const bytes = await this.readObjectBytes(row);
7185
8491
  if (bytes === null)
7186
8492
  return storageError(404, "not_found", "Object not found");
7187
- const meta = row.metadata ?? {};
7188
- const contentType = String(meta.mimetype ?? "application/octet-stream");
7189
- const headers = {
7190
- "content-type": contentType,
7191
- "content-length": String(bytes.length),
7192
- "cache-control": String(meta.cacheControl ?? "no-cache"),
7193
- etag: String(meta.eTag ?? '""'),
7194
- "last-modified": new Date(String(meta.lastModified ?? Date.now())).toUTCString(),
7195
- "x-content-type-options": "nosniff"
7196
- };
8493
+ const contentType = String(row.metadata?.mimetype ?? "application/octet-stream");
8494
+ const headers = objectHeaders(row, bytes.length);
7197
8495
  if (isRenderableActiveType(contentType))
7198
- headers["content-disposition"] = "attachment";
8496
+ headers.set("content-disposition", "attachment");
7199
8497
  return new Response(head ? null : bytes, { status: 200, headers });
7200
8498
  }
7201
8499
  async removeObjects(req, ctx, bucketId) {
@@ -7306,9 +8604,11 @@ class StorageHandler {
7306
8604
  throw error;
7307
8605
  }
7308
8606
  async readObjectBytes(row) {
7309
- if (isVersionedObjectVersion(row.version))
7310
- return this.driver.get(objectVersionKey(row.version));
7311
- return this.driver.get(legacyObjectKey(row));
8607
+ return this.driver.get(storageKey(row));
8608
+ }
8609
+ async readObjectSource(row) {
8610
+ const key = storageKey(row);
8611
+ return this.driver.getBlob ? this.driver.getBlob(key) : this.driver.get(key);
7312
8612
  }
7313
8613
  async cleanupObjectRows(rows) {
7314
8614
  const keys = rows.flatMap((row) => [
@@ -7411,6 +8711,12 @@ class StorageHandler {
7411
8711
  return json3(200, out);
7412
8712
  }
7413
8713
  async redeemSignedUrl(url, bucketId, key) {
8714
+ const row = await this.loadSignedObject(url, bucketId, key);
8715
+ if (row instanceof Response)
8716
+ return row;
8717
+ return this.serveObject(row, false);
8718
+ }
8719
+ async loadSignedObject(url, bucketId, key) {
7414
8720
  const token = url.searchParams.get("token") ?? "";
7415
8721
  const claims = await verifyJwt(token, this.config.jwtSecret);
7416
8722
  if (!claims || claims.url !== `${bucketId}/${key}` || claims.type !== "download") {
@@ -7423,7 +8729,7 @@ class StorageHandler {
7423
8729
  const row = res.rows[0];
7424
8730
  if (!row)
7425
8731
  return storageError(404, "not_found", "Object not found");
7426
- return this.serveObject(row, false);
8732
+ return row;
7427
8733
  }
7428
8734
  async signUploadUrl(ctx, bucketId, key) {
7429
8735
  const keyErr = invalidObjectKey(key);
@@ -7481,6 +8787,9 @@ function objectJson(r) {
7481
8787
 
7482
8788
  class StorageValidationError extends Error {
7483
8789
  }
8790
+
8791
+ class StorageObjectMissingError extends Error {
8792
+ }
7484
8793
  function parseSizeLimit(v) {
7485
8794
  if (v === null || v === undefined || v === "")
7486
8795
  return null;
@@ -7503,14 +8812,32 @@ function objectMetadata(size, contentType, cacheControl) {
7503
8812
  httpStatusCode: 200
7504
8813
  };
7505
8814
  }
8815
+ function objectHeaders(row, contentLength) {
8816
+ const metadata = row.metadata ?? {};
8817
+ return new Headers({
8818
+ "content-type": String(metadata.mimetype ?? "application/octet-stream"),
8819
+ "content-length": String(contentLength),
8820
+ "cache-control": String(metadata.cacheControl ?? "no-cache"),
8821
+ etag: String(metadata.eTag ?? '""'),
8822
+ "last-modified": new Date(String(metadata.lastModified ?? Date.now())).toUTCString(),
8823
+ "x-content-type-options": "nosniff"
8824
+ });
8825
+ }
8826
+ function objectSize(row) {
8827
+ const size = Number(row.metadata?.size);
8828
+ return Number.isFinite(size) && size >= 0 ? size : undefined;
8829
+ }
7506
8830
  function objectVersionKey(version) {
7507
8831
  return `.supacloud-lite/objects/${version}`;
7508
8832
  }
8833
+ function storageKey(row) {
8834
+ return isVersionedObjectVersion(row.version) ? objectVersionKey(row.version) : legacyObjectKey(row);
8835
+ }
7509
8836
  function createObjectVersion() {
7510
- return `${OBJECT_VERSION_PREFIX}${crypto.randomUUID()}`;
8837
+ return `${OBJECT_VERSION_PREFIX2}${crypto.randomUUID()}`;
7511
8838
  }
7512
8839
  function isVersionedObjectVersion(version) {
7513
- return version?.startsWith(OBJECT_VERSION_PREFIX) ?? false;
8840
+ return version?.startsWith(OBJECT_VERSION_PREFIX2) ?? false;
7514
8841
  }
7515
8842
  function isInternalStorageBucket(bucketId) {
7516
8843
  return bucketId === INTERNAL_STORAGE_BUCKET || bucketId.startsWith(`${INTERNAL_STORAGE_BUCKET}/`);
@@ -8859,21 +10186,797 @@ function withCors(res) {
8859
10186
  }
8860
10187
 
8861
10188
  // src/runtime/node/db-diff.ts
8862
- import { mkdir as mkdir2, writeFile } from "fs/promises";
10189
+ import { mkdtempSync as mkdtempSync2 } from "fs";
10190
+ import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
10191
+ import { tmpdir as tmpdir2 } from "os";
10192
+ import { dirname as dirname2, join as join2 } from "path";
10193
+
10194
+ // src/runtime/node/native/engine.ts
10195
+ import { execFileSync, spawn } from "child_process";
10196
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10197
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10198
+ import { writeFile } from "fs/promises";
10199
+ import { homedir, tmpdir } from "os";
8863
10200
  import { join } from "path";
10201
+ import { extract as extractTar } from "tar";
10202
+
10203
+ // src/runtime/node/native/wire.ts
10204
+ import { createConnection } from "net";
10205
+ import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
10206
+
10207
+ class PgWireError extends Error {
10208
+ code;
10209
+ detail;
10210
+ hint;
10211
+ severity;
10212
+ constructor(fields) {
10213
+ super(fields.get("M") ?? "postgres error");
10214
+ this.code = fields.get("C");
10215
+ this.detail = fields.get("D");
10216
+ this.hint = fields.get("H");
10217
+ this.severity = fields.get("S");
10218
+ }
10219
+ }
10220
+
10221
+ class PgWireClient {
10222
+ socket;
10223
+ buffer = Buffer.alloc(0);
10224
+ pending = null;
10225
+ queue = Promise.resolve();
10226
+ closed = false;
10227
+ onNotification = null;
10228
+ static async connect(opts) {
10229
+ const client = new PgWireClient;
10230
+ await client.open(opts);
10231
+ return client;
10232
+ }
10233
+ open(opts) {
10234
+ return new Promise((resolve2, reject) => {
10235
+ this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10236
+ this.socket.on("error", (e) => {
10237
+ if (this.pending)
10238
+ this.pending.reject(e);
10239
+ reject(e);
10240
+ });
10241
+ this.socket.on("close", () => {
10242
+ this.closed = true;
10243
+ this.pending?.reject(new Error("connection closed"));
10244
+ });
10245
+ this.socket.on("connect", () => {
10246
+ const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
10247
+ const body = Buffer.from(params, "utf8");
10248
+ const msg = Buffer.alloc(8 + body.length);
10249
+ msg.writeInt32BE(8 + body.length, 0);
10250
+ msg.writeInt32BE(196608, 4);
10251
+ body.copy(msg, 8);
10252
+ this.socket.write(msg);
10253
+ });
10254
+ let clientNonce = "";
10255
+ let clientFirstBare = "";
10256
+ let serverSignature = "";
10257
+ const needPassword = () => {
10258
+ if (opts.password == null) {
10259
+ reject(new Error("the server requested a password but none was provided"));
10260
+ return false;
10261
+ }
10262
+ return true;
10263
+ };
10264
+ const startupHandler = (chunk) => {
10265
+ this.buffer = Buffer.concat([this.buffer, chunk]);
10266
+ let msg;
10267
+ while ((msg = this.nextMessage()) !== null) {
10268
+ const [type, payload] = msg;
10269
+ if (type === 82) {
10270
+ const code = payload.readInt32BE(0);
10271
+ if (code === 0) {} else if (code === 3) {
10272
+ if (!needPassword())
10273
+ return;
10274
+ this.socket.write(message(112, cstring(opts.password)));
10275
+ } else if (code === 5) {
10276
+ if (!needPassword())
10277
+ return;
10278
+ const salt = payload.subarray(4, 8);
10279
+ const inner = md5Hex(Buffer.from(opts.password + opts.user, "utf8"));
10280
+ const token = "md5" + md5Hex(Buffer.concat([Buffer.from(inner, "utf8"), salt]));
10281
+ this.socket.write(message(112, cstring(token)));
10282
+ } else if (code === 10) {
10283
+ if (!needPassword())
10284
+ return;
10285
+ const mechs = payload.subarray(4).toString("utf8").split("\x00").filter(Boolean);
10286
+ if (!mechs.includes("SCRAM-SHA-256")) {
10287
+ reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(", ")})`));
10288
+ return;
10289
+ }
10290
+ clientNonce = randomBytes(18).toString("base64");
10291
+ clientFirstBare = `n=,r=${clientNonce}`;
10292
+ const initial = Buffer.from(`n,,${clientFirstBare}`, "utf8");
10293
+ this.socket.write(message(112, Buffer.concat([cstring("SCRAM-SHA-256"), int32(initial.length), initial])));
10294
+ } else if (code === 11) {
10295
+ const serverFirst = payload.subarray(4).toString("utf8");
10296
+ const attrs = scramAttrs(serverFirst);
10297
+ if (!attrs.r?.startsWith(clientNonce)) {
10298
+ reject(new Error("SCRAM: server nonce does not extend client nonce"));
10299
+ return;
10300
+ }
10301
+ const salt = Buffer.from(attrs.s, "base64");
10302
+ const iterations = parseInt(attrs.i, 10);
10303
+ const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, "sha256");
10304
+ const clientKey = hmac(saltedPassword, "Client Key");
10305
+ const storedKey = sha256(clientKey);
10306
+ const finalNoProof = `c=biws,r=${attrs.r}`;
10307
+ const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
10308
+ const clientSignature = hmac(storedKey, authMessage);
10309
+ const proof = xorBuffers(clientKey, clientSignature);
10310
+ serverSignature = hmac(hmac(saltedPassword, "Server Key"), authMessage).toString("base64");
10311
+ const clientFinal = `${finalNoProof},p=${proof.toString("base64")}`;
10312
+ this.socket.write(message(112, Buffer.from(clientFinal, "utf8")));
10313
+ } else if (code === 12) {
10314
+ const v = scramAttrs(payload.subarray(4).toString("utf8")).v;
10315
+ if (v && serverSignature && v !== serverSignature) {
10316
+ reject(new Error("SCRAM: server signature verification failed"));
10317
+ return;
10318
+ }
10319
+ } else {
10320
+ reject(new Error(`unsupported auth method ${code}`));
10321
+ return;
10322
+ }
10323
+ } else if (type === 69) {
10324
+ reject(new PgWireError(parseErrorFields(payload)));
10325
+ return;
10326
+ } else if (type === 90) {
10327
+ this.socket.off("data", startupHandler);
10328
+ this.socket.on("data", (c) => {
10329
+ this.buffer = Buffer.concat([this.buffer, c]);
10330
+ this.processMessages();
10331
+ });
10332
+ resolve2();
10333
+ return;
10334
+ }
10335
+ }
10336
+ };
10337
+ this.socket.on("data", startupHandler);
10338
+ });
10339
+ }
10340
+ run(send) {
10341
+ const op = this.queue.then(() => new Promise((resolve2, reject) => {
10342
+ if (this.closed)
10343
+ return reject(new Error("connection closed"));
10344
+ this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
10345
+ send();
10346
+ }));
10347
+ this.queue = op.catch(() => {});
10348
+ return op;
10349
+ }
10350
+ async exec(sql) {
10351
+ return this.run(() => this.socket.write(message(81, cstring(sql))));
10352
+ }
10353
+ async query(sql, params = []) {
10354
+ const results = await this.run(() => {
10355
+ const parse = message(80, Buffer.concat([cstring(""), cstring(sql), int16(0)]));
10356
+ const paramBufs = [int16(0), int16(params.length)];
10357
+ for (const p of params) {
10358
+ if (p === null || p === undefined) {
10359
+ paramBufs.push(int32(-1));
10360
+ } else {
10361
+ const b = Buffer.from(String(p), "utf8");
10362
+ paramBufs.push(int32(b.length), b);
10363
+ }
10364
+ }
10365
+ paramBufs.push(int16(0));
10366
+ const bind = message(66, Buffer.concat([cstring(""), cstring(""), ...paramBufs]));
10367
+ const describe = message(68, Buffer.concat([Buffer.from("P"), cstring("")]));
10368
+ const execute = message(69, Buffer.concat([cstring(""), int32(0)]));
10369
+ const sync = message(83, Buffer.alloc(0));
10370
+ this.socket.write(Buffer.concat([parse, bind, describe, execute, sync]));
10371
+ });
10372
+ return results[0] ?? { rows: [] };
10373
+ }
10374
+ close() {
10375
+ return new Promise((resolve2) => {
10376
+ if (this.closed)
10377
+ return resolve2();
10378
+ this.socket.write(message(88, Buffer.alloc(0)));
10379
+ this.socket.end(() => resolve2());
10380
+ });
10381
+ }
10382
+ nextMessage() {
10383
+ if (this.buffer.length < 5)
10384
+ return null;
10385
+ const type = this.buffer[0];
10386
+ const length = this.buffer.readInt32BE(1);
10387
+ if (this.buffer.length < 1 + length)
10388
+ return null;
10389
+ const payload = this.buffer.subarray(5, 1 + length);
10390
+ this.buffer = this.buffer.subarray(1 + length);
10391
+ return [type, Buffer.from(payload)];
10392
+ }
10393
+ processMessages() {
10394
+ let msg;
10395
+ while ((msg = this.nextMessage()) !== null) {
10396
+ const [type, payload] = msg;
10397
+ const p = this.pending;
10398
+ switch (type) {
10399
+ case 84: {
10400
+ if (!p)
10401
+ break;
10402
+ const count = payload.readInt16BE(0);
10403
+ let off = 2;
10404
+ const columns = [];
10405
+ for (let i = 0;i < count; i++) {
10406
+ const end = payload.indexOf(0, off);
10407
+ const name = payload.toString("utf8", off, end);
10408
+ off = end + 1;
10409
+ const typeOid = payload.readInt32BE(off + 6);
10410
+ off += 18;
10411
+ columns.push({ name, typeOid });
10412
+ }
10413
+ p.columns = columns;
10414
+ break;
10415
+ }
10416
+ case 68: {
10417
+ if (!p)
10418
+ break;
10419
+ const count = payload.readInt16BE(0);
10420
+ let off = 2;
10421
+ const row = {};
10422
+ for (let i = 0;i < count; i++) {
10423
+ const len = payload.readInt32BE(off);
10424
+ off += 4;
10425
+ let value = null;
10426
+ if (len >= 0) {
10427
+ value = decodeValue(payload.toString("utf8", off, off + len), p.columns[i]?.typeOid ?? 25);
10428
+ off += len;
10429
+ }
10430
+ row[p.columns[i]?.name ?? `col${i}`] = value;
10431
+ }
10432
+ if (p.results.length === 0)
10433
+ p.results.push({ rows: [] });
10434
+ p.results[p.results.length - 1].rows.push(row);
10435
+ break;
10436
+ }
10437
+ case 67: {
10438
+ if (!p)
10439
+ break;
10440
+ const tag = payload.toString("utf8", 0, payload.length - 1);
10441
+ const parts = tag.split(" ");
10442
+ const affected = parseInt(parts[parts.length - 1], 10);
10443
+ if (p.results.length === 0)
10444
+ p.results.push({ rows: [] });
10445
+ const current = p.results[p.results.length - 1];
10446
+ if (!Number.isNaN(affected))
10447
+ current.affectedRows = affected;
10448
+ p.results.push({ rows: [] });
10449
+ p.columns = [];
10450
+ break;
10451
+ }
10452
+ case 69: {
10453
+ if (p)
10454
+ p.error = new PgWireError(parseErrorFields(payload));
10455
+ break;
10456
+ }
10457
+ case 65: {
10458
+ payload.readInt32BE(0);
10459
+ const channelEnd = payload.indexOf(0, 4);
10460
+ const channel = payload.toString("utf8", 4, channelEnd);
10461
+ const payloadEnd = payload.indexOf(0, channelEnd + 1);
10462
+ const body = payload.toString("utf8", channelEnd + 1, payloadEnd);
10463
+ this.onNotification?.(channel, body);
10464
+ break;
10465
+ }
10466
+ case 90: {
10467
+ if (!p)
10468
+ break;
10469
+ this.pending = null;
10470
+ if (p.error)
10471
+ p.reject(p.error);
10472
+ else {
10473
+ const results = p.results.filter((r, i) => i < p.results.length - 1 || r.rows.length > 0 || r.affectedRows !== undefined);
10474
+ p.resolve(results.length > 0 ? results : [{ rows: [] }]);
10475
+ }
10476
+ break;
10477
+ }
10478
+ }
10479
+ }
10480
+ }
10481
+ }
10482
+ var hmac = (key, data) => createHmac("sha256", key).update(data, "utf8").digest();
10483
+ var sha256 = (b) => createHash("sha256").update(b).digest();
10484
+ var md5Hex = (b) => createHash("md5").update(b).digest("hex");
10485
+ function xorBuffers(a, b) {
10486
+ const out = Buffer.alloc(a.length);
10487
+ for (let i = 0;i < a.length; i++)
10488
+ out[i] = a[i] ^ b[i];
10489
+ return out;
10490
+ }
10491
+ function scramAttrs(s) {
10492
+ const out = {};
10493
+ for (const part of s.split(",")) {
10494
+ const eq = part.indexOf("=");
10495
+ if (eq > 0)
10496
+ out[part.slice(0, eq)] = part.slice(eq + 1);
10497
+ }
10498
+ return out;
10499
+ }
10500
+ function message(type, body) {
10501
+ const out = Buffer.alloc(5 + body.length);
10502
+ out[0] = type;
10503
+ out.writeInt32BE(4 + body.length, 1);
10504
+ body.copy(out, 5);
10505
+ return out;
10506
+ }
10507
+ var cstring = (s) => Buffer.from(s + "\x00", "utf8");
10508
+ var int16 = (n) => {
10509
+ const b = Buffer.alloc(2);
10510
+ b.writeInt16BE(n);
10511
+ return b;
10512
+ };
10513
+ var int32 = (n) => {
10514
+ const b = Buffer.alloc(4);
10515
+ b.writeInt32BE(n);
10516
+ return b;
10517
+ };
10518
+ function parseErrorFields(payload) {
10519
+ const fields = new Map;
10520
+ let off = 0;
10521
+ while (off < payload.length && payload[off] !== 0) {
10522
+ const key = String.fromCharCode(payload[off]);
10523
+ const end = payload.indexOf(0, off + 1);
10524
+ fields.set(key, payload.toString("utf8", off + 1, end));
10525
+ off = end + 1;
10526
+ }
10527
+ return fields;
10528
+ }
10529
+ function decodeValue(text, oid) {
10530
+ switch (oid) {
10531
+ case 16:
10532
+ return text === "t";
10533
+ case 20: {
10534
+ const n = Number(text);
10535
+ return Number.isSafeInteger(n) ? n : text;
10536
+ }
10537
+ case 21:
10538
+ case 23:
10539
+ case 26:
10540
+ return Number(text);
10541
+ case 700:
10542
+ case 701:
10543
+ return Number(text);
10544
+ case 114:
10545
+ case 3802:
10546
+ return JSON.parse(text);
10547
+ case 1114:
10548
+ return new Date(text.replace(" ", "T") + "Z");
10549
+ case 1184: {
10550
+ let iso3 = text.replace(" ", "T");
10551
+ if (/[+-]\d\d$/.test(iso3))
10552
+ iso3 += ":00";
10553
+ return new Date(iso3);
10554
+ }
10555
+ case 1000:
10556
+ return parsePgArray(text).map((v) => v === "t");
10557
+ case 1007:
10558
+ return parsePgArray(text).map((v) => v === null ? null : Number(v));
10559
+ case 1016:
10560
+ return parsePgArray(text).map((v) => {
10561
+ if (v === null)
10562
+ return null;
10563
+ const n = Number(v);
10564
+ return Number.isSafeInteger(n) ? n : v;
10565
+ });
10566
+ case 1003:
10567
+ case 1009:
10568
+ case 1015:
10569
+ return parsePgArray(text);
10570
+ default:
10571
+ return text;
10572
+ }
10573
+ }
10574
+ function parsePgArray(text) {
10575
+ const out = [];
10576
+ if (text.length < 2)
10577
+ return out;
10578
+ let i = 1;
10579
+ while (i < text.length - 1) {
10580
+ if (text[i] === ",") {
10581
+ i++;
10582
+ continue;
10583
+ }
10584
+ if (text[i] === '"') {
10585
+ let value = "";
10586
+ i++;
10587
+ while (text[i] !== '"') {
10588
+ if (text[i] === "\\")
10589
+ i++;
10590
+ value += text[i++];
10591
+ }
10592
+ i++;
10593
+ out.push(value);
10594
+ } else {
10595
+ let value = "";
10596
+ while (i < text.length - 1 && text[i] !== ",")
10597
+ value += text[i++];
10598
+ out.push(value === "NULL" ? null : value);
10599
+ }
10600
+ }
10601
+ return out;
10602
+ }
10603
+
10604
+ // src/runtime/db/engine.ts
10605
+ class Mutex {
10606
+ tail = Promise.resolve();
10607
+ async lock() {
10608
+ let release;
10609
+ const next = new Promise((r) => release = r);
10610
+ const prev = this.tail;
10611
+ this.tail = this.tail.then(() => next);
10612
+ await prev;
10613
+ return release;
10614
+ }
10615
+ async run(fn) {
10616
+ const release = await this.lock();
10617
+ try {
10618
+ return await fn();
10619
+ } finally {
10620
+ release();
10621
+ }
10622
+ }
10623
+ }
10624
+
10625
+ // src/runtime/node/native/wire-engine.ts
10626
+ async function buildWireEngine(options) {
10627
+ const queryClient = await options.connect();
10628
+ let listenerClient;
10629
+ try {
10630
+ listenerClient = await options.connect();
10631
+ } catch (error) {
10632
+ await queryClient.close().catch(() => {});
10633
+ throw error;
10634
+ }
10635
+ const queryMutex = new Mutex;
10636
+ const listenerMutex = new Mutex;
10637
+ const listeners = new Map;
10638
+ listenerClient.onNotification = (channel, payload) => {
10639
+ for (const listener of listeners.get(channel) ?? [])
10640
+ listener(payload);
10641
+ };
10642
+ const transactionClient = {
10643
+ async query(sql, params) {
10644
+ const queryResult = await queryClient.query(sql, normalizeParams(params));
10645
+ return { rows: queryResult.rows, affectedRows: queryResult.affectedRows };
10646
+ },
10647
+ async exec(sql) {
10648
+ await queryClient.exec(sql);
10649
+ }
10650
+ };
10651
+ let closePromise = null;
10652
+ return {
10653
+ query(sql, params) {
10654
+ return queryMutex.run(() => transactionClient.query(sql, params));
10655
+ },
10656
+ exec(sql) {
10657
+ return queryMutex.run(() => transactionClient.exec(sql));
10658
+ },
10659
+ transaction(callback) {
10660
+ return queryMutex.run(async () => {
10661
+ await queryClient.exec("begin");
10662
+ try {
10663
+ const response = await callback(transactionClient);
10664
+ await queryClient.exec("commit");
10665
+ return response;
10666
+ } catch (error) {
10667
+ await queryClient.exec("rollback").catch(() => {});
10668
+ throw error;
10669
+ }
10670
+ });
10671
+ },
10672
+ async listen(channel, listener) {
10673
+ return listenerMutex.run(async () => {
10674
+ let channelListeners = listeners.get(channel);
10675
+ if (!channelListeners) {
10676
+ channelListeners = new Set;
10677
+ await listenerClient.exec(`listen "${channel.replaceAll('"', '""')}"`);
10678
+ listeners.set(channel, channelListeners);
10679
+ }
10680
+ channelListeners.add(listener);
10681
+ return () => {
10682
+ channelListeners.delete(listener);
10683
+ };
10684
+ });
10685
+ },
10686
+ close() {
10687
+ closePromise ??= closeWireEngine(queryClient, listenerClient, options.onClose);
10688
+ return closePromise;
10689
+ }
10690
+ };
10691
+ }
10692
+ async function closeWireEngine(queryClient, listenerClient, onClose) {
10693
+ const closeResults = await Promise.allSettled([queryClient.close(), listenerClient.close()]);
10694
+ let engineCleanupError;
10695
+ try {
10696
+ await onClose?.();
10697
+ } catch (error) {
10698
+ engineCleanupError = error;
10699
+ }
10700
+ const connectionErrors = closeResults.flatMap((closeResult) => closeResult.status === "rejected" ? [closeResult.reason] : []);
10701
+ if (engineCleanupError !== undefined)
10702
+ connectionErrors.push(engineCleanupError);
10703
+ if (connectionErrors.length > 0)
10704
+ throw new AggregateError(connectionErrors, "native database cleanup failed");
10705
+ }
10706
+ function normalizeParams(params) {
10707
+ return params?.map((parameter) => {
10708
+ if (parameter === null || parameter === undefined)
10709
+ return null;
10710
+ if (Array.isArray(parameter))
10711
+ return toPgArrayLiteral(parameter);
10712
+ if (parameter instanceof Date)
10713
+ return parameter.toISOString();
10714
+ if (typeof parameter === "object")
10715
+ return JSON.stringify(parameter);
10716
+ return parameter;
10717
+ });
10718
+ }
10719
+ function toPgArrayLiteral(array) {
10720
+ const encoded = array.map((element) => {
10721
+ if (element === null || element === undefined)
10722
+ return "NULL";
10723
+ if (Array.isArray(element))
10724
+ return toPgArrayLiteral(element);
10725
+ if (typeof element === "number" || typeof element === "boolean")
10726
+ return String(element);
10727
+ const text = typeof element === "object" ? JSON.stringify(element) : String(element);
10728
+ return `"${text.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
10729
+ });
10730
+ return `{${encoded.join(",")}}`;
10731
+ }
10732
+
10733
+ // src/runtime/node/native/engine.ts
10734
+ var DEFAULT_PG_VERSION = "17.7.0";
10735
+ var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
10736
+ function isNativeEngineSupported() {
10737
+ return (process.platform === "darwin" || process.platform === "linux") && (process.arch === "arm64" || process.arch === "x64") && (process.platform !== "linux" || isGlibcLinux());
10738
+ }
10739
+ function isGlibcLinux() {
10740
+ try {
10741
+ const version = execFileSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
10742
+ return /glibc|gnu libc/i.test(version);
10743
+ } catch {
10744
+ return false;
10745
+ }
10746
+ }
10747
+ function target() {
10748
+ const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
10749
+ if (!arch)
10750
+ throw new Error(`unsupported architecture for native engine: ${process.arch}`);
10751
+ if (process.platform === "darwin")
10752
+ return `${arch}-apple-darwin`;
10753
+ if (process.platform === "linux")
10754
+ return `${arch}-unknown-linux-gnu`;
10755
+ throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10756
+ }
10757
+ function isCompleteInstall(dir) {
10758
+ return existsSync(join(dir, "bin", "postgres")) && existsSync(join(dir, "share", "postgres.bki"));
10759
+ }
10760
+ var PINNED_SHA256 = {
10761
+ "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
10762
+ "postgresql-17.7.0-aarch64-unknown-linux-gnu": "89cc2f089880cc8e5e6b7a29387829ec4e4779427855bc0b9fa187c8fce33c8b",
10763
+ "postgresql-17.7.0-x86_64-apple-darwin": "0dd8c25173524bad4ae8ef6b970da1ac40f4c1f231150c416ccb8cd06feff8f2",
10764
+ "postgresql-17.7.0-aarch64-apple-darwin": "727ac08d20a704014a0d51eb3300aa0c8e292c1cf0a1c99d4f4b1002e1420220"
10765
+ };
10766
+ async function verifyTarball(tarball, key, url) {
10767
+ const actual = createHash2("sha256").update(readFileSync(tarball)).digest("hex");
10768
+ const pinned = PINNED_SHA256[key];
10769
+ if (pinned) {
10770
+ if (actual !== pinned) {
10771
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${pinned}, got ${actual}`);
10772
+ }
10773
+ return;
10774
+ }
10775
+ const res = await fetchRelease(`${url}.sha256`);
10776
+ if (!res.ok)
10777
+ throw new Error(`could not fetch checksum for ${key}: HTTP ${res.status}`);
10778
+ const expected = (await res.text()).trim().split(/\s+/)[0].toLowerCase();
10779
+ if (!/^[0-9a-f]{64}$/.test(expected))
10780
+ throw new Error(`malformed published checksum for ${key}`);
10781
+ if (actual !== expected) {
10782
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${expected}, got ${actual}`);
10783
+ }
10784
+ }
10785
+ async function ensurePostgres(version = DEFAULT_PG_VERSION, cacheDir, log) {
10786
+ const t = target();
10787
+ const root = cacheDir ?? join(homedir(), ".cache", "supacloud-lite");
10788
+ const dir = join(root, `postgresql-${version}-${t}`);
10789
+ if (isCompleteInstall(dir))
10790
+ return dir;
10791
+ const url = `https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/postgresql-${version}-${t}.tar.gz`;
10792
+ mkdirSync(root, { recursive: true });
10793
+ const uniq = `${process.pid}-${randomBytes2(6).toString("hex")}`;
10794
+ const tarball = join(root, `pg-${version}-${uniq}.tar.gz`);
10795
+ const tmpDir = join(root, `.tmp-${version}-${t}-${uniq}`);
10796
+ try {
10797
+ if (isCompleteInstall(dir))
10798
+ return dir;
10799
+ log?.(`downloading postgres ${version} (${t})\u2026`);
10800
+ const res = await fetchRelease(url);
10801
+ if (!res.ok)
10802
+ throw new Error(`failed to download ${url}: HTTP ${res.status}`);
10803
+ await writeFile(tarball, Buffer.from(await res.arrayBuffer()));
10804
+ await verifyTarball(tarball, `postgresql-${version}-${t}`, url);
10805
+ mkdirSync(tmpDir, { recursive: true });
10806
+ await extractTar({ cwd: tmpDir, file: tarball, gzip: true, preserveOwner: false, strict: true, strip: 1 });
10807
+ if (!isCompleteInstall(tmpDir))
10808
+ throw new Error("postgres archive extracted incompletely");
10809
+ try {
10810
+ renameSync(tmpDir, dir);
10811
+ } catch {
10812
+ if (!isCompleteInstall(dir)) {
10813
+ rmSync(dir, { recursive: true, force: true });
10814
+ renameSync(tmpDir, dir);
10815
+ }
10816
+ }
10817
+ log?.(`postgres installed to ${dir}`);
10818
+ return dir;
10819
+ } finally {
10820
+ rmSync(tarball, { force: true });
10821
+ rmSync(tmpDir, { recursive: true, force: true });
10822
+ }
10823
+ }
10824
+ async function fetchRelease(url) {
10825
+ let lastError;
10826
+ for (let attempt = 1;attempt <= 3; attempt++) {
10827
+ try {
10828
+ const response = await fetch(url);
10829
+ if (response.ok || response.status < 500)
10830
+ return response;
10831
+ lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
10832
+ } catch (error) {
10833
+ lastError = error;
10834
+ }
10835
+ if (attempt < 3)
10836
+ await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
10837
+ }
10838
+ throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10839
+ }
10840
+ var TUNED_CONF = `
10841
+ # supacloud-lite: memory-lean settings for an embedded, single-app Postgres
10842
+ listen_addresses = ''
10843
+ shared_buffers = 16MB
10844
+ dynamic_shared_memory_type = posix
10845
+ max_connections = 10
10846
+ wal_level = minimal
10847
+ max_wal_senders = 0
10848
+ logging_collector = off
10849
+ `;
10850
+ async function createNativeEngine(opts) {
10851
+ const releaseLock = await acquireDataDirLock(opts.dataDir, "native PostgreSQL");
10852
+ let socketDirectory;
10853
+ let postgres;
10854
+ let removeExitHandler;
10855
+ try {
10856
+ const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log);
10857
+ const bin = (name) => join(installDir, "bin", name);
10858
+ if (!existsSync(join(opts.dataDir, "PG_VERSION"))) {
10859
+ mkdirSync(opts.dataDir, { recursive: true });
10860
+ try {
10861
+ execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
10862
+ stdio: "pipe"
10863
+ });
10864
+ } catch (error) {
10865
+ const stderr = error.stderr?.toString() ?? "";
10866
+ throw new Error(`initdb failed:
10867
+ ${stderr || error.message}`);
10868
+ }
10869
+ appendFileSync(join(opts.dataDir, "postgresql.conf"), TUNED_CONF);
10870
+ }
10871
+ removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
10872
+ socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
10873
+ chmodSync(socketDirectory, 448);
10874
+ postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
10875
+ stdio: ["ignore", "ignore", "pipe"],
10876
+ detached: false
10877
+ });
10878
+ let postgresExited = false;
10879
+ let postgresStderr = "";
10880
+ postgres.stderr?.on("data", (chunk) => {
10881
+ postgresStderr = (postgresStderr + chunk.toString()).slice(-4000);
10882
+ });
10883
+ postgres.on("exit", () => postgresExited = true);
10884
+ const killPostgres = () => {
10885
+ if (!postgresExited)
10886
+ postgres?.kill("SIGTERM");
10887
+ };
10888
+ process.once("exit", killPostgres);
10889
+ removeExitHandler = () => process.off("exit", killPostgres);
10890
+ const socketPath = join(socketDirectory, ".s.PGSQL.5432");
10891
+ const connect = async () => {
10892
+ const deadline = Date.now() + 20000;
10893
+ while (Date.now() <= deadline) {
10894
+ try {
10895
+ return await PgWireClient.connect({ socketPath, user: "postgres", database: "postgres" });
10896
+ } catch (error) {
10897
+ if (postgresExited) {
10898
+ const detail = postgresStderr.trim();
10899
+ throw new Error(`embedded postgres failed to start${detail ? `:
10900
+ ${detail}` : " (no output)"}
10901
+
10902
+ ` + `data dir: ${opts.dataDir}
10903
+ ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
10904
+ }
10905
+ await new Promise((resolve2) => setTimeout(resolve2, 150));
10906
+ }
10907
+ }
10908
+ throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
10909
+ };
10910
+ return await buildWireEngine({
10911
+ connect,
10912
+ onClose: async () => {
10913
+ removeExitHandler?.();
10914
+ await stopPostgres(postgres, () => postgresExited);
10915
+ rmSync(socketDirectory, { recursive: true, force: true });
10916
+ await releaseLock();
10917
+ }
10918
+ });
10919
+ } catch (error) {
10920
+ removeExitHandler?.();
10921
+ if (postgres)
10922
+ await stopPostgres(postgres, () => postgres.exitCode !== null);
10923
+ if (socketDirectory)
10924
+ rmSync(socketDirectory, { recursive: true, force: true });
10925
+ await releaseLock();
10926
+ throw error;
10927
+ }
10928
+ }
10929
+ async function stopPostgres(postgres, hasExited) {
10930
+ if (hasExited())
10931
+ return;
10932
+ postgres.kill("SIGINT");
10933
+ await new Promise((resolve2) => {
10934
+ const killTimeout = setTimeout(() => {
10935
+ postgres.kill("SIGKILL");
10936
+ resolve2();
10937
+ }, 5000);
10938
+ postgres.once("exit", () => {
10939
+ clearTimeout(killTimeout);
10940
+ resolve2();
10941
+ });
10942
+ });
10943
+ }
10944
+ function removeStalePidFile(pidPath) {
10945
+ if (!existsSync(pidPath))
10946
+ return;
10947
+ try {
10948
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
10949
+ `)[0]?.trim() ?? "", 10);
10950
+ if (!pid) {
10951
+ rmSync(pidPath, { force: true });
10952
+ return;
10953
+ }
10954
+ try {
10955
+ process.kill(pid, 0);
10956
+ } catch {
10957
+ rmSync(pidPath, { force: true });
10958
+ }
10959
+ } catch {}
10960
+ }
10961
+
10962
+ // src/runtime/node/db-diff.ts
8864
10963
  async function computeDbDiff(opts) {
8865
10964
  const schema = opts.schema ?? "public";
8866
- const shadow = await createBackend({
8867
- engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
8868
- migrations: opts.migrations,
8869
- startRuntimeServices: false
8870
- });
10965
+ let shadow;
8871
10966
  let live;
10967
+ let unclaimedLiveEngine = opts.liveEngine;
8872
10968
  let operationFailed = false;
8873
10969
  try {
10970
+ shadow = await createBackend({
10971
+ engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
10972
+ migrations: opts.migrations,
10973
+ startRuntimeServices: false
10974
+ });
10975
+ const liveEngine = unclaimedLiveEngine;
10976
+ unclaimedLiveEngine = undefined;
8874
10977
  live = await createBackend({
8875
- engine: opts.liveEngine,
8876
- dataDir: opts.liveEngine ? undefined : opts.liveDataDir,
10978
+ engine: liveEngine,
10979
+ dataDir: liveEngine ? undefined : opts.liveDataDir,
8877
10980
  migrations: opts.migrations,
8878
10981
  startRuntimeServices: false
8879
10982
  });
@@ -8885,26 +10988,57 @@ async function computeDbDiff(opts) {
8885
10988
  throw error;
8886
10989
  } finally {
8887
10990
  try {
8888
- await closeBackends(live, shadow);
10991
+ await closeResources(unclaimedLiveEngine, live, shadow);
8889
10992
  } catch (error) {
8890
10993
  if (!operationFailed)
8891
10994
  throw error;
8892
10995
  }
8893
10996
  }
8894
10997
  }
10998
+ function shadowNativeDataDir() {
10999
+ return join2(mkdtempSync2(join2(tmpdir2(), "supacloud-lite-shadow-")), "pg");
11000
+ }
11001
+ async function createTemporaryNativeEngine() {
11002
+ const dataDir = shadowNativeDataDir();
11003
+ let engine;
11004
+ try {
11005
+ engine = await createNativeEngine({ dataDir });
11006
+ } catch (error) {
11007
+ try {
11008
+ await rm(dirname2(dataDir), { recursive: true, force: true });
11009
+ } catch (cleanupError) {
11010
+ throw new AggregateError([error, cleanupError], "temporary native database initialization cleanup failed");
11011
+ }
11012
+ throw error;
11013
+ }
11014
+ return {
11015
+ ...engine,
11016
+ async close() {
11017
+ try {
11018
+ await engine.close();
11019
+ } finally {
11020
+ await rm(dirname2(dataDir), { recursive: true, force: true });
11021
+ }
11022
+ }
11023
+ };
11024
+ }
8895
11025
  async function pullSchema(opts) {
8896
11026
  const schema = opts.schema ?? "public";
8897
- const shadow = await createBackend({
8898
- engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
8899
- migrations: opts.migrations,
8900
- startRuntimeServices: false
8901
- });
11027
+ let shadow;
8902
11028
  let live;
11029
+ let unclaimedLiveEngine = opts.liveEngine;
8903
11030
  let operationFailed = false;
8904
11031
  try {
11032
+ shadow = await createBackend({
11033
+ engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
11034
+ migrations: opts.migrations,
11035
+ startRuntimeServices: false
11036
+ });
11037
+ const liveEngine = unclaimedLiveEngine;
11038
+ unclaimedLiveEngine = undefined;
8905
11039
  live = await createBackend({
8906
- engine: opts.liveEngine,
8907
- dataDir: opts.liveEngine ? undefined : opts.liveDataDir,
11040
+ engine: liveEngine,
11041
+ dataDir: liveEngine ? undefined : opts.liveDataDir,
8908
11042
  migrations: opts.migrations,
8909
11043
  startRuntimeServices: false
8910
11044
  });
@@ -8920,8 +11054,8 @@ async function pullSchema(opts) {
8920
11054
  let path = null;
8921
11055
  if (opts.migrationsDir) {
8922
11056
  await mkdir2(opts.migrationsDir, { recursive: true });
8923
- path = join(opts.migrationsDir, `${stamp}_${name}.sql`);
8924
- await writeFile(path, body);
11057
+ path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
11058
+ await writeFile2(path, body);
8925
11059
  }
8926
11060
  await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
8927
11061
  values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
@@ -8931,15 +11065,15 @@ async function pullSchema(opts) {
8931
11065
  throw error;
8932
11066
  } finally {
8933
11067
  try {
8934
- await closeBackends(live, shadow);
11068
+ await closeResources(unclaimedLiveEngine, live, shadow);
8935
11069
  } catch (error) {
8936
11070
  if (!operationFailed)
8937
11071
  throw error;
8938
11072
  }
8939
11073
  }
8940
11074
  }
8941
- async function closeBackends(...backends) {
8942
- const results = await Promise.allSettled(backends.filter((backend) => backend !== undefined).map(async (backend) => await backend.close()));
11075
+ async function closeResources(...resources) {
11076
+ const results = await Promise.allSettled(resources.filter((resource) => resource !== undefined).map(async (resource) => await resource.close()));
8943
11077
  const failed = results.find((result) => result.status === "rejected");
8944
11078
  if (failed?.status === "rejected")
8945
11079
  throw failed.reason;
@@ -8947,9 +11081,9 @@ async function closeBackends(...backends) {
8947
11081
 
8948
11082
  // src/runtime/node/project.ts
8949
11083
  import { readdir, readFile as readFile2 } from "fs/promises";
8950
- import { join as join2 } from "path";
11084
+ import { join as join3 } from "path";
8951
11085
  async function loadSupabaseProject(projectDir, seed = {}) {
8952
- const migrationsDir = join2(projectDir, "supabase", "migrations");
11086
+ const migrationsDir = join3(projectDir, "supabase", "migrations");
8953
11087
  const migrations = [];
8954
11088
  let entries = [];
8955
11089
  try {
@@ -8961,19 +11095,19 @@ async function loadSupabaseProject(projectDir, seed = {}) {
8961
11095
  for (const entry of entries.sort()) {
8962
11096
  if (!entry.endsWith(".sql"))
8963
11097
  continue;
8964
- const sql = await readFile2(join2(migrationsDir, entry), "utf8");
11098
+ const sql = await readFile2(join3(migrationsDir, entry), "utf8");
8965
11099
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
8966
11100
  }
8967
11101
  let seedSql;
8968
11102
  if (seed.enabled !== false) {
8969
11103
  const parts = [];
8970
- const supabaseDir = join2(projectDir, "supabase");
11104
+ const supabaseDir = join3(projectDir, "supabase");
8971
11105
  for (const configuredPath of seed.paths ?? ["seed.sql"]) {
8972
11106
  const pattern = configuredPath.replace(/^\.\//, "");
8973
11107
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
8974
11108
  for (const relativePath of matches) {
8975
11109
  try {
8976
- parts.push(await readFile2(join2(supabaseDir, relativePath), "utf8"));
11110
+ parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
8977
11111
  } catch (error) {
8978
11112
  if (!isNotFound(error))
8979
11113
  throw error;
@@ -8991,8 +11125,8 @@ function isNotFound(error) {
8991
11125
  }
8992
11126
 
8993
11127
  // src/project-runtime.ts
8994
- import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
8995
- import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve2 } from "path";
11128
+ import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
11129
+ import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve2 } from "path";
8996
11130
 
8997
11131
  // src/runtime/node/bun-server.ts
8998
11132
  async function serveBun(backend, opts = {}) {
@@ -9023,8 +11157,8 @@ async function serveBun(backend, opts = {}) {
9023
11157
  }, { vsn: ws.data.vsn });
9024
11158
  ws.data.session = session;
9025
11159
  },
9026
- message(ws, message) {
9027
- ws.data.session?.onMessage(typeof message === "string" ? message : new Uint8Array(message));
11160
+ message(ws, message2) {
11161
+ ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
9028
11162
  },
9029
11163
  close(ws) {
9030
11164
  ws.data.session?.onClose();
@@ -9042,8 +11176,8 @@ async function serveBun(backend, opts = {}) {
9042
11176
  }
9043
11177
 
9044
11178
  // src/runtime/node/fs-driver.ts
9045
- import { mkdir as mkdir3, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
9046
- import { dirname as dirname2, join as join3, normalize, sep } from "path";
11179
+ import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
11180
+ import { dirname as dirname3, join as join4, normalize, sep } from "path";
9047
11181
 
9048
11182
  class FsStorageDriver {
9049
11183
  root;
@@ -9051,7 +11185,7 @@ class FsStorageDriver {
9051
11185
  this.root = root;
9052
11186
  }
9053
11187
  resolve(key) {
9054
- const path = normalize(join3(this.root, key));
11188
+ const path = normalize(join4(this.root, key));
9055
11189
  if (!path.startsWith(normalize(this.root) + sep)) {
9056
11190
  throw new Error(`invalid storage key: ${key}`);
9057
11191
  }
@@ -9059,13 +11193,13 @@ class FsStorageDriver {
9059
11193
  }
9060
11194
  async put(key, data) {
9061
11195
  const path = this.resolve(key);
9062
- await mkdir3(dirname2(path), { recursive: true });
11196
+ await mkdir3(dirname3(path), { recursive: true });
9063
11197
  const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
9064
11198
  try {
9065
- await writeFile2(temporaryPath, data);
11199
+ await writeFile3(temporaryPath, data);
9066
11200
  await rename(temporaryPath, path);
9067
11201
  } catch (error) {
9068
- await rm(temporaryPath, { force: true }).catch(() => {});
11202
+ await rm2(temporaryPath, { force: true }).catch(() => {});
9069
11203
  throw error;
9070
11204
  }
9071
11205
  }
@@ -9078,8 +11212,12 @@ class FsStorageDriver {
9078
11212
  throw e;
9079
11213
  }
9080
11214
  }
11215
+ async getBlob(key) {
11216
+ const file = Bun.file(this.resolve(key));
11217
+ return await file.exists() ? file : null;
11218
+ }
9081
11219
  async delete(key) {
9082
- await rm(this.resolve(key), { force: true });
11220
+ await rm2(this.resolve(key), { force: true });
9083
11221
  }
9084
11222
  async deleteMany(keys) {
9085
11223
  for (const k of keys)
@@ -9088,15 +11226,15 @@ class FsStorageDriver {
9088
11226
  }
9089
11227
 
9090
11228
  // src/runtime/node/config-toml.ts
9091
- import { readFileSync } from "fs";
9092
- import { join as join4 } from "path";
11229
+ import { readFileSync as readFileSync2 } from "fs";
11230
+ import { join as join5 } from "path";
9093
11231
  function emptyTable() {
9094
11232
  return { values: new Map, children: new Map };
9095
11233
  }
9096
11234
  function loadConfigToml(projectDir, env = process.env) {
9097
11235
  let text;
9098
11236
  try {
9099
- text = readFileSync(join4(projectDir, "supabase", "config.toml"), "utf8");
11237
+ text = readFileSync2(join5(projectDir, "supabase", "config.toml"), "utf8");
9100
11238
  } catch {
9101
11239
  return emptyTable();
9102
11240
  }
@@ -9461,16 +11599,16 @@ function readFunctions(root) {
9461
11599
  }
9462
11600
 
9463
11601
  // src/runtime/node/load-functions.ts
9464
- import { readdir as readdir2, readFile as readFile5, realpath, rm as rm3, stat } from "fs/promises";
9465
- import { dirname as dirname3, join as join6 } from "path";
11602
+ import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
11603
+ import { dirname as dirname4, join as join7 } from "path";
9466
11604
  import { pathToFileURL } from "url";
9467
11605
 
9468
11606
  // src/runtime/node/bundle-function.ts
9469
- import { createHash } from "crypto";
9470
- import { mkdir as mkdir4, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
9471
- import { existsSync } from "fs";
9472
- import { tmpdir } from "os";
9473
- import { join as join5 } from "path";
11607
+ import { createHash as createHash3 } from "crypto";
11608
+ import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
11609
+ import { existsSync as existsSync2 } from "fs";
11610
+ import { tmpdir as tmpdir3 } from "os";
11611
+ import { join as join6 } from "path";
9474
11612
  function rewriteRemoteSpecifier(spec) {
9475
11613
  if (spec.startsWith("npm:"))
9476
11614
  return `https://esm.sh/${spec.slice(4)}`;
@@ -9478,18 +11616,18 @@ function rewriteRemoteSpecifier(spec) {
9478
11616
  return `https://esm.sh/jsr/${spec.slice(4)}`;
9479
11617
  return spec;
9480
11618
  }
9481
- var HTTP_CACHE = join5(tmpdir(), "supacloud-lite-fn-http");
11619
+ var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
9482
11620
  async function fetchModule(url) {
9483
- const key = createHash("sha256").update(url).digest("hex");
9484
- const cached = join5(HTTP_CACHE, key);
9485
- if (existsSync(cached))
11621
+ const key = createHash3("sha256").update(url).digest("hex");
11622
+ const cached = join6(HTTP_CACHE, key);
11623
+ if (existsSync2(cached))
9486
11624
  return readFile4(cached, "utf8");
9487
11625
  const res = await fetch(url, { redirect: "follow" });
9488
11626
  if (!res.ok)
9489
11627
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
9490
11628
  const body = await res.text();
9491
11629
  await mkdir4(HTTP_CACHE, { recursive: true });
9492
- await writeFile3(cached, body);
11630
+ await writeFile4(cached, body);
9493
11631
  return body;
9494
11632
  }
9495
11633
  function remotePlugin() {
@@ -9512,7 +11650,7 @@ function remotePlugin() {
9512
11650
  };
9513
11651
  }
9514
11652
  async function bundleFunction(entryPath, name) {
9515
- const outDir = join5(tmpdir(), "supacloud-lite-fn-bundle", name);
11653
+ const outDir = join6(tmpdir3(), "supacloud-lite-fn-bundle", name);
9516
11654
  await mkdir4(outDir, { recursive: true });
9517
11655
  try {
9518
11656
  const buildOutput = await Bun.build({
@@ -9530,7 +11668,7 @@ async function bundleFunction(entryPath, name) {
9530
11668
  return buildOutput.outputs[0].path;
9531
11669
  } catch (buildError) {
9532
11670
  try {
9533
- await rm2(outDir, { recursive: true, force: true });
11671
+ await rm3(outDir, { recursive: true, force: true });
9534
11672
  } catch (cleanupError) {
9535
11673
  throw new AggregateError([buildError, cleanupError], `failed to clean function bundle directory ${outDir}`);
9536
11674
  }
@@ -9542,7 +11680,7 @@ async function bundleFunction(entryPath, name) {
9542
11680
  async function loadFunctionEnv(projectDir) {
9543
11681
  let text;
9544
11682
  try {
9545
- text = await readFile5(join6(projectDir, "supabase", "functions", ".env"), "utf8");
11683
+ text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
9546
11684
  } catch {
9547
11685
  return {};
9548
11686
  }
@@ -9581,7 +11719,7 @@ async function loadFunctions2(projectDir, options = {}) {
9581
11719
  }
9582
11720
  async function loadFunctionsUnlocked(projectDir, options) {
9583
11721
  const functions = new Map;
9584
- const root = join6(projectDir, "supabase", "functions");
11722
+ const root = join7(projectDir, "supabase", "functions");
9585
11723
  let entries = [];
9586
11724
  try {
9587
11725
  entries = await readdir2(root);
@@ -9594,10 +11732,10 @@ async function loadFunctionsUnlocked(projectDir, options) {
9594
11732
  continue;
9595
11733
  if (options[name]?.enabled === false)
9596
11734
  continue;
9597
- const dir = join6(root, name);
11735
+ const dir = join7(root, name);
9598
11736
  if (!(await stat(dir)).isDirectory())
9599
11737
  continue;
9600
- const candidates = options[name]?.entrypoint ? [join6(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join6(dir, f));
11738
+ const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
9601
11739
  for (const path of candidates) {
9602
11740
  try {
9603
11741
  await stat(path);
@@ -9634,7 +11772,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
9634
11772
  }
9635
11773
  } finally {
9636
11774
  if (bundledPath)
9637
- await rm3(dirname3(bundledPath), { recursive: true, force: true }).catch(() => {});
11775
+ await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
9638
11776
  }
9639
11777
  break;
9640
11778
  }
@@ -9708,6 +11846,21 @@ class S3StorageDriver {
9708
11846
  throw error;
9709
11847
  }
9710
11848
  }
11849
+ async getBlob(key) {
11850
+ const file = this.client.file(this.objectKey(key));
11851
+ if (!await file.exists())
11852
+ return null;
11853
+ if (typeof file.arrayBuffer === "function")
11854
+ return file;
11855
+ try {
11856
+ const bytes = Uint8Array.from(await file.bytes());
11857
+ return new Blob([bytes.buffer]);
11858
+ } catch (error) {
11859
+ if (isNotFoundError(error))
11860
+ return null;
11861
+ throw error;
11862
+ }
11863
+ }
9711
11864
  async delete(key) {
9712
11865
  await this.client.file(this.objectKey(key)).delete();
9713
11866
  }
@@ -9727,14 +11880,16 @@ var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets mar
9727
11880
  function resolveProjectPaths(options = {}) {
9728
11881
  const projectDir = resolve2(options.projectDir ?? process.cwd());
9729
11882
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
9730
- const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, "db"));
9731
- const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join7(stateDir, "storage"));
11883
+ const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
11884
+ const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
11885
+ const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join8(stateDir, "storage"));
9732
11886
  return {
9733
11887
  projectDir,
9734
11888
  stateDir,
9735
11889
  dataDir,
9736
11890
  storageDir,
9737
- secretsFile: join7(stateDir, "secrets.json")
11891
+ secretsFile: join8(stateDir, "secrets.json"),
11892
+ databaseEngine
9738
11893
  };
9739
11894
  }
9740
11895
  async function assertResetPathsSafe(paths) {
@@ -9747,7 +11902,7 @@ async function assertResetPathsSafe(paths) {
9747
11902
  }
9748
11903
  const canonicalStateDir = await realpath2(stateDir);
9749
11904
  const secretsFile = resolve2(paths.secretsFile);
9750
- if (secretsFile !== join7(stateDir, "secrets.json")) {
11905
+ if (secretsFile !== join8(stateDir, "secrets.json")) {
9751
11906
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
9752
11907
  }
9753
11908
  const markerInfo = await requiredResetEntry(secretsFile);
@@ -9760,12 +11915,12 @@ async function assertResetPathsSafe(paths) {
9760
11915
  ["storage", paths.storageDir]
9761
11916
  ];
9762
11917
  for (const [label, targetPath] of targets) {
9763
- const target = resolve2(targetPath);
9764
- const relativePath = relative(stateDir, target);
11918
+ const target2 = resolve2(targetPath);
11919
+ const relativePath = relative(stateDir, target2);
9765
11920
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
9766
- throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
11921
+ throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
9767
11922
  }
9768
- await assertResetTargetCanonical(stateDir, canonicalStateDir, target, label);
11923
+ await assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label);
9769
11924
  }
9770
11925
  }
9771
11926
  async function requiredResetEntry(path) {
@@ -9792,8 +11947,8 @@ async function assertResetSecretsValid(path) {
9792
11947
  throw new Error(RESET_INVALID_SECRETS_ERROR);
9793
11948
  }
9794
11949
  }
9795
- async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, label) {
9796
- let current = target;
11950
+ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label) {
11951
+ let current = target2;
9797
11952
  while (current !== stateDir) {
9798
11953
  try {
9799
11954
  if ((await lstat(current)).isSymbolicLink()) {
@@ -9803,20 +11958,20 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
9803
11958
  if (error.code !== "ENOENT")
9804
11959
  throw error;
9805
11960
  }
9806
- const parent = dirname4(current);
11961
+ const parent = dirname5(current);
9807
11962
  if (parent === current)
9808
- throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
11963
+ throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
9809
11964
  current = parent;
9810
11965
  }
9811
- const existingAncestor = await nearestExistingAncestor(target);
9812
- const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target));
11966
+ const existingAncestor = await nearestExistingAncestor(target2);
11967
+ const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target2));
9813
11968
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
9814
11969
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
9815
- throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target}`);
11970
+ throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
9816
11971
  }
9817
11972
  }
9818
- async function nearestExistingAncestor(target) {
9819
- let current = target;
11973
+ async function nearestExistingAncestor(target2) {
11974
+ let current = target2;
9820
11975
  while (true) {
9821
11976
  try {
9822
11977
  await lstat(current);
@@ -9825,9 +11980,9 @@ async function nearestExistingAncestor(target) {
9825
11980
  if (error.code !== "ENOENT")
9826
11981
  throw error;
9827
11982
  }
9828
- const parent = dirname4(current);
11983
+ const parent = dirname5(current);
9829
11984
  if (parent === current)
9830
- throw new Error(`unable to resolve an existing ancestor for ${target}`);
11985
+ throw new Error(`unable to resolve an existing ancestor for ${target2}`);
9831
11986
  current = parent;
9832
11987
  }
9833
11988
  }
@@ -9851,7 +12006,7 @@ async function ensureProjectSecrets(paths) {
9851
12006
  };
9852
12007
  const temporaryFile = `${paths.secretsFile}.${crypto.randomUUID()}.tmp`;
9853
12008
  try {
9854
- await writeFile4(temporaryFile, `${JSON.stringify(candidate, null, 2)}
12009
+ await writeFile5(temporaryFile, `${JSON.stringify(candidate, null, 2)}
9855
12010
  `, { mode: 384, flag: "wx" });
9856
12011
  await link(temporaryFile, paths.secretsFile);
9857
12012
  stored = candidate;
@@ -9860,7 +12015,7 @@ async function ensureProjectSecrets(paths) {
9860
12015
  throw error2;
9861
12016
  stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
9862
12017
  } finally {
9863
- await unlink3(temporaryFile).catch((error2) => {
12018
+ await unlink2(temporaryFile).catch((error2) => {
9864
12019
  if (error2.code !== "ENOENT")
9865
12020
  throw error2;
9866
12021
  });
@@ -9892,42 +12047,58 @@ async function createProjectBackend(options = {}) {
9892
12047
  const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
9893
12048
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
9894
12049
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
12050
+ const databaseEngine = paths.databaseEngine;
9895
12051
  if (paths.dataDir) {
9896
12052
  await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
9897
12053
  await chmod(paths.dataDir, 448);
9898
12054
  }
9899
12055
  await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
9900
12056
  await chmod(paths.storageDir, 448);
9901
- const backend = await createBackend({
9902
- dataDir: paths.dataDir,
9903
- jwtSecret: secrets.jwtSecret,
9904
- vaultKey: secrets.vaultKey,
9905
- apiUrl: url,
9906
- siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
9907
- host,
9908
- jwtExpiry: config.auth.jwtExpiry,
9909
- uriAllowList: config.auth.uriAllowList,
9910
- authEnabled: config.auth.enabled,
9911
- authSettings: config.auth.settings,
9912
- authRateLimits: config.auth.rateLimits,
9913
- sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
9914
- sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
9915
- oauthProviders: config.auth.oauthProviders,
9916
- smsSender: options.smsSender,
9917
- dbSchemas: config.api.schemas,
9918
- maxRows: config.api.maxRows,
9919
- storageFileSizeLimit: config.storage.fileSizeLimit,
9920
- buckets: config.storage.buckets,
9921
- migrations: options.applyMigrations === false ? [] : project.migrations,
9922
- seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
9923
- functions,
9924
- functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
9925
- functionEnv,
9926
- webhooks,
9927
- startRuntimeServices: options.startRuntimeServices,
9928
- storageDriver: options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3),
9929
- log: options.log
9930
- });
12057
+ const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
12058
+ const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
12059
+ let backend;
12060
+ try {
12061
+ backend = await createBackend({
12062
+ engine,
12063
+ dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
12064
+ jwtSecret: secrets.jwtSecret,
12065
+ vaultKey: secrets.vaultKey,
12066
+ apiUrl: url,
12067
+ siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
12068
+ host,
12069
+ jwtExpiry: config.auth.jwtExpiry,
12070
+ uriAllowList: config.auth.uriAllowList,
12071
+ authEnabled: config.auth.enabled,
12072
+ authSettings: config.auth.settings,
12073
+ authRateLimits: config.auth.rateLimits,
12074
+ sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
12075
+ sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
12076
+ oauthProviders: config.auth.oauthProviders,
12077
+ smsSender: options.smsSender,
12078
+ dbSchemas: config.api.schemas,
12079
+ maxRows: config.api.maxRows,
12080
+ storageFileSizeLimit: config.storage.fileSizeLimit,
12081
+ buckets: config.storage.buckets,
12082
+ migrations: options.applyMigrations === false ? [] : project.migrations,
12083
+ seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
12084
+ functions,
12085
+ functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
12086
+ functionEnv,
12087
+ webhooks,
12088
+ startRuntimeServices: options.startRuntimeServices,
12089
+ storageDriver,
12090
+ log: options.log
12091
+ });
12092
+ } catch (error) {
12093
+ if (engine) {
12094
+ try {
12095
+ await engine.close();
12096
+ } catch (cleanupError) {
12097
+ throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
12098
+ }
12099
+ }
12100
+ throw error;
12101
+ }
9931
12102
  return {
9932
12103
  backend,
9933
12104
  config,
@@ -9938,9 +12109,22 @@ async function createProjectBackend(options = {}) {
9938
12109
  migrationCount: project.migrations.length,
9939
12110
  functionNames: [...functions.keys()],
9940
12111
  webhookCount: webhooks.length,
9941
- storageBackend
12112
+ storageBackend,
12113
+ databaseEngine
9942
12114
  };
9943
12115
  }
12116
+ function resolveDatabaseEngine(value, memory = false) {
12117
+ const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
12118
+ if (configured !== "pglite" && configured !== "native") {
12119
+ throw new Error(`unsupported SUPACLOUD_LITE_ENGINE: ${configured}`);
12120
+ }
12121
+ if (configured === "native" && memory)
12122
+ throw new Error("--memory is only supported by the pglite engine");
12123
+ if (configured === "native" && !isNativeEngineSupported()) {
12124
+ throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ` + `${process.platform}/${process.arch} must use --engine pglite`);
12125
+ }
12126
+ return configured;
12127
+ }
9944
12128
  function resolveStorageBackend(value) {
9945
12129
  const configured = value ?? process.env.SUPACLOUD_LITE_STORAGE_BACKEND ?? "fs";
9946
12130
  if (configured === "fs" || configured === "memory" || configured === "s3")
@@ -9995,7 +12179,7 @@ async function startProjectServer(options = {}) {
9995
12179
  }
9996
12180
  async function loadWebhooks(projectDir) {
9997
12181
  try {
9998
- const parsed = JSON.parse(await readFile6(join7(projectDir, "supabase", "webhooks.json"), "utf8"));
12182
+ const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
9999
12183
  return Array.isArray(parsed) ? parsed : [];
10000
12184
  } catch (error) {
10001
12185
  if (error.code === "ENOENT")
@@ -10046,9 +12230,9 @@ async function findEphemeralPort(host = "127.0.0.1") {
10046
12230
  }
10047
12231
 
10048
12232
  // src/snapshot.ts
10049
- import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
10050
- import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
10051
- import { create as createTar, extract as extractTar } from "tar";
12233
+ import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12234
+ import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
12235
+ import { create as createTar, extract as extractTar2 } from "tar";
10052
12236
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
10053
12237
  var SNAPSHOT_VERSION = 1;
10054
12238
  async function createSnapshot(options) {
@@ -10063,21 +12247,27 @@ async function createSnapshot(options) {
10063
12247
  storageBackend: options.storageBackend,
10064
12248
  includesDatabase: Boolean(paths.dataDir),
10065
12249
  includesLocalStorage: options.storageBackend === "fs",
10066
- includesSecrets: true
12250
+ includesSecrets: true,
12251
+ databaseEngine: paths.databaseEngine,
12252
+ ...paths.databaseEngine === "native" ? {
12253
+ platform: process.platform,
12254
+ architecture: process.arch,
12255
+ postgresMajor: await readPostgresMajor(paths.dataDir)
12256
+ } : {}
10067
12257
  };
10068
12258
  const output = resolve3(options.output);
10069
12259
  if (await existingInfo(output))
10070
12260
  throw new Error(`snapshot output already exists: ${output}`);
10071
- await mkdir6(dirname5(output), { recursive: true });
10072
- const stagingRoot = await mkdtemp(join8(dirname5(output), ".supacloud-lite-snapshot-"));
12261
+ await mkdir6(dirname6(output), { recursive: true });
12262
+ const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
10073
12263
  try {
10074
- await writeFile5(join8(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
12264
+ await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
10075
12265
  `);
10076
- await stageFile(paths.secretsFile, join8(stagingRoot, "secrets.json"));
12266
+ await stageFile(paths.secretsFile, join9(stagingRoot, "secrets.json"));
10077
12267
  if (paths.dataDir)
10078
- await stageDirectory(paths.dataDir, join8(stagingRoot, "database"));
12268
+ await stageDirectory(paths.dataDir, join9(stagingRoot, "database"));
10079
12269
  if (options.storageBackend === "fs")
10080
- await stageDirectory(paths.storageDir, join8(stagingRoot, "storage"));
12270
+ await stageDirectory(paths.storageDir, join9(stagingRoot, "storage"));
10081
12271
  const entries = ["manifest.json", "secrets.json"];
10082
12272
  if (paths.dataDir)
10083
12273
  entries.push("database");
@@ -10088,23 +12278,23 @@ async function createSnapshot(options) {
10088
12278
  await chmod2(output, 384);
10089
12279
  return manifest;
10090
12280
  } catch (error) {
10091
- await rm4(output, { force: true });
12281
+ await rm5(output, { force: true });
10092
12282
  throw error;
10093
12283
  } finally {
10094
- await rm4(stagingRoot, { recursive: true, force: true });
12284
+ await rm5(stagingRoot, { recursive: true, force: true });
10095
12285
  }
10096
12286
  }
10097
12287
  async function restoreSnapshot(options) {
10098
12288
  const paths = normalizePaths(options.paths);
10099
12289
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
10100
12290
  await assertNoDataDirectoryLock(paths);
10101
- const stagingRoot = await mkdtemp(join8(dirname5(paths.stateDir), ".supacloud-lite-restore-"));
10102
- const payloadRoot = join8(stagingRoot, "payload");
12291
+ const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
12292
+ const payloadRoot = join9(stagingRoot, "payload");
10103
12293
  const rollbackId = crypto.randomUUID();
10104
12294
  const rollbackPaths = [];
10105
12295
  try {
10106
12296
  await mkdir6(payloadRoot, { recursive: true });
10107
- await extractTar({
12297
+ await extractTar2({
10108
12298
  cwd: payloadRoot,
10109
12299
  file: resolve3(options.input),
10110
12300
  preserveOwner: false,
@@ -10129,6 +12319,7 @@ async function restoreSnapshot(options) {
10129
12319
  if (manifest.storageBackend !== options.storageBackend) {
10130
12320
  throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
10131
12321
  }
12322
+ assertDatabaseSnapshotCompatible(manifest, paths);
10132
12323
  if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
10133
12324
  throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
10134
12325
  }
@@ -10137,27 +12328,27 @@ async function restoreSnapshot(options) {
10137
12328
  }
10138
12329
  await assertSnapshotPayload(payloadRoot, manifest);
10139
12330
  if (manifest.includesDatabase)
10140
- await mkdir6(join8(payloadRoot, "database"), { recursive: true });
12331
+ await mkdir6(join9(payloadRoot, "database"), { recursive: true });
10141
12332
  if (manifest.includesLocalStorage)
10142
- await mkdir6(join8(payloadRoot, "storage"), { recursive: true });
12333
+ await mkdir6(join9(payloadRoot, "storage"), { recursive: true });
10143
12334
  await assertRestoreTargets(paths, manifest, options.force === true);
10144
- const stateStage = join8(stagingRoot, "state");
12335
+ const stateStage = join9(stagingRoot, "state");
10145
12336
  await mkdir6(stateStage, { recursive: true });
10146
- await copyEntry(join8(payloadRoot, "secrets.json"), join8(stateStage, "secrets.json"));
12337
+ await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
10147
12338
  if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
10148
- await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative2(paths.stateDir, paths.dataDir)));
12339
+ await copyEntry(join9(payloadRoot, "database"), join9(stateStage, relative2(paths.stateDir, paths.dataDir)));
10149
12340
  }
10150
12341
  if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
10151
- await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative2(paths.stateDir, paths.storageDir)));
12342
+ await copyEntry(join9(payloadRoot, "storage"), join9(stateStage, relative2(paths.stateDir, paths.storageDir)));
10152
12343
  }
10153
12344
  const swaps = [];
10154
12345
  try {
10155
12346
  await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
10156
12347
  if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
10157
- await applyDirectorySwap(join8(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
12348
+ await applyDirectorySwap(join9(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
10158
12349
  }
10159
12350
  if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
10160
- await applyDirectorySwap(join8(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
12351
+ await applyDirectorySwap(join9(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
10161
12352
  }
10162
12353
  } catch (error) {
10163
12354
  await rollbackDirectorySwaps(swaps);
@@ -10176,7 +12367,7 @@ async function restoreSnapshot(options) {
10176
12367
  } catch (error) {
10177
12368
  throw error instanceof Error ? error : new Error(String(error));
10178
12369
  } finally {
10179
- await rm4(stagingRoot, { recursive: true, force: true });
12370
+ await rm5(stagingRoot, { recursive: true, force: true });
10180
12371
  }
10181
12372
  }
10182
12373
  function normalizePaths(paths) {
@@ -10193,7 +12384,7 @@ async function assertSnapshotPaths(paths, options = {}) {
10193
12384
  try {
10194
12385
  if (paths.stateDir === parse2(paths.stateDir).root)
10195
12386
  throw new Error("snapshot state directory must not be the filesystem root");
10196
- if (paths.secretsFile !== join8(paths.stateDir, "secrets.json"))
12387
+ if (paths.secretsFile !== join9(paths.stateDir, "secrets.json"))
10197
12388
  throw new Error("snapshot secrets path must be inside the state directory");
10198
12389
  const stateInfo = await lstat2(paths.stateDir);
10199
12390
  if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
@@ -10251,10 +12442,10 @@ async function stageDirectory(root, destination) {
10251
12442
  throw error;
10252
12443
  }
10253
12444
  await mkdir6(destination, { recursive: true });
10254
- const walk = async (current, target) => {
12445
+ const walk = async (current, target2) => {
10255
12446
  for (const entry of await readdir3(current, { withFileTypes: true })) {
10256
- const fullPath = join8(current, entry.name);
10257
- const targetPath = join8(target, entry.name);
12447
+ const fullPath = join9(current, entry.name);
12448
+ const targetPath = join9(target2, entry.name);
10258
12449
  if (entry.isSymbolicLink())
10259
12450
  throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
10260
12451
  if (entry.isDirectory()) {
@@ -10269,14 +12460,14 @@ async function stageDirectory(root, destination) {
10269
12460
  };
10270
12461
  await walk(root, destination);
10271
12462
  }
10272
- async function stageFile(source, target) {
10273
- await mkdir6(dirname5(target), { recursive: true });
10274
- await copyFile(source, target);
12463
+ async function stageFile(source, target2) {
12464
+ await mkdir6(dirname6(target2), { recursive: true });
12465
+ await copyFile(source, target2);
10275
12466
  }
10276
12467
  async function readManifest(payloadRoot) {
10277
12468
  let parsed;
10278
12469
  try {
10279
- parsed = JSON.parse(await readFile7(join8(payloadRoot, "manifest.json"), "utf8"));
12470
+ parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
10280
12471
  } catch (error) {
10281
12472
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
10282
12473
  }
@@ -10288,13 +12479,38 @@ function isSnapshotManifest(value) {
10288
12479
  if (!value || typeof value !== "object")
10289
12480
  return false;
10290
12481
  const candidate = value;
10291
- return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true;
12482
+ return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true && (candidate.databaseEngine === undefined || candidate.databaseEngine === "pglite" || candidate.databaseEngine === "native") && (candidate.platform === undefined || typeof candidate.platform === "string") && (candidate.architecture === undefined || typeof candidate.architecture === "string") && (candidate.postgresMajor === undefined || typeof candidate.postgresMajor === "string");
12483
+ }
12484
+ function assertDatabaseSnapshotCompatible(manifest, paths) {
12485
+ const sourceEngine = manifest.databaseEngine ?? "pglite";
12486
+ if (sourceEngine !== paths.databaseEngine) {
12487
+ throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
12488
+ }
12489
+ if (sourceEngine !== "native")
12490
+ return;
12491
+ if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
12492
+ throw new Error(`native PostgreSQL snapshots require the same platform and architecture; ` + `source is ${manifest.platform ?? "unknown"}/${manifest.architecture ?? "unknown"}, ` + `target is ${process.platform}/${process.arch}`);
12493
+ }
12494
+ if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
12495
+ throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
12496
+ }
12497
+ }
12498
+ async function readPostgresMajor(dataDir) {
12499
+ if (!dataDir)
12500
+ return;
12501
+ try {
12502
+ return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
12503
+ } catch (error) {
12504
+ if (error.code === "ENOENT")
12505
+ return;
12506
+ throw error;
12507
+ }
10292
12508
  }
10293
12509
  async function assertSnapshotPayload(payloadRoot, manifest) {
10294
12510
  const required = ["manifest.json", "secrets.json"];
10295
12511
  for (const path of required) {
10296
12512
  try {
10297
- await lstat2(join8(payloadRoot, path));
12513
+ await lstat2(join9(payloadRoot, path));
10298
12514
  } catch {
10299
12515
  throw new Error(`snapshot is missing required payload: ${path}`);
10300
12516
  }
@@ -10317,9 +12533,9 @@ async function assertRestoreTargets(paths, manifest, force) {
10317
12533
  if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
10318
12534
  targets.push(paths.storageDir);
10319
12535
  if (!force) {
10320
- for (const target of targets) {
10321
- if (await directoryHasEntries(target))
10322
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
12536
+ for (const target2 of targets) {
12537
+ if (await directoryHasEntries(target2))
12538
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
10323
12539
  }
10324
12540
  }
10325
12541
  }
@@ -10332,34 +12548,34 @@ async function directoryHasEntries(path) {
10332
12548
  throw error;
10333
12549
  }
10334
12550
  }
10335
- async function applyDirectorySwap(source, target, force, rollbackId, swaps) {
10336
- const targetInfo = await existingInfo(target);
12551
+ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
12552
+ const targetInfo = await existingInfo(target2);
10337
12553
  if (targetInfo && !targetInfo.isDirectory())
10338
- throw new Error(`restore target is not a directory: ${target}`);
10339
- const swap = { target };
12554
+ throw new Error(`restore target is not a directory: ${target2}`);
12555
+ const swap = { target: target2 };
10340
12556
  if (targetInfo) {
10341
12557
  if (!force) {
10342
- if (await directoryHasEntries(target))
10343
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
10344
- await rm4(target, { recursive: true, force: true });
12558
+ if (await directoryHasEntries(target2))
12559
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
12560
+ await rm5(target2, { recursive: true, force: true });
10345
12561
  } else {
10346
- swap.rollbackPath = join8(dirname5(target), `.${target.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
10347
- await rename2(target, swap.rollbackPath);
12562
+ swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
12563
+ await rename2(target2, swap.rollbackPath);
10348
12564
  }
10349
12565
  }
10350
12566
  try {
10351
- await mkdir6(dirname5(target), { recursive: true });
10352
- await rename2(source, target);
12567
+ await mkdir6(dirname6(target2), { recursive: true });
12568
+ await rename2(source, target2);
10353
12569
  swaps.push(swap);
10354
12570
  } catch (error) {
10355
12571
  if (swap.rollbackPath)
10356
- await rename2(swap.rollbackPath, target).catch(() => {});
12572
+ await rename2(swap.rollbackPath, target2).catch(() => {});
10357
12573
  throw error;
10358
12574
  }
10359
12575
  }
10360
12576
  async function rollbackDirectorySwaps(swaps) {
10361
12577
  for (const swap of [...swaps].reverse()) {
10362
- await rm4(swap.target, { recursive: true, force: true });
12578
+ await rm5(swap.target, { recursive: true, force: true });
10363
12579
  if (swap.rollbackPath)
10364
12580
  await rename2(swap.rollbackPath, swap.target);
10365
12581
  }
@@ -10373,17 +12589,17 @@ async function existingInfo(path) {
10373
12589
  throw error;
10374
12590
  }
10375
12591
  }
10376
- async function copyEntry(source, target) {
12592
+ async function copyEntry(source, target2) {
10377
12593
  const info = await lstat2(source);
10378
12594
  if (info.isSymbolicLink())
10379
12595
  throw new Error(`snapshot refuses symbolic link: ${source}`);
10380
12596
  if (info.isDirectory()) {
10381
- await mkdir6(target, { recursive: true });
12597
+ await mkdir6(target2, { recursive: true });
10382
12598
  for (const entry of await readdir3(source))
10383
- await copyEntry(join8(source, entry), join8(target, entry));
12599
+ await copyEntry(join9(source, entry), join9(target2, entry));
10384
12600
  } else if (info.isFile()) {
10385
- await mkdir6(dirname5(target), { recursive: true });
10386
- await Bun.write(target, Bun.file(source));
12601
+ await mkdir6(dirname6(target2), { recursive: true });
12602
+ await Bun.write(target2, Bun.file(source));
10387
12603
  } else
10388
12604
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
10389
12605
  }
@@ -10394,7 +12610,7 @@ async function hardenRestoredTree(root) {
10394
12610
  if (info.isDirectory()) {
10395
12611
  await chmod2(root, 448);
10396
12612
  for (const entry of await readdir3(root))
10397
- await hardenRestoredTree(join8(root, entry));
12613
+ await hardenRestoredTree(join9(root, entry));
10398
12614
  return;
10399
12615
  }
10400
12616
  if (info.isFile()) {
@@ -10405,7 +12621,7 @@ async function hardenRestoredTree(root) {
10405
12621
  }
10406
12622
  async function assertNoSymlinks(root) {
10407
12623
  for (const entry of await readdir3(root, { withFileTypes: true })) {
10408
- const fullPath = join8(root, entry.name);
12624
+ const fullPath = join9(root, entry.name);
10409
12625
  if (entry.isSymbolicLink())
10410
12626
  throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
10411
12627
  if (entry.isDirectory())
@@ -10478,6 +12694,8 @@ function parseArgs(argv) {
10478
12694
  options.storageBackend = next();
10479
12695
  else if (argument === "--s3-prefix")
10480
12696
  options.s3 = { ...options.s3, prefix: next() };
12697
+ else if (argument === "--engine")
12698
+ options.engine = next();
10481
12699
  else if (argument === "--memory")
10482
12700
  options.memory = true;
10483
12701
  else if (argument === "--output" || argument === "-o")
@@ -10540,6 +12758,7 @@ ${privilegedKey}
10540
12758
  }
10541
12759
  const project2 = await createProjectBackend({
10542
12760
  ...options,
12761
+ applyMigrations: false,
10543
12762
  includeFunctions: false,
10544
12763
  includeWebhooks: false,
10545
12764
  startRuntimeServices: false,
@@ -10548,8 +12767,8 @@ ${privilegedKey}
10548
12767
  try {
10549
12768
  const source = await generateTypes(project2.backend.db, "public");
10550
12769
  if (options.output) {
10551
- await mkdir7(dirname6(options.output), { recursive: true });
10552
- await writeFile6(options.output, source);
12770
+ await mkdir7(dirname7(options.output), { recursive: true });
12771
+ await writeFile7(options.output, source);
10553
12772
  await writeStandardOutput(`Wrote ${options.output}
10554
12773
  `);
10555
12774
  } else
@@ -10562,6 +12781,7 @@ ${privilegedKey}
10562
12781
  if (options.command === "inspect") {
10563
12782
  const project2 = await createProjectBackend({
10564
12783
  ...options,
12784
+ applyMigrations: false,
10565
12785
  includeFunctions: false,
10566
12786
  includeWebhooks: false,
10567
12787
  startRuntimeServices: false,
@@ -10597,13 +12817,13 @@ ${privilegedKey}
10597
12817
  }
10598
12818
  if (options.command !== "start")
10599
12819
  throw new Error(`unknown command: ${options.command}`);
10600
- const project = await startProjectServer({ ...options, log: (message) => console.log(` ${message}`) });
12820
+ const project = await startProjectServer({ ...options, log: (message2) => console.log(` ${message2}`) });
10601
12821
  const shutdown = waitForShutdown(() => project.close());
10602
12822
  await writeStandardOutput(`
10603
12823
  SupaCloud Lite running
10604
12824
 
10605
12825
  API URL: ${project.url}
10606
- Engine: PGlite${paths.dataDir ? ` (${paths.dataDir})` : " (memory)"}
12826
+ Engine: ${formatDatabaseEngine(project.databaseEngine, paths.dataDir)}
10607
12827
  Storage: ${formatStorage(project.storageBackend, paths.storageDir)}
10608
12828
  Migrations: ${project.migrationCount} file(s)
10609
12829
  Functions: ${project.functionNames.length ? project.functionNames.join(", ") : "none"}
@@ -10625,9 +12845,10 @@ async function runDbCommand(options) {
10625
12845
  throw new Error("db reset refuses the s3 storage backend because remote objects cannot be deleted atomically");
10626
12846
  }
10627
12847
  await assertResetPathsSafe(paths);
12848
+ await assertDataDirUnlocked(paths.dataDir);
10628
12849
  if (paths.dataDir)
10629
- await rm5(paths.dataDir, { recursive: true, force: true });
10630
- await rm5(paths.storageDir, { recursive: true, force: true });
12850
+ await rm6(paths.dataDir, { recursive: true, force: true });
12851
+ await rm6(paths.storageDir, { recursive: true, force: true });
10631
12852
  const project2 = await createProjectBackend({
10632
12853
  ...options,
10633
12854
  includeFunctions: false,
@@ -10646,7 +12867,13 @@ async function runDbCommand(options) {
10646
12867
  }
10647
12868
  const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
10648
12869
  if (subcommand === "diff") {
10649
- const ddl = await computeDbDiff({ liveDataDir: paths.dataDir, migrations: project.migrations });
12870
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
12871
+ const ddl = await computeDbDiff({
12872
+ liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
12873
+ liveEngine,
12874
+ migrations: project.migrations,
12875
+ makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined
12876
+ });
10650
12877
  if (ddl.length === 0) {
10651
12878
  await writeStandardError(`No schema changes found.
10652
12879
  `);
@@ -10658,9 +12885,9 @@ async function runDbCommand(options) {
10658
12885
  `;
10659
12886
  if (options.diffFile) {
10660
12887
  const stamp = timestamp();
10661
- const output = join9(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
10662
- await mkdir7(join9(paths.projectDir, "supabase", "migrations"), { recursive: true });
10663
- await writeFile6(output, source);
12888
+ const output = join10(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
12889
+ await mkdir7(join10(paths.projectDir, "supabase", "migrations"), { recursive: true });
12890
+ await writeFile7(output, source);
10664
12891
  await writeStandardOutput(`Wrote ${output}
10665
12892
  `);
10666
12893
  } else
@@ -10668,10 +12895,13 @@ async function runDbCommand(options) {
10668
12895
  return;
10669
12896
  }
10670
12897
  if (subcommand === "pull") {
12898
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
10671
12899
  const result = await pullSchema({
10672
- liveDataDir: paths.dataDir,
12900
+ liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
12901
+ liveEngine,
10673
12902
  migrations: project.migrations,
10674
- migrationsDir: join9(paths.projectDir, "supabase", "migrations"),
12903
+ makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined,
12904
+ migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
10675
12905
  name: options.positionals[1] ?? "remote_schema"
10676
12906
  });
10677
12907
  if (!result.path)
@@ -10692,7 +12922,7 @@ async function runSnapshotCommand(options) {
10692
12922
  throw new Error("snapshot does not support --memory because the database is not durable");
10693
12923
  if (subcommand === "create") {
10694
12924
  await ensureProjectSecrets(paths);
10695
- const output = options.output ?? join9(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
12925
+ const output = options.output ?? join10(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
10696
12926
  const manifest = await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
10697
12927
  await writeStandardOutput(`Snapshot created: ${output}
10698
12928
  `);
@@ -10726,7 +12956,7 @@ async function runUpgradeCommand(options) {
10726
12956
  const paths = resolveProjectPaths(options);
10727
12957
  const storageBackend = resolveStorageBackend(options.storageBackend);
10728
12958
  await ensureProjectSecrets(paths);
10729
- const output = options.output ?? join9(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
12959
+ const output = options.output ?? join10(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
10730
12960
  await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
10731
12961
  await writeStandardOutput(`Pre-upgrade snapshot: ${output}
10732
12962
  `);
@@ -10810,6 +13040,7 @@ Options:
10810
13040
  --storage-dir <p> object storage directory
10811
13041
  --storage-backend <b> fs, memory, or s3 (default fs)
10812
13042
  --s3-prefix <p> optional key prefix for the s3 backend
13043
+ --engine <e> pglite (default) or native (macOS/glibc Linux x64/arm64)
10813
13044
  --memory use an in-memory PGlite database
10814
13045
  -o, --output <p> output file for gen types
10815
13046
  -f, --file <name> migration suffix for db diff
@@ -10825,6 +13056,10 @@ function formatStorage(backend, storageDir) {
10825
13056
  return "custom driver";
10826
13057
  return storageDir;
10827
13058
  }
13059
+ function formatDatabaseEngine(engine, dataDir) {
13060
+ const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
13061
+ return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
13062
+ }
10828
13063
  var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
10829
13064
  var exitCode = 0;
10830
13065
  try {