Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only select required columns from sql databases #15169

Open
wants to merge 54 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
544302a
Request only needed fields
adrinr Dec 2, 2024
0e0b647
Generate proper select
adrinr Dec 11, 2024
191b632
Dry
adrinr Dec 11, 2024
a415095
Lint
adrinr Dec 11, 2024
74e1bbc
Require only visible fields on views
adrinr Dec 12, 2024
e3c9156
Trim selected fields
adrinr Dec 12, 2024
00557e5
Don't include unnecessary joins
adrinr Dec 12, 2024
910faa6
Fix getting relations
adrinr Dec 12, 2024
be00741
Request relation only when required
adrinr Dec 12, 2024
112b7c7
Fix sqs views
adrinr Dec 13, 2024
bbb60af
Relations, select only required fields
adrinr Dec 13, 2024
80dcc51
Fix sql alias tests
adrinr Dec 16, 2024
f92fcea
Include * when having formulas
adrinr Dec 16, 2024
f8ece82
Fix formulas on internal
adrinr Dec 16, 2024
e6a27ad
Ensure required fields are included
adrinr Dec 16, 2024
2d771c9
Fix wrong relationship select
adrinr Dec 16, 2024
14b5a42
Fix patch issues
adrinr Dec 16, 2024
d60cc7a
Fix encoding
adrinr Dec 16, 2024
0ef4a15
Prevent repeated fields on select
adrinr Dec 16, 2024
fc22db3
Add back cte
adrinr Dec 16, 2024
aa28896
Fix tests back
adrinr Dec 16, 2024
eb7fcd0
Don't select * on relationships
adrinr Dec 16, 2024
df62845
Fix encodings
adrinr Dec 16, 2024
740069e
Use table for get before row
adrinr Dec 16, 2024
7cd412b
Add basic sql alias test
adrinr Dec 16, 2024
fc75728
Add more tests
adrinr Dec 16, 2024
7932ee7
Fix sqs calculations
adrinr Dec 17, 2024
499d3f2
Fix sql calculations
adrinr Dec 17, 2024
51ba1f0
Add required fields on table search as well
adrinr Dec 17, 2024
c398412
Fix pg money
adrinr Dec 17, 2024
95f7eea
Don't add breaking changes
adrinr Dec 17, 2024
8765a28
Move responsability to sql utils
adrinr Dec 17, 2024
8eb82d3
Fix mysql formula test
adrinr Dec 17, 2024
23531e1
Fix sqs formula
adrinr Dec 17, 2024
bcc9bba
Fix test
adrinr Dec 17, 2024
b05b523
Fix issues with display names not being sql tables
adrinr Dec 17, 2024
7d8cfeb
Merge branch 'master' into BUDI-8885/only-select-required-columns-fro…
adrinr Dec 18, 2024
3de0795
Merge branch 'chore/guard-display-column-in-the-api' into BUDI-8885/o…
adrinr Dec 18, 2024
27d1929
Add basic sqlUtils test
adrinr Dec 18, 2024
875319e
Relationship tests
adrinr Dec 18, 2024
96bddbb
More relationship tests
adrinr Dec 18, 2024
e23753a
Add describe
adrinr Dec 18, 2024
14da902
Regenerate table before each test
adrinr Dec 18, 2024
8da96ab
Add initial view tests
adrinr Dec 18, 2024
460b5fd
More tests
adrinr Dec 18, 2024
9396292
Create table test builder
adrinr Dec 18, 2024
92e791f
Add calculation tests
adrinr Dec 19, 2024
8891976
Comment to explain the behaviour of the junction document select.
mike12345567 Dec 19, 2024
0ee432d
Always use tableid prefix for sqs
adrinr Dec 19, 2024
5a9ed4f
Include hidden fields for formulas
adrinr Dec 19, 2024
7cc03b1
Create sql tests for sqs
adrinr Dec 19, 2024
16fb865
Fix tests
adrinr Dec 19, 2024
8905f9d
Fix tests
adrinr Dec 19, 2024
3168b42
Merge branch 'master' into BUDI-8885/only-select-required-columns-fro…
mike12345567 Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 38 additions & 42 deletions packages/backend-core/src/sql/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,17 +272,6 @@ class InternalBuilder {
return parts.join(".")
}

private isFullSelectStatementRequired(): boolean {
for (let column of Object.values(this.table.schema)) {
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(column)) {
return true
} else if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(column)) {
return true
}
}
return false
}

