@graphoria/server 0.1.1 → 0.1.3

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 (39) hide show
  1. package/dist/src/configuration/getSchemas/index.d.ts +4 -4
  2. package/dist/src/configuration/getSchemas/mergeEntities/index.d.ts +2 -2
  3. package/dist/src/configuration/index.d.ts +2 -2
  4. package/dist/src/databases/common.d.ts +18 -10
  5. package/dist/src/databases/common.d.ts.map +1 -1
  6. package/dist/src/databases/engines/mssql/connection.d.ts.map +1 -1
  7. package/dist/src/databases/engines/mssql/getStructure.d.ts +0 -2
  8. package/dist/src/databases/engines/mssql/getStructure.d.ts.map +1 -1
  9. package/dist/src/databases/engines/mssql/query/index.d.ts.map +1 -1
  10. package/dist/src/databases/engines/mysql/getStructure.d.ts +0 -2
  11. package/dist/src/databases/engines/mysql/getStructure.d.ts.map +1 -1
  12. package/dist/src/databases/engines/mysql/query/index.d.ts.map +1 -1
  13. package/dist/src/databases/engines/postgresql/getStructure.d.ts +0 -2
  14. package/dist/src/databases/engines/postgresql/getStructure.d.ts.map +1 -1
  15. package/dist/src/databases/engines/postgresql/query/index.d.ts.map +1 -1
  16. package/dist/src/databases/high-level-operations.d.ts +1 -3
  17. package/dist/src/databases/high-level-operations.d.ts.map +1 -1
  18. package/dist/src/databases/metadata/structure.d.ts +0 -2
  19. package/dist/src/databases/metadata/structure.d.ts.map +1 -1
  20. package/dist/src/databases/schemaBuilder/generateInsert/index.d.ts.map +1 -1
  21. package/dist/src/databases/transformers/data-transformers.d.ts +2 -2
  22. package/dist/src/databases/transformers/data-transformers.d.ts.map +1 -1
  23. package/dist/src/index.d.ts.map +1 -1
  24. package/dist/src/types/zod/db.d.ts +0 -5
  25. package/dist/src/types/zod/db.d.ts.map +1 -1
  26. package/dist/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/playgrounds/graphiql/index.html +1 -1
  29. package/src/databases/common.ts +49 -34
  30. package/src/databases/engines/mssql/connection.ts +2 -0
  31. package/src/databases/engines/mssql/query/index.ts +34 -21
  32. package/src/databases/engines/mysql/connection.ts +1 -1
  33. package/src/databases/engines/mysql/query/index.ts +19 -18
  34. package/src/databases/engines/postgresql/connection.ts +1 -1
  35. package/src/databases/engines/postgresql/query/index.ts +0 -3
  36. package/src/databases/schemaBuilder/generateInsert/index.ts +6 -21
  37. package/src/databases/transformers/data-transformers.ts +3 -0
  38. package/src/index.ts +0 -2
  39. package/src/types/zod/db.ts +0 -2
