diff --git a/package.json b/package.json index 8d1902f..9ca34b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@outerbase/sdk", - "version": "2.0.0-rc.0", + "version": "2.0.0-rc.1", "description": "", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/connections/bigquery.ts b/src/connections/bigquery.ts index 5a92a6c..8edcab4 100644 --- a/src/connections/bigquery.ts +++ b/src/connections/bigquery.ts @@ -1,7 +1,7 @@ import { QueryType } from '../query-params'; import { Query } from '../query'; -import { QueryResult } from './index'; -import { Database, Table, TableColumn } from '../models/database'; +import { ConnectionSelectOptions, QueryResult } from './index'; +import { Database, Schema, Table, TableColumn } from '../models/database'; import { BigQueryDialect } from '../query-builder/dialects/bigquery'; import { BigQuery } from '@google-cloud/bigquery'; import { @@ -10,49 +10,188 @@ import { } from './../utils/transformer'; import { SqlConnection } from './sql-base'; +const NUMERIC_TYPE = [ + 'INT64', + 'FLOAT64', + 'INTEGER', + 'FLOAT', + 'NUMERIC', + 'BIGNUMERIC', +]; export class BigQueryConnection extends SqlConnection { bigQuery: BigQuery; - - // Default query type to positional for BigQuery - queryType = QueryType.positional; - - // Default dialect for BigQuery dialect = new BigQueryDialect(); + cacheFields: Record> = {}; - /** - * Creates a new BigQuery object. - * - * @param keyFileName - Path to a .json, .pem, or .p12 key file. - * @param region - Region for your dataset - */ constructor(bigQuery: any) { super(); this.bigQuery = bigQuery; } - /** - * Performs a connect action on the current Connection object. - * In this particular use case, BigQuery has no connect - * So this is a no-op - * - * @param details - Unused in the BigQuery scenario. - * @returns Promise - */ async connect(): Promise { return Promise.resolve(); } - /** - * Performs a disconnect action on the current Connection object. - * In this particular use case, BigQuery has no disconnect - * So this is a no-op - * - * @returns Promise - */ async disconnect(): Promise { return Promise.resolve(); } + createTable( + schemaName: string | undefined, + tableName: string, + columns: TableColumn[] + ): Promise { + // BigQuery does not support PRIMARY KEY. We can remove if here + const tempColumns = structuredClone(columns); + for (const column of tempColumns) { + delete column.definition.references; + } + + return super.createTable(schemaName, tableName, tempColumns); + } + + async getFields( + schemaName: string, + tableName: string + ): Promise> { + if (this.cacheFields[schemaName]) return this.cacheFields[schemaName]; + + if (!schemaName) + throw new Error('Schema name is required for BigQuery'); + + const [metadata] = await this.bigQuery + .dataset(schemaName) + .table(tableName) + .getMetadata(); + + const fields: { name: string; type: string }[] = metadata.schema.fields; + const fieldsType: Record = fields.reduce( + (acc, field) => { + acc[field.name] = field.type; + return acc; + }, + {} as Record + ); + + this.cacheFields[schemaName] = fieldsType; + return fieldsType; + } + + transformTypedValue(type: string, value: unknown) { + if (value === null) return value; + + if (NUMERIC_TYPE.includes(type)) { + return Number(value); + } + + return value; + } + + async autoCastingType( + schemaName: string | undefined, + tableName: string, + data: Record + ): Promise> { + const tmp = structuredClone(data); + + if (!schemaName) + throw new Error('Schema name is required for BigQuery'); + + const fieldsType: Record = await this.getFields( + schemaName, + tableName + ); + + for (const key in tmp) { + const type = fieldsType[key]; + if (!type) continue; + tmp[key] = this.transformTypedValue(type, tmp[key]); + } + + return tmp; + } + + async insert( + schemaName: string | undefined, + tableName: string, + data: Record + ): Promise { + return super.insert( + schemaName, + tableName, + await this.autoCastingType(schemaName, tableName, data) + ); + } + + async insertMany( + schemaName: string | undefined, + tableName: string, + data: Record[] + ): Promise { + const newData: Record[] = []; + + for (const item of data) { + newData.push( + await this.autoCastingType(schemaName, tableName, item) + ); + } + + return super.insertMany(schemaName, tableName, newData); + } + + async update( + schemaName: string | undefined, + tableName: string, + data: Record, + where: Record + ): Promise { + return super.update( + schemaName, + tableName, + await this.autoCastingType(schemaName, tableName, data), + await this.autoCastingType(schemaName, tableName, where) + ); + } + + async delete( + schemaName: string, + tableName: string, + where: Record + ): Promise { + return super.delete( + schemaName, + tableName, + await this.autoCastingType(schemaName, tableName, where) + ); + } + + async select( + schemaName: string, + tableName: string, + options: ConnectionSelectOptions + ): Promise { + // Auto casting the where + let where = options.where; + + if (where && where.length > 0) { + const fields = await this.getFields(schemaName, tableName); + where = where.map((t) => { + const type = fields[t.name]; + if (!type) return t; + + return { + ...t, + value: this.transformTypedValue(type, t.value), + }; + }); + } + + return super.select(schemaName, tableName, { + ...options, + where, + }); + } + /** * Triggers a query action on the current Connection object. * @@ -86,70 +225,104 @@ export class BigQueryConnection extends SqlConnection { } } - createTable( - schemaName: string | undefined, - tableName: string, - columns: TableColumn[] - ): Promise { - // BigQuery does not support PRIMARY KEY. We can remove if here - const tempColumns = structuredClone(columns); - for (const column of tempColumns) { - delete column.definition.primaryKey; - delete column.definition.references; - } + public async fetchDatabaseSchema(): Promise { + const [datasetList] = await this.bigQuery.getDatasets(); - return super.createTable(schemaName, tableName, tempColumns); - } + // Construct the query to get all the table in one go + const sql = datasetList + .map((dataset) => { + const schemaPath = `${this.bigQuery.projectId}.${dataset.id}`; - public async fetchDatabaseSchema(): Promise { - const database: Database = {}; + return `( + SELECT + a.table_schema, + a.table_name, + a.column_name, + a.data_type, + b.constraint_schema, + b.constraint_name, + c.constraint_type + FROM \`${schemaPath}.INFORMATION_SCHEMA.COLUMNS\` AS a LEFT JOIN \`${schemaPath}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE\` AS b ON ( + a.table_schema = b.table_schema AND + a.table_name = b.table_name AND + a.column_name = b.column_name + ) LEFT JOIN \`${schemaPath}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS\` AS c ON ( + b.constraint_schema = c.constraint_schema AND + b.constraint_name = c.constraint_name + ) +)`; + }) + .join(' UNION ALL '); - // Fetch all datasets - const [datasets] = await this.bigQuery.getDatasets(); - if (datasets.length === 0) { - throw new Error('No datasets found in the project.'); - } + const { data } = await this.query<{ + table_schema: string; + table_name: string; + column_name: string; + data_type: string; + constraint_schema: string; + constraint_name: string; + constraint_type: null | 'PRIMARY KEY' | 'FOREIGN KEY'; + }>({ query: sql }); + + // Group the database schema by table + const database: Database = datasetList.reduce( + (acc, dataset) => { + acc[dataset.id ?? ''] = {}; + return acc; + }, + {} as Record + ); - // Iterate over each dataset - for (const dataset of datasets) { - const datasetId = dataset.id; - if (!datasetId) continue; + // Group the table by database + data.forEach((row) => { + const schema = database[row.table_schema]; + if (!schema) { + return; + } - const [tables] = await dataset.getTables(); + const table = schema[row.table_name] ?? { + name: row.table_name, + columns: [], + indexes: [], + constraints: [], + }; - if (!database[datasetId]) { - database[datasetId] = {}; // Initialize schema in the database + if (!schema[row.table_name]) { + schema[row.table_name] = table; } - for (const table of tables) { - const [metadata] = await table.getMetadata(); - - const columns = metadata.schema.fields.map( - (field: any, index: number): TableColumn => { - return { - name: field.name, - position: index, - definition: { - type: field.type, - nullable: field.mode === 'NULLABLE', - default: null, // BigQuery does not support default values in the schema metadata - primaryKey: false, // BigQuery does not have a concept of primary keys - unique: false, // BigQuery does not have a concept of unique constraints - }, - }; - } + // Add the column to the table + table.columns.push({ + name: row.column_name, + definition: { + type: row.data_type, + primaryKey: row.constraint_type === 'PRIMARY KEY', + }, + }); + + // Add the constraint to the table + if (row.constraint_name && row.constraint_type === 'PRIMARY KEY') { + let constraint = table.constraints.find( + (c) => c.name === row.constraint_name ); - const currentTable: Table = { - name: table.id ?? '', - columns: columns, - indexes: [], // BigQuery does not support indexes - constraints: [], // BigQuery does not support primary keys, foreign keys, or unique constraints - }; + if (!constraint) { + constraint = { + name: row.constraint_name, + schema: row.constraint_schema, + tableName: row.table_name, + type: row.constraint_type, + columns: [], + }; + + table.constraints.push(constraint); + } - database[datasetId][table.id ?? ''] = currentTable; + constraint.columns.push({ + columnName: row.column_name, + }); } - } + }); return database; } diff --git a/src/connections/mysql.ts b/src/connections/mysql.ts index 04ce698..0106911 100644 --- a/src/connections/mysql.ts +++ b/src/connections/mysql.ts @@ -155,7 +155,11 @@ export function buildMySQLDatabaseSchmea({ } as Constraint; constraintLookup[ - constraint.TABLE_SCHEMA + '.' + constraint.CONSTRAINT_NAME + constraint.TABLE_SCHEMA + + '.' + + constraint.TABLE_NAME + + '.' + + constraint.CONSTRAINT_NAME ] = constraintObject; table.constraints.push(constraintObject); @@ -166,6 +170,8 @@ export function buildMySQLDatabaseSchmea({ const constraint = constraintLookup[ constraintColumn.TABLE_SCHEMA + + '.' + + constraintColumn.TABLE_NAME + '.' + constraintColumn.CONSTRAINT_NAME ]; diff --git a/src/connections/sqlite/base.ts b/src/connections/sqlite/base.ts index 5101193..4f85a22 100644 --- a/src/connections/sqlite/base.ts +++ b/src/connections/sqlite/base.ts @@ -86,6 +86,25 @@ FROM } } + // Building primary key constraint + Object.values(tableLookup).forEach((table) => { + const primaryKeyColumns = table.columns + .filter((column) => column.definition.primaryKey) + .map((column) => column.name); + + if (primaryKeyColumns.length) { + table.constraints.push({ + name: `pk_${table.name}`, + schema: 'main', + tableName: table.name, + type: 'PRIMARY KEY', + columns: primaryKeyColumns.map((columnName) => ({ + columnName, + })), + }); + } + }); + // Sqlite default schema is "main", since we don't support // ATTACH, we don't need to worry about other schemas return { diff --git a/src/connections/sqlite/cloudflare.ts b/src/connections/sqlite/cloudflare.ts index f488fc1..0ac5190 100644 --- a/src/connections/sqlite/cloudflare.ts +++ b/src/connections/sqlite/cloudflare.ts @@ -153,145 +153,151 @@ export class CloudflareD1Connection extends SqliteBaseConnection { }; } + public async fetchDatabaseSchema(): Promise { + const result = await super.fetchDatabaseSchema(); + delete result.main['_cf_KV']; + return result; + } + // For some reason, Cloudflare D1 does not support // cross join with pragma_table_info, so we have to // to expensive loops to get the same data - public async fetchDatabaseSchema(): Promise { - const exclude_tables = [ - '_cf_kv', - 'sqlite_schema', - 'sqlite_temp_schema', - ]; - - const schemaMap: Record> = {}; - - const { data } = await this.query({ - query: `PRAGMA table_list`, - }); - - const allTables = ( - data as { - schema: string; - name: string; - type: string; - }[] - ).filter( - (row) => - !row.name.startsWith('_lite') && - !row.name.startsWith('sqlite_') && - !exclude_tables.includes(row.name?.toLowerCase()) - ); - - for (const table of allTables) { - if (exclude_tables.includes(table.name?.toLowerCase())) continue; - - const { data: pragmaData } = await this.query({ - query: `PRAGMA table_info('${table.name}')`, - }); - - const tableData = pragmaData as { - cid: number; - name: string; - type: string; - notnull: 0 | 1; - dflt_value: string | null; - pk: 0 | 1; - }[]; - - const { data: fkConstraintResponse } = await this.query({ - query: `PRAGMA foreign_key_list('${table.name}')`, - }); - - const fkConstraintData = ( - fkConstraintResponse as { - id: number; - seq: number; - table: string; - from: string; - to: string; - on_update: 'NO ACTION' | unknown; - on_delete: 'NO ACTION' | unknown; - match: 'NONE' | unknown; - }[] - ).filter( - (row) => - !row.table.startsWith('_lite') && - !row.table.startsWith('sqlite_') - ); - - const constraints: Constraint[] = []; - - if (fkConstraintData.length > 0) { - const fkConstraints: Constraint = { - name: 'FOREIGN KEY', - schema: table.schema, - tableName: table.name, - type: 'FOREIGN KEY', - referenceSchema: table.schema, - referenceTableName: fkConstraintData[0].table, - columns: [], - }; - - fkConstraintData.forEach((fkConstraint) => { - const currentConstraint: ConstraintColumn = { - columnName: fkConstraint.from, - referenceColumnName: fkConstraint.to, - }; - fkConstraints.columns.push(currentConstraint); - }); - constraints.push(fkConstraints); - } - - const indexes: TableIndex[] = []; - const columns = tableData.map((column) => { - // Primary keys are ALWAYS considered indexes - if (column.pk === 1) { - indexes.push({ - name: column.name, - type: TableIndexType.PRIMARY, - columns: [column.name], - }); - } - - const columnConstraint = fkConstraintData.find( - (fk) => fk.from === column.name - ); - - const currentColumn: TableColumn = { - name: column.name, - position: column.cid, - definition: { - type: column.type, - nullable: column.notnull === 0, - default: column.dflt_value, - primaryKey: column.pk === 1, - unique: column.pk === 1, - references: columnConstraint - ? { - column: [columnConstraint.to], - table: columnConstraint.table, - } - : undefined, - }, - }; - - return currentColumn; - }); - - const currentTable: Table = { - name: table.name, - columns: columns, - indexes: indexes, - constraints: constraints, - }; - - if (!schemaMap[table.schema]) { - schemaMap[table.schema] = {}; - } - - schemaMap[table.schema][table.name] = currentTable; - } - - return schemaMap; - } + // public async fetchDatabaseSchema(): Promise { + // const exclude_tables = [ + // '_cf_kv', + // 'sqlite_schema', + // 'sqlite_temp_schema', + // ]; + + // const schemaMap: Record> = {}; + + // const { data } = await this.query({ + // query: `PRAGMA table_list`, + // }); + + // const allTables = ( + // data as { + // schema: string; + // name: string; + // type: string; + // }[] + // ).filter( + // (row) => + // !row.name.startsWith('_lite') && + // !row.name.startsWith('sqlite_') && + // !exclude_tables.includes(row.name?.toLowerCase()) + // ); + + // for (const table of allTables) { + // if (exclude_tables.includes(table.name?.toLowerCase())) continue; + + // const { data: pragmaData } = await this.query({ + // query: `PRAGMA table_info('${table.name}')`, + // }); + + // const tableData = pragmaData as { + // cid: number; + // name: string; + // type: string; + // notnull: 0 | 1; + // dflt_value: string | null; + // pk: 0 | 1; + // }[]; + + // const { data: fkConstraintResponse } = await this.query({ + // query: `PRAGMA foreign_key_list('${table.name}')`, + // }); + + // const fkConstraintData = ( + // fkConstraintResponse as { + // id: number; + // seq: number; + // table: string; + // from: string; + // to: string; + // on_update: 'NO ACTION' | unknown; + // on_delete: 'NO ACTION' | unknown; + // match: 'NONE' | unknown; + // }[] + // ).filter( + // (row) => + // !row.table.startsWith('_lite') && + // !row.table.startsWith('sqlite_') + // ); + + // const constraints: Constraint[] = []; + + // if (fkConstraintData.length > 0) { + // const fkConstraints: Constraint = { + // name: 'FOREIGN KEY', + // schema: table.schema, + // tableName: table.name, + // type: 'FOREIGN KEY', + // referenceSchema: table.schema, + // referenceTableName: fkConstraintData[0].table, + // columns: [], + // }; + + // fkConstraintData.forEach((fkConstraint) => { + // const currentConstraint: ConstraintColumn = { + // columnName: fkConstraint.from, + // referenceColumnName: fkConstraint.to, + // }; + // fkConstraints.columns.push(currentConstraint); + // }); + // constraints.push(fkConstraints); + // } + + // const indexes: TableIndex[] = []; + // const columns = tableData.map((column) => { + // // Primary keys are ALWAYS considered indexes + // if (column.pk === 1) { + // indexes.push({ + // name: column.name, + // type: TableIndexType.PRIMARY, + // columns: [column.name], + // }); + // } + + // const columnConstraint = fkConstraintData.find( + // (fk) => fk.from === column.name + // ); + + // const currentColumn: TableColumn = { + // name: column.name, + // position: column.cid, + // definition: { + // type: column.type, + // nullable: column.notnull === 0, + // default: column.dflt_value, + // primaryKey: column.pk === 1, + // unique: column.pk === 1, + // references: columnConstraint + // ? { + // column: [columnConstraint.to], + // table: columnConstraint.table, + // } + // : undefined, + // }, + // }; + + // return currentColumn; + // }); + + // const currentTable: Table = { + // name: table.name, + // columns: columns, + // indexes: indexes, + // constraints: constraints, + // }; + + // if (!schemaMap[table.schema]) { + // schemaMap[table.schema] = {}; + // } + + // schemaMap[table.schema][table.name] = currentTable; + // } + + // return schemaMap; + // } } diff --git a/src/query-builder/dialects/bigquery.ts b/src/query-builder/dialects/bigquery.ts index b9ac7a0..88e0f5f 100644 --- a/src/query-builder/dialects/bigquery.ts +++ b/src/query-builder/dialects/bigquery.ts @@ -1,5 +1,7 @@ import { MySQLDialect } from './mysql'; export class BigQueryDialect extends MySQLDialect { + protected ALWAY_NO_ENFORCED_CONSTRAINT = true; + escapeId(identifier: string): string { return `\`${identifier}\``; } diff --git a/src/query-builder/index.ts b/src/query-builder/index.ts index ef45648..f0847dc 100644 --- a/src/query-builder/index.ts +++ b/src/query-builder/index.ts @@ -43,6 +43,10 @@ export abstract class AbstractDialect implements Dialect { protected AUTO_INCREMENT_KEYWORD = 'AUTO_INCREMENT'; protected SUPPORT_COLUMN_COMMENT = true; + // BigQuery does not support enforced constraint + // This flag is primary for BigQuery only. + protected ALWAY_NO_ENFORCED_CONSTRAINT = false; + escapeId(identifier: string): string { return identifier .split('.') @@ -134,6 +138,15 @@ export abstract class AbstractDialect implements Dialect { } return merged; } else { + // BigQuery does not provide easy way to bind NULL value, + // so we will skip binding NULL values and use raw NULL in query + if (where.value === null) { + return [ + `${this.escapeId(where.column)} ${where.operator} NULL`, + [], + ]; + } + return [ `${this.escapeId(where.column)} ${where.operator} ?`, [where.value], @@ -177,6 +190,10 @@ export abstract class AbstractDialect implements Dialect { const bindings: unknown[] = []; const setClauses = columns.map((column) => { + // BigQuery does not provide easy way to bind NULL value, + // so we will skip binding NULL values and use raw NULL in query + if (data[column] === null) return `${this.escapeId(column)} = NULL`; + bindings.push(data[column]); return `${this.escapeId(column)} = ?`; }); @@ -199,11 +216,20 @@ export abstract class AbstractDialect implements Dialect { const bindings: unknown[] = []; const columnNames = columns.map((column) => { - bindings.push(data[column]); + // BigQuery does not provide easy way to bind NULL value, + // so we will skip binding NULL values and use raw NULL in query + if (data[column] !== null) bindings.push(data[column]); return this.escapeId(column); }); - const placeholders = columns.map(() => '?').join(', '); + const placeholders = columns + .map((column) => { + // BigQuery does not provide easy way to bind NULL value, + // so we will skip binding NULL values and use raw NULL in query + if (data[column] === null) return 'NULL'; + return '?'; + }) + .join(', '); return [ `(${columnNames.join(', ')}) VALUES(${placeholders})`, @@ -218,6 +244,9 @@ export abstract class AbstractDialect implements Dialect { def.nullable === false ? 'NOT NULL' : '', def.invisible ? 'INVISIBLE' : '', // This is for MySQL case def.primaryKey ? 'PRIMARY KEY' : '', + def.primaryKey && this.ALWAY_NO_ENFORCED_CONSTRAINT + ? 'NOT ENFORCED' + : '', def.unique ? 'UNIQUE' : '', def.default ? `DEFAULT ${this.escapeValue(def.default)}` : '', def.defaultExpression ? `DEFAULT (${def.defaultExpression})` : '', @@ -278,7 +307,7 @@ export abstract class AbstractDialect implements Dialect { const tableName = builder.table; if (!tableName) { - throw new Error('Table name is required to build a UPDATE query.'); + throw new Error('Table name is required to build a INSERT query.'); } // Remove all empty value from object and check if there is any data to update @@ -369,6 +398,7 @@ export abstract class AbstractDialect implements Dialect { ref.match ? `MATCH ${ref.match}` : '', ref.onDelete ? `ON DELETE ${ref.onDelete}` : '', ref.onUpdate ? `ON UPDATE ${ref.onUpdate}` : '', + this.ALWAY_NO_ENFORCED_CONSTRAINT ? 'NOT ENFORCED' : '', ] .filter(Boolean) .join(' '); diff --git a/tests/connections/connection.test.ts b/tests/connections/connection.test.ts index 51894ab..8c45dc6 100644 --- a/tests/connections/connection.test.ts +++ b/tests/connections/connection.test.ts @@ -194,6 +194,24 @@ describe('Database Connection', () => { expect(fkConstraint!.referenceTableName).toBe('teams'); expect(fkConstraint!.columns[0].referenceColumnName).toBe('id'); } + + // Check the primary key + if (process.env.CONNECTION_TYPE !== 'mongodb') { + const pkList = Object.values(schemas[DEFAULT_SCHEMA]) + .map((c) => c.constraints) + .flat() + .filter((c) => c.type === 'PRIMARY KEY') + .map((constraint) => + constraint.columns.map( + (column) => + `${constraint.tableName}.${column.columnName}` + ) + ) + .flat() + .sort(); + + expect(pkList).toEqual(['persons.id', 'teams.id']); + } }); test('Select data', async () => { @@ -361,6 +379,10 @@ describe('Database Connection', () => { }); test('Rename table name', async () => { + // Skip BigQuery because you cannot rename table with + // primary key column + if (process.env.CONNECTION_TYPE === 'bigquery') return; + const { error } = await db.renameTable( DEFAULT_SCHEMA, 'persons', @@ -374,12 +396,15 @@ describe('Database Connection', () => { }); expect(cleanup(data).length).toEqual(2); + + // Revert the operation back + await db.renameTable(DEFAULT_SCHEMA, 'people', 'persons'); }); test('Delete a row', async () => { - await db.delete(DEFAULT_SCHEMA, 'people', { id: 1 }); + await db.delete(DEFAULT_SCHEMA, 'persons', { id: 1 }); - const { data } = await db.select(DEFAULT_SCHEMA, 'people', { + const { data } = await db.select(DEFAULT_SCHEMA, 'persons', { orderBy: ['id'], }); diff --git a/tests/units/query-builder/postgre.test.ts b/tests/units/query-builder/postgre.test.ts index 786af46..9f1b06b 100644 --- a/tests/units/query-builder/postgre.test.ts +++ b/tests/units/query-builder/postgre.test.ts @@ -173,12 +173,12 @@ describe('Query Builder - Postgre Dialect', () => { test('Update query without where condition', () => { const { query, parameters } = qb() - .update({ last_name: 'Visal', first_name: 'In' }) + .update({ last_name: 'Visal', banned: null, first_name: 'In' }) .into('persons') .toQuery(); expect(query).toBe( - 'UPDATE "persons" SET "last_name" = ?, "first_name" = ?' + 'UPDATE "persons" SET "last_name" = ?, "banned" = NULL, "first_name" = ?' ); expect(parameters).toEqual(['Visal', 'In']); }); @@ -191,10 +191,11 @@ describe('Query Builder - Postgre Dialect', () => { id: 123, active: 1, }) + .where('banned', 'IS', null) .toQuery(); expect(query).toBe( - 'UPDATE "persons" SET "last_name" = ?, "first_name" = ? WHERE "id" = ? AND "active" = ?' + 'UPDATE "persons" SET "last_name" = ?, "first_name" = ? WHERE "id" = ? AND "active" = ? AND "banned" IS NULL' ); expect(parameters).toEqual(['Visal', 'In', 123, 1]); }); @@ -211,12 +212,12 @@ describe('Query Builder - Postgre Dialect', () => { test('Insert data', () => { const { query, parameters } = qb() - .insert({ last_name: 'Visal', first_name: 'In' }) + .insert({ last_name: 'Visal', banned: null, first_name: 'In' }) .into('persons') .toQuery(); expect(query).toBe( - 'INSERT INTO "persons"("last_name", "first_name") VALUES(?, ?)' + 'INSERT INTO "persons"("last_name", "banned", "first_name") VALUES(?, NULL, ?)' ); expect(parameters).toEqual(['Visal', 'In']); });