private generateSelectStatement(): (string | Knex.Raw)[] | "*" {
const { table, resource } = this.query

Expand All @@ -292,11 +281,9 @@ class InternalBuilder {

const alias = this.getTableName(table)
const schema = this.table.schema
if (!this.isFullSelectStatementRequired()) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for it has been moved to sqlUtils

return [this.knex.raw("??", [`${alias}.*`])]
}

// get just the fields for this table
return resource.fields
const tableFields = resource.fields
.map(field => {
const parts = field.split(/\./g)
let table: string | undefined = undefined
Expand All @@ -311,34 +298,35 @@ class InternalBuilder {
return { table, column, field }
})
.filter(({ table }) => !table || table === alias)
.map(({ table, column, field }) => {
const columnSchema = schema[column]

if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
return this.knex.raw(`??::money::numeric as ??`, [
this.rawQuotedIdentifier([table, column].join(".")),
this.knex.raw(this.quote(field)),
])
}
return tableFields.map(({ table, column, field }) => {
const columnSchema = schema[column]

if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
// Time gets returned as timestamp from mssql, not matching the expected
// HH:mm format
if (this.SPECIAL_SELECT_CASES.POSTGRES_MONEY(columnSchema)) {
return this.knex.raw(`??::money::numeric as ??`, [
this.rawQuotedIdentifier([table, column].join(".")),
this.knex.raw(this.quote(field)),
])
}

// TODO: figure out how to express this safely without string
// interpolation.
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
this.rawQuotedIdentifier(field),
this.knex.raw(this.quote(field)),
])
}
if (this.SPECIAL_SELECT_CASES.MSSQL_DATES(columnSchema)) {
// Time gets returned as timestamp from mssql, not matching the expected
// HH:mm format

if (table) {
return this.rawQuotedIdentifier(`${table}.${column}`)
} else {
return this.rawQuotedIdentifier(field)
}
})
// TODO: figure out how to express this safely without string
// interpolation.
return this.knex.raw(`CONVERT(varchar, ??, 108) as ??`, [
this.rawQuotedIdentifier(field),
this.knex.raw(this.quote(field)),
])
}

if (table) {
return this.rawQuotedIdentifier(`${table}.${column}`)
} else {
return this.rawQuotedIdentifier(field)
}
})
}

