UNPKG

adaptus2-orm

Version:

A multi-database ORM supporting MySQL, PostgreSQL, MongoDB, and Snowflake

785 lines (677 loc) 25.4 kB
// src/adapters/SnowflakeAdapter.js class SnowflakeAdapter { constructor() { this.snowflakeTypes = { string: 'VARCHAR', text: 'VARCHAR', longtext: 'VARCHAR', integer: 'NUMBER(38,0)', biginteger: 'NUMBER(19,0)', smallinteger: 'NUMBER(5,0)', decimal: 'NUMBER', float: 'FLOAT', double: 'FLOAT', boolean: 'BOOLEAN', date: 'DATE', datetime: 'TIMESTAMP_NTZ', timestamp: 'TIMESTAMP_NTZ', time: 'TIME', year: 'NUMBER(4,0)', json: 'VARIANT', uuid: 'VARCHAR(36)', binary: 'BINARY', enum: 'VARCHAR' }; } async create(connection, table, data) { try { const fields = Object.keys(data); const values = Object.values(data); const placeholders = fields.map((_, index) => `:${index + 1}`).join(','); const query = `INSERT INTO ${table} (${fields.join(',')}) VALUES (${placeholders})`; const result = await this._executeQuery(connection, query, values); if (result.success) { // Get the last inserted ID if available const lastInsertId = await this._executeQuery(connection, 'SELECT LAST_INSERT_ID() AS id'); return { success: true, data: { insertId: lastInsertId.success ? lastInsertId.data[0]?.ID : null, affectedRows: result.data.rowCount || 0, record: data } }; } return result; } catch (error) { return { success: false, error: error.message }; } } async read(connection, table, params = {}) { try { let query = `SELECT * FROM ${table}`; let queryParams = []; let paramCounter = 1; if (params.where) { const conditions = Object.keys(params.where).map(key => { queryParams.push(params.where[key]); return `${key} = :${paramCounter++}`; }).join(' AND '); query += ` WHERE ${conditions}`; } if (params.orderBy) { query += ` ORDER BY ${params.orderBy} ${params.order || 'ASC'}`; } if (params.limit) { query += ` LIMIT ${parseInt(params.limit)}`; if (params.offset) { query += ` OFFSET ${parseInt(params.offset)}`; } } const result = await this._executeQuery(connection, query, queryParams); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async update(connection, table, params) { try { const { where, data } = params; if (!where || !data) { throw new Error('Both where and data parameters are required for update'); } let paramCounter = 1; const setFields = Object.keys(data).map(key => { return `${key} = :${paramCounter++}`; }).join(','); const setValues = Object.values(data); const whereFields = Object.keys(where).map(key => { return `${key} = :${paramCounter++}`; }).join(' AND '); const whereValues = Object.values(where); const query = `UPDATE ${table} SET ${setFields} WHERE ${whereFields}`; const queryParams = [...setValues, ...whereValues]; const result = await this._executeQuery(connection, query, queryParams); return { success: result.success, data: { affectedRows: result.data?.rowCount || 0 }, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async delete(connection, table, params) { try { if (!params.where) { throw new Error('Where parameter is required for delete'); } let paramCounter = 1; const whereFields = Object.keys(params.where).map(key => { return `${key} = :${paramCounter++}`; }).join(' AND '); const whereValues = Object.values(params.where); const query = `DELETE FROM ${table} WHERE ${whereFields}`; const result = await this._executeQuery(connection, query, whereValues); return { success: result.success, data: { affectedRows: result.data?.rowCount || 0 }, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async exists(connection, table, params) { try { let query = `SELECT EXISTS(SELECT 1 FROM ${table}`; let queryParams = []; let paramCounter = 1; if (params.where) { const conditions = Object.keys(params.where).map(key => { queryParams.push(params.where[key]); return `${key} = :${paramCounter++}`; }).join(' AND '); query += ` WHERE ${conditions}`; } query += ') AS record_exists'; const result = await this._executeQuery(connection, query, queryParams); return { success: result.success, data: result.data?.[0]?.RECORD_EXISTS || false, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async query(connection, queryString, params = []) { try { const result = await this._executeQuery(connection, queryString, params); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async initDatabase(connection, config) { try { // Test the connection with a simple query const result = await this._executeQuery(connection, 'SELECT CURRENT_DATABASE() AS db_name'); if (result.success) { return { success: true, data: `Database initialized successfully. Connected to: ${result.data[0]?.DB_NAME}` }; } return result; } catch (error) { return { success: false, error: error.message }; } } // Utility methods for batch operations async bulkInsert(connection, table, records) { try { if (!records.length) { return { success: true, data: { affectedRows: 0 } }; } const fields = Object.keys(records[0]); // Snowflake has a limit on the number of expressions in a single INSERT // Split into chunks if necessary const chunkSize = 1000; let affectedRows = 0; for (let i = 0; i < records.length; i += chunkSize) { const chunk = records.slice(i, i + chunkSize); const chunkPlaceholders = chunk.map((_, index) => { return `(${fields.map((_, fieldIndex) => `:${i * fields.length + index * fields.length + fieldIndex + 1}` ).join(',')})`; }).join(','); const query = `INSERT INTO ${table} (${fields.join(',')}) VALUES ${chunkPlaceholders}`; const allValues = []; chunk.forEach(record => { allValues.push(...Object.values(record)); }); const result = await this._executeQuery(connection, query, allValues); if (result.success && result.data) { affectedRows += result.data.rowCount || 0; } } return { success: true, data: { affectedRows } }; } catch (error) { return { success: false, error: error.message }; } } async upsert(connection, table, data, conflictFields = []) { try { const fields = Object.keys(data); const values = Object.values(data); // Snowflake uses MERGE for upsert operations const placeholders = fields.map((field, index) => `:${index + 1} AS ${field}`).join(', '); const onClause = conflictFields.map(field => `target.${field} = source.${field}`).join(' AND '); const updateClause = fields .filter(field => !conflictFields.includes(field)) .map(field => `target.${field} = source.${field}`) .join(', '); const insertFields = fields.join(', '); const insertValues = fields.map(field => `source.${field}`).join(', '); let query = ` MERGE INTO ${table} AS target USING (SELECT ${placeholders}) AS source ON ${onClause} `; if (updateClause) { query += ` WHEN MATCHED THEN UPDATE SET ${updateClause} `; } query += ` WHEN NOT MATCHED THEN INSERT (${insertFields}) VALUES (${insertValues}) `; const result = await this._executeQuery(connection, query, values); return { success: result.success, data: { affectedRows: result.data?.rowCount || 0 }, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Transaction support async beginTransaction(connection) { try { const result = await this._executeQuery(connection, 'BEGIN TRANSACTION'); return { success: result.success, data: result.data, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async commit(connection) { try { const result = await this._executeQuery(connection, 'COMMIT'); return { success: result.success, data: result.data, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async rollback(connection) { try { const result = await this._executeQuery(connection, 'ROLLBACK'); return { success: result.success, data: result.data, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Snowflake-specific features async switchWarehouse(connection, warehouse) { try { const query = `USE WAREHOUSE ${warehouse}`; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.success ? `Switched to warehouse: ${warehouse}` : null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async getCurrentWarehouse(connection) { try { const query = 'SELECT CURRENT_WAREHOUSE() AS warehouse_name'; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data?.[0]?.WAREHOUSE_NAME || null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async switchSchema(connection, schema) { try { const query = `USE SCHEMA ${schema}`; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.success ? `Switched to schema: ${schema}` : null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async getCurrentSchema(connection) { try { const query = 'SELECT CURRENT_SCHEMA() AS schema_name'; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data?.[0]?.SCHEMA_NAME || null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Data type conversion for Snowflake getDataType(type) { return this.snowflakeTypes[type] || 'VARCHAR'; } // Clone table (Snowflake-specific feature) async cloneTable(connection, sourceTable, targetTable, copyData = true) { try { const query = `CREATE TABLE ${targetTable} CLONE ${sourceTable}${copyData ? '' : ' COPY GRANTS'}`; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.success ? `Table ${targetTable} cloned from ${sourceTable}` : null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Time travel queries (Snowflake-specific feature) async queryTimeTravelBefore(connection, table, timestamp, params = {}) { try { let query = `SELECT * FROM ${table} BEFORE (TIMESTAMP => '${timestamp}')`; let queryParams = []; let paramCounter = 1; if (params.where) { const conditions = Object.keys(params.where).map(key => { queryParams.push(params.where[key]); return `${key} = :${paramCounter++}`; }).join(' AND '); query += ` WHERE ${conditions}`; } const result = await this._executeQuery(connection, query, queryParams); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Storage integration operations async createStage(connection, stageName, url, credentials = {}) { try { let query = `CREATE STAGE ${stageName} URL='${url}'`; if (credentials.aws_key_id && credentials.aws_secret_key) { query += ` CREDENTIALS=(AWS_KEY_ID='${credentials.aws_key_id}' AWS_SECRET_KEY='${credentials.aws_secret_key}')`; } if (credentials.azure_sas_token) { query += ` CREDENTIALS=(AZURE_SAS_TOKEN='${credentials.azure_sas_token}')`; } const result = await this._executeQuery(connection, query); return { success: result.success, data: result.success ? `Stage ${stageName} created successfully` : null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // COPY INTO operations for bulk loading async copyInto(connection, table, stageName, filePattern = null, options = {}) { try { let query = `COPY INTO ${table} FROM @${stageName}`; if (filePattern) { query += `/${filePattern}`; } if (options.fileFormat) { query += ` FILE_FORMAT = ${options.fileFormat}`; } if (options.onError) { query += ` ON_ERROR = '${options.onError}'`; // CONTINUE, SKIP_FILE, etc. } if (options.force) { query += ` FORCE = TRUE`; } const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Variant/JSON operations async queryVariant(connection, table, variantColumn, path, params = {}) { try { let query = `SELECT * FROM ${table} WHERE ${variantColumn}:${path} IS NOT NULL`; let queryParams = []; let paramCounter = 1; if (params.where) { const conditions = Object.keys(params.where).map(key => { queryParams.push(params.where[key]); return `${key} = :${paramCounter++}`; }).join(' AND '); query += ` AND ${conditions}`; } const result = await this._executeQuery(connection, query, queryParams); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Information schema queries async listTables(connection, databaseName = null, schemaName = null) { try { let query = ` SELECT table_name, table_type FROM information_schema.tables WHERE table_type = 'BASE TABLE' `; if (databaseName) { query += ` AND table_catalog = '${databaseName}'`; } if (schemaName) { query += ` AND table_schema = '${schemaName}'`; } const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Helper method to execute queries with proper error handling async _executeQuery(connection, query, binds = []) { return new Promise((resolve, reject) => { try { connection.connection.execute({ sqlText: query, binds: binds, streamResult: false, complete: (err, statement, rows) => { if (err) { reject(err); } else { resolve({ success: true, data: rows, rowCount: statement?.getNumRowsInserted?.() || statement?.getNumRowsUpdated?.() || statement?.getNumRowsDeleted?.() || 0, statement: statement }); } } }); } catch (error) { reject(error); } }); } // Resource management async listWarehouses(connection) { try { const query = 'SHOW WAREHOUSES'; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } async createWarehouse(connection, warehouseName, options = {}) { try { let query = `CREATE WAREHOUSE ${warehouseName}`; if (options.size) { query += ` WITH WAREHOUSE_SIZE = '${options.size}'`; } if (options.autoSuspend) { query += ` AUTO_SUSPEND = ${options.autoSuspend}`; } if (options.autoResume !== undefined) { query += ` AUTO_RESUME = ${options.autoResume ? 'TRUE' : 'FALSE'}`; } const result = await this._executeQuery(connection, query); return { success: result.success, data: result.success ? `Warehouse ${warehouseName} created successfully` : null, error: result.error }; } catch (error) { return { success: false, error: error.message }; } } // Account usage and monitoring async getQueryHistory(connection, options = {}) { try { const { startTime, endTime, userName, warehouseName, databaseName, limit = 100 } = options; let query = ` SELECT query_id, query_text, user_name, warehouse_name, start_time, end_time, total_elapsed_time/1000 as duration_seconds, rows_produced FROM snowflake.account_usage.query_history WHERE 1=1 `; if (startTime) { query += ` AND start_time >= '${startTime}'`; } if (endTime) { query += ` AND end_time <= '${endTime}'`; } if (userName) { query += ` AND user_name = '${userName}'`; } if (warehouseName) { query += ` AND warehouse_name = '${warehouseName}'`; } if (databaseName) { query += ` AND database_name = '${databaseName}'`; } query += ` ORDER BY start_time DESC LIMIT ${limit}`; const result = await this._executeQuery(connection, query); return { success: result.success, data: result.data || [], error: result.error }; } catch (error) { return { success: false, error: error.message }; } } } module.exports = SnowflakeAdapter;