Genesis
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
monitor_logs
|
||||||
|
tables
|
||||||
|
.env
|
||||||
129
extract_mssql_defs.js
Normal file
129
extract_mssql_defs.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Node.js script to extract table, view, and procedure definitions from a MSSQL database
|
||||||
|
// Requires: npm install mssql
|
||||||
|
// Fill in your connection details below
|
||||||
|
|
||||||
|
const sql = require('mssql');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
user: 'sa',
|
||||||
|
password: 'sa_tekno23',
|
||||||
|
server: '10.10.10.3', // e.g. 'localhost'
|
||||||
|
database: 'eazybusiness',
|
||||||
|
options: {
|
||||||
|
encrypt: false, // Set to true if using Azure
|
||||||
|
trustServerCertificate: true // For local dev/testing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getTableDefinitions(pool) {
|
||||||
|
// Get table columns, PKs, and constraints
|
||||||
|
const tables = await pool.request().query(`
|
||||||
|
SELECT t.object_id, s.name AS schema_name, t.name AS table_name
|
||||||
|
FROM sys.tables t
|
||||||
|
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, t.name
|
||||||
|
`);
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
for (const row of tables.recordset) {
|
||||||
|
const { object_id, schema_name, table_name } = row;
|
||||||
|
|
||||||
|
// Get columns
|
||||||
|
const columns = await pool.request().query(`
|
||||||
|
SELECT c.name AS column_name,
|
||||||
|
TYPE_NAME(c.user_type_id) AS data_type,
|
||||||
|
c.max_length, c.precision, c.scale, c.is_nullable, c.is_identity
|
||||||
|
FROM sys.columns c
|
||||||
|
WHERE c.object_id = ${object_id}
|
||||||
|
ORDER BY c.column_id
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get primary key
|
||||||
|
const pk = await pool.request().query(`
|
||||||
|
SELECT k.name AS pk_name, c.name AS column_name
|
||||||
|
FROM sys.key_constraints k
|
||||||
|
INNER JOIN sys.index_columns ic ON k.parent_object_id = ic.object_id AND k.unique_index_id = ic.index_id
|
||||||
|
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||||
|
WHERE k.parent_object_id = ${object_id} AND k.type = 'PK'
|
||||||
|
ORDER BY ic.key_ordinal
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Compose CREATE TABLE statement
|
||||||
|
let createStmt = `CREATE TABLE [${schema_name}].[${table_name}] (\n`;
|
||||||
|
createStmt += columns.recordset.map(col => {
|
||||||
|
let line = ` [${col.column_name}] ${col.data_type}`;
|
||||||
|
if (col.data_type.match(/char|binary|text|nchar|nvarchar|varbinary/i) && col.max_length > 0) {
|
||||||
|
line += `(${col.max_length === -1 ? 'MAX' : col.max_length})`;
|
||||||
|
} else if (col.data_type.match(/decimal|numeric/i)) {
|
||||||
|
line += `(${col.precision},${col.scale})`;
|
||||||
|
}
|
||||||
|
if (col.is_identity) line += ' IDENTITY(1,1)';
|
||||||
|
line += col.is_nullable ? ' NULL' : ' NOT NULL';
|
||||||
|
return line;
|
||||||
|
}).join(',\n');
|
||||||
|
if (pk.recordset.length > 0) {
|
||||||
|
const pkCols = pk.recordset.map(r => `[${r.column_name}]`).join(', ');
|
||||||
|
createStmt += `,\n CONSTRAINT [${pk.recordset[0].pk_name}] PRIMARY KEY (${pkCols})`;
|
||||||
|
}
|
||||||
|
createStmt += '\n);';
|
||||||
|
results.push(createStmt);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getViewDefinitions(pool) {
|
||||||
|
const views = await pool.request().query(`
|
||||||
|
SELECT s.name AS schema_name, v.name AS view_name, OBJECT_DEFINITION(v.object_id) AS definition
|
||||||
|
FROM sys.views v
|
||||||
|
INNER JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, v.name
|
||||||
|
`);
|
||||||
|
return views.recordset.map(v =>
|
||||||
|
`CREATE VIEW [${v.schema_name}].[${v.view_name}] AS\n${v.definition}\nGO`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProcedureDefinitions(pool) {
|
||||||
|
const procs = await pool.request().query(`
|
||||||
|
SELECT s.name AS schema_name, p.name AS proc_name, OBJECT_DEFINITION(p.object_id) AS definition
|
||||||
|
FROM sys.procedures p
|
||||||
|
INNER JOIN sys.schemas s ON p.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, p.name
|
||||||
|
`);
|
||||||
|
return procs.recordset.map(p =>
|
||||||
|
`CREATE PROCEDURE [${p.schema_name}].[${p.proc_name}]\nAS\n${p.definition}\nGO`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
let pool = await sql.connect(config);
|
||||||
|
|
||||||
|
console.log('--- TABLES ---');
|
||||||
|
const tables = await getTableDefinitions(pool);
|
||||||
|
tables.forEach(def => {
|
||||||
|
console.log(def);
|
||||||
|
console.log('GO\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- VIEWS ---');
|
||||||
|
const views = await getViewDefinitions(pool);
|
||||||
|
views.forEach(def => {
|
||||||
|
console.log(def);
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('--- PROCEDURES ---');
|
||||||
|
const procs = await getProcedureDefinitions(pool);
|
||||||
|
procs.forEach(def => {
|
||||||
|
console.log(def);
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
await pool.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
202
extract_mssql_defs_to_files.js
Normal file
202
extract_mssql_defs_to_files.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* extract_mssql_defs_to_files.js
|
||||||
|
*
|
||||||
|
* Connects to a MSSQL database, for each user table:
|
||||||
|
* 1. Extracts the CREATE TABLE DDL.
|
||||||
|
* 2. Selects the TOP 10 rows by primary key DESC (or just TOP 10 if no PK).
|
||||||
|
* 3. Writes both the DDL and the rows (as INSERT statements) into
|
||||||
|
* tables/<schema>.<table>.sql
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npm install mssql
|
||||||
|
* node extract_mssql_defs_to_files.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sql = require('mssql');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// --- CONFIGURE YOUR DATABASE CONNECTION HERE ---
|
||||||
|
const config = {
|
||||||
|
user: 'sa',
|
||||||
|
password: 'sa_tekno23',
|
||||||
|
server: '10.10.10.3',
|
||||||
|
database: 'eazybusiness',
|
||||||
|
options: { encrypt: false, trustServerCertificate: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
async function ensureDir(dir) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeSqlString(val) {
|
||||||
|
if (val === null || val === undefined) return 'NULL';
|
||||||
|
if (typeof val === 'number') return val;
|
||||||
|
if (typeof val === 'boolean') return val ? 1 : 0;
|
||||||
|
return "'" + String(val).replace(/'/g, "''") + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTables(pool) {
|
||||||
|
const tables = await pool.request().query(`
|
||||||
|
SELECT t.object_id, s.name AS schema_name, t.name AS table_name
|
||||||
|
FROM sys.tables t
|
||||||
|
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, t.name
|
||||||
|
`);
|
||||||
|
return tables.recordset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getColumns(pool, object_id) {
|
||||||
|
const columns = await pool.request().query(`
|
||||||
|
SELECT c.name AS column_name,
|
||||||
|
TYPE_NAME(c.user_type_id) AS data_type,
|
||||||
|
c.max_length, c.precision, c.scale, c.is_nullable, c.is_identity
|
||||||
|
FROM sys.columns c
|
||||||
|
WHERE c.object_id = ${object_id}
|
||||||
|
ORDER BY c.column_id
|
||||||
|
`);
|
||||||
|
return columns.recordset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPrimaryKey(pool, object_id) {
|
||||||
|
const pk = await pool.request().query(`
|
||||||
|
SELECT k.name AS pk_name, c.name AS column_name
|
||||||
|
FROM sys.key_constraints k
|
||||||
|
INNER JOIN sys.index_columns ic ON k.parent_object_id = ic.object_id AND k.unique_index_id = ic.index_id
|
||||||
|
INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||||
|
WHERE k.parent_object_id = ${object_id} AND k.type = 'PK'
|
||||||
|
ORDER BY ic.key_ordinal
|
||||||
|
`);
|
||||||
|
return pk.recordset;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCreateTable(schema_name, table_name, columns, pk) {
|
||||||
|
let createStmt = `CREATE TABLE [${schema_name}].[${table_name}] (\n`;
|
||||||
|
createStmt += columns.map(col => {
|
||||||
|
let line = ` [${col.column_name}] ${col.data_type}`;
|
||||||
|
if (col.data_type.match(/char|binary|text|nchar|nvarchar|varbinary/i) && col.max_length > 0) {
|
||||||
|
line += `(${col.max_length === -1 ? 'MAX' : col.max_length})`;
|
||||||
|
} else if (col.data_type.match(/decimal|numeric/i)) {
|
||||||
|
line += `(${col.precision},${col.scale})`;
|
||||||
|
}
|
||||||
|
if (col.is_identity) line += ' IDENTITY(1,1)';
|
||||||
|
line += col.is_nullable ? ' NULL' : ' NOT NULL';
|
||||||
|
return line;
|
||||||
|
}).join(',\n');
|
||||||
|
if (pk.length > 0) {
|
||||||
|
const pkCols = pk.map(r => `[${r.column_name}]`).join(', ');
|
||||||
|
createStmt += `,\n CONSTRAINT [${pk[0].pk_name}] PRIMARY KEY (${pkCols})`;
|
||||||
|
}
|
||||||
|
createStmt += '\n);\n\n';
|
||||||
|
return createStmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLatestRows(pool, schema_name, table_name, pkCols, columns) {
|
||||||
|
let orderBy = '';
|
||||||
|
if (pkCols.length > 0) {
|
||||||
|
orderBy = pkCols.map(col => `[${col}] DESC`).join(', ');
|
||||||
|
}
|
||||||
|
const colList = columns.map(c => `[${c.column_name}]`).join(', ');
|
||||||
|
const query = `
|
||||||
|
SELECT TOP 10 ${colList}
|
||||||
|
FROM [${schema_name}].[${table_name}]
|
||||||
|
${orderBy ? `ORDER BY ${orderBy}` : ''}
|
||||||
|
`;
|
||||||
|
try {
|
||||||
|
const rows = await pool.request().query(query);
|
||||||
|
return rows.recordset;
|
||||||
|
} catch (e) {
|
||||||
|
// Table may be empty or inaccessible
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInsertStatements(schema_name, table_name, columns, rows) {
|
||||||
|
if (!rows || rows.length === 0) return '-- No data rows found\n';
|
||||||
|
let inserts = '-- Latest 10 rows:\n';
|
||||||
|
const colNames = columns.map(c => `[${c.column_name}]`).join(', ');
|
||||||
|
for (const row of rows) {
|
||||||
|
const vals = columns.map(c => escapeSqlString(row[c.column_name])).join(', ');
|
||||||
|
inserts += `INSERT INTO [${schema_name}].[${table_name}] (${colNames}) VALUES (${vals});\n`;
|
||||||
|
}
|
||||||
|
return inserts;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getViews(pool) {
|
||||||
|
const views = await pool.request().query(`
|
||||||
|
SELECT s.name AS schema_name, v.name AS view_name, OBJECT_DEFINITION(v.object_id) AS definition
|
||||||
|
FROM sys.views v
|
||||||
|
INNER JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, v.name
|
||||||
|
`);
|
||||||
|
return views.recordset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProcedures(pool) {
|
||||||
|
const procs = await pool.request().query(`
|
||||||
|
SELECT s.name AS schema_name, p.name AS proc_name, OBJECT_DEFINITION(p.object_id) AS definition
|
||||||
|
FROM sys.procedures p
|
||||||
|
INNER JOIN sys.schemas s ON p.schema_id = s.schema_id
|
||||||
|
ORDER BY s.name, p.name
|
||||||
|
`);
|
||||||
|
return procs.recordset;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await ensureDir(path.join(__dirname, 'tables'));
|
||||||
|
let pool;
|
||||||
|
try {
|
||||||
|
pool = await sql.connect(config);
|
||||||
|
const tables = await getTables(pool);
|
||||||
|
|
||||||
|
// TABLES
|
||||||
|
for (const t of tables) {
|
||||||
|
const { object_id, schema_name, table_name } = t;
|
||||||
|
const columns = await getColumns(pool, object_id);
|
||||||
|
const pk = await getPrimaryKey(pool, object_id);
|
||||||
|
const pkCols = pk.map(r => r.column_name);
|
||||||
|
|
||||||
|
const ddl = buildCreateTable(schema_name, table_name, columns, pk);
|
||||||
|
const rows = await getLatestRows(pool, schema_name, table_name, pkCols, columns);
|
||||||
|
const inserts = buildInsertStatements(schema_name, table_name, columns, rows);
|
||||||
|
|
||||||
|
const fileName = `${schema_name}.${table_name}.sql`;
|
||||||
|
const outPath = path.join(__dirname, 'tables', fileName);
|
||||||
|
fs.writeFileSync(outPath, ddl + inserts, 'utf8');
|
||||||
|
console.log('Written:', fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// VIEWS
|
||||||
|
const views = await getViews(pool);
|
||||||
|
for (const v of views) {
|
||||||
|
const { schema_name, view_name, definition } = v;
|
||||||
|
const viewDef = `CREATE VIEW [${schema_name}].[${view_name}] AS\n${definition}\nGO\n`;
|
||||||
|
const fileName = `${schema_name}.${view_name}.sql`;
|
||||||
|
const outPath = path.join(__dirname, 'tables', fileName);
|
||||||
|
fs.writeFileSync(outPath, viewDef, 'utf8');
|
||||||
|
console.log('Written:', fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROCEDURES
|
||||||
|
const procs = await getProcedures(pool);
|
||||||
|
for (const p of procs) {
|
||||||
|
const { schema_name, proc_name, definition } = p;
|
||||||
|
const procDef = `CREATE PROCEDURE [${schema_name}].[${proc_name}]\nAS\n${definition}\nGO\n`;
|
||||||
|
const fileName = `${schema_name}.${proc_name}.sql`;
|
||||||
|
const outPath = path.join(__dirname, 'tables', fileName);
|
||||||
|
fs.writeFileSync(outPath, procDef, 'utf8');
|
||||||
|
console.log('Written:', fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
await pool.close();
|
||||||
|
console.log('Done.');
|
||||||
|
} catch (err) {
|
||||||
|
if (pool) await pool.close();
|
||||||
|
console.error('Error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
195
mssql_monitor.js
Normal file
195
mssql_monitor.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
// SQL Server statement / procedure live monitor
|
||||||
|
// -------------------------------------------------
|
||||||
|
// • Starts a dedicated Extended-Event session on the SQL-Server instance.
|
||||||
|
// • Streams finished RPC & batch events every second.
|
||||||
|
// • Each event (sql-text, parameters, duration, row-count …) is appended
|
||||||
|
// to a session-specific log-file using fs.appendFileSync so the file
|
||||||
|
// can be watched while it grows (e.g. with “tail-f”).
|
||||||
|
// • A fresh log directory is created per run. Left-overs from previous
|
||||||
|
// runs (old XE sessions + log files) are removed automatically and
|
||||||
|
// again on Ctrl-C / SIGTERM.
|
||||||
|
//
|
||||||
|
// REQUIREMENTS
|
||||||
|
// -------------
|
||||||
|
// • npm install mssql (already present in package.json)
|
||||||
|
// • The executing principal must have ALTER ANY EVENT SESSION permission.
|
||||||
|
//
|
||||||
|
// USAGE
|
||||||
|
// ------
|
||||||
|
// > node mssql_monitor.js # uses env-vars for credentials
|
||||||
|
//
|
||||||
|
// Environment variables recognised:
|
||||||
|
// SQLSERVER ( default: 'localhost' )
|
||||||
|
// SQLUSER
|
||||||
|
// SQLPASSWORD
|
||||||
|
// SQLDATABASE ( default: 'master' )
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const sql = require('mssql');
|
||||||
|
|
||||||
|
// ---------- configuration ----------------------------------------------------
|
||||||
|
const cfg = {
|
||||||
|
server : process.env.SQLSERVER || '10.10.10.3',
|
||||||
|
port : process.env.SQLPORT || 1433, // Add port configuration
|
||||||
|
user : process.env.SQLUSER || 'sa',
|
||||||
|
password: process.env.SQLPASSWORD || 'sa_tekno23',
|
||||||
|
database: process.env.SQLDATABASE || 'eazybusiness',
|
||||||
|
options : { encrypt: false, trustServerCertificate: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
// delay (ms) between polling the ring-buffer for new events
|
||||||
|
const POLL_INTERVAL = 1_000;
|
||||||
|
// keep log-files / XE sessions younger than:
|
||||||
|
const KEEP_HOURS = 24;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
(async function main () {
|
||||||
|
const sessionName = `js_monitor_${Date.now()}`; // XE session
|
||||||
|
const logDir = path.join(__dirname, 'monitor_logs');
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
await cleanupLeftovers(logDir, sessionName); // old sessions
|
||||||
|
const pool = await sql.connect(cfg); // connect
|
||||||
|
await createXeSession(pool, sessionName); // XE start
|
||||||
|
console.log(`Monitoring started → ${logDir} (per client session)`);
|
||||||
|
|
||||||
|
let lastRead = new Date(0); // first run
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const events = await fetchNewEvents(pool, sessionName, lastRead);
|
||||||
|
for (const ev of events) {
|
||||||
|
// Compose a unique log file name per client session
|
||||||
|
const sid = ev.session_id || 'unknown';
|
||||||
|
const user = (ev.username || 'unknown').replace(/[\\/:<>|?*"]/g, '_');
|
||||||
|
const host = (ev.client_hostname || 'unknown').replace(/[\\/:<>|?*"]/g, '_');
|
||||||
|
const perClientFile = path.join(
|
||||||
|
logDir,
|
||||||
|
`js_monitor_${user}_${host}_sid${sid}.log`
|
||||||
|
);
|
||||||
|
// Remove fields not needed in log entry
|
||||||
|
const { username, session_id, client_hostname, ...logEntry } = ev;
|
||||||
|
fs.appendFileSync(perClientFile, JSON.stringify(logEntry,null,2) + '\n', 'utf8');
|
||||||
|
lastRead = ev.timestamp;
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('Polling error:', e); }
|
||||||
|
}, POLL_INTERVAL);
|
||||||
|
|
||||||
|
// graceful shutdown -------------------------------------------------------
|
||||||
|
const shutdown = async () => {
|
||||||
|
console.log('\nCtrl-C received, cleaning up...');
|
||||||
|
clearInterval(timer);
|
||||||
|
await dropXeSession(pool, sessionName);
|
||||||
|
await pool.close();
|
||||||
|
console.log('Monitor stopped and cleaned-up.');
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
process.on('SIGINT' , shutdown);
|
||||||
|
process.on('SIGTERM', shutdown);
|
||||||
|
})().catch(e => { console.error(e); process.exit(1); });
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
async function createXeSession (pool, name) {
|
||||||
|
const batch = `
|
||||||
|
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = '${name}')
|
||||||
|
DROP EVENT SESSION [${name}] ON SERVER;
|
||||||
|
|
||||||
|
DECLARE @sql NVARCHAR(MAX) = '
|
||||||
|
CREATE EVENT SESSION [${name}] ON SERVER
|
||||||
|
ADD EVENT sqlserver.rpc_completed
|
||||||
|
(SET collect_statement=(1)
|
||||||
|
ACTION(sqlserver.sql_text, sqlserver.username, sqlserver.session_id, sqlserver.client_hostname)),
|
||||||
|
ADD EVENT sqlserver.sql_batch_completed
|
||||||
|
(ACTION(sqlserver.sql_text, sqlserver.username, sqlserver.session_id, sqlserver.client_hostname))
|
||||||
|
ADD TARGET package0.ring_buffer
|
||||||
|
WITH (MAX_DISPATCH_LATENCY = 1 SECONDS);
|
||||||
|
';
|
||||||
|
EXEC (@sql);
|
||||||
|
|
||||||
|
ALTER EVENT SESSION [${name}] ON SERVER STATE = START;`;
|
||||||
|
await pool.request().batch(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
async function dropXeSession (pool, name) {
|
||||||
|
const cmd = `
|
||||||
|
IF EXISTS (SELECT 1 FROM sys.server_event_sessions WHERE name = @name)
|
||||||
|
BEGIN
|
||||||
|
ALTER EVENT SESSION [${name}] ON SERVER STATE = STOP;
|
||||||
|
DROP EVENT SESSION [${name}] ON SERVER;
|
||||||
|
END`;
|
||||||
|
await pool.request().input('name', sql.NVarChar, name).batch(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
async function fetchNewEvents (pool, name, last) {
|
||||||
|
// convert last JS Date → SQL datetime2
|
||||||
|
const lastTS = last.toISOString();
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
;WITH src AS (
|
||||||
|
SELECT CAST(t.target_data AS XML) AS x
|
||||||
|
FROM sys.dm_xe_session_targets AS t
|
||||||
|
JOIN sys.dm_xe_sessions AS s
|
||||||
|
ON t.event_session_address = s.address
|
||||||
|
WHERE s.name = @name
|
||||||
|
AND t.target_name = 'ring_buffer'
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
evt.value('@timestamp', 'datetime2') AS [timestamp],
|
||||||
|
evt.value('(data[@name="statement"]/value)[1]', 'nvarchar(max)')
|
||||||
|
AS statement_text,
|
||||||
|
evt.value('(action[@name="sql_text"]/value)[1]', 'nvarchar(max)')
|
||||||
|
AS batch_text,
|
||||||
|
evt.value('(data[@name="object_name"]/value)[1]', 'nvarchar(max)')
|
||||||
|
AS object_name,
|
||||||
|
evt.value('(data[@name="row_count"]/value)[1]', 'bigint')
|
||||||
|
AS rows,
|
||||||
|
evt.value('(data[@name="duration"]/value)[1]', 'bigint') / 1000
|
||||||
|
AS duration_ms,
|
||||||
|
evt.value('(action[@name="username"]/value)[1]', 'nvarchar(128)')
|
||||||
|
AS username,
|
||||||
|
evt.value('(action[@name="session_id"]/value)[1]', 'int')
|
||||||
|
AS session_id,
|
||||||
|
evt.value('(action[@name="client_hostname"]/value)[1]', 'nvarchar(128)')
|
||||||
|
AS client_hostname
|
||||||
|
FROM src
|
||||||
|
CROSS APPLY x.nodes('//RingBufferTarget/event') n(evt)
|
||||||
|
WHERE evt.value('@timestamp', 'datetime2') > @last
|
||||||
|
ORDER BY [timestamp];`;
|
||||||
|
|
||||||
|
const rs = await pool.request()
|
||||||
|
.input('name', sql.NVarChar, name)
|
||||||
|
.input('last', sql.DateTime2, lastTS)
|
||||||
|
.query(query);
|
||||||
|
|
||||||
|
// ensure proper JS dates
|
||||||
|
return rs.recordset.map(r => ({ ...r, timestamp: new Date(r.timestamp) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
async function cleanupLeftovers (logDir, currentSession) {
|
||||||
|
// delete old log files ----------------------------------------------------
|
||||||
|
const now = Date.now();
|
||||||
|
for (const f of fs.readdirSync(logDir)) {
|
||||||
|
const p = path.join(logDir, f);
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(p);
|
||||||
|
if (now - stat.mtimeMs > KEEP_HOURS * 3_600_000) fs.rmSync(p);
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop / drop stale XE sessions ------------------------------------------
|
||||||
|
try {
|
||||||
|
const pool = await sql.connect(cfg);
|
||||||
|
const rs = await pool.request()
|
||||||
|
.query(`SELECT name FROM sys.server_event_sessions
|
||||||
|
WHERE name LIKE 'js_monitor_%'`);
|
||||||
|
for (const { name } of rs.recordset) {
|
||||||
|
if (name !== currentSession) await dropXeSession(pool, name);
|
||||||
|
}
|
||||||
|
await pool.close();
|
||||||
|
} catch (e) { /* might lack permission – ignore */ }
|
||||||
|
}
|
||||||
1702
package-lock.json
generated
Normal file
1702
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "som",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^17.2.2",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"mssql": "^11.0.1",
|
||||||
|
"openai": "^5.10.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
44
server.js
Normal file
44
server.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
const DIRECTORY = path.join(__dirname, 'tables'); // Change to your directory
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
fs.readdir(DIRECTORY, (err, files) => {
|
||||||
|
if (err) return res.status(500).send('Error reading directory');
|
||||||
|
let html = '<h1>File Index</h1><ul>';
|
||||||
|
files.forEach(file => {
|
||||||
|
html += `<li><a href="/file/${encodeURIComponent(file)}">${file}</a></li>`;
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
res.send(html);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/file/:filename', (req, res) => {
|
||||||
|
const filename = req.params.filename;
|
||||||
|
const filepath = path.join(DIRECTORY, filename);
|
||||||
|
fs.readFile(filepath, 'utf8', (err, data) => {
|
||||||
|
if (err) return res.status(404).send('File not found');
|
||||||
|
res.send(`<h1>${filename}</h1><pre>${escapeHtml(data)}</pre><a href="/">Back</a>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return text.replace(/[&<>"']/g, function(m) {
|
||||||
|
return ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
})[m];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running at http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
146
summarize_sql.js
Normal file
146
summarize_sql.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// summarize_sql.js
|
||||||
|
// Usage: node summarize_sql.js <inputfile.sql>
|
||||||
|
// Requires: OPENAI_API_KEY in environment
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { OpenAI } = require('openai');
|
||||||
|
|
||||||
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
|
console.error('Error: OPENAI_API_KEY environment variable is required.');
|
||||||
|
console.error('Please create a .env file with your OpenAI API key or set the environment variable.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.argv.length < 3) {
|
||||||
|
console.error('Usage: node summarize_sql.js <inputfile.sql>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputFile = process.argv[2];
|
||||||
|
const inputDir = path.dirname(inputFile);
|
||||||
|
const baseName = path.basename(inputFile, '.sql');
|
||||||
|
const summaryFile = path.join(inputDir, baseName + '.summary.txt');
|
||||||
|
|
||||||
|
function findTableOrViewFile(schemaDotName) {
|
||||||
|
// e.g. "Amazon.tVcsLiteUploadQueue" => tables/Amazon.tVcsLiteUploadQueue.sql
|
||||||
|
const tablesDir = path.join(__dirname, 'tables');
|
||||||
|
const file = path.join(tablesDir, schemaDotName + '.sql');
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readTableOrView(schemaDotName) {
|
||||||
|
const file = findTableOrViewFile(schemaDotName);
|
||||||
|
if (!file) return `-- Definition for ${schemaDotName} not found.`;
|
||||||
|
return fs.readFileSync(file, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const SYSTEM_PROMPT = `
|
||||||
|
You are a SQL expert assistant. Your task is to help the user understand the definition and logic of a given SQL stored procedure or view.
|
||||||
|
- Focus on explaining the purpose, main logic, and important details of the procedure/view.
|
||||||
|
- If you need to look up the definition of a table or view referenced in the code, you can call the function read_table_or_view(schemaDotName) (e.g. read_table_or_view('Amazon.tVcsLiteUploadQueue')) and you will receive the full CREATE statement for that object.
|
||||||
|
- If you need more than one table/view definition, call read_table_or_view multiple times.
|
||||||
|
- Be concise but thorough. Output your summary in clear, readable language.
|
||||||
|
`;
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const sqlText = fs.readFileSync(inputFile, 'utf8');
|
||||||
|
let messages = [
|
||||||
|
{ role: 'system', content: SYSTEM_PROMPT },
|
||||||
|
{ role: 'user', content: `Please summarize the following SQL stored procedure or view:\n\n${sqlText}` }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Function tool definition for OpenAI function-calling
|
||||||
|
const functions = [
|
||||||
|
{
|
||||||
|
name: "read_table_or_view",
|
||||||
|
description: "Get the CREATE statement for a table or view by schema.name",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
schemaDotName: {
|
||||||
|
type: "string",
|
||||||
|
description: "The schema and name, e.g. 'Amazon.tVcsLiteUploadQueue'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ["schemaDotName"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let summary = null;
|
||||||
|
let maxToolCalls = 5; // Prevent infinite loops
|
||||||
|
|
||||||
|
while (maxToolCalls-- > 0) {
|
||||||
|
// Log model call
|
||||||
|
console.log('\n--- Model Call ---');
|
||||||
|
console.log('Model:', "o4-mini");
|
||||||
|
console.log('Messages:', JSON.stringify(messages, null, 2));
|
||||||
|
console.log('Functions:', JSON.stringify(functions, null, 2));
|
||||||
|
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: "o4-mini", // or "gpt-3.5-turbo-1106" if you don't have access to gpt-4o
|
||||||
|
messages,
|
||||||
|
service_tier: "flex",
|
||||||
|
functions,
|
||||||
|
function_call: "auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
const msg = response.choices[0].message;
|
||||||
|
|
||||||
|
if (msg.content) {
|
||||||
|
summary = msg.content;
|
||||||
|
break;
|
||||||
|
} else if (msg.function_call) {
|
||||||
|
// LLM is calling our tool
|
||||||
|
const { name, arguments: argsJson } = msg.function_call;
|
||||||
|
console.log('\n--- Tool Call ---');
|
||||||
|
console.log('Function:', name);
|
||||||
|
console.log('Arguments:', argsJson);
|
||||||
|
if (name === "read_table_or_view") {
|
||||||
|
let args;
|
||||||
|
try {
|
||||||
|
args = JSON.parse(argsJson);
|
||||||
|
} catch (e) {
|
||||||
|
messages.push({ role: 'assistant', content: "Error: Invalid function call arguments." });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const def = readTableOrView(args.schemaDotName);
|
||||||
|
// Log tool result (truncate if large)
|
||||||
|
const defPreview = def.length > 500 ? def.slice(0, 500) + '...[truncated]' : def;
|
||||||
|
console.log('Result:', defPreview);
|
||||||
|
messages.push({
|
||||||
|
role: 'function',
|
||||||
|
name: 'read_table_or_view',
|
||||||
|
content: def
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages.push({ role: 'assistant', content: `Error: Unknown function ${name}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messages.push({ role: 'assistant', content: "Error: No content or function call in response." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summary) {
|
||||||
|
summary = "Error: Could not generate summary after several tool calls.";
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(summaryFile, summary, 'utf8');
|
||||||
|
console.log(`Summary written to ${summaryFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(e => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
hi
|
||||||
Reference in New Issue
Block a user