@reasoningco/infer 0.1.4 → 0.1.6

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/dist/schemas.d.ts CHANGED
@@ -2,15 +2,15 @@ import { z } from "zod";
2
2
  export declare const FilterSchema: z.ZodObject<{
3
3
  field: z.ZodString;
4
4
  op: z.ZodEnum<["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]>;
5
- value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>;
5
+ value: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>>;
6
6
  }, "strip", z.ZodTypeAny, {
7
- value: string | number | boolean | (string | number)[];
8
7
  field: string;
9
8
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
9
+ value?: string | number | boolean | (string | number)[] | undefined;
10
10
  }, {
11
- value: string | number | boolean | (string | number)[];
12
11
  field: string;
13
12
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
13
+ value?: string | number | boolean | (string | number)[] | undefined;
14
14
  }>;
15
15
  export declare const EntityFilterSchema: z.ZodObject<{
16
16
  resolver: z.ZodString;
@@ -33,15 +33,15 @@ export declare const ExistsFilterSchema: z.ZodObject<{
33
33
  filters: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
34
34
  field: z.ZodString;
35
35
  op: z.ZodEnum<["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]>;
36
- value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>;
36
+ value: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>>;
37
37
  }, "strip", z.ZodTypeAny, {
38
- value: string | number | boolean | (string | number)[];
39
38
  field: string;
40
39
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
40
+ value?: string | number | boolean | (string | number)[] | undefined;
41
41
  }, {
42
- value: string | number | boolean | (string | number)[];
43
42
  field: string;
44
43
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
44
+ value?: string | number | boolean | (string | number)[] | undefined;
45
45
  }>, "many">>>;
46
46
  }, "strip", z.ZodTypeAny, {
47
47
  mode: "not_exists" | "exists";
@@ -49,9 +49,9 @@ export declare const ExistsFilterSchema: z.ZodObject<{
49
49
  joinColumn: string;
50
50
  foreignColumn: string;
51
51
  filters: {
52
- value: string | number | boolean | (string | number)[];
53
52
  field: string;
54
53
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
54
+ value?: string | number | boolean | (string | number)[] | undefined;
55
55
  }[];
56
56
  }, {
57
57
  mode: "not_exists" | "exists";
@@ -59,9 +59,9 @@ export declare const ExistsFilterSchema: z.ZodObject<{
59
59
  joinColumn: string;
60
60
  foreignColumn: string;
61
61
  filters?: {
62
- value: string | number | boolean | (string | number)[];
63
62
  field: string;
64
63
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
64
+ value?: string | number | boolean | (string | number)[] | undefined;
65
65
  }[] | undefined;
66
66
  }>;
67
67
  export declare const DateRangeSchema: z.ZodObject<{
@@ -92,40 +92,44 @@ export declare const OrderBySchema: z.ZodObject<{
92
92
  }>;
93
93
  export declare const SmartQueryPlanSchema: z.ZodObject<{
94
94
  dataset: z.ZodString;
95
- select: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
96
- metrics: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
97
- groupBy: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
95
+ select: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>, string[], unknown>;
96
+ metrics: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>, string[], unknown>;
97
+ groupBy: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>, string[], unknown>;
98
98
  timeBucket: z.ZodEffects<z.ZodOptional<z.ZodEnum<["hour", "day", "week", "month", "quarter", "year"]>>, "month" | "day" | "week" | "quarter" | "year" | "hour" | undefined, unknown>;
99
99
  extractBucket: z.ZodEffects<z.ZodOptional<z.ZodEnum<["hour_of_day", "day_of_week", "month_of_year", "quarter", "year", "week"]>>, "week" | "quarter" | "year" | "hour_of_day" | "day_of_week" | "month_of_year" | undefined, unknown>;
100
- filters: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
100
+ filters: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
101
101
  field: z.ZodString;
102
102
  op: z.ZodEnum<["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]>;
103
- value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>;
103
+ value: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>>;
104
104
  }, "strip", z.ZodTypeAny, {
105
- value: string | number | boolean | (string | number)[];
106
105
  field: string;
107
106
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
107
+ value?: string | number | boolean | (string | number)[] | undefined;
108
108
  }, {
109
- value: string | number | boolean | (string | number)[];
110
109
  field: string;
111
110
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
112
- }>, "many">>>;
111
+ value?: string | number | boolean | (string | number)[] | undefined;
112
+ }>, "many">>>, {
113
+ field: string;
114
+ op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
115
+ value?: string | number | boolean | (string | number)[] | undefined;
116
+ }[], unknown>;
113
117
  having: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
114
118
  field: z.ZodString;
115
119
  op: z.ZodEnum<["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]>;
116
- value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>;
120
+ value: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>>;
117
121
  }, "strip", z.ZodTypeAny, {
118
- value: string | number | boolean | (string | number)[];
119
122
  field: string;
120
123
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
124
+ value?: string | number | boolean | (string | number)[] | undefined;
121
125
  }, {
122
- value: string | number | boolean | (string | number)[];
123
126
  field: string;
124
127
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
128
+ value?: string | number | boolean | (string | number)[] | undefined;
125
129
  }>, "many">>>, {
126
- value: string | number | boolean | (string | number)[];
127
130
  field: string;
128
131
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
132
+ value?: string | number | boolean | (string | number)[] | undefined;
129
133
  }[], unknown>;
130
134
  existsFilters: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
131
135
  mode: z.ZodEnum<["exists", "not_exists"]>;
@@ -135,15 +139,15 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
135
139
  filters: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
136
140
  field: z.ZodString;
137
141
  op: z.ZodEnum<["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]>;
138
- value: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>;
142
+ value: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodArray<z.ZodUnion<[z.ZodString, z.ZodNumber]>, "many">]>>;
139
143
  }, "strip", z.ZodTypeAny, {
140
- value: string | number | boolean | (string | number)[];
141
144
  field: string;
142
145
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
146
+ value?: string | number | boolean | (string | number)[] | undefined;
143
147
  }, {
144
- value: string | number | boolean | (string | number)[];
145
148
  field: string;
146
149
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
150
+ value?: string | number | boolean | (string | number)[] | undefined;
147
151
  }>, "many">>>;
148
152
  }, "strip", z.ZodTypeAny, {
149
153
  mode: "not_exists" | "exists";
@@ -151,9 +155,9 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
151
155
  joinColumn: string;
152
156
  foreignColumn: string;
153
157
  filters: {
154
- value: string | number | boolean | (string | number)[];
155
158
  field: string;
156
159
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
160
+ value?: string | number | boolean | (string | number)[] | undefined;
157
161
  }[];
158
162
  }, {
159
163
  mode: "not_exists" | "exists";
@@ -161,9 +165,9 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
161
165
  joinColumn: string;
162
166
  foreignColumn: string;
163
167
  filters?: {
164
- value: string | number | boolean | (string | number)[];
165
168
  field: string;
166
169
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
170
+ value?: string | number | boolean | (string | number)[] | undefined;
167
171
  }[] | undefined;
168
172
  }>, "many">>>, {
169
173
  mode: "not_exists" | "exists";
@@ -171,12 +175,12 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
171
175
  joinColumn: string;
172
176
  foreignColumn: string;
173
177
  filters: {
174
- value: string | number | boolean | (string | number)[];
175
178
  field: string;
176
179
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
180
+ value?: string | number | boolean | (string | number)[] | undefined;
177
181
  }[];
178
182
  }[], unknown>;
179
- entityFilters: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
183
+ entityFilters: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
180
184
  resolver: z.ZodString;
181
185
  input: z.ZodString;
182
186
  outputColumn: z.ZodString;
@@ -188,7 +192,11 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
188
192
  input: string;
189
193
  resolver: string;
190
194
  outputColumn: string;
191
- }>, "many">>>;
195
+ }>, "many">>>, {
196
+ input: string;
197
+ resolver: string;
198
+ outputColumn: string;
199
+ }[], unknown>;
192
200
  dateRange: z.ZodEffects<z.ZodOptional<z.ZodObject<{
193
201
  mode: z.ZodEnum<["relative", "explicit", "latest", "none"]>;
194
202
  phrase: z.ZodOptional<z.ZodString>;
@@ -210,7 +218,7 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
210
218
  endDate?: string | undefined;
211
219
  phrase?: string | undefined;
212
220
  } | undefined, unknown>;
213
- orderBy: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
221
+ orderBy: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
214
222
  field: z.ZodString;
215
223
  dir: z.ZodDefault<z.ZodEnum<["asc", "desc"]>>;
216
224
  }, "strip", z.ZodTypeAny, {
@@ -219,22 +227,25 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
219
227
  }, {
220
228
  field: string;
221
229
  dir?: "desc" | "asc" | undefined;
222
- }>, "many">>>;
230
+ }>, "many">>>, {
231
+ field: string;
232
+ dir: "desc" | "asc";
233
+ }[], unknown>;
223
234
  limit: z.ZodEffects<z.ZodOptional<z.ZodNumber>, number | undefined, unknown>;
