dzql 0.5.33 → 0.6.1

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 (142) hide show
  1. package/.env.sample +28 -0
  2. package/compose.yml +28 -0
  3. package/dist/client/index.ts +1 -0
  4. package/dist/client/stores/useMyProfileStore.ts +114 -0
  5. package/dist/client/stores/useOrgDashboardStore.ts +131 -0
  6. package/dist/client/stores/useVenueDetailStore.ts +117 -0
  7. package/dist/client/ws.ts +716 -0
  8. package/dist/db/migrations/000_core.sql +92 -0
  9. package/dist/db/migrations/20251229T212912022Z_schema.sql +3020 -0
  10. package/dist/db/migrations/20251229T212912022Z_subscribables.sql +371 -0
  11. package/dist/runtime/manifest.json +1562 -0
  12. package/docs/README.md +309 -36
  13. package/docs/feature-requests/applyPatch-bug-report.md +85 -0
  14. package/docs/feature-requests/connection-ready-profile.md +57 -0
  15. package/docs/feature-requests/hidden-bug-report.md +111 -0
  16. package/docs/feature-requests/hidden-fields-subscribables.md +34 -0
  17. package/docs/feature-requests/subscribable-param-key-bug.md +38 -0
  18. package/docs/feature-requests/todo.md +146 -0
  19. package/docs/for_ai.md +653 -0
  20. package/docs/project-setup.md +456 -0
  21. package/examples/blog.ts +50 -0
  22. package/examples/invalid.ts +18 -0
  23. package/examples/venues.js +485 -0
  24. package/package.json +23 -60
  25. package/src/cli/codegen/client.ts +99 -0
  26. package/src/cli/codegen/manifest.ts +95 -0
  27. package/src/cli/codegen/pinia.ts +174 -0
  28. package/src/cli/codegen/realtime.ts +58 -0
  29. package/src/cli/codegen/sql.ts +698 -0
  30. package/src/cli/codegen/subscribable_sql.ts +547 -0
  31. package/src/cli/codegen/subscribable_store.ts +184 -0
  32. package/src/cli/codegen/types.ts +142 -0
  33. package/src/cli/compiler/analyzer.ts +52 -0
  34. package/src/cli/compiler/graph_rules.ts +251 -0
  35. package/src/cli/compiler/ir.ts +233 -0
  36. package/src/cli/compiler/loader.ts +132 -0
  37. package/src/cli/compiler/permissions.ts +227 -0
  38. package/src/cli/index.ts +166 -0
  39. package/src/client/index.ts +1 -0
  40. package/src/client/ws.ts +286 -0
  41. package/src/runtime/auth.ts +39 -0
  42. package/src/runtime/db.ts +33 -0
  43. package/src/runtime/errors.ts +51 -0
  44. package/src/runtime/index.ts +98 -0
  45. package/src/runtime/js_functions.ts +63 -0
  46. package/src/runtime/manifest_loader.ts +29 -0
  47. package/src/runtime/namespace.ts +483 -0
  48. package/src/runtime/server.ts +87 -0
  49. package/src/runtime/ws.ts +197 -0
  50. package/src/shared/ir.ts +197 -0
  51. package/tests/client.test.ts +38 -0
  52. package/tests/codegen.test.ts +71 -0
  53. package/tests/compiler.test.ts +45 -0
  54. package/tests/graph_rules.test.ts +173 -0
  55. package/tests/integration/db.test.ts +174 -0
  56. package/tests/integration/e2e.test.ts +65 -0
  57. package/tests/integration/features.test.ts +922 -0
  58. package/tests/integration/full_stack.test.ts +262 -0
  59. package/tests/integration/setup.ts +45 -0
  60. package/tests/ir.test.ts +32 -0
  61. package/tests/namespace.test.ts +395 -0
  62. package/tests/permissions.test.ts +55 -0
  63. package/tests/pinia.test.ts +48 -0
  64. package/tests/realtime.test.ts +22 -0
  65. package/tests/runtime.test.ts +80 -0
  66. package/tests/subscribable_gen.test.ts +72 -0
  67. package/tests/subscribable_reactivity.test.ts +258 -0
  68. package/tests/venues_gen.test.ts +25 -0
  69. package/tsconfig.json +20 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/README.md +0 -90
  72. package/bin/cli.js +0 -727
  73. package/docs/compiler/ADVANCED_FILTERS.md +0 -183
  74. package/docs/compiler/CODING_STANDARDS.md +0 -415
  75. package/docs/compiler/COMPARISON.md +0 -673
  76. package/docs/compiler/QUICKSTART.md +0 -326
  77. package/docs/compiler/README.md +0 -134
  78. package/docs/examples/README.md +0 -38
  79. package/docs/examples/blog.sql +0 -160
  80. package/docs/examples/venue-detail-simple.sql +0 -8
  81. package/docs/examples/venue-detail-subscribable.sql +0 -45
  82. package/docs/for-ai/claude-guide.md +0 -1210
  83. package/docs/getting-started/quickstart.md +0 -125
  84. package/docs/getting-started/subscriptions-quick-start.md +0 -203
  85. package/docs/getting-started/tutorial.md +0 -1104
  86. package/docs/guides/atomic-updates.md +0 -299
  87. package/docs/guides/client-stores.md +0 -730
  88. package/docs/guides/composite-primary-keys.md +0 -158
  89. package/docs/guides/custom-functions.md +0 -362
  90. package/docs/guides/drop-semantics.md +0 -554
  91. package/docs/guides/field-defaults.md +0 -240
  92. package/docs/guides/interpreter-vs-compiler.md +0 -237
  93. package/docs/guides/many-to-many.md +0 -929
  94. package/docs/guides/subscriptions.md +0 -537
  95. package/docs/reference/api.md +0 -1373
  96. package/docs/reference/client.md +0 -224
  97. package/src/client/stores/index.js +0 -8
  98. package/src/client/stores/useAppStore.js +0 -285
  99. package/src/client/stores/useWsStore.js +0 -289
  100. package/src/client/ws.js +0 -762
  101. package/src/compiler/cli/compile-example.js +0 -33
  102. package/src/compiler/cli/compile-subscribable.js +0 -43
  103. package/src/compiler/cli/debug-compile.js +0 -44
  104. package/src/compiler/cli/debug-parse.js +0 -26
  105. package/src/compiler/cli/debug-path-parser.js +0 -18
  106. package/src/compiler/cli/debug-subscribable-parser.js +0 -21
  107. package/src/compiler/cli/index.js +0 -174
  108. package/src/compiler/codegen/auth-codegen.js +0 -153
  109. package/src/compiler/codegen/drop-semantics-codegen.js +0 -553
  110. package/src/compiler/codegen/graph-rules-codegen.js +0 -450
  111. package/src/compiler/codegen/notification-codegen.js +0 -232
  112. package/src/compiler/codegen/operation-codegen.js +0 -1382
  113. package/src/compiler/codegen/permission-codegen.js +0 -318
  114. package/src/compiler/codegen/subscribable-codegen.js +0 -827
  115. package/src/compiler/compiler.js +0 -371
  116. package/src/compiler/index.js +0 -11
  117. package/src/compiler/parser/entity-parser.js +0 -440
  118. package/src/compiler/parser/path-parser.js +0 -290
  119. package/src/compiler/parser/subscribable-parser.js +0 -244
  120. package/src/database/dzql-core.sql +0 -161
  121. package/src/database/migrations/001_schema.sql +0 -60
  122. package/src/database/migrations/002_functions.sql +0 -890
  123. package/src/database/migrations/003_operations.sql +0 -1135
  124. package/src/database/migrations/004_search.sql +0 -581
  125. package/src/database/migrations/005_entities.sql +0 -730
  126. package/src/database/migrations/006_auth.sql +0 -94
  127. package/src/database/migrations/007_events.sql +0 -133
  128. package/src/database/migrations/008_hello.sql +0 -18
  129. package/src/database/migrations/008a_meta.sql +0 -172
  130. package/src/database/migrations/009_subscriptions.sql +0 -240
  131. package/src/database/migrations/010_atomic_updates.sql +0 -157
  132. package/src/database/migrations/010_fix_m2m_events.sql +0 -94
  133. package/src/index.js +0 -40
  134. package/src/server/api.js +0 -9
  135. package/src/server/db.js +0 -442
  136. package/src/server/index.js +0 -317
  137. package/src/server/logger.js +0 -259
  138. package/src/server/mcp.js +0 -594
  139. package/src/server/meta-route.js +0 -251
  140. package/src/server/namespace.js +0 -292
  141. package/src/server/subscriptions.js +0 -351
  142. package/src/server/ws.js +0 -573