// OracleDB can't use character-large-objects (CLOBs) in WHERE clauses,
Expand Down Expand Up @@ -1239,6 +1227,7 @@ class InternalBuilder {
if (!toTable || !fromTable) {
continue
}

const relatedTable = tables[toTable]
if (!relatedTable) {
throw new Error(`related table "${toTable}" not found in datasource`)
Expand Down Expand Up @@ -1267,6 +1256,10 @@ class InternalBuilder {
const fieldList = relationshipFields.map(field =>
this.buildJsonField(relatedTable, field)
)
if (!fieldList.length) {
continue
}

const fieldListFormatted = fieldList
.map(f => {
const separator = this.client === SqlClient.ORACLE ? " VALUE " : ","
Expand Down Expand Up @@ -1307,7 +1300,9 @@ class InternalBuilder {
)

const standardWrap = (select: Knex.Raw): Knex.QueryBuilder => {
subQuery = subQuery.select(`${toAlias}.*`).limit(getRelationshipLimit())
subQuery = subQuery
.select(relationshipFields)
.limit(getRelationshipLimit())
// @ts-ignore - the from alias syntax isn't in Knex typing
return knex.select(select).from({
[toAlias]: subQuery,
Expand Down Expand Up @@ -1537,11 +1532,12 @@ class InternalBuilder {
limits?: { base: number; query: number }
} = {}
): Knex.QueryBuilder {
let { operation, filters, paginate, relationships, table } = this.query
const { operation, filters, paginate, relationships, table } = this.query
const { limits } = opts

// start building the query
let query = this.qualifiedKnex()

// handle pagination
let foundOffset: number | null = null
let foundLimit = limits?.query || limits?.base
Expand Down Expand Up @@ -1590,7 +1586,7 @@ class InternalBuilder {
const mainTable = this.query.tableAliases?.[table.name] || table.name
const cte = this.addSorting(
this.knex
.with("paginated", query)
.with("paginated", query.clone().clearSelect().select("*"))
.select(this.generateSelectStatement())
.from({
[mainTable]: "paginated",
Expand Down
5 changes: 4 additions & 1 deletion packages/server/src/api/controllers/row/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export async function handleRequest<T extends Operation>(
export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
const source = await utils.getSource(ctx)

const { viewId, tableId } = utils.getSourceId(ctx)
const sourceId = viewId || tableId

if (sdk.views.isView(source) && helpers.views.isCalculationView(source)) {
ctx.throw(400, "Cannot update rows through a calculation view")
}
Expand Down Expand Up @@ -86,7 +89,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
// The id might have been changed, so the refetching would fail. Recalculating the id just in case
const updatedId =
generateIdForRow({ ...beforeRow, ...dataToUpdate }, table) || _id
const row = await sdk.rows.external.getRow(table._id!, updatedId, {
const row = await sdk.rows.external.getRow(sourceId, updatedId, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find!

relationships: true,
})

Expand Down
123 changes: 105 additions & 18 deletions packages/server/src/api/controllers/row/utils/sqlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
import { breakExternalTableId } from "../../../../integrations/utils"
import { generateJunctionTableID } from "../../../../db/utils"
import sdk from "../../../../sdk"
import { helpers } from "@budibase/shared-core"
import { helpers, PROTECTED_INTERNAL_COLUMNS } from "@budibase/shared-core"
import { sql } from "@budibase/backend-core"

type TableMap = Record<string, Table>

Expand Down Expand Up @@ -118,45 +119,131 @@ export async function buildSqlFieldList(
opts?: { relationships: boolean }
) {
const { relationships } = opts || {}

const nonMappedColumns = [FieldType.LINK, FieldType.FORMULA, FieldType.AI]

function extractRealFields(table: Table, existing: string[] = []) {
return Object.entries(table.schema)
.filter(
([columnName, column]) =>
column.type !== FieldType.LINK &&
column.type !== FieldType.FORMULA &&
column.type !== FieldType.AI &&
!existing.find(
(field: string) => field === `${table.name}.${columnName}`
)
!nonMappedColumns.includes(column.type) &&
!existing.find((field: string) => field === columnName)
)
.map(([columnName]) => `${table.name}.${columnName}`)
.map(([columnName]) => columnName)
}

let fields: string[] = []
if (sdk.views.isView(source)) {
fields = Object.keys(helpers.views.basicFields(source))
} else {
fields = extractRealFields(source)
function getRequiredFields(table: Table, existing: string[] = []) {
const requiredFields: string[] = []
if (table.primary) {
requiredFields.push(...table.primary)
}
if (table.primaryDisplay) {
requiredFields.push(table.primaryDisplay)
}

if (!sql.utils.isExternalTable(table)) {
requiredFields.push(...PROTECTED_INTERNAL_COLUMNS)
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
}

return requiredFields.filter(
column =>
!existing.find((field: string) => field === column) &&
table.schema[column] &&
!nonMappedColumns.includes(table.schema[column].type)
)
}

let fields: string[] = []

const isView = sdk.views.isView(source)

let table: Table
if (sdk.views.isView(source)) {
if (isView) {
table = await sdk.views.getTable(source.id)

fields = Object.keys(helpers.views.basicFields(source)).filter(
f => table.schema[f].type !== FieldType.LINK
)
} else {
table = source
fields = extractRealFields(source).filter(
f => table.schema[f].visible !== false
)
}

for (let field of Object.values(table.schema)) {
const containsFormula = (isView ? fields : Object.keys(table.schema)).some(
f => table.schema[f]?.type === FieldType.FORMULA
)
// If are requesting for a formula field, we need to retrieve all fields
if (containsFormula) {
fields = extractRealFields(table)
}

if (!isView || !helpers.views.isCalculationView(source)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For calculation views, I assume this ends up just leaving the fields empty (since its up to the SQL layer to generate the select statements for these)

fields.push(
...getRequiredFields(
{
...table,
primaryDisplay: source.primaryDisplay || table.primaryDisplay,
},
fields
)
)
}

fields = fields.map(c => `${table.name}.${c}`)

for (const field of Object.values(table.schema)) {
if (field.type !== FieldType.LINK || !relationships || !field.tableId) {
continue
}

if (
isView &&
(!source.schema?.[field.name] ||
!helpers.views.isVisible(source.schema[field.name])) &&
!containsFormula
) {
continue
}

const { tableName } = breakExternalTableId(field.tableId)
if (tables[tableName]) {
fields = fields.concat(extractRealFields(tables[tableName], fields))
const relatedTable = tables[tableName]
if (!relatedTable) {
continue
}

const viewFields = new Set<string>()
if (containsFormula) {
extractRealFields(relatedTable).forEach(f => viewFields.add(f))
} else {
relatedTable.primary?.forEach(f => viewFields.add(f))
if (relatedTable.primaryDisplay) {
viewFields.add(relatedTable.primaryDisplay)
}

if (isView) {
Object.entries(source.schema?.[field.name]?.columns || {})
.filter(
([columnName, columnConfig]) =>
relatedTable.schema[columnName] &&
helpers.views.isVisible(columnConfig) &&
![FieldType.LINK, FieldType.FORMULA].includes(
relatedTable.schema[columnName].type
)
)
.forEach(([field]) => viewFields.add(field))
}
}

const fieldsToAdd = Array.from(viewFields)
.filter(f => !nonMappedColumns.includes(relatedTable.schema[f].type))
.map(f => `${relatedTable.name}.${f}`)
.filter(f => !fields.includes(f))
fields.push(...fieldsToAdd)
}

return fields
return [...new Set(fields)]
}

export function isKnexEmptyReadResponse(resp: DatasourcePlusQueryResponse) {
Expand Down
Loading
Loading