@@ -47,7 +47,6 @@ export const buildConditions = (
47
47
  tableAlias: string,
48
48
  level: number,
49
49
  aliasMap: { [alias: string]: string },
50
- quoted = false,
51
50
  ): string => {
52
51
  if (!whereArgs) return "";
53
52
 
@@ -71,7 +70,7 @@ export const buildConditions = (
71
70
  ? `${mappingDbTypeCharVar[dbType]}${variablesDefinition.findIndex((v) => v.name === operand.substring(1)) + 1}`
72
71
  : operand;
73
72
 
74
- const condition = buildCondition(tableAlias, fieldName, operator, resolvedValue, quoted);
73
+ const condition = buildCondition(tableAlias, fieldName, operator, resolvedValue, dbType);
75
74
 
76
75
  if (condition) {
77
76
  conditions.push(condition);
@@ -88,7 +87,7 @@ export const buildConditions = (
88
87
  fieldName,
89
88
  tableAlias,
90
89
  nestedTableAlias,
91
- quoted,
90
+ dbType,
92
91
  );
93
92
 
94
93
  const nestedConditions = buildConditions(
@@ -100,12 +99,11 @@ export const buildConditions = (
100
99
  nestedTableAlias,
101
100
  level + 1,
102
101
  aliasMap,
103
- quoted,
104
102
  );
105
103
 
106
104
  const existsClause = `EXISTS (
107
105
  SELECT 1
108
- FROM ${entities.queriesMap[fieldName]!.dottedName} ${nestedTableAlias}
106
+ FROM ${entities.queriesMap[fieldName]!.dottedQuotedName} ${nestedTableAlias}
109
107
  WHERE ${joinCondition}${nestedConditions ? ` AND (${nestedConditions})` : ""}
110
108
  )`;
111
109
 
@@ -116,7 +114,33 @@ export const buildConditions = (
116
114
  return conditions.length > 0 ? conditions.join(" AND ") : "";
117
115
  };
118
116
 
119
- const wrapIf = (value: string, quoted: boolean): string => (quoted ? `"${value}"` : value);
117
+ // Reserved words (e.g. PRINT, ORDER) are valid GraphQL names; always delimit
118
+ // identifiers so they parse as identifiers in every dialect.
119
+ const identifierDelimiters: Record<DatabaseType, [string, string]> = {
120
+ pg: ['"', '"'],
121
+ mysql: ["`", "`"],
122
+ mssql: ["[", "]"],
123
+ };
124
+
125
+ export const wrapIdentifierFp =
126
+ (dbType: DatabaseType) =>
127
+ (value: string): string => {
128
+ const [open, close] = identifierDelimiters[dbType];
129
+ // Doubling the closing delimiter is the escape in all three dialects; only
130
+ // introspected names can contain one (GraphQL names never do).
131
+ return `${open}${value.replaceAll(close, `${close}${close}`)}${close}`;
132
+ };
133
+
134
+ export const wrapIdentifierPG = wrapIdentifierFp("pg");
135
+ export const wrapIdentifierMSSQL = wrapIdentifierFp("mssql");
136
+ export const wrapIdentifierMySQL = wrapIdentifierFp("mysql");
137
+
138
+ export const qualifiedNameFp =
139
+ (dbType: DatabaseType) =>
140
+ ({ schema, name }: { schema: string; name: string }): string => {
141
+ const wrap = wrapIdentifierFp(dbType);
142
+ return `${wrap(schema)}.${wrap(name)}`;
143
+ };
120
144
 
121
145
  // Helper to resolve variable values
122
146
  const resolveVariable = <T>(value: unknown, variables: Record<string, unknown>): T => {
@@ -131,9 +155,9 @@ const buildCondition = (
131
155
  field: string,
132
156
  operator: string,
133
157
  value: unknown,
134
- quoted = false,
158
+ dbType: DatabaseType,
135
159
  ): string | null => {
136
- const wrappedField = wrapIf(field, quoted);
160
+ const wrappedField = wrapIdentifierFp(dbType)(field);
137
161
 
138
162
  switch (operator) {
139
163
  case "eq":
@@ -174,14 +198,13 @@ const pairsToAnd = (
174
198
  parentColumn: string;
175
199
  childColumn: string;
176
200
  }[],
177
- quoted = false,
178
- ) =>
179
- pairs
180
- .map(
181
- (p) =>
182
- `${p.parentAlias}.${wrapIf(p.parentColumn, quoted)} = ${p.childAlias}.${wrapIf(p.childColumn, quoted)}`,
183
- )
201
+ dbType: DatabaseType,
202
+ ) => {
203
+ const wrap = wrapIdentifierFp(dbType);
204
+ return pairs
205
+ .map((p) => `${p.parentAlias}.${wrap(p.parentColumn)} = ${p.childAlias}.${wrap(p.childColumn)}`)
184
206
  .join(" AND ");
207
+ };
185
208
 
186
209
  export const findJoinCondition = (
187
210
  entities: MergedEntities,
@@ -189,7 +212,7 @@ export const findJoinCondition = (
189
212
  childTableName: string,
190
213
  parentAlias: string,
191
214
  childAlias: string,
192
- quoted = false,
215
+ dbType: DatabaseType,
193
216
  ): string => {
194
217
  const relations = entities.getForeignKeysBetweenTables(parentTableName, childTableName);
195
218
 
@@ -204,7 +227,7 @@ export const findJoinCondition = (
204
227
  parentColumn: c.source,
205
228
  childColumn: c.target,
206
229
  })),
207
- quoted,
230
+ dbType,
208
231
  ),
209
232
  ) ?? []),
210
233
  ...(relations.relationshipsReversed?.map((relation) =>
@@ -215,7 +238,7 @@ export const findJoinCondition = (
215
238
  parentColumn: c.target,
216
239
  childColumn: c.source,
217
240
  })),
218
- quoted,
241
+ dbType,
219
242
  ),
220
243
  ) ?? []),
