@reasoningco/infer 0.1.4 → 0.1.5

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,CA2YpI"}
@@ -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, '""')}"`;
@@ -45,16 +45,38 @@ export function compileSQL(plan, ctx, catalog) {
45
45
  const nameCol = targetDs.selectableColumns.find(c => ['name', 'title', 'label', 'display_name', 'full_name', 'username', 'email'].includes(c));
46
46
  if (!nameCol)
47
47
  continue;
48
- if (!joinAliases.has(bareTable)) {
48
+ let alias = bareTable;
49
+ if (joinAliases.has(bareTable)) {
50
+ // Table already joined — use a unique alias to avoid conflicts (e.g. two FKs to same table)
51
+ alias = `${bareTable}__${col}`;
52
+ if (!joinAliases.has(alias)) {
53
+ const targetFull = `${quoteIdentifier(targetDs.table.schema)}.${quoteIdentifier(targetDs.table.name)}`;
54
+ joinAliases.set(alias, alias);
55
+ joinClauses.push(`LEFT JOIN ${targetFull} AS ${quoteIdentifier(alias)} ON ${quoteIdentifier(mainAlias)}.${quoteIdentifier(col)} = ${quoteIdentifier(alias)}.${quoteIdentifier(joinDef.foreignColumn)}`);
56
+ }
57
+ }
58
+ else {
49
59
  const targetFull = `${quoteIdentifier(targetDs.table.schema)}.${quoteIdentifier(targetDs.table.name)}`;
50
60
  joinAliases.set(bareTable, bareTable);
51
61
  joinClauses.push(`LEFT JOIN ${targetFull} AS ${quoteIdentifier(bareTable)} ON ${quoteIdentifier(mainAlias)}.${quoteIdentifier(col)} = ${quoteIdentifier(bareTable)}.${quoteIdentifier(joinDef.foreignColumn)}`);
52
62
  }
53
- const fkRef = `${quoteIdentifier(bareTable)}.${quoteIdentifier(nameCol)}`;
54
- fkNameColumns.push(`${fkRef} AS ${quoteIdentifier(col.replace(/_id$/, '_name'))}`);
63
+ const fkRef = `${quoteIdentifier(alias)}.${quoteIdentifier(nameCol)}`;
64
+ // Use a unique alias that won't collide with existing columns
65
+ const fkAlias = col.replace(/_id$/, '') + '_name';
66
+ const safeFkAlias = plan.dataset.selectableColumns.includes(fkAlias) ? `${fkAlias}_fk` : fkAlias;
67
+ fkNameColumns.push(`${fkRef} AS ${quoteIdentifier(safeFkAlias)}`);
55
68
  fkGroupByRefs.push(fkRef);
56
69
  }
57
70
  }
71
+ // ── Validate time/extract buckets at runtime ──
72
+ const VALID_TIME_BUCKETS = new Set(['hour', 'day', 'week', 'month', 'quarter', 'year']);
73
+ const VALID_EXTRACT_BUCKETS = new Set(['hour_of_day', 'day_of_week', 'month_of_year', 'quarter', 'year', 'week']);
74
+ if (plan.timeBucket && !VALID_TIME_BUCKETS.has(plan.timeBucket)) {
75
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid timeBucket: ${plan.timeBucket}`);
76
+ }
77
+ if (plan.extractBucket && !VALID_EXTRACT_BUCKETS.has(plan.extractBucket)) {
78
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid extractBucket: ${plan.extractBucket}`);
79
+ }
58
80
  // ── Determine which groupBy columns need DATE_TRUNC or EXTRACT ──
59
81
  const dateGroupColumns = new Set();
60
82
  const extractGroupColumns = new Set();
