@sixb/pg 0.1.20 → 0.1.22
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.
- package/README.md +1 -0
- package/dist/agents/agent-storage.d.ts +8 -2
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/approvals.d.ts +48 -0
- package/dist/agents/approvals.d.ts.map +1 -0
- package/dist/agents/checkpoints.d.ts +1 -0
- package/dist/agents/checkpoints.d.ts.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/memory.d.ts +19 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +15 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/settings.d.ts +29 -0
- package/dist/agents/settings.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +10 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +3 -2
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +2 -1
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/users.d.ts +8 -4
- package/dist/auth-storage/users.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6001 -2923
- package/dist/index.js.map +45 -37
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/conditional-reader.d.ts +17 -0
- package/dist/objects/conditional-reader.d.ts.map +1 -0
- package/dist/objects/query-compiler.d.ts +27 -1
- package/dist/objects/query-compiler.d.ts.map +1 -1
- package/dist/objects/query-indexes.d.ts.map +1 -1
- package/dist/objects/read-scope.d.ts +10 -1
- package/dist/objects/read-scope.d.ts.map +1 -1
- package/dist/objects/reader.d.ts +2 -0
- package/dist/objects/reader.d.ts.map +1 -1
- package/dist/objects/scoped-reader.d.ts +12 -0
- package/dist/objects/scoped-reader.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +1 -0
- package/dist/objects/selected-reader.d.ts.map +1 -1
- package/dist/objects/storage.d.ts +7 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/ontology-storage/index.d.ts +2 -0
- package/dist/ontology-storage/index.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-session.d.ts +6 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.d.ts.map +1 -1
- package/dist/ontology-storage/replacement-plans.d.ts.map +1 -1
- package/dist/ontology-storage/sources.d.ts +2 -1
- package/dist/ontology-storage/sources.d.ts.map +1 -1
- package/dist/ontology-storage/unique-values.d.ts +60 -0
- package/dist/ontology-storage/unique-values.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +4 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-activity-storage.d.ts +22 -0
- package/dist/pg-activity-storage.d.ts.map +1 -0
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts +8 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-artifact-storage.d.ts +26 -0
- package/dist/pg-artifact-storage.d.ts.map +1 -0
- package/dist/pg-execution-storage.d.ts +3 -1
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts +4 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +2 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +11 -2
- package/src/agents/approvals.ts +400 -0
- package/src/agents/checkpoints.ts +8 -4
- package/src/agents/index.ts +1 -0
- package/src/agents/memory.ts +175 -0
- package/src/agents/messages.ts +14 -7
- package/src/agents/rows.ts +46 -1
- package/src/agents/runs.ts +154 -22
- package/src/agents/settings.ts +105 -0
- package/src/agents/threads.ts +379 -3
- package/src/ai-cost-groups.ts +11 -1
- package/src/auth-storage/auth-storage.ts +45 -13
- package/src/auth-storage/sessions.ts +20 -0
- package/src/auth-storage/users.ts +105 -43
- package/src/index.ts +10 -0
- package/src/migrations/063-artifacts.sql +67 -0
- package/src/migrations/064-drop-output-diagnostics.sql +14 -0
- package/src/migrations/065-plan-work-payload-statistics.sql +5 -0
- package/src/migrations/066-activity.sql +61 -0
- package/src/migrations/067-activity-timeline.sql +15 -0
- package/src/migrations/068-activity-interactions.sql +20 -0
- package/src/migrations/069-artifact-kinds.sql +4 -0
- package/src/migrations/070-artifact-state.sql +16 -0
- package/src/migrations/071-agent-thread-participants.sql +16 -0
- package/src/migrations/072-forked-context-checkpoints.sql +23 -0
- package/src/migrations/073-unique-values.sql +25 -0
- package/src/migrations/074-generated-media-usage.sql +21 -0
- package/src/migrations/075-model-call-files.sql +11 -0
- package/src/migrations/076-agent-memory.sql +33 -0
- package/src/migrations/077-artifact-threads.sql +21 -0
- package/src/migrations/078-agent-approvals.sql +58 -0
- package/src/migrations/079-agent-approval-resume.sql +20 -0
- package/src/migrations/080-subagent-threads.sql +145 -0
- package/src/migrations/081-subagent-questions.sql +6 -0
- package/src/migrations.ts +52 -0
- package/src/objects/conditional-reader.ts +44 -0
- package/src/objects/prepare-queries.ts +1 -1
- package/src/objects/query-compiler.ts +96 -33
- package/src/objects/query-indexes.ts +29 -10
- package/src/objects/read-scope.ts +140 -25
- package/src/objects/reader.ts +50 -6
- package/src/objects/scoped-reader.ts +101 -0
- package/src/objects/selected-reader.ts +16 -76
- package/src/objects/storage.ts +24 -5
- package/src/objects/text-count-index.ts +1 -0
- package/src/ontology-storage/index.ts +3 -0
- package/src/ontology-storage/materialization-session.ts +8 -0
- package/src/ontology-storage/materialization-writer.ts +76 -1
- package/src/ontology-storage/materializations.ts +20 -1
- package/src/ontology-storage/replacement-plans.ts +25 -7
- package/src/ontology-storage/sources.ts +37 -0
- package/src/ontology-storage/unique-values.ts +274 -0
- package/src/pg-action-run-storage.ts +23 -0
- package/src/pg-activity-storage.ts +486 -0
- package/src/pg-ai-cost-storage.ts +62 -12
- package/src/pg-ai-usage-storage.ts +90 -1
- package/src/pg-artifact-storage.ts +521 -0
- package/src/pg-execution-storage.ts +83 -0
- package/src/pg-workflow-run-storage.ts +23 -0
- package/src/share-grant-storage.ts +21 -0
- package/src/storage-errors.ts +7 -0
- package/src/transactions.ts +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- Calls of the Agent that wait for whoever asked for the turn. A call's run id is its key: a
|
|
2
|
+
-- thread holds at most one pending, approved or denied approval per key, and a cancelled,
|
|
3
|
+
-- superseded or expired one lets the same call ask again.
|
|
4
|
+
CREATE TABLE agent_approvals (
|
|
5
|
+
project_id TEXT NOT NULL,
|
|
6
|
+
id TEXT NOT NULL CHECK (length(trim(id)) > 0),
|
|
7
|
+
thread_id TEXT NOT NULL,
|
|
8
|
+
run_id TEXT NOT NULL CHECK (length(run_id) > 0),
|
|
9
|
+
caller_run_id TEXT NOT NULL CHECK (length(caller_run_id) > 0),
|
|
10
|
+
requested_by_type TEXT NOT NULL CHECK (requested_by_type IN ('user', 'serviceAccount', 'system')),
|
|
11
|
+
requested_by_id TEXT NOT NULL CHECK (length(requested_by_id) > 0),
|
|
12
|
+
mode TEXT NOT NULL CHECK (mode IN ('manual', 'accept-edits')),
|
|
13
|
+
call_key TEXT NOT NULL CHECK (length(call_key) > 0),
|
|
14
|
+
call JSONB NOT NULL,
|
|
15
|
+
status TEXT NOT NULL
|
|
16
|
+
CHECK (status IN ('pending', 'approved', 'denied', 'cancelled', 'superseded', 'expired')),
|
|
17
|
+
created_at TIMESTAMPTZ NOT NULL,
|
|
18
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
19
|
+
decided_at TIMESTAMPTZ,
|
|
20
|
+
decided_by_type TEXT CHECK (decided_by_type IN ('user', 'serviceAccount', 'system')),
|
|
21
|
+
decided_by_id TEXT,
|
|
22
|
+
outcome JSONB,
|
|
23
|
+
reported_in_message_id TEXT,
|
|
24
|
+
PRIMARY KEY (project_id, id),
|
|
25
|
+
FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE UNIQUE INDEX idx_agent_approvals_live_call
|
|
29
|
+
ON agent_approvals (project_id, thread_id, call_key)
|
|
30
|
+
WHERE status IN ('pending', 'approved', 'denied');
|
|
31
|
+
|
|
32
|
+
CREATE INDEX idx_agent_approvals_thread ON agent_approvals (project_id, thread_id, created_at);
|
|
33
|
+
CREATE INDEX idx_agent_approvals_run ON agent_approvals (project_id, run_id, status);
|
|
34
|
+
CREATE INDEX idx_agent_approvals_caller_run ON agent_approvals (project_id, caller_run_id, status);
|
|
35
|
+
CREATE INDEX idx_agent_approvals_requester
|
|
36
|
+
ON agent_approvals (project_id, requested_by_type, requested_by_id, status);
|
|
37
|
+
|
|
38
|
+
-- What each user lets the Agent change without asking: a default, then their own choice in a
|
|
39
|
+
-- thread. Absent rows fall back, in that order.
|
|
40
|
+
CREATE TABLE agent_user_settings (
|
|
41
|
+
project_id TEXT NOT NULL,
|
|
42
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
|
|
43
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
44
|
+
approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
|
|
45
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
46
|
+
PRIMARY KEY (project_id, principal_type, principal_id)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE agent_thread_user_settings (
|
|
50
|
+
project_id TEXT NOT NULL,
|
|
51
|
+
thread_id TEXT NOT NULL,
|
|
52
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'serviceAccount', 'system')),
|
|
53
|
+
principal_id TEXT NOT NULL CHECK (length(principal_id) > 0),
|
|
54
|
+
approval_mode TEXT NOT NULL CHECK (approval_mode IN ('manual', 'accept-edits', 'auto')),
|
|
55
|
+
updated_at TIMESTAMPTZ NOT NULL,
|
|
56
|
+
PRIMARY KEY (project_id, thread_id, principal_type, principal_id),
|
|
57
|
+
FOREIGN KEY (project_id, thread_id) REFERENCES agent_threads (project_id, id) ON DELETE CASCADE
|
|
58
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- A paused Agent turn resumes once its requester decided on every call it held. What that needs:
|
|
2
|
+
-- the mode "Always allow" raised the turn to, whether the turn may still resume on a call, and the
|
|
3
|
+
-- claim a process takes before it runs an approved call left without its outcome.
|
|
4
|
+
ALTER TABLE agent_approvals
|
|
5
|
+
ADD COLUMN raised_mode TEXT CHECK (raised_mode IN ('manual', 'accept-edits', 'auto')),
|
|
6
|
+
ADD COLUMN turn_stopped_at TIMESTAMPTZ,
|
|
7
|
+
ADD COLUMN execution_claimed_at TIMESTAMPTZ;
|
|
8
|
+
|
|
9
|
+
-- The repair pass: calls past their time, approved calls without an outcome, and decided calls of
|
|
10
|
+
-- turns that may still resume. Each index covers only the rows that are still work.
|
|
11
|
+
CREATE INDEX idx_agent_approvals_due
|
|
12
|
+
ON agent_approvals (project_id, expires_at)
|
|
13
|
+
WHERE status = 'pending';
|
|
14
|
+
CREATE INDEX idx_agent_approvals_unfinished
|
|
15
|
+
ON agent_approvals (project_id, created_at, id)
|
|
16
|
+
WHERE status = 'approved' AND outcome IS NULL;
|
|
17
|
+
CREATE INDEX idx_agent_approvals_resumable
|
|
18
|
+
ON agent_approvals (project_id, created_at, id)
|
|
19
|
+
WHERE status IN ('approved', 'denied') AND reported_in_message_id IS NULL
|
|
20
|
+
AND turn_stopped_at IS NULL;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
-- A sub-agent works in a thread of its own: its task and its work are that thread's messages, and
|
|
2
|
+
-- each of its runs has the thread and a trigger message, as a turn does. The thread's parent is the
|
|
3
|
+
-- conversation, which every read of it goes through, and its spawning turn and key name it.
|
|
4
|
+
ALTER TABLE agent_threads
|
|
5
|
+
ADD COLUMN kind TEXT NOT NULL DEFAULT 'conversation'
|
|
6
|
+
CHECK (kind IN ('conversation', 'subagent')),
|
|
7
|
+
ADD COLUMN parent_thread_id TEXT,
|
|
8
|
+
ADD COLUMN parent_run_id TEXT,
|
|
9
|
+
ADD COLUMN spawn_key TEXT,
|
|
10
|
+
ADD CONSTRAINT agent_threads_kind_fields CHECK (
|
|
11
|
+
(
|
|
12
|
+
kind = 'conversation'
|
|
13
|
+
AND parent_thread_id IS NULL
|
|
14
|
+
AND parent_run_id IS NULL
|
|
15
|
+
AND spawn_key IS NULL
|
|
16
|
+
)
|
|
17
|
+
OR
|
|
18
|
+
(
|
|
19
|
+
kind = 'subagent'
|
|
20
|
+
AND parent_thread_id IS NOT NULL
|
|
21
|
+
AND parent_run_id IS NOT NULL
|
|
22
|
+
AND spawn_key IS NOT NULL
|
|
23
|
+
AND sandbox_params IS NULL
|
|
24
|
+
)
|
|
25
|
+
),
|
|
26
|
+
-- A conversation's sub-agent threads go with it.
|
|
27
|
+
ADD CONSTRAINT fk_agent_threads_parent
|
|
28
|
+
FOREIGN KEY (project_id, parent_thread_id)
|
|
29
|
+
REFERENCES agent_threads (project_id, id)
|
|
30
|
+
ON DELETE CASCADE,
|
|
31
|
+
ADD CONSTRAINT fk_agent_threads_parent_run
|
|
32
|
+
FOREIGN KEY (project_id, parent_run_id)
|
|
33
|
+
REFERENCES agent_runs (project_id, id)
|
|
34
|
+
ON DELETE RESTRICT,
|
|
35
|
+
ADD CONSTRAINT uq_agent_threads_spawn UNIQUE (project_id, parent_run_id, spawn_key);
|
|
36
|
+
|
|
37
|
+
ALTER TABLE agent_runs DROP CONSTRAINT agent_runs_kind_fields;
|
|
38
|
+
|
|
39
|
+
-- The children admitted before this have no thread. Each gets one named as a new child's is, with
|
|
40
|
+
-- its task as the only message: what it answered stays in its parent's `wait_agent` output.
|
|
41
|
+
CREATE TEMPORARY TABLE legacy_subagent_threads ON COMMIT DROP AS
|
|
42
|
+
SELECT
|
|
43
|
+
child.project_id,
|
|
44
|
+
child.id AS run_id,
|
|
45
|
+
'agt_thr_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS thread_id,
|
|
46
|
+
'agt_msg_child_' || regexp_replace(child.id, '^agt_run_child_', '') AS message_id
|
|
47
|
+
FROM agent_runs AS child
|
|
48
|
+
WHERE child.kind = 'subagent' AND child.thread_id IS NULL;
|
|
49
|
+
|
|
50
|
+
INSERT INTO agent_threads (
|
|
51
|
+
project_id, id, kind, parent_thread_id, parent_run_id, spawn_key, owner_principal_type,
|
|
52
|
+
owner_principal_id, status, active_run_id, last_message_at, message_count, created_at,
|
|
53
|
+
updated_at
|
|
54
|
+
)
|
|
55
|
+
SELECT
|
|
56
|
+
child.project_id,
|
|
57
|
+
legacy.thread_id,
|
|
58
|
+
'subagent',
|
|
59
|
+
parent.thread_id,
|
|
60
|
+
child.parent_run_id,
|
|
61
|
+
child.spawn_key,
|
|
62
|
+
conversation.owner_principal_type,
|
|
63
|
+
conversation.owner_principal_id,
|
|
64
|
+
'active',
|
|
65
|
+
CASE WHEN child.status IN ('queued', 'running') THEN child.id END,
|
|
66
|
+
child.created_at,
|
|
67
|
+
1,
|
|
68
|
+
child.created_at,
|
|
69
|
+
COALESCE(child.completed_at, child.created_at)
|
|
70
|
+
FROM legacy_subagent_threads AS legacy
|
|
71
|
+
JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id
|
|
72
|
+
JOIN agent_runs AS parent
|
|
73
|
+
ON parent.project_id = child.project_id AND parent.id = child.parent_run_id
|
|
74
|
+
JOIN agent_threads AS conversation
|
|
75
|
+
ON conversation.project_id = parent.project_id AND conversation.id = parent.thread_id;
|
|
76
|
+
|
|
77
|
+
INSERT INTO agent_messages (
|
|
78
|
+
project_id, id, thread_id, run_id, role, seq, parts, content_version, created_at
|
|
79
|
+
)
|
|
80
|
+
SELECT
|
|
81
|
+
child.project_id,
|
|
82
|
+
legacy.message_id,
|
|
83
|
+
legacy.thread_id,
|
|
84
|
+
child.id,
|
|
85
|
+
'user',
|
|
86
|
+
1,
|
|
87
|
+
jsonb_build_array(jsonb_build_object('type', 'text', 'text', child.spec->>'task')),
|
|
88
|
+
2,
|
|
89
|
+
child.created_at
|
|
90
|
+
FROM legacy_subagent_threads AS legacy
|
|
91
|
+
JOIN agent_runs AS child ON child.project_id = legacy.project_id AND child.id = legacy.run_id;
|
|
92
|
+
|
|
93
|
+
UPDATE agent_runs AS child
|
|
94
|
+
SET thread_id = legacy.thread_id, trigger_message_id = legacy.message_id
|
|
95
|
+
FROM legacy_subagent_threads AS legacy
|
|
96
|
+
WHERE child.project_id = legacy.project_id AND child.id = legacy.run_id;
|
|
97
|
+
|
|
98
|
+
DROP TABLE legacy_subagent_threads;
|
|
99
|
+
|
|
100
|
+
ALTER TABLE agent_runs ADD CONSTRAINT agent_runs_kind_fields CHECK (
|
|
101
|
+
(
|
|
102
|
+
kind = 'conversation'
|
|
103
|
+
AND thread_id IS NOT NULL
|
|
104
|
+
AND trigger_message_id IS NOT NULL
|
|
105
|
+
AND parent_run_id IS NULL
|
|
106
|
+
AND spawn_key IS NULL
|
|
107
|
+
AND result IS NULL
|
|
108
|
+
)
|
|
109
|
+
OR
|
|
110
|
+
(
|
|
111
|
+
kind = 'subagent'
|
|
112
|
+
AND thread_id IS NOT NULL
|
|
113
|
+
AND trigger_message_id IS NOT NULL
|
|
114
|
+
AND parent_run_id IS NOT NULL
|
|
115
|
+
AND spawn_key IS NOT NULL
|
|
116
|
+
AND spec IS NOT NULL
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
-- Lists read conversations only, so a conversation's children never weigh on them.
|
|
121
|
+
DROP INDEX idx_agent_threads_project_activity;
|
|
122
|
+
DROP INDEX idx_agent_threads_project_status_activity;
|
|
123
|
+
DROP INDEX idx_agent_threads_project_owner_activity;
|
|
124
|
+
|
|
125
|
+
CREATE INDEX idx_agent_threads_project_activity
|
|
126
|
+
ON agent_threads (project_id, (COALESCE(last_message_at, created_at)) DESC, id DESC)
|
|
127
|
+
WHERE kind = 'conversation';
|
|
128
|
+
CREATE INDEX idx_agent_threads_project_status_activity
|
|
129
|
+
ON agent_threads (project_id, status, (COALESCE(last_message_at, created_at)) DESC, id DESC)
|
|
130
|
+
WHERE kind = 'conversation';
|
|
131
|
+
CREATE INDEX idx_agent_threads_project_owner_activity
|
|
132
|
+
ON agent_threads (
|
|
133
|
+
project_id,
|
|
134
|
+
owner_principal_type,
|
|
135
|
+
owner_principal_id,
|
|
136
|
+
(COALESCE(last_message_at, created_at)) DESC,
|
|
137
|
+
id DESC
|
|
138
|
+
)
|
|
139
|
+
WHERE kind = 'conversation';
|
|
140
|
+
|
|
141
|
+
-- A conversation's children, in the order they were spawned. Only a child has a parent, so the
|
|
142
|
+
-- index holds children only, and a read by `parent_thread_id` can use it.
|
|
143
|
+
CREATE INDEX idx_agent_threads_children
|
|
144
|
+
ON agent_threads (project_id, parent_thread_id, created_at, id)
|
|
145
|
+
WHERE parent_thread_id IS NOT NULL;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
-- A child that pauses on its question waits for a later turn of its parent's conversation to
|
|
2
|
+
-- answer it. Each turn reads which of the conversation's children wait so: few do, so a partial
|
|
3
|
+
-- index keeps that read small however many children the conversation had.
|
|
4
|
+
CREATE INDEX idx_agent_runs_paused_on_question
|
|
5
|
+
ON agent_runs (project_id, parent_run_id)
|
|
6
|
+
WHERE kind = 'subagent' AND finish_reason = 'awaiting_answer';
|
package/src/migrations.ts
CHANGED
|
@@ -433,6 +433,25 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
|
|
|
433
433
|
pgSql("060-drained-source-versions", drainedSourceVersionsSql),
|
|
434
434
|
pgSql("061-terminal-action-runs", terminalActionRunsSql),
|
|
435
435
|
pgSql("062-drop-auth-audience", dropAuthAudienceSql),
|
|
436
|
+
pgSql("063-artifacts", artifactsSql),
|
|
437
|
+
pgSql("064-drop-output-diagnostics", dropOutputDiagnosticsSql),
|
|
438
|
+
pgSql("065-plan-work-payload-statistics", planWorkPayloadStatisticsSql),
|
|
439
|
+
pgSql("066-activity", activitySql),
|
|
440
|
+
pgSql("067-activity-timeline", activityTimelineSql),
|
|
441
|
+
pgSql("068-activity-interactions", activityInteractionsSql),
|
|
442
|
+
pgSql("069-artifact-kinds", artifactKindsSql),
|
|
443
|
+
pgSql("070-artifact-state", artifactStateSql),
|
|
444
|
+
pgSql("071-agent-thread-participants", agentThreadParticipantsSql),
|
|
445
|
+
pgSql("072-forked-context-checkpoints", forkedContextCheckpointsSql),
|
|
446
|
+
pgSql("073-unique-values", uniqueValuesSql),
|
|
447
|
+
pgSql("074-generated-media-usage", generatedMediaUsageSql),
|
|
448
|
+
pgSql("075-model-call-files", modelCallFilesSql),
|
|
449
|
+
pgSql("076-agent-memory", agentMemorySql),
|
|
450
|
+
pgSql("077-artifact-threads", artifactThreadsSql),
|
|
451
|
+
pgSql("078-agent-approvals", agentApprovalsSql),
|
|
452
|
+
pgSql("079-agent-approval-resume", agentApprovalResumeSql),
|
|
453
|
+
pgSql("080-subagent-threads", subagentThreadsSql),
|
|
454
|
+
pgSql("081-subagent-questions", subagentQuestionsSql),
|
|
436
455
|
],
|
|
437
456
|
})
|
|
438
457
|
|
|
@@ -463,3 +482,36 @@ import drainedSourceVersionsSql from "./migrations/060-drained-source-versions.s
|
|
|
463
482
|
}
|
|
464
483
|
import terminalActionRunsSql from "./migrations/061-terminal-action-runs.sql" with { type: "text" }
|
|
465
484
|
import dropAuthAudienceSql from "./migrations/062-drop-auth-audience.sql" with { type: "text" }
|
|
485
|
+
import artifactsSql from "./migrations/063-artifacts.sql" with { type: "text" }
|
|
486
|
+
import dropOutputDiagnosticsSql from "./migrations/064-drop-output-diagnostics.sql" with {
|
|
487
|
+
type: "text",
|
|
488
|
+
}
|
|
489
|
+
import planWorkPayloadStatisticsSql from "./migrations/065-plan-work-payload-statistics.sql" with {
|
|
490
|
+
type: "text",
|
|
491
|
+
}
|
|
492
|
+
import activitySql from "./migrations/066-activity.sql" with { type: "text" }
|
|
493
|
+
import activityTimelineSql from "./migrations/067-activity-timeline.sql" with { type: "text" }
|
|
494
|
+
import activityInteractionsSql from "./migrations/068-activity-interactions.sql" with {
|
|
495
|
+
type: "text",
|
|
496
|
+
}
|
|
497
|
+
import artifactKindsSql from "./migrations/069-artifact-kinds.sql" with { type: "text" }
|
|
498
|
+
import artifactStateSql from "./migrations/070-artifact-state.sql" with { type: "text" }
|
|
499
|
+
import agentThreadParticipantsSql from "./migrations/071-agent-thread-participants.sql" with {
|
|
500
|
+
type: "text",
|
|
501
|
+
}
|
|
502
|
+
import forkedContextCheckpointsSql from "./migrations/072-forked-context-checkpoints.sql" with {
|
|
503
|
+
type: "text",
|
|
504
|
+
}
|
|
505
|
+
import uniqueValuesSql from "./migrations/073-unique-values.sql" with { type: "text" }
|
|
506
|
+
import generatedMediaUsageSql from "./migrations/074-generated-media-usage.sql" with {
|
|
507
|
+
type: "text",
|
|
508
|
+
}
|
|
509
|
+
import modelCallFilesSql from "./migrations/075-model-call-files.sql" with { type: "text" }
|
|
510
|
+
import agentMemorySql from "./migrations/076-agent-memory.sql" with { type: "text" }
|
|
511
|
+
import artifactThreadsSql from "./migrations/077-artifact-threads.sql" with { type: "text" }
|
|
512
|
+
import agentApprovalsSql from "./migrations/078-agent-approvals.sql" with { type: "text" }
|
|
513
|
+
import agentApprovalResumeSql from "./migrations/079-agent-approval-resume.sql" with {
|
|
514
|
+
type: "text",
|
|
515
|
+
}
|
|
516
|
+
import subagentThreadsSql from "./migrations/080-subagent-threads.sql" with { type: "text" }
|
|
517
|
+
import subagentQuestionsSql from "./migrations/081-subagent-questions.sql" with { type: "text" }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ObjectQuery } from "@sixb/core"
|
|
2
|
+
import { hasVectorProfile } from "@sixb/core/internal/query"
|
|
3
|
+
import type {
|
|
4
|
+
CompiledConditionalObjectReadScope,
|
|
5
|
+
ObjectQueryCapabilities,
|
|
6
|
+
ObjectReadStorage,
|
|
7
|
+
} from "@sixb/core/storage"
|
|
8
|
+
import { type PgStoreClient, runPgRepeatableReadTransaction } from "../transactions"
|
|
9
|
+
import type { PgAggregateAdmission } from "./aggregate-admission"
|
|
10
|
+
import { compilePgConditionalObjectReadSource } from "./read-scope"
|
|
11
|
+
import { PgObjectReader } from "./reader"
|
|
12
|
+
import { createPgScopedReader } from "./scoped-reader"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read objects and links through relations that apply the scope's conditions as they are read.
|
|
16
|
+
*
|
|
17
|
+
* Reads run as unscoped ones do, under the same aggregate admission: on the pool, and a vector
|
|
18
|
+
* query on one repeatable-read snapshot for its candidate probe and its rows.
|
|
19
|
+
*/
|
|
20
|
+
export function createPgConditionalReader(
|
|
21
|
+
sql: PgStoreClient,
|
|
22
|
+
params: {
|
|
23
|
+
projectId: string
|
|
24
|
+
scope: CompiledConditionalObjectReadScope
|
|
25
|
+
},
|
|
26
|
+
options: {
|
|
27
|
+
aggregateAdmission?: PgAggregateAdmission
|
|
28
|
+
queryCapabilities: () => ObjectQueryCapabilities
|
|
29
|
+
}
|
|
30
|
+
): ObjectReadStorage {
|
|
31
|
+
const source = compilePgConditionalObjectReadSource(params.projectId, params.scope)
|
|
32
|
+
const reader = new PgObjectReader(sql, source, options.aggregateAdmission)
|
|
33
|
+
|
|
34
|
+
return createPgScopedReader(
|
|
35
|
+
{
|
|
36
|
+
projectId: params.projectId,
|
|
37
|
+
read: (run, query?: ObjectQuery) =>
|
|
38
|
+
query && hasVectorProfile(query)
|
|
39
|
+
? runPgRepeatableReadTransaction(sql, (tx) => run(new PgObjectReader(tx, source)))
|
|
40
|
+
: run(reader),
|
|
41
|
+
},
|
|
42
|
+
options.queryCapabilities
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -59,7 +59,7 @@ function preparationFingerprint(plan: ObjectQueryPreparationPlan): string {
|
|
|
59
59
|
.sort((a, b) => a.objectTypeId.localeCompare(b.objectTypeId))
|
|
60
60
|
|
|
61
61
|
// Bump the version when physical generation changes, even if the ontology stays the same.
|
|
62
|
-
const definition = { version:
|
|
62
|
+
const definition = { version: 2, projectId: plan.projectId, objectTypes }
|
|
63
63
|
return createHash("sha256").update(JSON.stringify(definition)).digest("hex")
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -181,6 +181,12 @@ export interface PgObjectQuerySource {
|
|
|
181
181
|
readonly objectsTable: string
|
|
182
182
|
readonly linksTable: string
|
|
183
183
|
readonly objectPropertyPermissionsTable?: string
|
|
184
|
+
/**
|
|
185
|
+
* Where to read one concrete type's objects, when the source can say it more precisely than
|
|
186
|
+
* `objectsTable`: a table and that type's own filter over its `properties`. A statement that
|
|
187
|
+
* reads a single type then holds the filter as a top-level condition, which an index can serve.
|
|
188
|
+
*/
|
|
189
|
+
objectsOfType?(objectTypeId: string): PgTypedObjects
|
|
184
190
|
/** Wrap a terminal SELECT without its own WITH clause; the selected source owns the only WITH. */
|
|
185
191
|
wrapStatement(
|
|
186
192
|
sql: string,
|
|
@@ -192,6 +198,26 @@ export interface PgObjectQuerySource {
|
|
|
192
198
|
wrapQuery(query: CompiledPgObjectQuery): CompiledPgObjectQuery
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
export interface PgTypedObjects {
|
|
202
|
+
readonly table: string
|
|
203
|
+
readonly filter?: { readonly sql: string; readonly args: readonly unknown[] }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The table and `AND ...` filter a statement reads one type's objects with. */
|
|
207
|
+
export function pgObjectsOfType(
|
|
208
|
+
source: PgObjectQuerySource,
|
|
209
|
+
objectTypeId: string
|
|
210
|
+
): { table: string; filter: string; args: readonly unknown[] } {
|
|
211
|
+
const typed = source.objectsOfType?.(objectTypeId)
|
|
212
|
+
if (!typed) return { table: source.objectsTable, filter: "", args: [] }
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
table: typed.table,
|
|
216
|
+
filter: typed.filter ? `AND (${typed.filter.sql})` : "",
|
|
217
|
+
args: typed.filter?.args ?? [],
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
/** Prefix an optional query source, then assign PostgreSQL positional placeholders. */
|
|
196
222
|
export function compilePgObjectStatement(
|
|
197
223
|
sql: string,
|
|
@@ -462,10 +488,11 @@ function compileStart(
|
|
|
462
488
|
}
|
|
463
489
|
|
|
464
490
|
const order = compileOrder(identityOrderFields())
|
|
491
|
+
const objects = pgObjectsOfType(ctx.source, query.objectTypeId)
|
|
465
492
|
const sql = `
|
|
466
493
|
SELECT *, properties AS _cursor_properties
|
|
467
|
-
FROM ${
|
|
468
|
-
WHERE project_id = ? AND object_type_id = ?
|
|
494
|
+
FROM ${objects.table}
|
|
495
|
+
WHERE project_id = ? AND object_type_id = ? ${objects.filter}
|
|
469
496
|
${ctx.startPredicate ? `AND (${ctx.startPredicate})` : ""}
|
|
470
497
|
${ctx.unordered ? "" : `ORDER BY ${order.sql}`}
|
|
471
498
|
${ctx.isolateSortOverflow ? "OFFSET 0" : ""}
|
|
@@ -473,9 +500,9 @@ function compileStart(
|
|
|
473
500
|
|
|
474
501
|
return {
|
|
475
502
|
sql,
|
|
476
|
-
args: [projectId, query.objectTypeId, ...(ctx.unordered ? [] : order.args)],
|
|
477
|
-
totalSql: `SELECT COUNT(*)::bigint AS total FROM ${
|
|
478
|
-
totalArgs: [projectId, query.objectTypeId],
|
|
503
|
+
args: [projectId, query.objectTypeId, ...objects.args, ...(ctx.unordered ? [] : order.args)],
|
|
504
|
+
totalSql: `SELECT COUNT(*)::bigint AS total FROM ${objects.table} WHERE project_id = ? AND object_type_id = ? ${objects.filter}`,
|
|
505
|
+
totalArgs: [projectId, query.objectTypeId, ...objects.args],
|
|
479
506
|
order,
|
|
480
507
|
hasMore: () => false,
|
|
481
508
|
trimRows: identityRows,
|
|
@@ -1070,11 +1097,7 @@ function compileExpansionOrder(
|
|
|
1070
1097
|
const direction = field.direction === "desc" ? "DESC" : "ASC"
|
|
1071
1098
|
clauses.push(
|
|
1072
1099
|
`CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END ASC`,
|
|
1073
|
-
`${
|
|
1074
|
-
field.scalarKind === "decimal"
|
|
1075
|
-
? `(${jsonTextExpression(propertyColumn)})::numeric`
|
|
1076
|
-
: `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
|
|
1077
|
-
} ${direction}`
|
|
1100
|
+
`${orderedValueExpression(field.scalarKind, propertyColumn)} ${direction}`
|
|
1078
1101
|
)
|
|
1079
1102
|
args.push(field.propertyId, field.propertyId, field.propertyId)
|
|
1080
1103
|
}
|
|
@@ -1239,13 +1262,14 @@ function compileAggregateStart(
|
|
|
1239
1262
|
throw new Error("[SixbPg] PostgreSQL object storage does not support start.includeSubtypes")
|
|
1240
1263
|
}
|
|
1241
1264
|
|
|
1265
|
+
const objects = pgObjectsOfType(source, query.objectTypeId)
|
|
1242
1266
|
return {
|
|
1243
1267
|
sql: `
|
|
1244
1268
|
SELECT project_id, object_type_id, primary_id, properties
|
|
1245
|
-
FROM ${
|
|
1246
|
-
WHERE project_id = ? AND object_type_id = ?
|
|
1269
|
+
FROM ${objects.table}
|
|
1270
|
+
WHERE project_id = ? AND object_type_id = ? ${objects.filter}
|
|
1247
1271
|
`,
|
|
1248
|
-
args: [projectId, query.objectTypeId],
|
|
1272
|
+
args: [projectId, query.objectTypeId, ...objects.args],
|
|
1249
1273
|
}
|
|
1250
1274
|
}
|
|
1251
1275
|
|
|
@@ -1452,7 +1476,8 @@ function compileProjectionExpression(properties: readonly string[]): CompiledPre
|
|
|
1452
1476
|
}
|
|
1453
1477
|
}
|
|
1454
1478
|
|
|
1455
|
-
|
|
1479
|
+
/** Compile a predicate over the unqualified `properties` column of the relation it filters. */
|
|
1480
|
+
export function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
|
|
1456
1481
|
switch (predicate.op) {
|
|
1457
1482
|
case "and":
|
|
1458
1483
|
case "or": {
|
|
@@ -1467,6 +1492,8 @@ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
|
|
|
1467
1492
|
const item = compilePredicate(predicate.item)
|
|
1468
1493
|
return negatePredicate(item)
|
|
1469
1494
|
}
|
|
1495
|
+
case "constant":
|
|
1496
|
+
return { sql: predicate.value ? "true" : "false", args: [] }
|
|
1470
1497
|
case "eq":
|
|
1471
1498
|
return predicate.scalarKind === "decimal"
|
|
1472
1499
|
? compileDecimalEqualityPredicate(predicate.propertyId, "eq", predicate.value)
|
|
@@ -1486,6 +1513,14 @@ function compilePredicate(predicate: ObjectQueryPredicate): CompiledPredicate {
|
|
|
1486
1513
|
args: [predicate.propertyId, predicate.propertyId, predicate.value],
|
|
1487
1514
|
}
|
|
1488
1515
|
}
|
|
1516
|
+
if (isTextScalarKind(predicate.scalarKind)) {
|
|
1517
|
+
// The null rank makes the range a prefix of the property's sort index keys.
|
|
1518
|
+
return {
|
|
1519
|
+
sql: `(${nullRankExpression()} = 0 AND ${orderedValueExpression(predicate.scalarKind)} ${op} ?::text)`,
|
|
1520
|
+
args: [predicate.propertyId, predicate.propertyId, predicate.propertyId, predicate.value],
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1489
1524
|
return {
|
|
1490
1525
|
sql: `${jsonValueExpression()} ${op} ?::text::jsonb`,
|
|
1491
1526
|
args: [predicate.propertyId, jsonbValue(predicate.value)],
|
|
@@ -1809,13 +1844,13 @@ function compileKeysetRanges(
|
|
|
1809
1844
|
})
|
|
1810
1845
|
if (value.nullish) return
|
|
1811
1846
|
const expression = compiledPropertyValueExpression(field, propertyColumn)
|
|
1812
|
-
const parameter = field.
|
|
1813
|
-
const args = [
|
|
1814
|
-
|
|
1815
|
-
field.
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
prefix.push({ sql: `${expression} = ${parameter}`, args })
|
|
1847
|
+
const parameter = compiledPropertyParameter(field, value.value)
|
|
1848
|
+
const args = [field.propertyId, parameter.value]
|
|
1849
|
+
addRange({
|
|
1850
|
+
sql: `${expression} ${field.direction === "desc" ? "<" : ">"} ${parameter.sql}`,
|
|
1851
|
+
args,
|
|
1852
|
+
})
|
|
1853
|
+
prefix.push({ sql: `${expression} = ${parameter.sql}`, args })
|
|
1819
1854
|
})
|
|
1820
1855
|
return ranges
|
|
1821
1856
|
}
|
|
@@ -1884,14 +1919,11 @@ function compileFieldEquality(
|
|
|
1884
1919
|
}
|
|
1885
1920
|
}
|
|
1886
1921
|
|
|
1922
|
+
const parameter = compiledPropertyParameter(field, cursor.value)
|
|
1923
|
+
|
|
1887
1924
|
return {
|
|
1888
|
-
sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${compiledPropertyValueExpression(field, propertyColumn)} = ${
|
|
1889
|
-
args: [
|
|
1890
|
-
field.propertyId,
|
|
1891
|
-
field.propertyId,
|
|
1892
|
-
field.propertyId,
|
|
1893
|
-
field.scalarKind === "decimal" ? cursor.value : jsonbValue(cursor.value),
|
|
1894
|
-
],
|
|
1925
|
+
sql: `(${jsonTypeExpression(propertyColumn)} IS NOT NULL AND ${jsonTypeExpression(propertyColumn)} != 'null' AND ${compiledPropertyValueExpression(field, propertyColumn)} = ${parameter.sql})`,
|
|
1926
|
+
args: [field.propertyId, field.propertyId, field.propertyId, parameter.value],
|
|
1895
1927
|
}
|
|
1896
1928
|
}
|
|
1897
1929
|
|
|
@@ -1911,16 +1943,17 @@ function compileFieldAfter(
|
|
|
1911
1943
|
|
|
1912
1944
|
const rankSql = `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
|
|
1913
1945
|
const valueOperator = field.direction === "desc" ? "<" : ">"
|
|
1946
|
+
const parameter = compiledPropertyParameter(field, cursor.value)
|
|
1914
1947
|
|
|
1915
1948
|
return {
|
|
1916
|
-
sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${compiledPropertyValueExpression(field, propertyColumn)} ${valueOperator} ${
|
|
1949
|
+
sql: `(${rankSql} > 0 OR (${rankSql} = 0 AND ${compiledPropertyValueExpression(field, propertyColumn)} ${valueOperator} ${parameter.sql}))`,
|
|
1917
1950
|
args: [
|
|
1918
1951
|
field.propertyId,
|
|
1919
1952
|
field.propertyId,
|
|
1920
1953
|
field.propertyId,
|
|
1921
1954
|
field.propertyId,
|
|
1922
1955
|
field.propertyId,
|
|
1923
|
-
|
|
1956
|
+
parameter.value,
|
|
1924
1957
|
],
|
|
1925
1958
|
}
|
|
1926
1959
|
}
|
|
@@ -2010,9 +2043,39 @@ function compiledPropertyValueExpression(
|
|
|
2010
2043
|
field: Extract<CompiledOrderField, { kind: "property" }>,
|
|
2011
2044
|
propertyColumn = "properties"
|
|
2012
2045
|
): string {
|
|
2013
|
-
return field.scalarKind
|
|
2014
|
-
|
|
2015
|
-
|
|
2046
|
+
return orderedValueExpression(field.scalarKind, propertyColumn)
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/** The parameter a cursor value binds to, typed like `compiledPropertyValueExpression`. */
|
|
2050
|
+
function compiledPropertyParameter(
|
|
2051
|
+
field: Extract<CompiledOrderField, { kind: "property" }>,
|
|
2052
|
+
value: unknown
|
|
2053
|
+
): { sql: string; value: unknown } {
|
|
2054
|
+
if (field.scalarKind === "decimal") return { sql: "?::numeric", value }
|
|
2055
|
+
if (isTextScalarKind(field.scalarKind)) return { sql: "?::text", value }
|
|
2056
|
+
return { sql: "?::text::jsonb", value: jsonbValue(value) }
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/**
|
|
2060
|
+
* The expression a property sorts and range-compares by. Strings compare by code point, as
|
|
2061
|
+
* SQLite and the in-memory store do: JSONB and the database's own collation would order case and
|
|
2062
|
+
* accents linguistically. Sort indexes repeat this expression, collation included.
|
|
2063
|
+
*/
|
|
2064
|
+
function orderedValueExpression(
|
|
2065
|
+
scalarKind: QueryScalarKind | undefined,
|
|
2066
|
+
propertyColumn = "properties"
|
|
2067
|
+
): string {
|
|
2068
|
+
if (scalarKind === "decimal") return `(${jsonTextExpression(propertyColumn)})::numeric`
|
|
2069
|
+
if (isTextScalarKind(scalarKind)) return `((${jsonTextExpression(propertyColumn)}) COLLATE "C")`
|
|
2070
|
+
return `NULLIF(${jsonValueExpression(propertyColumn)}, 'null'::jsonb)`
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
export function isTextScalarKind(scalarKind: QueryScalarKind | undefined): boolean {
|
|
2074
|
+
return scalarKind === "string" || scalarKind === "uuid"
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function nullRankExpression(propertyColumn = "properties"): string {
|
|
2078
|
+
return `CASE WHEN ${jsonTypeExpression(propertyColumn)} IS NULL OR ${jsonTypeExpression(propertyColumn)} = 'null' THEN 1 ELSE 0 END`
|
|
2016
2079
|
}
|
|
2017
2080
|
|
|
2018
2081
|
function columnExpression(
|
|
@@ -4,6 +4,7 @@ import type { ReservedSQL, SQL, SQLClient } from "../pg-client"
|
|
|
4
4
|
import { isConnectionLost } from "../storage-errors"
|
|
5
5
|
import { withReservedPgConnection } from "../transactions"
|
|
6
6
|
import { lockPgPreparation } from "./preparation-lock"
|
|
7
|
+
import { isTextScalarKind } from "./query-compiler"
|
|
7
8
|
|
|
8
9
|
export interface PgObjectQueryIndexScope {
|
|
9
10
|
readonly projectId: string
|
|
@@ -110,6 +111,22 @@ async function createQueryIndex(
|
|
|
110
111
|
await connection.unsafe(
|
|
111
112
|
`CREATE INDEX CONCURRENTLY IF NOT EXISTS ${identifier(`${name}_overflow`)} ON ${identifier(schema)}.objects (primary_id) WHERE ${scope} AND NOT (${sortIndexBound(definition.index)})`
|
|
112
113
|
)
|
|
114
|
+
|
|
115
|
+
// The same definition under another name is an earlier generation of this index, built
|
|
116
|
+
// with an expression queries no longer use. Writes would keep maintaining it.
|
|
117
|
+
const superseded = await connection<{ name: string }[]>`
|
|
118
|
+
SELECT c.relname AS name FROM pg_index i
|
|
119
|
+
JOIN pg_class c ON c.oid = i.indexrelid
|
|
120
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
121
|
+
WHERE n.nspname = ${schema} AND c.relname LIKE 'sixb_query_%' AND c.relname <> ${name}
|
|
122
|
+
AND obj_description(c.oid, 'pg_class') = ${JSON.stringify(definition.index)}`
|
|
123
|
+
for (const previous of superseded) {
|
|
124
|
+
for (const index of [previous.name, `${previous.name}_overflow`]) {
|
|
125
|
+
await connection.unsafe(
|
|
126
|
+
`DROP INDEX CONCURRENTLY IF EXISTS ${identifier(schema)}.${identifier(index)}`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
113
130
|
}
|
|
114
131
|
return name
|
|
115
132
|
}
|
|
@@ -150,14 +167,7 @@ function statisticsExpressions(index: PgObjectQueryIndex): string[] {
|
|
|
150
167
|
if (index.kind === "value") expressions.push(`(properties -> ${literal(index.propertyId)})`)
|
|
151
168
|
if (index.kind === "text")
|
|
152
169
|
expressions.push(`lower(coalesce(properties ->> ${literal(index.propertyId)}, ''))`)
|
|
153
|
-
if (index.kind === "sort")
|
|
154
|
-
for (const field of index.fields) {
|
|
155
|
-
expressions.push(
|
|
156
|
-
field.scalarKind === "decimal"
|
|
157
|
-
? `((properties ->> ${literal(field.propertyId)})::numeric)`
|
|
158
|
-
: `(NULLIF(properties -> ${literal(field.propertyId)}, 'null'::jsonb))`
|
|
159
|
-
)
|
|
160
|
-
}
|
|
170
|
+
if (index.kind === "sort") for (const field of index.fields) expressions.push(sortValue(field))
|
|
161
171
|
return expressions
|
|
162
172
|
}
|
|
163
173
|
|
|
@@ -220,7 +230,7 @@ function compileIndex(index: PgObjectQueryIndex): string {
|
|
|
220
230
|
const property = `properties -> ${literal(field.propertyId)}`
|
|
221
231
|
fields.push(
|
|
222
232
|
`(CASE WHEN jsonb_typeof(${property}) IS NULL OR jsonb_typeof(${property}) = 'null' THEN 1 ELSE 0 END)`,
|
|
223
|
-
`${
|
|
233
|
+
`${sortValue(field)} ${field.direction === "desc" ? "DESC" : "ASC"}`
|
|
224
234
|
)
|
|
225
235
|
}
|
|
226
236
|
fields.push("primary_id")
|
|
@@ -228,6 +238,14 @@ function compileIndex(index: PgObjectQueryIndex): string {
|
|
|
228
238
|
return `(${fields.join(", ")}) WHERE ${predicate.join(" AND ")}`
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
/** The query compiler's ordered value expression; an index serves a sort only when they match. */
|
|
242
|
+
function sortValue(field: Extract<PgObjectQueryIndex, { kind: "sort" }>["fields"][number]): string {
|
|
243
|
+
const text = `(properties ->> ${literal(field.propertyId)})`
|
|
244
|
+
if (field.scalarKind === "decimal") return `(${text}::numeric)`
|
|
245
|
+
if (isTextScalarKind(field.scalarKind)) return `(${text} COLLATE "C")`
|
|
246
|
+
return `(NULLIF(properties -> ${literal(field.propertyId)}, 'null'::jsonb))`
|
|
247
|
+
}
|
|
248
|
+
|
|
231
249
|
function literal(value: string): string {
|
|
232
250
|
if (value.includes("\0"))
|
|
233
251
|
throw new Error("[SixbPg] Query index names and values cannot contain NUL")
|
|
@@ -337,7 +355,8 @@ export async function findPgSortIndexBound(
|
|
|
337
355
|
fields[i]?.kind === "property" &&
|
|
338
356
|
fields[i].propertyId === field.propertyId &&
|
|
339
357
|
(field.direction ?? "asc") === (fields[i].direction ?? "asc") &&
|
|
340
|
-
(field.scalarKind === "decimal") === (fields[i].scalarKind === "decimal")
|
|
358
|
+
(field.scalarKind === "decimal") === (fields[i].scalarKind === "decimal") &&
|
|
359
|
+
isTextScalarKind(field.scalarKind) === isTextScalarKind(fields[i].scalarKind)
|
|
341
360
|
)
|
|
342
361
|
)
|
|
343
362
|
continue
|