@nine-lab/nine-connector 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nine-lab/nine-connector",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "NineQuery AI Connector for Database",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -22,6 +22,7 @@
22
22
  "express": "^4.22.1",
23
23
  "mariadb": "^3.5.2",
24
24
  "mysql2": "^3.9.7",
25
+ "oracledb": "^6.10.0",
25
26
  "pg": "^8.11.5"
26
27
  },
27
28
  "publishConfig": {
@@ -24,6 +24,7 @@ export class AIProcessor {
24
24
 
25
25
  async filterTables(question, schemaSummary) {
26
26
  if (!this.#chains['table-filter']) throw new Error("table-filter 체인 미등록");
27
+
27
28
  return await this.#chains['table-filter'].invoke({
28
29
  question,
29
30
  schema_summary: JSON.stringify(schemaSummary, null, 2)
@@ -13,14 +13,19 @@ export class AIService {
13
13
 
14
14
  // 2) AI: 필요한 테이블 필터링
15
15
  // AI 결과가 문자열일 경우를 대비해 파싱 로직이 AIProcessor에 없다면 여기서 보완 가능
16
- const { tables } = await this.ai.filterTables(question, schemaSummary);
16
+ //const { selected_tables } = await this.ai.filterTables(question, schemaSummary);
17
+ const { selected_tables } = await this.ai.filterTables(question, schemaSummary);
17
18
 
18
- if (!tables || tables.length === 0) {
19
+ console.log(selected_tables);
20
+
21
+ if (!selected_tables || selected_tables.length === 0) {
19
22
  throw new Error("질문과 관련된 테이블을 찾을 수 없습니다.");
20
23
  }
21
24
 
25
+ console.log(schemaSummary);
26
+
22
27
  // 3) 상세 스키마 필터링 및 SQL 생성
23
- const detailedSchema = schemaSummary.filter(t => tables.includes(t.tableName));
28
+ const detailedSchema = schemaSummary.filter(t => selected_tables.includes(t.tableName));
24
29
  const { sql, explanation } = await this.ai.generateSql(
25
30
  question,
26
31
  detailedSchema,
package/src/core/init.js CHANGED
@@ -18,7 +18,7 @@ async function createEnvFile(rl) {
18
18
  { key: 'SERVER_PORT', label: '1. 커넥터 서버 포트', default: '3000' },
19
19
  { key: 'GEMINI_API_KEY', label: '2. Gemini API Key', default: '' },
20
20
  { key: 'GEMINI_MODEL', label: '3. 사용할 모델', default: 'gemini-2.5-flash' },
21
- { key: 'DB_TYPE', label: '4. DB 종류 (mysql, pg, mariadb)', default: 'mysql' },
21
+ { key: 'DB_TYPE', label: '4. DB 종류 (mysql, mariadb, postgres, oracle)', default: 'mysql' },
22
22
  { key: 'DB_HOST', label: '5. DB 호스트', default: '127.0.0.1' },
23
23
  { key: 'DB_PORT', label: '6. DB 포트', default: '3306' },
24
24
  { key: 'DB_USER', label: '7. DB 사용자 계정', default: 'root' },
@@ -16,13 +16,14 @@ class DatabaseManager {
16
16
  /**
17
17
  * DatabaseFactory의 init() 역할을 수행합니다.
18
18
  */
19
+ // DatabaseManager.js 내부
19
20
  async connect() {
20
21
  try {
21
- // Pool 생성은 전적으로 PoolManager에게 맡깁니다.
22
- this.pool = PoolManager.createPool(this.config);
22
+ // 이제 createPool이 async이므로 await를 붙입니다.
23
+ this.pool = await PoolManager.createPool(this.config);
23
24
 
24
- // 단순 연결 확인 쿼리
25
- await this.query('SELECT 1');
25
+ // 연결 확인
26
+ await this.query(this.dialect.testQuery || 'SELECT 1');
26
27
  console.log(`[${this.type}] Database connection successful.`);
27
28
  } catch (error) {
28
29
  console.error(`[${this.type}] Connection failed:`, error.message);
@@ -39,6 +40,7 @@ class DatabaseManager {
39
40
 
40
41
  const result = await this.pool.query(sql, params);
41
42
 
43
+ /**
42
44
  if (this.type === 'postgres') {
43
45
  return result.rows;
44
46
  } else {
@@ -47,7 +49,20 @@ class DatabaseManager {
47
49
  return (Array.isArray(result) && Array.isArray(result[0]))
48
50
  ? result[0]
49
51
  : result;
52
+ } */
53
+ if (this.type === 'postgres' || this.type === 'postgresql') {
54
+ return result.rows;
55
+ }
56
+
57
+ // MariaDB/MySQL 대응:
58
+ // DDL(INSERT/UPDATE) 결과와 DQL(SELECT) 결과를 구분해야 함
59
+ if (Array.isArray(result)) {
60
+ // mysql2 대응: rows가 0번에 담겨오는 경우
61
+ if (Array.isArray(result[0])) return result[0];
62
+ return result;
50
63
  }
64
+
65
+ return result;
51
66
  }
52
67
 
53
68
  /**
@@ -63,18 +78,18 @@ class DatabaseManager {
63
78
  async getTableSchema() {
64
79
  const rows = await this.query(this.dialect.schemaQuery);
65
80
 
66
- return rows.reduce((acc, row) => {
81
+ // 1. 우선 객체 형태로 그룹화 (중복 방지 효율적 정리를 위해)
82
+ const grouped = rows.reduce((acc, row) => {
67
83
  const { table_name, table_comment, column_name, data_type } = row;
68
84
 
69
- // 테이블이 처음 등장하면 초기화
70
85
  if (!acc[table_name]) {
71
86
  acc[table_name] = {
72
- description: table_comment || "", // 테이블 설명 추가
87
+ tableName: table_name, // 배열로 변환 시 식별을 위해 추가
88
+ description: table_comment || "",
73
89
  columns: []
74
90
  };
75
91
  }
76
92
 
77
- // 컬럼 정보 push
78
93
  acc[table_name].columns.push({
79
94
  column: column_name,
80
95
  type: data_type
@@ -82,6 +97,9 @@ class DatabaseManager {
82
97
 
83
98
  return acc;
84
99
  }, {});
100
+
101
+ // 2. 최종적으로 객체의 값들만 뽑아서 배열로 변환
102
+ return Object.values(grouped);
85
103
  }
86
104
  }
87
105
 
@@ -1,6 +1,7 @@
1
1
  const Dialects = {
2
2
  // MySQL 추가 (MariaDB와 동일)
3
3
  mysql: {
4
+ testQuery: 'SELECT 1',
4
5
  schemaQuery: `
5
6
  SELECT
6
7
  c.TABLE_NAME as table_name,
@@ -15,6 +16,7 @@ const Dialects = {
15
16
  ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
16
17
  },
17
18
  mariadb: {
19
+ testQuery: 'SELECT 1',
18
20
  schemaQuery: `
19
21
  SELECT
20
22
  c.TABLE_NAME as table_name,
@@ -29,19 +31,24 @@ const Dialects = {
29
31
  ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`
30
32
  },
31
33
  postgres: {
34
+ testQuery: 'SELECT 1',
32
35
  schemaQuery: `
33
36
  SELECT
34
37
  t.table_name,
35
- obj_description(pgc.oid, 'pg_class') AS table_comment,
38
+ (SELECT obj_description(pgc.oid, 'pg_class')
39
+ FROM pg_class pgc
40
+ WHERE pgc.relname = t.table_name
41
+ AND pgc.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = t.table_schema)
42
+ ) AS table_comment,
36
43
  c.column_name,
37
44
  c.data_type
38
45
  FROM information_schema.tables t
39
- JOIN information_schema.columns c ON t.table_name = c.table_name
40
- JOIN pg_class pgc ON t.table_name = pgc.relname
41
- WHERE t.table_schema = 'public'
46
+ JOIN information_schema.columns c ON t.table_name = c.table_name AND t.table_schema = c.table_schema
47
+ WHERE t.table_schema = CURRENT_SCHEMA() -- 현재 스키마 자동 대응
42
48
  ORDER BY t.table_name, c.ordinal_position`
43
49
  },
44
50
  oracle: {
51
+ testQuery: 'SELECT 1 FROM DUAL',
45
52
  schemaQuery: `
46
53
  SELECT
47
54
  t.TABLE_NAME as table_name,
@@ -1,5 +1,6 @@
1
1
  import * as mariadb from 'mariadb';
2
2
  import pg from 'pg';
3
+ //import oracledb from 'oracledb';
3
4
 
4
5
  class PoolManager {
5
6
  /**
@@ -15,11 +16,49 @@ class PoolManager {
15
16
  case 'postgres':
16
17
  case 'postgresql':
17
18
  return this.#createPostgresPool(config);
19
+ case 'oracle':
20
+ return this.#createOraclePool(config); // 추가
18
21
  default:
19
22
  throw new Error(`[PoolManager] Unsupported DB_TYPE: ${type}`);
20
23
  }
21
24
  }
22
25
 
26
+ static async #createOraclePool(config) {
27
+ try {
28
+ const { default: oracledb } = await import('oracledb');
29
+
30
+ oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
31
+ oracledb.autoCommit = true;
32
+
33
+ const pool = await oracledb.createPool({
34
+ user: config.user,
35
+ password: config.password,
36
+ connectString: `${config.host}:${config.port}/${config.database}`,
37
+ poolMax: 10,
38
+ poolMin: 2,
39
+ poolIncrement: 1
40
+ });
41
+
42
+ // 다른 DB들과 사용법을 맞추기 위해 래핑(Wrapping)
43
+ return {
44
+ query: async (sql, params = []) => {
45
+ let conn;
46
+ try {
47
+ conn = await pool.getConnection();
48
+ const result = await conn.execute(sql, params);
49
+ return result.rows; // 객체 배열 반환
50
+ } finally {
51
+ if (conn) await conn.close();
52
+ }
53
+ },
54
+ close: () => pool.close()
55
+ };
56
+ } catch (err) {
57
+ console.error("Oracle Pool 생성 실패:", err.message);
58
+ throw err;
59
+ }
60
+ }
61
+
23
62
  static #createMariaDBPool(config) {
24
63
  return mariadb.createPool({
25
64
  host: config.host,
package/src/index.js CHANGED
@@ -69,6 +69,9 @@ async function bootstrap() {
69
69
 
70
70
  // [API] 자연어 질의 엔드포인트
71
71
  app.post('/api/ask', async (req, res) => {
72
+
73
+
74
+
72
75
  const { question } = req.body;
73
76
  if (!question) return res.status(400).json({ success: false, error: "질문을 입력해주세요." });
74
77