@@ -83,7 +105,7 @@ export function compileSQL(plan, ctx, catalog) {
83
105
  case 'quarter': return `EXTRACT(QUARTER FROM ${ref})`;
84
106
  case 'year': return `EXTRACT(YEAR FROM ${ref})`;
85
107
  case 'week': return `EXTRACT(WEEK FROM ${ref})`;
86
- default: return ref;
108
+ default: throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid extractBucket: ${plan.extractBucket}`);
87
109
  }
88
110
  };
89
111
  // Helper: resolve a column reference (supports dot-notation for joins)
@@ -147,11 +169,11 @@ export function compileSQL(plan, ctx, catalog) {
147
169
  return expr;
148
170
  // Replace bare column refs (not already qualified with table.) inside the expression
149
171
  // 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) => {
172
+ return expr.replace(/(?<!\.)("([a-zA-Z_][a-zA-Z0-9_]*)")(?!\.)/g, (match, quoted, colName) => {
151
173
  // Check if this column exists on the main dataset
152
174
  if (plan.dataset.selectableColumns.includes(colName) ||
153
175
  plan.dataset.filterableColumns.includes(colName)) {
154
- return `${quoteIdentifier(mainAlias)}.${match}`;
176
+ return `${quoteIdentifier(mainAlias)}.${quoted}`;
155
177
  }
156
178
  return match;
157
179
  });
@@ -164,14 +186,14 @@ export function compileSQL(plan, ctx, catalog) {
164
186
  selectCols = [...groupCols, ...metricCols];
165
187
  }
166
188
  else if (plan.select.length > 0) {
167
- selectCols = plan.select.map(col => quoteIdentifier(col));
189
+ selectCols = plan.select.map(col => resolveColRef(col));
168
190
  }
169
191
  else if (plan.groupBy.length > 0 && plan.metrics.length === 0) {
170
192
  // In rows mode with groupBy but no explicit select, only select grouped columns
171
- selectCols = plan.groupBy.map(col => quoteIdentifier(col));
193
+ selectCols = plan.groupBy.map(col => resolveColRef(col));
172
194
  }
173
195
  else {
174
- selectCols = plan.dataset.selectableColumns.map(col => quoteIdentifier(col));
196
+ selectCols = plan.dataset.selectableColumns.map(col => resolveColRef(col));
175
197
  }
176
198
  // Append FK name columns (auto-resolved human-readable names)
177
199
  if (fkNameColumns.length > 0) {
@@ -182,19 +204,23 @@ export function compileSQL(plan, ctx, catalog) {
182
204
  }
183
205
  // ── Build WHERE clause ──
184
206
  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}`);
207
+ // Tenant isolation (ALWAYS injected server-side when multi-tenant)
208
+ let tenantParamIdx = null;
209
+ if (ctx.tenantColumn !== undefined || (ctx.tenantId && ctx.tenantId.trim() !== '')) {
210
+ if (!ctx.tenantId || ctx.tenantId.trim() === '') {
211
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', 'Tenant ID is required but was not provided');
212
+ }
213
+ const tenantCol = ctx.tenantColumn ?? 'tenant_id';
214
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tenantCol)) {
215
+ throw new InferError('SQL_VALIDATION_FAILED', 'compiler', `Invalid tenant column name: ${tenantCol}`);
216
+ }
217
+ const tenantRef = joinClauses.length > 0
218
+ ? `${quoteIdentifier(mainAlias)}.${quoteIdentifier(tenantCol)}`
219
+ : quoteIdentifier(tenantCol);
220
+ params.push(ctx.tenantId);
221
+ tenantParamIdx = paramIdx;
222
+ whereParts.push(`${tenantRef} = $${paramIdx++}`);
192
223
  }
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
224
  // Location scoping (optional)
