mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 18:47:29 +03:00
Do nested JSON decoding in SQL in SQLite
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8138 GitOrigin-RevId: f22ebe94664a112536acaa37e13189d9fddcd222
This commit is contained in:
parent
bf16f628ec
commit
6a5fe902b7
@ -1,8 +1,8 @@
|
||||
import { ArrayRelationInsertFieldValue, ColumnFieldValue, ColumnInsertFieldValue, DeleteMutationOperation, Expression, Field, InsertFieldSchema, InsertMutationOperation, MutationOperation, MutationOperationResults, MutationRequest, MutationResponse, NullColumnFieldValue, ObjectRelationInsertFieldValue, QueryRequest, QueryResponse, RowObject, RowUpdate, TableInsertSchema, TableName, TableRelationships, UpdateMutationOperation } from "@hasura/dc-api-types";
|
||||
import { ArrayRelationInsertFieldValue, ColumnInsertFieldValue, DeleteMutationOperation, Expression, Field, InsertFieldSchema, InsertMutationOperation, MutationOperation, MutationOperationResults, MutationRequest, MutationResponse, ObjectRelationInsertFieldValue, RowUpdate, TableInsertSchema, TableName, TableRelationships, UpdateMutationOperation } from "@hasura/dc-api-types";
|
||||
import { Config } from "./config";
|
||||
import { Connection, defaultMode, SqlLogger, withConnection } from "./db";
|
||||
import { escapeIdentifier, escapeTableName, escapeTableNameSansSchema, json_object, parseRowFields, where_clause, } from "./query";
|
||||
import { asyncSequenceFromInputs, ErrorWithStatusCode, mapObjectToArray, tableNameEquals, unreachable, zip } from "./util";
|
||||
import { escapeIdentifier, escapeTableName, escapeTableNameSansSchema, json_object, where_clause, } from "./query";
|
||||
import { asyncSequenceFromInputs, ErrorWithStatusCode, mapObjectToArray, tableNameEquals, unreachable } from "./util";
|
||||
|
||||
// Types
|
||||
|
||||
@ -260,7 +260,7 @@ async function mutationOperation(db: Connection, relationships: Array<TableRelat
|
||||
if (!row.ok) {
|
||||
insertFailed.push(row);
|
||||
}
|
||||
return parseMutationResultRow(row, op.returning_fields ?? {});
|
||||
return JSON.parse(row.row);
|
||||
});
|
||||
if(insertFailed.length > 0) {
|
||||
await db.query('ROLLBACK', {});
|
||||
@ -283,7 +283,7 @@ async function mutationOperation(db: Connection, relationships: Array<TableRelat
|
||||
if (!row.ok) {
|
||||
updateFailed.push(row);
|
||||
}
|
||||
return parseMutationResultRow(row, op.returning_fields ?? {});
|
||||
return JSON.parse(row.row);
|
||||
});
|
||||
if(updateFailed.length > 0) {
|
||||
await db.query('ROLLBACK', {});
|
||||
@ -299,7 +299,7 @@ async function mutationOperation(db: Connection, relationships: Array<TableRelat
|
||||
case 'delete':
|
||||
await db.query('BEGIN',{});
|
||||
const deleteResults = await deleteRows(db, relationships, op);
|
||||
const mappedDeleteResults = deleteResults.map(row => parseMutationResultRow(row, op.returning_fields ?? {}));
|
||||
const mappedDeleteResults = deleteResults.map(row => JSON.parse(row.row));
|
||||
await db.query('COMMIT',{});
|
||||
return {
|
||||
affected_rows: mappedDeleteResults.length,
|
||||
@ -311,11 +311,6 @@ async function mutationOperation(db: Connection, relationships: Array<TableRelat
|
||||
}
|
||||
}
|
||||
|
||||
function parseMutationResultRow(e: Row, returningFields: Record<string, Field>): Record<string, (ColumnFieldValue | QueryResponse | NullColumnFieldValue)> {
|
||||
const parsedRow = JSON.parse(e.row);
|
||||
return parseRowFields(parsedRow, returningFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param config
|
||||
* @param sqlLogger
|
||||
|
@ -343,7 +343,10 @@ function table_query(
|
||||
): string {
|
||||
const tableAlias = generateTableAlias(tableName);
|
||||
const aggregateSelect = aggregates_query(ts, tableName, joinInfo, aggregates, wWhere, wLimit, wOffset, wOrder);
|
||||
const fieldSelect = isEmptyObject(fields) ? [] : [`'rows', JSON_GROUP_ARRAY(j)`];
|
||||
// The use of the JSON function inside JSON_GROUP_ARRAY is necessary from SQLite 3.39.0 due to breaking changes in
|
||||
// SQLite. See https://sqlite.org/forum/forumpost/e3b101fb3234272b for more details. This approach still works fine
|
||||
// for older versions too.
|
||||
const fieldSelect = isEmptyObject(fields) ? [] : [`'rows', JSON_GROUP_ARRAY(JSON(j))`];
|
||||
const fieldFrom = isEmptyObject(fields) ? '' : (() => {
|
||||
const whereClause = where(ts, wWhere, joinInfo, tableName, tableAlias);
|
||||
// NOTE: The reuse of the 'j' identifier should be safe due to scoping. This is confirmed in testing.
|
||||
@ -663,59 +666,6 @@ function query(request: QueryRequest): string {
|
||||
return tag('query', `SELECT ${result} as data`);
|
||||
}
|
||||
|
||||
/** Format the DB response into a /query response.
|
||||
*
|
||||
* Note: There should always be one result since 0 rows still generates an empty JSON array.
|
||||
*/
|
||||
function parseDbResult(rows: any, request: QueryRequest): QueryResponse {
|
||||
const rawResponse = JSON.parse(rows[0].data);
|
||||
return parseRawQueryResponse(request.query, rawResponse);
|
||||
}
|
||||
|
||||
type RawQueryResponse = {
|
||||
rows?: string[] | null,
|
||||
aggregates?: Record<string, any> | null,
|
||||
}
|
||||
|
||||
function parseRawQueryResponse(query: Query, rawQueryResponse: RawQueryResponse): QueryResponse {
|
||||
const rows = query.fields
|
||||
? (rawQueryResponse.rows ?? []).map(rowJson => {
|
||||
const row: Record<string, (ColumnFieldValue | RawQueryResponse | NullColumnFieldValue)> = JSON.parse(rowJson);
|
||||
return parseRowFields(row, query.fields ?? {});
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
aggregates: rawQueryResponse.aggregates,
|
||||
... (rows ? { rows } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
// It seems that since SQLite 3.39.0, the JSON_GROUP_ARRAY function that is used to create the arrays
|
||||
// for object/array relationships now string-encodes any JSON inside the array. This function works
|
||||
// around the problem by decoding the stringified-JSON back into JSON.
|
||||
// The issue has been raised here: https://sqlite.org/forum/forumpost/e3b101fb32
|
||||
export function parseRowFields(row: Record<string, (ColumnFieldValue | RawQueryResponse | NullColumnFieldValue)>, fields: Record<string, Field>): Record<string, (ColumnFieldValue | QueryResponse | NullColumnFieldValue)> {
|
||||
return mapObject(row, ([fieldName, fieldValue]) => {
|
||||
const queryField = fields[fieldName];
|
||||
if (queryField === undefined)
|
||||
throw new Error(`Unable to find response field ${fieldName} on amongst original query fields`);
|
||||
|
||||
switch (queryField.type) {
|
||||
case "column":
|
||||
return [fieldName, fieldValue];
|
||||
|
||||
case "relationship":
|
||||
if (fieldValue === null || (!("rows" in fieldValue) && !("aggregates" in fieldValue)))
|
||||
throw new Error(`Did not find a query response in field ${fieldName} where one was expected`);
|
||||
return [fieldName, parseRawQueryResponse(queryField.query, fieldValue)];
|
||||
|
||||
default:
|
||||
return unreachable(queryField["type"]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Function to add SQL comments to the generated SQL to tag which procedures generated what text.
|
||||
*
|
||||
* comment('a','b') => '/*\<a>\*\/ b /*\</a>*\/'
|
||||
@ -788,7 +738,7 @@ export async function queryData(config: Config, sqlLogger: SqlLogger, queryReque
|
||||
}
|
||||
|
||||
const results = await db.query(q);
|
||||
return parseDbResult(results, queryRequest);
|
||||
return JSON.parse(results[0].data);
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user