@@ -0,0 +1,173 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { compileGraphRules } from "../src/cli/compiler/graph_rules.js";
3
+ import type { GraphRuleIR } from "../src/shared/ir.js";
4
+
5
+ // Mock entity graph rules in IR format
6
+ const mockGraphRules = {
7
+ onCreate: [
8
+ {
9
+ trigger: 'create' as const,
10
+ action: 'reactor' as const,
11
+ target: 'notify_subscribers',
12
+ params: { post_id: '@id' }
13
+ }
14
+ ] as GraphRuleIR[],
15
+ onDelete: [
16
+ {
17
+ trigger: 'delete' as const,
18
+ action: 'delete' as const,
19
+ target: 'comments',
20
+ params: { post_id: '@id' }
21
+ }
22
+ ] as GraphRuleIR[]
23
+ };
24
+
25
+ describe("Graph Rules Compiler", () => {
26
+
27
+ test("should compile reactor to event insertion", () => {
28
+ // Should insert into dzql_v2.events with op='reactor:notify_subscribers'
29
+ const sql = compileGraphRules('posts', 'create', mockGraphRules.onCreate);
30
+
31
+ expect(sql).toContain("INSERT INTO dzql_v2.events");
32
+ expect(sql).toContain("'reactor:notify_subscribers'");
33
+ // Simply check for the variable resolution logic
34
+ expect(sql).toContain("v_result->>'id'");
35
+ });
36
+
37
+ test("should compile delete action to SQL delete", () => {
38
+ // Should generate DELETE FROM comments ...
39
+ const sql = compileGraphRules('posts', 'delete', mockGraphRules.onDelete);
40
+
41
+ expect(sql).toContain("DELETE FROM comments");
42
+ expect(sql).toContain("post_id = (v_old_data->>'id')::int"); // Variable resolution from old record
43
+ });
44
+
45
+ test("should compile create action with field defaults", () => {
46
+ const rules: GraphRuleIR[] = [{
47
+ trigger: 'create',
48
+ action: 'create',
49
+ target: 'memberships',
50
+ params: { user_id: '@user_id', org_id: '@id', valid_from: '@today' }
51
+ }];
52
+
53
+ const sql = compileGraphRules('organisations', 'create', rules);
54
+
55
+ expect(sql).toContain("INSERT INTO memberships");
56
+ expect(sql).toContain("user_id, org_id, valid_from");
57
+ expect(sql).toContain("p_user_id");
58
+ expect(sql).toContain("(v_result->>'id')::int");
59
+ expect(sql).toContain("CURRENT_DATE");
60
+ });
61
+
62
+ test("should compile update action with match clause", () => {
63
+ const rules: GraphRuleIR[] = [{
64
+ trigger: 'update',
65
+ action: 'update',
66
+ target: 'audit_log',
67
+ params: { last_modified: '@now' },
68
+ match: { entity_id: '@id' }
69
+ }];
70
+
71
+ const sql = compileGraphRules('posts', 'update', rules);
72
+
73
+ expect(sql).toContain("UPDATE audit_log");
74
+ expect(sql).toContain("SET last_modified = NOW()");
75
+ expect(sql).toContain("WHERE entity_id = (v_result->>'id')::int");
76
+ });
77
+
78
+ test("should compile validate action", () => {
79
+ const rules: GraphRuleIR[] = [{
80
+ trigger: 'create',
81
+ action: 'validate',
82
+ target: 'check_quota',
83
+ params: { user_id: '@user_id' },
84
+ error_message: 'Quota exceeded'
85
+ }];
86
+
87
+ const sql = compileGraphRules('posts', 'create', rules);
88
+
89
+ expect(sql).toContain("IF NOT check_quota(");
90
+ expect(sql).toContain("user_id => p_user_id");
91
+ expect(sql).toContain("RAISE EXCEPTION 'Quota exceeded'");
92
+ });
93
+
94
+ test("should compile execute action", () => {
95
+ const rules: GraphRuleIR[] = [{
96
+ trigger: 'create',
97
+ action: 'execute',
98
+ target: 'send_notification',
99
+ params: { post_id: '@id', author_id: '@author_id' }
100
+ }];
101
+
102
+ const sql = compileGraphRules('posts', 'create', rules);
103
+
104
+ expect(sql).toContain("PERFORM send_notification(");
105
+ expect(sql).toContain("post_id => (v_result->>'id')");
106
+ expect(sql).toContain("author_id => (v_result->>'author_id')");
107
+ });
108
+
109
+ test("should compile conditional rule with @before/@after", () => {
110
+ const rules: GraphRuleIR[] = [{
111
+ trigger: 'update',
112
+ action: 'reactor',
113
+ target: 'status_changed',
114
+ condition: "@before.status = 'draft' AND @after.status = 'published'",
115
+ params: { post_id: '@id' }
116
+ }];
117
+
118
+ const sql = compileGraphRules('posts', 'update', rules);
119
+
120
+ expect(sql).toContain("-- Condition:");
121
+ expect(sql).toContain("IF (v_old_data->>'status') = 'draft' AND (v_result->>'status') = 'published' THEN");
122
+ expect(sql).toContain("INSERT INTO dzql_v2.events");
123
+ expect(sql).toContain("END IF;");
124
+ });
125
+
126
+ test("should resolve @before variables in delete trigger", () => {
127
+ const rules: GraphRuleIR[] = [{
128
+ trigger: 'delete',
129
+ action: 'execute',
130
+ target: 'archive_post',
131
+ params: { title: '@before.title', author_id: '@before.author_id' }
132
+ }];
133
+
134
+ const sql = compileGraphRules('posts', 'delete', rules);
135
+
136
+ expect(sql).toContain("PERFORM archive_post(");
137
+ expect(sql).toContain("title => (v_old_data->>'title')");
138
+ // Note: execute/validate params don't get ::int cast - they're named parameters
139
+ expect(sql).toContain("author_id => (v_old_data->>'author_id')");
140
+ });
141
+
142
+ test("should handle multiple actions", () => {
143
+ const rules: GraphRuleIR[] = [
144
+ {
145
+ trigger: 'create',
146
+ action: 'validate',
147
+ target: 'check_permissions',
148
+ params: { user_id: '@user_id' },
149
+ error_message: 'Not allowed'
150
+ },
151
+ {
152
+ trigger: 'create',
153
+ action: 'create',
154
+ target: 'audit_log',
155
+ params: { entity_id: '@id', action: 'created' }
156
+ },
157
+ {
158
+ trigger: 'create',
159
+ action: 'reactor',
160
+ target: 'notify_admins',
161
+ params: { post_id: '@id' }
162
+ }
163
+ ];
164
+
165
+ const sql = compileGraphRules('posts', 'create', rules);
166
+
167
+ // Should have all three actions
168
+ expect(sql).toContain("IF NOT check_permissions");
169
+ expect(sql).toContain("INSERT INTO audit_log");
170
+ expect(sql).toContain("reactor:notify_admins");
171
+ });
172
+
173
+ });
@@ -0,0 +1,174 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { V2TestDatabase } from "./setup.js";
3
+ import { generateCoreSQL, generateEntitySQL, generateSchemaSQL } from "../../src/cli/codegen/sql.js";
4
+ import { generateIR } from "../../src/cli/compiler/ir.js";
5
+ import { entities } from "../../examples/blog.js";
6
+
7
+ const blogDomain = { entities, subscribables: {} };
8
+
9
+ describe("V2 Database Integration (Real Postgres)", () => {
10
+ let db: V2TestDatabase;
11
+ let sql: any;
12
+
13
+ beforeAll(async () => {
14
+ db = new V2TestDatabase();
15
+ // This connects to localhost:5433 by default (from setup.ts)
16
+ sql = await db.setup();
17
+
18
+ try {
19
+ // 1. Generate SQL
20
+ const ir = generateIR(blogDomain);
21
+ const coreSQL = generateCoreSQL();
22
+ const postsSchema = generateSchemaSQL("posts", ir.entities.posts);
23
+ const commentsSchema = generateSchemaSQL("comments", ir.entities.comments);
24
+ const postsSQL = generateEntitySQL("posts", ir.entities.posts);
25
+ const commentsSQL = generateEntitySQL("comments", ir.entities.comments);
26
+
27
+ // 2. Apply SQL to the real DB
28
+ await db.applySQL(coreSQL);
29
+ await db.applySQL(postsSchema);
30
+ await db.applySQL(commentsSchema);
31
+ await db.applySQL(postsSQL);
32
+ await db.applySQL(commentsSQL);
33
+ } catch (e) {
34
+ console.error("Setup failed:", e);
35
+ throw e;
36
+ }
37
+ });
38
+
39
+ afterAll(async () => {
40
+ await db.teardown();
41
+ });
42
+
43
+ test("should save a post via compiled function (Atomic Upsert)", async () => {
44
+ const userId = 1;
45
+ const postData = { id: 1, title: "Real DB Test", content: "It works!", author_id: 1 };
46
+
47
+ // Call the compiled PL/pgSQL function
48
+ const result = await sql`
49
+ SELECT dzql_v2.save_posts(${userId}, ${sql.json(postData)}) as data
50
+ `;
51
+
52
+ const saved = result[0].data;
53
+ expect(saved.id).toBe(1);
54
+ expect(saved.title).toBe("Real DB Test");
55
+
56
+ // Verify persistence
57
+ const rows = await sql`SELECT * FROM posts WHERE id = 1`;
58
+ expect(rows.length).toBe(1);
59
+ expect(rows[0].title).toBe("Real DB Test");
60
+ });
61
+
62
+ test("should enforce permissions (Inlined SQL)", async () => {
63
+ const hackerId = 2;
64
+ const postData = { id: 2, title: "Hacked", content: "Bad", author_id: 1 };
65
+
66
+ // Should fail because @author_id (1) != @user_id (2)
67
+ try {
68
+ await sql`
69
+ SELECT dzql_v2.save_posts(${hackerId}, ${sql.json(postData)})
70
+ `;
71
+ expect(true).toBe(false); // Should not fail
72
+ } catch (e: any) {
73
+ expect(e.message).toContain("permission_denied");
74
+ }
75
+ });
76
+
77
+ test("should emit normalized row events (Commit Batching)", async () => {
78
+ // We expect the 'insert' from the first test to be in the events table
79
+ const events = await sql`
80
+ SELECT * FROM dzql_v2.events
81
+ WHERE table_name = 'posts'
82
+ ORDER BY id DESC LIMIT 1
83
+ `;
84
+
85
+ expect(events.length).toBe(1);
86
+ expect(events[0].op).toBe("insert");
87
+ expect(events[0].data.title).toBe("Real DB Test");
88
+ expect(events[0].old_data).toBeNull(); // Insert has no old data
89
+ expect(events[0].commit_id).toBeDefined(); // Ensure commit ID was generated
90
+ });
91
+
92
+ test("should populate old_data on update", async () => {
93
+ const userId = 1;
94
+ // Update the existing post (id: 1)
95
+ const updateData = { id: 1, title: "Updated Title" };
96
+
97
+ await sql`
98
+ SELECT dzql_v2.save_posts(${userId}, ${sql.json(updateData)})
99
+ `;
100
+
101
+ const events = await sql`
102
+ SELECT * FROM dzql_v2.events
103
+ WHERE table_name = 'posts' AND op = 'update'
104
+ ORDER BY id DESC LIMIT 1
105
+ `;
106
+
107
+ expect(events.length).toBe(1);
108
+ expect(events[0].op).toBe("update");
109
+ expect(events[0].data.title).toBe("Updated Title");
110
+ expect(events[0].old_data).not.toBeNull();
111
+ expect(events[0].old_data.title).toBe("Real DB Test"); // Previous title
112
+ });
113
+
114
+ test("should trigger reactors (Graph Rules)", async () => {
115
+ // The blog.ts example has a reactor 'notify_subscribers' on create
116
+ const reactorEvents = await sql`
117
+ SELECT * FROM dzql_v2.events
118
+ WHERE op LIKE 'reactor:%'
119
+ ORDER BY id DESC LIMIT 1
120
+ `;
121
+
122
+ expect(reactorEvents.length).toBe(1);
123
+ expect(reactorEvents[0].op).toBe("reactor:notify_subscribers");
124
+ expect(reactorEvents[0].data.post_id).toBe("1");
125
+ });
126
+
127
+ test("should delete a post via compiled function (Atomic Delete)", async () => {
128
+ const userId = 1;
129
+ // Call the compiled PL/pgSQL function
130
+ const result = await sql`
131
+ SELECT dzql_v2.delete_posts(${userId}, ${sql.json({ id: 1 })}) as data
132
+ `;
133
+
134
+ const deleted = result[0].data;
135
+ expect(deleted.id).toBe(1);
136
+
137
+ // Verify gone
138
+ const rows = await sql`SELECT * FROM posts WHERE id = 1`;
139
+ expect(rows.length).toBe(0);
140
+
141
+ // Verify Event
142
+ const events = await sql`
143
+ SELECT * FROM dzql_v2.events
144
+ WHERE table_name = 'posts' AND op = 'delete'
145
+ ORDER BY id DESC LIMIT 1
146
+ `;
147
+ expect(events.length).toBe(1);
148
+ expect(events[0].pk.id).toBe(1);
149
+ });
150
+
151
+ test("should get a post via compiled function", async () => {
152
+ // Re-create post first
153
+ await sql`INSERT INTO posts (id, title, author_id) VALUES (100, 'Get Test', 1)`;
154
+
155
+ const userId = 1;
156
+ const result = await sql`
157
+ SELECT dzql_v2.get_posts(${userId}, ${sql.json({ id: 100 })}) as data
158
+ `;
159
+
160
+ expect(result[0].data.title).toBe("Get Test");
161
+ });
162
+
163
+ test("should search posts via compiled function", async () => {
164
+ // Search
165
+ const userId = 1;
166
+ const result = await sql`
167
+ SELECT dzql_v2.search_posts(${userId}, ${sql.json({ limit: 5 })}) as data
168
+ `;
169
+
170
+ expect(result[0].data).toBeArray();
171
+ expect(result[0].data.length).toBeGreaterThan(0);
172
+ expect(result[0].data[0].title).toBe("Get Test");
173
+ });
174
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, test, expect, beforeAll } from "bun:test";
2
+ import { generateCoreSQL, generateEntitySQL } from "../../src/cli/codegen/sql.js";
3
+ import { generateIR } from "../../src/cli/compiler/ir.js";
4
+
5
+ // Load our blog example directly (bypassing loader for test isolation)
6
+ import { entities } from "../../examples/blog.js";
7
+
8
+ const blogDomain = { entities, subscribables: {} };
9
+
10
+ describe("V2 End-to-End Compiler Integration", () => {
11
+ let ir: any;
12
+ let coreSQL: string;
13
+ let postsSQL: string;
14
+ let commentsSQL: string;
15
+
16
+ beforeAll(() => {
17
+ // 1. Generate IR from Domain Object
18
+ ir = generateIR(blogDomain);
19
+
20
+ // 2. Generate SQL
21
+ coreSQL = generateCoreSQL();
22
+ postsSQL = generateEntitySQL("posts", ir.entities.posts);
23
+ commentsSQL = generateEntitySQL("comments", ir.entities.comments);
24
+ });
25
+
26
+ test("should generate valid IR for blog", () => {
27
+ expect(ir.entities.posts).toBeDefined();
28
+ expect(ir.entities.posts.primaryKey).toEqual(["id"]);
29
+ // Check permission parsing
30
+ expect(ir.entities.posts.permissions.create).toEqual(['@author_id == @user_id']);
31
+ });
32
+
33
+ test("should generate core SQL schema", () => {
34
+ expect(coreSQL).toContain("CREATE SCHEMA IF NOT EXISTS dzql_v2");
35
+ expect(coreSQL).toContain("CREATE TABLE IF NOT EXISTS dzql_v2.events");
36
+ });
37
+
38
+ test("should generate atomic UPSERT for posts", () => {
39
+ // Check function signature
40
+ expect(postsSQL).toContain("CREATE OR REPLACE FUNCTION dzql_v2.save_posts");
41
+
42
+ // Check security
43
+ expect(postsSQL).toContain("SECURITY DEFINER");
44
+ expect(postsSQL).toContain("SET search_path = dzql_v2, public");
45
+
46
+ // Check Update/Insert Branching (New V2 Pattern with composite PK support)
47
+ expect(postsSQL).toContain("AND EXISTS(SELECT 1 FROM posts WHERE");
48
+ expect(postsSQL).toContain("UPDATE posts SET");
49
+ expect(postsSQL).toContain("INSERT INTO posts");
50
+
51
+ // Check column handling in update
52
+ expect(postsSQL).toContain("title = CASE WHEN (p_data ? 'title')");
53
+ });
54
+
55
+ test("should inline permission checks", () => {
56
+ // Check that @author_id == @user_id was compiled correctly
57
+ expect(postsSQL).toContain("IF NOT ((p_data->>'author_id')::int = p_user_id) THEN");
58
+ });
59
+
60
+ test("should compile graph rules", () => {
61
+ // Check reactor trigger
62
+ expect(postsSQL).toContain("INSERT INTO dzql_v2.events");
63
+ expect(postsSQL).toContain("'reactor:notify_subscribers'");
64
+ });
65
+ });