221
244
  ].join(" AND ");
@@ -233,7 +256,6 @@ export const buildWhereClauseFp =
233
256
  parentTableAlias: string | null,
234
257
  level: number,
235
258
  aliasMap: { [alias: string]: string },
236
- quoted = false,
237
259
  ): string => {
238
260
  const whereConditions: string[] = [
239
261
  buildConditions(
@@ -245,7 +267,6 @@ export const buildWhereClauseFp =
245
267
  tableAlias,
246
268
  level + 1,
247
269
  aliasMap,
248
- quoted,
249
270
  ),
250
271
  parentTableName && parentTableAlias
251
272
  ? findJoinCondition(
@@ -254,7 +275,7 @@ export const buildWhereClauseFp =
254
275
  field.name,
255
276
  parentTableAlias,
256
277
  tableAlias,
257
- quoted,
278
+ dbType,
258
279
  )
259
280
  : null,
260
281
  ].filter(Boolean) as string[];
@@ -282,6 +303,7 @@ const parseOrderDirection = (direction: string): { sort: string; nulls?: string
282
303
  export const buildOrderByClauseFp =
283
304
  (dbType: DatabaseType = "pg") =>
284
305
  (entities: MergedEntities, field: SelectionAnalysis, tableAlias: string): string => {
306
+ const wrap = wrapIdentifierFp(dbType);
285
307
  if (field.arguments?.["orderBy"]) {
286
308
  const orderBy = field.arguments?.["orderBy"];
287
309
  // orderBy is an array or object of { column: direction } - no variable resolution needed
@@ -306,11 +328,11 @@ export const buildOrderByClauseFp =
306
328
  if (nulls) {
307
329
  if (dbType === "pg") {
308
330
  // PostgreSQL supports NULLS FIRST/LAST natively
309
- return `${tableAlias}."${colName}" ${sort} NULLS ${nulls}`;
331
+ return `${tableAlias}.${wrap(colName)} ${sort} NULLS ${nulls}`;
310
332
  } else if (dbType === "mysql") {
311
333
  // MySQL: Use CASE statement for NULL handling
312
334
  const nullFirst = nulls === "FIRST";
313
- return `CASE WHEN ${tableAlias}.${colName} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${colName} ${sort}`;
335
+ return `CASE WHEN ${tableAlias}.${wrap(colName)} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${wrap(colName)} ${sort}`;
314
336
  } else {
315
337
  // MSSQL requires CASE statements for NULL handling
316
338
  const nullFirst = nulls === "FIRST";
@@ -318,10 +340,10 @@ export const buildOrderByClauseFp =
318
340
 
319
341
  if ((nullFirst && isAsc) || (!nullFirst && !isAsc)) {
320
342
  // NULLs naturally come first in this combination
321
- return `${tableAlias}.[${colName}] ${sort}`;
343
+ return `${tableAlias}.${wrap(colName)} ${sort}`;
322
344
  } else {
323
345
  // Need to force NULL positioning with CASE
324
- return `CASE WHEN ${tableAlias}.${colName} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${colName} ${sort}`;
346
+ return `CASE WHEN ${tableAlias}.${wrap(colName)} IS NULL THEN ${nullFirst ? 0 : 1} ELSE ${nullFirst ? 1 : 0} END, ${tableAlias}.${wrap(colName)} ${sort}`;
325
347
  }
326
348
  }
327
349
  } else {
@@ -333,13 +355,7 @@ export const buildOrderByClauseFp =
333
355
  ) ${sort}`;
334
356
  }
335
357
 
336
- if (dbType === "pg") {
337
- return `${tableAlias}."${colName}" ${sort}`;
338
- } else if (dbType === "mysql") {
339
- return `${tableAlias}.${colName} ${sort}`;
340
- } else {
341
- return `${tableAlias}.${colName} ${sort}`;
342
- }
358
+ return `${tableAlias}.${wrap(colName)} ${sort}`;
343
359
  }
344
360
  })
345
361
  .join(", ");
@@ -463,7 +479,6 @@ export const processFieldSelectionsFp =
463
479
  level: number,
464
480
  buildSubquery: (sel: SelectionAnalysis, level: number) => string,
465
481
  selectBuilder: (entry: [name: string, selector: string]) => string,
466
- quoted = false,
467
482
  ): string => {
468
483
  const selectClauses: [string, string][] = [];
469
484
  const subQueries: [string, string][] = [];
@@ -487,7 +502,7 @@ export const processFieldSelectionsFp =
487
502
  selectClauses.push([`${colName}`, `(${vc.expression})`]);
488
503
  }
489
504
  } else {
490
- let querySelector = `${tableAlias}.${wrapIf(sel.name, quoted)}`;
505
+ let querySelector = `${tableAlias}.${wrapIdentifierFp(dbType)(sel.name)}`;
491
506
 
492
507
  // Apply all directives using the handler registry
493
508
  querySelector = applyDirectives(
@@ -141,6 +141,8 @@ export const callStoredProcedure = async (
141
141
  }
142
142
  }
143
143
 
144
+ // Unquoted on purpose: tedious sends this as the TDS RPC procedure name,
145
+ // never as SQL text, so reserved words need no delimiting here.
144
146
  const data = await request.execute(sp.dottedName);
145
147
 
146
148
  return data?.recordset ?? false;
@@ -16,12 +16,16 @@ import {
16
16
  isAggregationField,
17
17
  isSingleQuery,
18
18
  processFieldSelectionsMSSQL,
19
+ wrapIdentifierMSSQL,
19
20
  } from "../../../common";
20
21
 
22
+ // FOR JSON PATH uses the unbracketed alias as the JSON key, so bracket-quoting
23
+ // (required for reserved words like PRINT) never leaks into results.
24
+
21
25
  // Generate CTE for aggregations
22
26
  const buildAggregationCTE = (
23
27
  groupByInfo: GroupByInfo,
24
- dottedName: string,
28
+ dottedQuotedName: string,
25
29
  tableAlias: string,
26
30
  whereClause: string,
27
31
  ): string => {
@@ -31,7 +35,7 @@ const buildAggregationCTE = (
31
35
 
32
36
  // Add group by fields
33
37
  groupByFields.forEach((field) => {
34
- selectClauses.push(`${tableAlias}.${field}`);
38
+ selectClauses.push(`${tableAlias}.${wrapIdentifierMSSQL(field)}`);
35
39
  });
36
40
 
37
41
  // Add aggregations
@@ -40,16 +44,18 @@ const buildAggregationCTE = (
40
44
  selectClauses.push(`COUNT(*) AS ${agg.alias}`);
41
45
  } else {
42
46
  const func = agg.name.toUpperCase();
43
- selectClauses.push(`${func}(${tableAlias}.${agg.fieldName}) AS ${agg.alias}`);
47
+ selectClauses.push(
48
+ `${func}(${tableAlias}.${wrapIdentifierMSSQL(agg.fieldName)}) AS ${agg.alias}`,
49
+ );
44
50
  }
45
51
  });
46
52
 
47
- const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${field}`).join(", ")}`;
53
+ const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMSSQL(field)}`).join(", ")}`;
48
54
 
49
55
  return `${cteAlias} AS (
50
56
  SELECT
51
57
  ${selectClauses.join(",\n ")}
52
- FROM ${dottedName} ${tableAlias}
58
+ FROM ${dottedQuotedName} ${tableAlias}
53
59
  ${whereClause}
54
60
  ${groupByClause}
55
61
  )`;
@@ -60,7 +66,7 @@ const buildGroupedQuery = (
60
66
  entities: MergedEntities,
61
67
  field: SelectionAnalysis,
62
68
  groupByInfo: GroupByInfo,
63
- dottedName: string,
69
+ dottedQuotedName: string,
64
70
  tableAlias: string,
65
71
  whereClause: string,
66
72
  ): string => {
@@ -72,21 +78,24 @@ const buildGroupedQuery = (
72
78
  if (hasKey) {
73
79
  // Add key object with group by fields
74
80
  const keyFields = keys
75
- .map((field) => `${cteAlias}.${field.name} AS ${field.alias || field.name}`)
81
+ .map(
82
+ (field) =>
83
+ `${cteAlias}.${wrapIdentifierMSSQL(field.name)} AS ${wrapIdentifierMSSQL(field.alias || field.name)}`,
84
+ )
76
85
  .join(", ");
77
86
 
78
87
  selectClauses.push(
79
- `JSON_QUERY((SELECT ${keyFields} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS [${keyResolved}]`,
88
+ `JSON_QUERY((SELECT ${keyFields} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${wrapIdentifierMSSQL(keyResolved)}`,
80
89
  );
81
90
  }
82
91
 
83
92
  // Add aggregation results
84
93
  aggregations.forEach((agg) => {
85
94
  if (agg.name === "count") {
86
- selectClauses.push(`${cteAlias}.${agg.alias} AS ${agg.nameResolved}`);
95
+ selectClauses.push(`${cteAlias}.${agg.alias} AS ${wrapIdentifierMSSQL(agg.nameResolved)}`);
87
96
  } else {
88
97
  selectClauses.push(
89
- `JSON_QUERY((SELECT ${cteAlias}.${agg.alias} AS ${agg.fieldAlias} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${agg.nameResolved}`,
98
+ `JSON_QUERY((SELECT ${cteAlias}.${agg.alias} AS ${wrapIdentifierMSSQL(agg.fieldAlias)} FOR JSON PATH, WITHOUT_ARRAY_WRAPPER, INCLUDE_NULL_VALUES)) AS ${wrapIdentifierMSSQL(agg.nameResolved)}`,
90
99
  );
91
100
  }
92
101
  });
@@ -98,20 +107,24 @@ const buildGroupedQuery = (
98
107
  if (itemsSelection?.selections) {
99
108
  const itemFields = itemsSelection.selections
100
109
  .filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
101
- .map((sel) => `${tableAlias}.${sel.name} AS ${sel.alias || sel.name}`)
110
+ .map(
111
+ (sel) =>
112
+ `${tableAlias}.${wrapIdentifierMSSQL(sel.name)} AS ${wrapIdentifierMSSQL(sel.alias || sel.name)}`,
113
+ )
102
114
  .join(", ");
103
115
 
104
116
  if (itemFields) {
105
117
  const joinConditions = groupByFields.map(
106
- (field) => `${tableAlias}.${field} = ${cteAlias}.${field}`,
118
+ (field) =>
119
+ `${tableAlias}.${wrapIdentifierMSSQL(field)} = ${cteAlias}.${wrapIdentifierMSSQL(field)}`,
107
120
  );
108
121
 
109
122
  selectClauses.push(`JSON_QUERY(ISNULL((
110
123
  SELECT ${itemFields}
111
- FROM ${dottedName} ${tableAlias}
124
+ FROM ${dottedQuotedName} ${tableAlias}
112
125
  ${whereClause ? [whereClause, ...joinConditions].join(" AND ") : `WHERE ${joinConditions.join(" AND ")}`}
113
126
  FOR JSON PATH, INCLUDE_NULL_VALUES
114
- ), '[]')) AS ${itemsSelection.alias || itemsSelection.name}`);
127
+ ), '[]')) AS ${wrapIdentifierMSSQL(itemsSelection.alias || itemsSelection.name)}`);
115
128
  }
116
129
  }
117
130
  }
@@ -168,7 +181,7 @@ export const generateSQL = (
168
181
 
169
182
  if (groupByInfo) {
170
183
  // This field requires a CTE
171
- const { dottedName } = entities.queriesMap[field.name]!;
184
+ const { dottedQuotedName } = entities.queriesMap[field.name]!;
172
185
 
173
186
  const whereClause = buildWhereClauseMSSQL(
174
187
  entities,
@@ -182,7 +195,7 @@ export const generateSQL = (
182
195
  {},
183
196
  );
184
197
 
185
- const cte = buildAggregationCTE(groupByInfo, dottedName, tableAlias, whereClause);
198
+ const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
186
199
  ctes.push(cte);
187
200
  }
188
201
 
@@ -197,7 +210,7 @@ export const generateSQL = (
197
210
  {},
198
211
  );
199
212
 
200
- fieldQueries.push(`(${fieldSQL}) as ${field.alias || field.name}`);
213
+ fieldQueries.push(`(${fieldSQL}) as ${wrapIdentifierMSSQL(field.alias || field.name)}`);
201
214
  });
202
215
 
203
216
  const cteClause = ctes.length > 0 ? `WITH\n${ctes.join(",\n")}\n` : "";
@@ -229,7 +242,7 @@ export const buildSQLForField = (
229
242
  throw new Error(`Table not found for field: ${field.name}`);
230
243
  }
231
244
 
232
- const { dottedName, resolverName } = foundTable;
245
+ const { dottedQuotedName, resolverName } = foundTable;
233
246
 
234
247
  aliasMap[tableAlias] = resolverName;
235
248
 
@@ -254,7 +267,7 @@ export const buildSQLForField = (
254
267
  entities,
255
268
  field,
256
269
  groupByInfo,
257
- dottedName,
270
+ dottedQuotedName,
258
271
  tableAlias,
259
272
  whereClause,
260
273
  );
@@ -282,10 +295,10 @@ export const buildSQLForField = (
282
295
  level,
283
296
  aliasMap,
284
297
  ),
285
- ([name, selector]) => `${selector} AS ${name}`,
298
+ ([name, selector]) => `${selector} AS ${wrapIdentifierMSSQL(name)}`,
286
299
  );
287
300
 
288
- const fromClause = `FROM ${dottedName} ${tableAlias}`;
301
+ const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
289
302
 
290
303
  const orderByClause = buildOrderByClauseMSSQL(entities, field, tableAlias);
291
304
  const paginationClause = buildPaginationClauseMSSQL(field, variablesDefinition);
@@ -85,7 +85,7 @@ export const callStoredProcedure = async (
85
85
  ) => {
86
86
  try {
87
87
  const data = await executeQuery(
88
- `CALL ${sp.dottedName}(${Object.keys(variables)
88
+ `CALL ${sp.dottedQuotedName}(${Object.keys(variables)
89
89
  .map((_, i) => `$${i + 1}`)
90
90
  .join(", ")});`,
91
91
  sp.db!,
@@ -16,12 +16,13 @@ import {
16
16
  isAggregationField,
17
17
  isSingleQuery,
18
18
  processFieldSelectionsMySQL,
19
+ wrapIdentifierMySQL,
19
20
  } from "../../../common";
20
21
 
21
22
  // Generate CTE for aggregations
22
23
  const buildAggregationCTE = (
23
24
  groupByInfo: GroupByInfo,
24
- dottedName: string,
25
+ dottedQuotedName: string,
25
26
  tableAlias: string,
26
27
  whereClause: string,
27
28
  ): string => {
@@ -31,7 +32,7 @@ const buildAggregationCTE = (
31
32
 
32
33
  // Add group by fields
33
34
  groupByFields.forEach((field) => {
34
- selectClauses.push(`${tableAlias}.${field}`);
35
+ selectClauses.push(`${tableAlias}.${wrapIdentifierMySQL(field)}`);
35
36
  });
36
37
 
37
38
  // Add aggregations
@@ -40,16 +41,18 @@ const buildAggregationCTE = (
40
41
  selectClauses.push(`COUNT(*) AS ${agg.alias}`);
41
42
  } else {
42
43
  const func = agg.name.toUpperCase();
43
- selectClauses.push(`${func}(${tableAlias}.${agg.fieldName}) AS ${agg.alias}`);
44
+ selectClauses.push(
45
+ `${func}(${tableAlias}.${wrapIdentifierMySQL(agg.fieldName)}) AS ${agg.alias}`,
46
+ );
44
47
  }
45
48
  });
46
49
 
47
- const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${field}`).join(", ")}`;
50
+ const groupByClause = `GROUP BY ${groupByFields.map((field) => `${tableAlias}.${wrapIdentifierMySQL(field)}`).join(", ")}`;
48
51
 
49
52
  return `${cteAlias} AS (
50
53
  SELECT
51
54
  ${selectClauses.join(",\n ")}
52
- FROM ${dottedName} ${tableAlias}
55
+ FROM ${dottedQuotedName} ${tableAlias}
53
56
  ${whereClause}
54
57
  ${groupByClause}
55
58
  )`;
@@ -60,7 +63,7 @@ const buildGroupedQuery = (
60
63
  entities: MergedEntities,
61
64
  field: SelectionAnalysis,
62
65
  groupByInfo: GroupByInfo,
63
- dottedName: string,
66
+ dottedQuotedName: string,
64
67
  tableAlias: string,
65
68
  whereClause: string,
66
69
  ): string => {
@@ -72,7 +75,7 @@ const buildGroupedQuery = (
72
75
  if (hasKey) {
73
76
  // Add key object with group by fields
74
77
  const keyFields = keys
75
- .map((key) => `'${key.alias || key.name}', ${cteAlias}.${key.name}`)
78
+ .map((key) => `'${key.alias || key.name}', ${cteAlias}.${wrapIdentifierMySQL(key.name)}`)
76
79
  .join(", ");
77
80
 
78
81
  selectClauses.push(`'${keyResolved}', JSON_OBJECT(${keyFields})`);
@@ -96,12 +99,13 @@ const buildGroupedQuery = (
96
99
  if (itemsSelection?.selections) {
97
100
  const itemFields = itemsSelection.selections
98
101
  .filter((sel) => !isAggregationField(sel.name) && sel.name !== "items")
99
- .map((sel) => `'${sel.alias || sel.name}', ${tableAlias}.${sel.name}`)
102
+ .map((sel) => `'${sel.alias || sel.name}', ${tableAlias}.${wrapIdentifierMySQL(sel.name)}`)
100
103
  .join(", ");
101
104
 
102
105
  if (itemFields) {
103
106
  const joinConditions = groupByFields.map(
104
- (field) => `${tableAlias}.${field} = ${cteAlias}.${field}`,
107
+ (field) =>
108
+ `${tableAlias}.${wrapIdentifierMySQL(field)} = ${cteAlias}.${wrapIdentifierMySQL(field)}`,
105
109
  );
106
110
 
107
111
  const whereConditions = whereClause
@@ -110,7 +114,7 @@ const buildGroupedQuery = (
110
114
 
111
115
  selectClauses.push(`'${itemsSelection.alias || itemsSelection.name}', COALESCE((
112
116
  SELECT JSON_ARRAYAGG(JSON_OBJECT(${itemFields}))
113
- FROM ${dottedName} ${tableAlias}
117
+ FROM ${dottedQuotedName} ${tableAlias}
114
118
  ${whereConditions}
115
119
  ), JSON_ARRAY())`);
116
120
  }
@@ -157,7 +161,7 @@ export const generateSQL = (
157
161
 
158
162
  if (groupByInfo) {
159
163
  // This field requires a CTE
160
- const { dottedName } = entities.queriesMap[field.name]!;
164
+ const { dottedQuotedName } = entities.queriesMap[field.name]!;
161
165
 
162
166
  const whereClause = buildWhereClauseMySQL(
163
167
  entities,
@@ -169,10 +173,9 @@ export const generateSQL = (
169
173
  null,
170
174
  index + 1,
171
175
  {},
172
- false,
173
176
  );
174
177
 
175
- const cte = buildAggregationCTE(groupByInfo, dottedName, tableAlias, whereClause);
178
+ const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
176
179
  ctes.push(cte);
177
180
  }
178
181
 
@@ -218,7 +221,7 @@ export const buildSQLForField = (
218
221
  throw new Error(`Table not found for field: ${field.name}`);
219
222
  }
220
223
 
221
- const { dottedName, resolverName } = foundTable;
224
+ const { dottedQuotedName, resolverName } = foundTable;
222
225
 
223
226
  aliasMap[tableAlias] = resolverName;
224
227
 
@@ -232,7 +235,6 @@ export const buildSQLForField = (
232
235
  parentTableAlias,
233
236
  level,
234
237
  aliasMap,
235
- false,
236
238
  );
237
239
 
238
240
  // Check if this is a GROUP BY query
@@ -244,7 +246,7 @@ export const buildSQLForField = (
244
246
  entities,
245
247
  field,
246
248
  groupByInfo,
247
- dottedName,
249
+ dottedQuotedName,
248
250
  tableAlias,
249
251
  whereClause,
250
252
  );
@@ -273,10 +275,9 @@ export const buildSQLForField = (
273
275
  aliasMap,
274
276
  ),
275
277
  ([name, selector]) => `'${name}', ${selector}`,
276
- false,
277
278
  );
278
279
 
279
- const fromClause = `FROM ${dottedName} ${tableAlias}${withoutArrayWrapper ? " LIMIT 1" : ""}`;
280
+ const fromClause = `FROM ${dottedQuotedName} ${tableAlias}${withoutArrayWrapper ? " LIMIT 1" : ""}`;
280
281
 
281
282
  const orderByClause = buildOrderByClauseMySQL(entities, field, tableAlias);
282
283
  const paginationClause = buildPaginationClauseMySQL(field, variablesDefinition);
@@ -84,7 +84,7 @@ export const callStoredProcedure = async (
84
84
  ) => {
85
85
  try {
86
86
  const data = await executeQuery(
87
- `SELECT * FROM ${sp.dottedName}(${Object.keys(variables)
87
+ `SELECT * FROM ${sp.dottedQuotedName}(${Object.keys(variables)
88
88
  .map((_, i) => `$${i + 1}`)
89
89
  .join(", ")});`,
90
90
  sp.db!,
@@ -169,7 +169,6 @@ export const generateSQL = (
169
169
  null,
170
170
  index + 1,
171
171
  {},
172
- true,
173
172
  );
174
173
 
175
174
  const cte = buildAggregationCTE(groupByInfo, dottedQuotedName, tableAlias, whereClause);
@@ -232,7 +231,6 @@ export const buildSQLForField = (
232
231
  parentTableAlias,
233
232
  level,
234
233
  aliasMap,
235
- true,
236
234
  );
237
235
 
238
236
  // Check if this is a GROUP BY query
@@ -273,7 +271,6 @@ export const buildSQLForField = (
273
271
  aliasMap,
274
272
  ),
275
273
  ([name, selector]) => `'${name}', ${selector}`,
276
- true,
277
274
  );
278
275
 
279
276
  const fromClause = `FROM ${dottedQuotedName} ${tableAlias}`;
@@ -1,19 +1,13 @@
1
1
  import type { DatabaseType } from "../../../types/configuration";
2
2
  import type { TableResolver } from "../../../types/db";
3
3
 
4
+ import { qualifiedNameFp, wrapIdentifierFp } from "../../common";
5
+
4
6
  export const generateInsertSQL =
5
7
  (toDb: DatabaseType) =>
6
8
  // oxlint-disable-next-line typescript/no-explicit-any
7
9
  (table: TableResolver, data: Record<string, any>[]) => {
8
- const columnNames = table.columns.map((column) => {
9
- if (toDb === "mssql") {
10
- return column.name;
11
- } else if (toDb === "mysql") {
12
- return `\`${column.name}\``;
13
- } else {
14
- return `"${column.name}"`;
15
- }
16
- });
10
+ const columnNames = table.columns.map((column) => wrapIdentifierFp(toDb)(column.name));
17
11
 
18
12
  const valueRows = data.map((row) => {
19
13
  const values = table.columns.map((column) => {
@@ -26,18 +20,9 @@ export const generateInsertSQL =
26
20
  return `(${values.join(",")})`;
27
21
  });
28
22
 
29
- if (toDb === "pg") {
30
- return `INSERT INTO ${table?.dottedQuotedName} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
31
- } else if (toDb === "mssql") {
32
- return `INSERT INTO ${table?.dottedName} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
33
- } else if (toDb === "mysql") {
34
- // Generate backtick version of dotted name
35
- const parts = table.dottedName.split(".");
36
- const dottedBacktickName = parts.map((p) => `\`${p}\``).join(".");
37
- return `INSERT INTO ${dottedBacktickName} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
38
- }
39
-
40
- return "";
23
+ // Quoted for the target engine, not the source: table.dottedQuotedName
24
+ // carries the source dialect's delimiters and the two differ when converting.
25
+ return `INSERT INTO ${qualifiedNameFp(toDb)(table)} (${columnNames.join(",")}) VALUES\n${valueRows.join(",\n")}`;
41
26
  };
42
27
 
43
28
  export type GenerateInsertSQL = ReturnType<typeof generateInsertSQL>;
@@ -1,6 +1,7 @@
1
1
  import type { Database, Relationships } from "../../types/configuration";
2
2
  import type { RelationshipResolver, StoredProcedure, Table, Tables } from "../../types/db";
3
3
 
4
+ import { qualifiedNameFp } from "../common";
4
5
  import { genResolverName } from "./genResolverName";
5
6
 
6
7
  /**
@@ -103,6 +104,7 @@ export const buildTableResolver = (tables: Tables, table: Table, db: Database) =
103
104
  return {
104
105
  ...table,
105
106
  db,
107
+ dottedQuotedName: qualifiedNameFp(db.type)(table),
106
108
  internalName,
107
109
  resolverName: internalName,
108
110
  relationships,
@@ -124,5 +126,6 @@ export const buildProcedureResolver = (sp: StoredProcedure, db: Database) => {
124
126
  internalName,
125
127
  resolverName,
126
128
  dottedName,
129
+ dottedQuotedName: qualifiedNameFp(db.type)(sp),
127
130
  };
128
131
  };
package/src/index.ts CHANGED
@@ -450,8 +450,6 @@ export async function createBunServer(options?: Partial<Env>) {
450
450
 
451
451
  const server = serve(serverHandlers);
452
452
 
453
- logger("graphoria").info({ port: server.port, prefixes }, "server ready");
454
-
455
453
  return {
456
454
  server,
457
455
  prefixes,