224
235
  answerMode: z.ZodEffects<z.ZodDefault<z.ZodOptional<z.ZodEnum<["rows", "aggregate_table", "scalar"]>>>, "aggregate_table" | "rows" | "scalar", unknown>;
225
236
  compareMode: z.ZodEffects<z.ZodOptional<z.ZodEnum<["previous_period", "year_over_year"]>>, "previous_period" | "year_over_year" | undefined, unknown>;
226
237
  }, "strip", z.ZodTypeAny, {
227
238
  select: string[];
228
239
  having: {
229
- value: string | number | boolean | (string | number)[];
230
240
  field: string;
231
241
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
242
+ value?: string | number | boolean | (string | number)[] | undefined;
232
243
  }[];
233
244
  dataset: string;
234
245
  filters: {
235
- value: string | number | boolean | (string | number)[];
236
246
  field: string;
237
247
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
248
+ value?: string | number | boolean | (string | number)[] | undefined;
238
249
  }[];
239
250
  metrics: string[];
240
251
  groupBy: string[];
@@ -244,9 +255,9 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
244
255
  joinColumn: string;
245
256
  foreignColumn: string;
246
257
  filters: {
247
- value: string | number | boolean | (string | number)[];
248
258
  field: string;
249
259
  op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
260
+ value?: string | number | boolean | (string | number)[] | undefined;
250
261
  }[];
251
262
  }[];
