Merge pull request #4795 from Budibase/fix/sql-issues

Various SQL fixes
This commit is contained in:
Michael Drury 2022-03-07 10:08:17 +00:00 committed by GitHub
commit ee09b504c4
8 changed files with 166 additions and 1225 deletions

View File

@ -1,5 +1,10 @@
USE master; USE master;
IF NOT EXISTS(SELECT 1 FROM sys.schemas WHERE name = 'Chains')
BEGIN
EXEC sys.sp_executesql N'CREATE SCHEMA Chains;'
END
IF OBJECT_ID ('dbo.products', 'U') IS NOT NULL IF OBJECT_ID ('dbo.products', 'U') IS NOT NULL
DROP TABLE products; DROP TABLE products;
GO GO
@ -61,3 +66,15 @@ VALUES ('Bob', '30'),
('Bobert', '99'), ('Bobert', '99'),
('Jan', '22'), ('Jan', '22'),
('Megan', '11'); ('Megan', '11');
IF OBJECT_ID ('Chains.sizes', 'U') IS NOT NULL
DROP TABLE Chains.sizes;
GO
CREATE TABLE Chains.sizes
(
sizeid int IDENTITY(1, 1),
name varchar(30),
CONSTRAINT pk_size PRIMARY KEY NONCLUSTERED (sizeid)
);

View File

@ -1,5 +1,6 @@
SELECT 'CREATE DATABASE main' SELECT 'CREATE DATABASE main'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
CREATE SCHEMA test;
CREATE TYPE person_job AS ENUM ('qa', 'programmer', 'designer'); CREATE TYPE person_job AS ENUM ('qa', 'programmer', 'designer');
CREATE TABLE Persons ( CREATE TABLE Persons (
PersonID SERIAL PRIMARY KEY, PersonID SERIAL PRIMARY KEY,
@ -37,6 +38,10 @@ CREATE TABLE Products_Tasks (
REFERENCES Tasks(TaskID), REFERENCES Tasks(TaskID),
PRIMARY KEY (ProductID, TaskID) PRIMARY KEY (ProductID, TaskID)
); );
CREATE TABLE test.table1 (
id SERIAL PRIMARY KEY,
Name varchar(255)
);
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa'); INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast', 'qa');
INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer'); INSERT INTO Persons (FirstName, LastName, Address, City, Type) VALUES ('John', 'Smith', '64 Updown Road', 'Dublin', 'programmer');
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
@ -48,3 +53,4 @@ INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);
INSERT INTO test.table1 (Name) VALUES ('Test');

View File