199
225
  if (ctx.locationIds && ctx.locationIds.length > 0) {
200
226
  const locationRef = joinClauses.length > 0
@@ -206,9 +232,9 @@ export function compileSQL(plan, ctx, catalog) {
206
232
  // Date range
207
233
  if (plan.dateRange && plan.dataset.dateColumn) {
208
234
  params.push(plan.dateRange.startDate);
209
- whereParts.push(`${quoteIdentifier(plan.dataset.dateColumn)} >= $${paramIdx++}`);
235
+ whereParts.push(`${resolveColRef(plan.dataset.dateColumn)} >= $${paramIdx++}`);
210
236
  params.push(plan.dateRange.endDate);
211
- whereParts.push(`${quoteIdentifier(plan.dataset.dateColumn)} <= $${paramIdx++}`);
237
+ whereParts.push(`${resolveColRef(plan.dataset.dateColumn)} <= $${paramIdx++}`);
212
238
  }
213
239
  // User filters (parameterized)
214
240
  for (const filter of plan.filters) {
@@ -218,7 +244,7 @@ export function compileSQL(plan, ctx, catalog) {
218
244
  col = buildExtractExpr(resolveColRef(filter.field));
219
245
  }
220
246
  else {
221
- col = quoteIdentifier(filter.field);
247
+ col = resolveColRef(filter.field);
222
248
  }
223
249
  const op = sanitizeOperator(filter.op);
224
250
  if (op === 'IS NULL') {
@@ -243,7 +269,7 @@ export function compileSQL(plan, ctx, catalog) {
243
269
  // Entity filters (resolved IDs)
244
270
  for (const entity of plan.resolvedEntities) {
245
271
  if (entity.resolvedIds.length > 0) {
246
- const col = quoteIdentifier(entity.outputColumn);
272
+ const col = resolveColRef(entity.outputColumn);
247
273
  params.push(entity.resolvedIds);
248
274
  whereParts.push(`${col} = ANY($${paramIdx++})`);
249
275
  }
@@ -266,6 +292,11 @@ export function compileSQL(plan, ctx, catalog) {
266
292
  subWhere += ` AND ${quoteIdentifier(subAlias)}.${quoteIdentifier(f.field)} ${op} $${paramIdx++}`;
267
293
  }
268
294
  }
295
+ // Add tenant isolation to EXISTS subquery
296
+ if (tenantParamIdx !== null) {
297
+ const tenantCol = ctx.tenantColumn ?? 'tenant_id';
298
+ subWhere += ` AND ${quoteIdentifier(subAlias)}.${quoteIdentifier(tenantCol)} = $${tenantParamIdx}`;
299
+ }
269
300
  const existsOp = ef.mode === 'not_exists' ? 'NOT EXISTS' : 'EXISTS';
270
301
  whereParts.push(`${existsOp} (SELECT 1 FROM ${targetTable} AS ${quoteIdentifier(subAlias)} WHERE ${subWhere})`);
271
302
  }
@@ -288,6 +319,10 @@ export function compileSQL(plan, ctx, catalog) {
288
319
  else if (op === 'IS NOT NULL') {
289
320
  havingParts.push(`${fieldRef} IS NOT NULL`);
290
321
  }
322
+ else if (op === 'IN' || op === 'NOT IN') {
323
+ params.push(filter.value);
324
+ havingParts.push(`${fieldRef} ${op === 'NOT IN' ? 'NOT ' : ''}= ANY($${paramIdx++})`);
325
+ }
291
326
  else {
292
327
  params.push(filter.value);
293
328
  havingParts.push(`${fieldRef} ${op} $${paramIdx++}`);
@@ -298,14 +333,16 @@ export function compileSQL(plan, ctx, catalog) {
298
333
  // ── Build ORDER BY (direction whitelisted to ASC/DESC only) ──
299
334
  const orderBySql = plan.orderBy.length > 0
300
335
  ? `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'}`;
336
+ const ref = extractGroupColumns.has(o.field) && plan.extractBucket
337
+ ? buildExtractExpr(resolveColRef(o.field))
338
+ : dateGroupColumns.has(o.field) && plan.timeBucket
339
+ ? formatGroupByRef(o.field)
340
+ : resolveColRef(o.field);
341
+ return `${ref} ${o.dir === 'desc' ? 'DESC NULLS LAST' : 'ASC'}`;
305
342
  }).join(', ')}`
306
343
  : '';
307
344
  // ── LIMIT (always applied) ──
308
- params.push(plan.limit);
345
+ params.push(Math.max(1, plan.limit));
309
346
  const limitSql = `LIMIT $${paramIdx++}`;
310
347
  // ── OFFSET (optional pagination) ──
311
348
  let offsetSql = '';
@@ -331,8 +368,16 @@ export function compileSQL(plan, ctx, catalog) {
331
368
  // compareMode: wrap in CTE with LAG window functions
332
369
  if (plan.compareMode && plan.metrics.length > 0 && (plan.timeBucket || plan.extractBucket)) {
333
370
  const metricAliases = plan.metrics.map(m => quoteIdentifier(m.id));
371
+ const timeCol = plan.groupBy.find(col => {
372
+ const t = plan.dataset.columnTypes[col];
373
+ return t === 'date' || t === 'timestamp';
374
+ }) || plan.groupBy[0] || 'period';
375
+ // Use the column alias as it appears in the inner query (after DATE_TRUNC/EXTRACT)
376
+ const timeAlias = colAlias(timeCol);
377
+ const partitionCols = plan.groupBy.filter(col => col !== timeCol).map(c => colAlias(c));
378
+ const partitionBy = partitionCols.length > 0 ? `PARTITION BY ${partitionCols.map(c => quoteIdentifier(c)).join(', ')} ` : '';
334
379
  const lagCols = metricAliases.map(alias => {
335
- return `${alias} - LAG(${alias}) OVER (ORDER BY ${quoteIdentifier(plan.groupBy[0] || 'period')}) AS ${alias.replace(/"$/, '_delta"')}`;
380
+ return `${alias} - LAG(${alias}) OVER (${partitionBy}ORDER BY ${quoteIdentifier(timeAlias)}) AS ${alias.replace(/"$/, '_delta"')}`;
336
381
  });
337
382
  const wrappedSql = `WITH _base AS (\n${sql}\n)\nSELECT *, ${lagCols.join(', ')} FROM _base`;
338
383
  return { sql: wrappedSql, params };