252
263
  entityFilters: {
@@ -271,28 +282,17 @@ export declare const SmartQueryPlanSchema: z.ZodObject<{
271
282
  compareMode?: "previous_period" | "year_over_year" | undefined;
272
283
  }, {
273
284
  dataset: string;
274
- select?: string[] | undefined;
285
+ select?: unknown;
275
286
  having?: unknown;
276
- filters?: {
277
- value: string | number | boolean | (string | number)[];
278
- field: string;
279
- op: "=" | ">" | "!=" | "<" | ">=" | "<=" | "in" | "not_in" | "like" | "ilike" | "is_null" | "is_not_null";
280
- }[] | undefined;
281
- metrics?: string[] | undefined;
282
- groupBy?: string[] | undefined;
287
+ filters?: unknown;
288
+ metrics?: unknown;
289
+ groupBy?: unknown;
283
290
  timeBucket?: unknown;
284
291
  extractBucket?: unknown;
285
292
  existsFilters?: unknown;
286
- entityFilters?: {
287
- input: string;
288
- resolver: string;
289
- outputColumn: string;
290
- }[] | undefined;
293
+ entityFilters?: unknown;
291
294
  dateRange?: unknown;
292
- orderBy?: {
293
- field: string;
294
- dir?: "desc" | "asc" | undefined;
295
- }[] | undefined;
295
+ orderBy?: unknown;
296
296
  limit?: unknown;
297
297
  answerMode?: unknown;
298
298
  compareMode?: unknown;
package/dist/schemas.js CHANGED
@@ -5,10 +5,10 @@ export const FilterSchema = z.object({
5
5
  op: z.enum(["=", "!=", ">", "<", ">=", "<=", "in", "not_in", "like", "ilike", "is_null", "is_not_null"]),
6
6
  value: z.union([
7
7
  z.string(),
8
- z.number(),
8
+ z.number().finite(),
9
9
  z.boolean(),
10
- z.array(z.union([z.string(), z.number()])),
11
- ]),
10
+ z.array(z.union([z.string(), z.number().finite()])),
11
+ ]).optional(),
12
12
  });