@ -153,6 +153,7 @@ export interface QueryJson {
datasourceId: string datasourceId: string
entityId: string entityId: string
operation: Operation operation: Operation
schema?: string
} }
resource: { resource: {
fields: string[] fields: string[]

View File

@ -249,6 +249,9 @@ class InternalBuilder {
create(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery { create(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, body } = json const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId) let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
const parsedBody = parseBody(body) const parsedBody = parseBody(body)
// make sure no null values in body for creation // make sure no null values in body for creation
for (let [key, value] of Object.entries(parsedBody)) { for (let [key, value] of Object.entries(parsedBody)) {
@ -267,6 +270,9 @@ class InternalBuilder {
bulkCreate(knex: Knex, json: QueryJson): KnexQuery { bulkCreate(knex: Knex, json: QueryJson): KnexQuery {
const { endpoint, body } = json const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId) let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
if (!Array.isArray(body)) { if (!Array.isArray(body)) {
return query return query
} }
@ -275,7 +281,7 @@ class InternalBuilder {
} }
read(knex: Knex, json: QueryJson, limit: number): KnexQuery { read(knex: Knex, json: QueryJson, limit: number): KnexQuery {
let { endpoint, resource, filters, sort, paginate, relationships } = json let { endpoint, resource, filters, paginate, relationships } = json
const tableName = endpoint.entityId const tableName = endpoint.entityId
// select all if not specified // select all if not specified
if (!resource) { if (!resource) {
@ -302,6 +308,9 @@ class InternalBuilder {
} }
// start building the query // start building the query
let query: KnexQuery = knex(tableName).limit(foundLimit) let query: KnexQuery = knex(tableName).limit(foundLimit)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
if (foundOffset) { if (foundOffset) {
query = query.offset(foundOffset) query = query.offset(foundOffset)
} }
@ -331,6 +340,9 @@ class InternalBuilder {
update(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery { update(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, body, filters } = json const { endpoint, body, filters } = json
let query: KnexQuery = knex(endpoint.entityId) let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
const parsedBody = parseBody(body) const parsedBody = parseBody(body)
query = this.addFilters(query, filters, { tableName: endpoint.entityId }) query = this.addFilters(query, filters, { tableName: endpoint.entityId })
// mysql can't use returning // mysql can't use returning
@ -344,6 +356,9 @@ class InternalBuilder {
delete(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery { delete(knex: Knex, json: QueryJson, opts: QueryOptions): KnexQuery {
const { endpoint, filters } = json const { endpoint, filters } = json
let query: KnexQuery = knex(endpoint.entityId) let query: KnexQuery = knex(endpoint.entityId)
if (endpoint.schema) {
query = query.withSchema(endpoint.schema)
}
query = this.addFilters(query, filters, { tableName: endpoint.entityId }) query = this.addFilters(query, filters, { tableName: endpoint.entityId })
// mysql can't use returning // mysql can't use returning
if (opts.disableReturning) { if (opts.disableReturning) {

View File

@ -101,28 +101,28 @@ function generateSchema(
} }
function buildCreateTable( function buildCreateTable(
knex: Knex, knex: SchemaBuilder,
table: Table, table: Table,
tables: Record<string, Table> tables: Record<string, Table>
): SchemaBuilder { ): SchemaBuilder {
return knex.schema.createTable(table.name, schema => { return knex.createTable(table.name, schema => {
generateSchema(schema, table, tables) generateSchema(schema, table, tables)
}) })
} }
function buildUpdateTable( function buildUpdateTable(
knex: Knex, knex: SchemaBuilder,
table: Table, table: Table,
tables: Record<string, Table>, tables: Record<string, Table>,
oldTable: Table oldTable: Table
): SchemaBuilder { ): SchemaBuilder {
return knex.schema.alterTable(table.name, schema => { return knex.alterTable(table.name, schema => {
generateSchema(schema, table, tables, oldTable) generateSchema(schema, table, tables, oldTable)
}) })
} }
function buildDeleteTable(knex: Knex, table: Table): SchemaBuilder { function buildDeleteTable(knex: SchemaBuilder, table: Table): SchemaBuilder {
return knex.schema.dropTable(table.name) return knex.dropTable(table.name)
} }
class SqlTableQueryBuilder { class SqlTableQueryBuilder {
@ -146,7 +146,11 @@ class SqlTableQueryBuilder {
} }
_tableQuery(json: QueryJson): any { _tableQuery(json: QueryJson): any {
const client = knex({ client: this.sqlClient }) let client = knex({ client: this.sqlClient }).schema
if (json?.endpoint?.schema) {
client = client.withSchema(json.endpoint.schema)
}
let query let query
if (!json.table || !json.meta || !json.meta.tables) { if (!json.table || !json.meta || !json.meta.tables) {
throw "Cannot execute without table being specified" throw "Cannot execute without table being specified"

View File

@ -19,6 +19,7 @@ import { Table, TableSchema } from "../definitions/common"
module MSSQLModule { module MSSQLModule {
const sqlServer = require("mssql") const sqlServer = require("mssql")
const Sql = require("./base/sql") const Sql = require("./base/sql")
const DEFAULT_SCHEMA = "dbo"
interface MSSQLConfig { interface MSSQLConfig {
user: string user: string
@ -26,9 +27,17 @@ module MSSQLModule {
server: string server: string
port: number port: number
database: string database: string
schema: string
encrypt?: boolean encrypt?: boolean
} }
interface TablesResponse {
TABLE_CATALOG: string
TABLE_SCHEMA: string
TABLE_NAME: string
TABLE_TYPE: string
}
const SCHEMA: Integration = { const SCHEMA: Integration = {
docs: "https://github.com/tediousjs/node-mssql", docs: "https://github.com/tediousjs/node-mssql",
plus: true, plus: true,
@ -58,6 +67,10 @@ module MSSQLModule {
type: DatasourceFieldTypes.STRING, type: DatasourceFieldTypes.STRING,
default: "root", default: "root",
}, },
schema: {
type: DatasourceFieldTypes.STRING,
default: DEFAULT_SCHEMA,
},
encrypt: { encrypt: {
type: DatasourceFieldTypes.BOOLEAN, type: DatasourceFieldTypes.BOOLEAN,
default: true, default: true,
@ -96,6 +109,35 @@ module MSSQLModule {
TABLES_SQL = TABLES_SQL =
"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'" "SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'"
constructor(config: MSSQLConfig) {
super(SqlClients.MS_SQL)
this.config = config
const clientCfg = {
...this.config,
options: {
encrypt: this.config.encrypt,
enableArithAbort: true,
},
}
delete clientCfg.encrypt
if (!this.pool) {
this.pool = new sqlServer.ConnectionPool(clientCfg)
}
}
getBindingIdentifier(): string {
return `(@p${this.index++})`
}
async connect() {
try {
this.client = await this.pool.connect()
} catch (err) {
// @ts-ignore
throw new Error(err)
}
}
async internalQuery( async internalQuery(
query: SqlQuery, query: SqlQuery,
operation: string | undefined = undefined operation: string | undefined = undefined
@ -151,35 +193,6 @@ module MSSQLModule {
WHERE TABLE_NAME='${tableName}'` WHERE TABLE_NAME='${tableName}'`
} }
constructor(config: MSSQLConfig) {
super(SqlClients.MS_SQL)
this.config = config
const clientCfg = {
...this.config,
options: {
encrypt: this.config.encrypt,
enableArithAbort: true,
},
}
delete clientCfg.encrypt
if (!this.pool) {
this.pool = new sqlServer.ConnectionPool(clientCfg)
}
}
getBindingIdentifier(): string {
return `(@p${this.index++})`
}
async connect() {
try {
this.client = await this.pool.connect()
} catch (err) {
// @ts-ignore
throw new Error(err)
}
}
async runSQL(sql: string) { async runSQL(sql: string) {
return (await this.internalQuery(getSqlQuery(sql))).recordset return (await this.internalQuery(getSqlQuery(sql))).recordset
} }
@ -191,11 +204,14 @@ module MSSQLModule {
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(datasourceId: string, entities: Record<string, Table>) {
await this.connect() await this.connect()
let tableNames = await this.runSQL(this.TABLES_SQL) let tableInfo: TablesResponse[] = await this.runSQL(this.TABLES_SQL)
if (tableNames == null || !Array.isArray(tableNames)) { if (tableInfo == null || !Array.isArray(tableInfo)) {
throw "Unable to get list of tables in database" throw "Unable to get list of tables in database"
} }
tableNames = tableNames
const schema = this.config.schema || DEFAULT_SCHEMA
const tableNames = tableInfo
.filter((record: any) => record.TABLE_SCHEMA === schema)
.map((record: any) => record.TABLE_NAME) .map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1) .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
@ -267,7 +283,11 @@ module MSSQLModule {
} }
async query(json: QueryJson) { async query(json: QueryJson) {
const schema = this.config.schema
await this.connect() await this.connect()
if (schema && schema !== DEFAULT_SCHEMA && json?.endpoint) {
json.endpoint.schema = schema
}
const operation = this._operation(json) const operation = this._operation(json)
const queryFn = (query: any, op: string) => this.internalQuery(query, op) const queryFn = (query: any, op: string) => this.internalQuery(query, op)
const processFn = (result: any) => const processFn = (result: any) =>

View File

@ -6,35 +6,63 @@ import { FieldTypes, BuildSchemaErrors, InvalidColumns } from "../constants"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g const ROW_ID_REGEX = /^\[.*]$/g
const SQL_TYPE_MAP = { const SQL_NUMBER_TYPE_MAP = {
text: FieldTypes.LONGFORM,
varchar: FieldTypes.STRING,
integer: FieldTypes.NUMBER, integer: FieldTypes.NUMBER,
int: FieldTypes.NUMBER,
bigint: FieldTypes.NUMBER, bigint: FieldTypes.NUMBER,
decimal: FieldTypes.NUMBER, decimal: FieldTypes.NUMBER,
smallint: FieldTypes.NUMBER, smallint: FieldTypes.NUMBER,
real: FieldTypes.NUMBER, real: FieldTypes.NUMBER,
"double precision": FieldTypes.NUMBER,
timestamp: FieldTypes.DATETIME,
time: FieldTypes.DATETIME,
boolean: FieldTypes.BOOLEAN,
json: FieldTypes.JSON,
date: FieldTypes.DATETIME,
blob: FieldTypes.LONGFORM,
enum: FieldTypes.STRING,
float: FieldTypes.NUMBER, float: FieldTypes.NUMBER,
int: FieldTypes.NUMBER,
numeric: FieldTypes.NUMBER, numeric: FieldTypes.NUMBER,
mediumint: FieldTypes.NUMBER, mediumint: FieldTypes.NUMBER,
dec: FieldTypes.NUMBER, dec: FieldTypes.NUMBER,
double: FieldTypes.NUMBER, double: FieldTypes.NUMBER,
fixed: FieldTypes.NUMBER, fixed: FieldTypes.NUMBER,
datetime: FieldTypes.DATETIME, "double precision": FieldTypes.NUMBER,
tinyint: FieldTypes.BOOLEAN,
long: FieldTypes.LONGFORM,
number: FieldTypes.NUMBER, number: FieldTypes.NUMBER,
binary_float: FieldTypes.NUMBER, binary_float: FieldTypes.NUMBER,
binary_double: FieldTypes.NUMBER, binary_double: FieldTypes.NUMBER,
money: FieldTypes.NUMBER,
smallmoney: FieldTypes.NUMBER,
}
const SQL_DATE_TYPE_MAP = {
timestamp: FieldTypes.DATETIME,
time: FieldTypes.DATETIME,
datetime: FieldTypes.DATETIME,
smalldatetime: FieldTypes.DATETIME,
date: FieldTypes.DATETIME,
}
const SQL_STRING_TYPE_MAP = {
varchar: FieldTypes.STRING,
char: FieldTypes.STRING,
nchar: FieldTypes.STRING,
nvarchar: FieldTypes.STRING,
ntext: FieldTypes.STRING,
enum: FieldTypes.STRING,
blob: FieldTypes.LONGFORM,
long: FieldTypes.LONGFORM,
text: FieldTypes.LONGFORM,
}
const SQL_BOOLEAN_TYPE_MAP = {
boolean: FieldTypes.BOOLEAN,
bit: FieldTypes.BOOLEAN,
tinyint: FieldTypes.BOOLEAN,
}
const SQL_MISC_TYPE_MAP = {
json: FieldTypes.JSON,
}
const SQL_TYPE_MAP = {
...SQL_NUMBER_TYPE_MAP,
...SQL_DATE_TYPE_MAP,
...SQL_STRING_TYPE_MAP,
...SQL_BOOLEAN_TYPE_MAP,
...SQL_MISC_TYPE_MAP,
} }
export enum SqlClients { export enum SqlClients {

File diff suppressed because it is too large Load Diff