13
13
  // ── Entity filter schema ──
14
14
  export const EntityFilterSchema = z.object({
@@ -39,15 +39,15 @@ export const OrderBySchema = z.object({
39
39
  // ── Smart query plan schema ──
40
40
  export const SmartQueryPlanSchema = z.object({
41
41
  dataset: z.string(),
42
- select: z.array(z.string()).optional().default([]),
43
- metrics: z.array(z.string()).optional().default([]),
44
- groupBy: z.array(z.string()).optional().default([]),
42
+ select: z.preprocess((v) => (v === null ? [] : v), z.array(z.string()).optional().default([])),
43
+ metrics: z.preprocess((v) => (v === null ? [] : v), z.array(z.string()).optional().default([])),
44
+ groupBy: z.preprocess((v) => (v === null ? [] : v), z.array(z.string()).optional().default([])),
45
45
  timeBucket: z.preprocess((v) => (v === null ? undefined : v), z.enum(["hour", "day", "week", "month", "quarter", "year"]).optional()),
46
46
  extractBucket: z.preprocess((v) => (v === null ? undefined : v), z.enum(["hour_of_day", "day_of_week", "month_of_year", "quarter", "year", "week"]).optional()),
47
- filters: z.array(FilterSchema).optional().default([]),
47
+ filters: z.preprocess((v) => (v === null ? [] : v), z.array(FilterSchema).optional().default([])),
48
48
  having: z.preprocess((v) => (v === null ? [] : v), z.array(FilterSchema).optional().default([])),
49
49
  existsFilters: z.preprocess((v) => (v === null ? [] : v), z.array(ExistsFilterSchema).optional().default([])),
50
- entityFilters: z.array(EntityFilterSchema).optional().default([]),
50
+ entityFilters: z.preprocess((v) => (v === null ? [] : v), z.array(EntityFilterSchema).optional().default([])),
51
51
  dateRange: z.preprocess((v) => {
52
52
  if (v === null)
53
53
  return undefined;
@@ -55,8 +55,8 @@ export const SmartQueryPlanSchema = z.object({
55
55
  return undefined;
56
56
  return v;
57
57
  }, DateRangeSchema.optional()),
58
- orderBy: z.array(OrderBySchema).optional().default([]),
59
- limit: z.preprocess((v) => (v === null ? undefined : v), z.number().int().positive().max(200).optional()),
58
+ orderBy: z.preprocess((v) => (v === null ? [] : v), z.array(OrderBySchema).optional().default([])),
59
+ limit: z.preprocess((v) => (v === null ? undefined : v), z.number().finite().int().positive().max(200).optional()),
60
60
  answerMode: z.preprocess((v) => (v === null ? undefined : v), z.enum(["rows", "aggregate_table", "scalar"]).optional().default("rows")),
61
61
  compareMode: z.preprocess((v) => (v === null ? undefined : v), z.enum(['previous_period', 'year_over_year']).optional()),
62
62
  });
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sql-builder.d.ts","sourceRoot":"","sources":["../src/sql-builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpF,iBAAS,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,OAAO,YAAY,EAAE,YAAY,GAAG,WAAW,CA6VpI"}
1
+ {"version":3,"file":"sql-builder.d.ts","sourceRoot":"","sources":["../src/sql-builder.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIpF,iBAAS,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,OAAO,YAAY,EAAE,YAAY,GAAG,WAAW,CAwZpI"}
@@ -2,8 +2,8 @@ import { InferError } from './errors.js';
2
2
  // Use pg driver's native escapeIdentifier pattern
3
3
  // Since we can't import pg at compile time (it's optional), we implement the safe quoting
4
4
  function quoteIdentifier(id) {
5
- // Validate: identifiers must be alphanumeric + underscore (no SQL injection via identifiers)
6
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id)) {
5
+ // Validate: identifiers must be alphanumeric, underscore, or hyphen (no SQL injection)
6
+ if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(id)) {
7
7
  throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid identifier: ${id}`);
8
8
  }
9
9
  return `"${id.replace(/"/g, '""')}"`;
@@ -29,6 +29,7 @@ export function compileSQL(plan, ctx, catalog) {
29
29
  // ── Auto-resolve FK columns to human-readable names ──
30
30
  const fkNameColumns = [];
31
31
  const fkGroupByRefs = [];
32
+ const fkLabelAliases = new Set();
32
33
  if (catalog && plan.dataset.joins) {
33
34
  const columnsToResolve = plan.metrics.length > 0 ? plan.groupBy : plan.select;
34
35
  for (const col of columnsToResolve) {
@@ -45,16 +46,39 @@ export function compileSQL(plan, ctx, catalog) {
45
46
  const nameCol = targetDs.selectableColumns.find(c => ['name', 'title', 'label', 'display_name', 'full_name', 'username', 'email'].includes(c));
46
47
  if (!nameCol)
47
48
  continue;
48
- if (!joinAliases.has(bareTable)) {
49
+ let alias = bareTable;
50
+ if (joinAliases.has(bareTable)) {
51
+ // Table already joined — use a unique alias to avoid conflicts (e.g. two FKs to same table)
52
+ alias = `${bareTable}__${col}`;
53
+ if (!joinAliases.has(alias)) {
54
+ const targetFull = `${quoteIdentifier(targetDs.table.schema)}.${quoteIdentifier(targetDs.table.name)}`;
55
+ joinAliases.set(alias, alias);
56
+ joinClauses.push(`LEFT JOIN ${targetFull} AS ${quoteIdentifier(alias)} ON ${quoteIdentifier(mainAlias)}.${quoteIdentifier(col)} = ${quoteIdentifier(alias)}.${quoteIdentifier(joinDef.foreignColumn)}`);
57
+ }
58
+ }
59
+ else {
49
60
  const targetFull = `${quoteIdentifier(targetDs.table.schema)}.${quoteIdentifier(targetDs.table.name)}`;
50
61
  joinAliases.set(bareTable, bareTable);
51
62
  joinClauses.push(`LEFT JOIN ${targetFull} AS ${quoteIdentifier(bareTable)} ON ${quoteIdentifier(mainAlias)}.${quoteIdentifier(col)} = ${quoteIdentifier(bareTable)}.${quoteIdentifier(joinDef.foreignColumn)}`);
52
63
  }
53
- const fkRef = `${quoteIdentifier(bareTable)}.${quoteIdentifier(nameCol)}`;
54
- fkNameColumns.push(`${fkRef} AS ${quoteIdentifier(col.replace(/_id$/, '_name'))}`);
64
+ const fkRef = `${quoteIdentifier(alias)}.${quoteIdentifier(nameCol)}`;
65
+ // Use a unique alias that won't collide with existing columns
66
+ const fkAlias = col.replace(/_id$/, '') + '_name';
67
+ const safeFkAlias = plan.dataset.selectableColumns.includes(fkAlias) ? `${fkAlias}_fk` : fkAlias;
68
+ fkNameColumns.push(`${fkRef} AS ${quoteIdentifier(safeFkAlias)}`);
55
69
  fkGroupByRefs.push(fkRef);
70
+ fkLabelAliases.add(safeFkAlias);
56
71
  }
57
72
  }
73
+ // ── Validate time/extract buckets at runtime ──
74
+ const VALID_TIME_BUCKETS = new Set(['hour', 'day', 'week', 'month', 'quarter', 'year']);
75
+ const VALID_EXTRACT_BUCKETS = new Set(['hour_of_day', 'day_of_week', 'month_of_year', 'quarter', 'year', 'week']);
76
+ if (plan.timeBucket && !VALID_TIME_BUCKETS.has(plan.timeBucket)) {
77
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid timeBucket: ${plan.timeBucket}`);
78
+ }
79
+ if (plan.extractBucket && !VALID_EXTRACT_BUCKETS.has(plan.extractBucket)) {
80
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid extractBucket: ${plan.extractBucket}`);
81
+ }
58
82
  // ── Determine which groupBy columns need DATE_TRUNC or EXTRACT ──
59
83
  const dateGroupColumns = new Set();
60
84
  const extractGroupColumns = new Set();
@@ -83,7 +107,7 @@ export function compileSQL(plan, ctx, catalog) {
83
107
  case 'quarter': return `EXTRACT(QUARTER FROM ${ref})`;
84
108
  case 'year': return `EXTRACT(YEAR FROM ${ref})`;
85
109
  case 'week': return `EXTRACT(WEEK FROM ${ref})`;
86
- default: return ref;
110
+ default: throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid extractBucket: ${plan.extractBucket}`);
87
111
  }
88
112
  };
89
113
  // Helper: resolve a column reference (supports dot-notation for joins)
@@ -147,11 +171,11 @@ export function compileSQL(plan, ctx, catalog) {
147
171
  return expr;
148
172
  // Replace bare column refs (not already qualified with table.) inside the expression
149
173
  // Match word chars that aren't SQL keywords or already qualified
150
- return expr.replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"/g, (match, colName) => {
174
+ return expr.replace(/(?<!\.)("([a-zA-Z_][a-zA-Z0-9_]*)")(?!\.)/g, (match, quoted, colName) => {
151
175
  // Check if this column exists on the main dataset
152
176
  if (plan.dataset.selectableColumns.includes(colName) ||
153
177
  plan.dataset.filterableColumns.includes(colName)) {
154
- return `${quoteIdentifier(mainAlias)}.${match}`;
178
+ return `${quoteIdentifier(mainAlias)}.${quoted}`;
155
179
  }
156
180
  return match;
157
181
  });
@@ -164,14 +188,14 @@ export function compileSQL(plan, ctx, catalog) {
164
188
  selectCols = [...groupCols, ...metricCols];
165
189
  }
166
190
  else if (plan.select.length > 0) {
167
- selectCols = plan.select.map(col => quoteIdentifier(col));
191
+ selectCols = plan.select.map(col => resolveColRef(col));
168
192
  }
169
193
  else if (plan.groupBy.length > 0 && plan.metrics.length === 0) {
170
194
  // In rows mode with groupBy but no explicit select, only select grouped columns
171
- selectCols = plan.groupBy.map(col => quoteIdentifier(col));
195
+ selectCols = plan.groupBy.map(col => resolveColRef(col));
172
196
  }
173
197
  else {
174
- selectCols = plan.dataset.selectableColumns.map(col => quoteIdentifier(col));
198
+ selectCols = plan.dataset.selectableColumns.map(col => resolveColRef(col));
175
199
  }
176
200
  // Append FK name columns (auto-resolved human-readable names)
177
201
  if (fkNameColumns.length > 0) {
@@ -182,19 +206,23 @@ export function compileSQL(plan, ctx, catalog) {
182
206
  }
183
207
  // ── Build WHERE clause ──
184
208
  const whereParts = [];
185
- // Tenant isolation (ALWAYS injected server-side)
186
- if (!ctx.tenantId || ctx.tenantId.trim() === '') {
187
- throw new InferError('SQL_VALIDATION_FAILED', 'compiler', 'Tenant ID is required but was not provided');
188
- }
189
- const tenantCol = ctx.tenantColumn ?? 'tenant_id';
190
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tenantCol)) {
191
- throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid tenant column name: ${tenantCol}`);
209
+ // Tenant isolation (ALWAYS injected server-side when multi-tenant)
210
+ let tenantParamIdx = null;
211
+ if (ctx.tenantColumn !== undefined || (ctx.tenantId && ctx.tenantId.trim() !== '')) {
212
+ if (!ctx.tenantId || ctx.tenantId.trim() === '') {
213
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', 'Tenant ID is required but was not provided');
214
+ }
215
+ const tenantCol = ctx.tenantColumn ?? 'tenant_id';
216
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tenantCol)) {
217
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid tenant column name: ${tenantCol}`);
218
+ }
219
+ const tenantRef = joinClauses.length > 0
220
+ ? `${quoteIdentifier(mainAlias)}.${quoteIdentifier(tenantCol)}`
221
+ : quoteIdentifier(tenantCol);
222
+ params.push(ctx.tenantId);
223
+ tenantParamIdx = paramIdx;
224
+ whereParts.push(`${tenantRef} = $${paramIdx++}`);
192
225
  }
193
- const tenantRef = joinClauses.length > 0
194
- ? `${quoteIdentifier(mainAlias)}.${quoteIdentifier(tenantCol)}`
195
- : quoteIdentifier(tenantCol);
196
- params.push(ctx.tenantId);
197
- whereParts.push(`${tenantRef} = $${paramIdx++}`);
198
226
  // Location scoping (optional)
199
227
  if (ctx.locationIds && ctx.locationIds.length > 0) {
200
228
  const locationRef = joinClauses.length > 0
@@ -206,9 +234,9 @@ export function compileSQL(plan, ctx, catalog) {
206
234
  // Date range
207
235
  if (plan.dateRange && plan.dataset.dateColumn) {
208
236
  params.push(plan.dateRange.startDate);
209
- whereParts.push(`${quoteIdentifier(plan.dataset.dateColumn)} >= $${paramIdx++}`);
237
+ whereParts.push(`${resolveColRef(plan.dataset.dateColumn)} >= $${paramIdx++}`);
210
238
  params.push(plan.dateRange.endDate);
211
- whereParts.push(`${quoteIdentifier(plan.dataset.dateColumn)} <= $${paramIdx++}`);
239
+ whereParts.push(`${resolveColRef(plan.dataset.dateColumn)} <= $${paramIdx++}`);
212
240
  }
213
241
  // User filters (parameterized)
214
242
  for (const filter of plan.filters) {
@@ -218,7 +246,7 @@ export function compileSQL(plan, ctx, catalog) {
218
246
  col = buildExtractExpr(resolveColRef(filter.field));
219
247
  }
220
248
  else {
221
- col = quoteIdentifier(filter.field);
249
+ col = resolveColRef(filter.field);
222
250
  }
223
251
  const op = sanitizeOperator(filter.op);
224
252
  if (op === 'IS NULL') {
@@ -243,7 +271,7 @@ export function compileSQL(plan, ctx, catalog) {
243
271
  // Entity filters (resolved IDs)
244
272
  for (const entity of plan.resolvedEntities) {
245
273
  if (entity.resolvedIds.length > 0) {
246
- const col = quoteIdentifier(entity.outputColumn);
274
+ const col = resolveColRef(entity.outputColumn);
247
275
  params.push(entity.resolvedIds);
248
276
  whereParts.push(`${col} = ANY($${paramIdx++})`);
249
277
  }
@@ -266,6 +294,11 @@ export function compileSQL(plan, ctx, catalog) {
266
294
  subWhere += ` AND ${quoteIdentifier(subAlias)}.${quoteIdentifier(f.field)} ${op} $${paramIdx++}`;
267
295
  }
268
296
  }
297
+ // Add tenant isolation to EXISTS subquery
298
+ if (tenantParamIdx !== null) {
299
+ const tenantCol = ctx.tenantColumn ?? 'tenant_id';
300
+ subWhere += ` AND ${quoteIdentifier(subAlias)}.${quoteIdentifier(tenantCol)} = $${tenantParamIdx}`;
301
+ }
269
302
  const existsOp = ef.mode === 'not_exists' ? 'NOT EXISTS' : 'EXISTS';
270
303
  whereParts.push(`${existsOp} (SELECT 1 FROM ${targetTable} AS ${quoteIdentifier(subAlias)} WHERE ${subWhere})`);
271
304
  }
@@ -288,6 +321,10 @@ export function compileSQL(plan, ctx, catalog) {
288
321
  else if (op === 'IS NOT NULL') {
289
322
  havingParts.push(`${fieldRef} IS NOT NULL`);
290
323
  }
324
+ else if (op === 'IN' || op === 'NOT IN') {
325
+ params.push(filter.value);
326
+ havingParts.push(`${fieldRef} ${op === 'NOT IN' ? 'NOT ' : ''}= ANY($${paramIdx++})`);
327
+ }
291
328
  else {
292
329
  params.push(filter.value);
293
330
  havingParts.push(`${fieldRef} ${op} $${paramIdx++}`);
@@ -295,17 +332,33 @@ export function compileSQL(plan, ctx, catalog) {
295
332
  }
296
333
  havingSql = `HAVING ${havingParts.join(' AND ')}`;
297
334
  }
335
+ // Helper: check if a field is a computed alias (not a real table column)
336
+ const isComputedAlias = (field) => {
337
+ if (plan.metrics.some(m => m.id === field))
338
+ return true;
339
+ if (fkLabelAliases.has(field))
340
+ return true;
341
+ if (field.endsWith('_delta') || field.endsWith('_growth_pct'))
342
+ return true;
343
+ if (plan.extractBucket && (field.endsWith('_hour') || field.endsWith('_dow') || field.endsWith('_month')))
344
+ return true;
345
+ return false;
346
+ };
298
347
  // ── Build ORDER BY (direction whitelisted to ASC/DESC only) ──
299
348
  const orderBySql = plan.orderBy.length > 0
300
349
  ? `ORDER BY ${plan.orderBy.map(o => {
301
- const ref = dateGroupColumns.has(o.field) && plan.timeBucket
302
- ? formatGroupByRef(o.field)
303
- : quoteIdentifier(o.field);
304
- return `${ref} ${o.dir === 'desc' ? 'DESC' : 'ASC'}`;
350
+ const ref = isComputedAlias(o.field)
351
+ ? quoteIdentifier(o.field)
352
+ : extractGroupColumns.has(o.field) && plan.extractBucket
353
+ ? buildExtractExpr(resolveColRef(o.field))
354
+ : dateGroupColumns.has(o.field) && plan.timeBucket
355
+ ? formatGroupByRef(o.field)
356
+ : resolveColRef(o.field);
357
+ return `${ref} ${o.dir === 'desc' ? 'DESC NULLS LAST' : 'ASC'}`;
305
358
  }).join(', ')}`
306
359
  : '';
307
360
  // ── LIMIT (always applied) ──
308
- params.push(plan.limit);
361
+ params.push(Math.max(1, plan.limit));
309
362
  const limitSql = `LIMIT $${paramIdx++}`;
310
363
  // ── OFFSET (optional pagination) ──
311
364
  let offsetSql = '';
@@ -331,8 +384,16 @@ export function compileSQL(plan, ctx, catalog) {
331
384
  // compareMode: wrap in CTE with LAG window functions
332
385
  if (plan.compareMode && plan.metrics.length > 0 && (plan.timeBucket || plan.extractBucket)) {
333
386
  const metricAliases = plan.metrics.map(m => quoteIdentifier(m.id));
387
+ const timeCol = plan.groupBy.find(col => {
388
+ const t = plan.dataset.columnTypes[col];
389
+ return t === 'date' || t === 'timestamp';
390
+ }) || plan.groupBy[0] || 'period';
391
+ // Use the column alias as it appears in the inner query (after DATE_TRUNC/EXTRACT)
392
+ const timeAlias = colAlias(timeCol);
393
+ const partitionCols = plan.groupBy.filter(col => col !== timeCol).map(c => colAlias(c));
394
+ const partitionBy = partitionCols.length > 0 ? `PARTITION BY ${partitionCols.map(c => quoteIdentifier(c)).join(', ')} ` : '';
334
395
  const lagCols = metricAliases.map(alias => {
335
- return `${alias} - LAG(${alias}) OVER (ORDER BY ${quoteIdentifier(plan.groupBy[0] || 'period')}) AS ${alias.replace(/"$/, '_delta"')}`;
396
+ return `${alias} - LAG(${alias}) OVER (${partitionBy}ORDER BY ${quoteIdentifier(timeAlias)}) AS ${alias.replace(/"$/, '_delta"')}`;
336
397
  });
337
398
  const wrappedSql = `WITH _base AS (\n${sql}\n)\nSELECT *, ${lagCols.join(', ')} FROM _base`;
338
399
  return { sql: wrappedSql, params };