@optave/codegraph 3.1.5 → 3.2.0
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/README.md +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
package/src/db/query-builder.js
CHANGED
|
@@ -72,6 +72,55 @@ export function escapeLike(s) {
|
|
|
72
72
|
return s.replace(/[%_\\]/g, '\\$&');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Normalize a file filter value (string, string[], or falsy) into a flat array.
|
|
77
|
+
* Returns an empty array when the input is falsy.
|
|
78
|
+
* @param {string|string[]|undefined|null} file
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeFileFilter(file) {
|
|
82
|
+
if (!file) return [];
|
|
83
|
+
return Array.isArray(file) ? file : [file];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a SQL condition + params for a multi-value file LIKE filter.
|
|
88
|
+
* Returns `{ sql: '', params: [] }` when the filter is empty.
|
|
89
|
+
*
|
|
90
|
+
* @param {string|string[]} file - One or more partial file paths
|
|
91
|
+
* @param {string} [column='file'] - The column name to filter on (e.g. 'n.file', 'a.file')
|
|
92
|
+
* @returns {{ sql: string, params: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
export function buildFileConditionSQL(file, column = 'file') {
|
|
95
|
+
validateColumn(column);
|
|
96
|
+
const files = normalizeFileFilter(file);
|
|
97
|
+
if (files.length === 0) return { sql: '', params: [] };
|
|
98
|
+
if (files.length === 1) {
|
|
99
|
+
return {
|
|
100
|
+
sql: ` AND ${column} LIKE ? ESCAPE '\\'`,
|
|
101
|
+
params: [`%${escapeLike(files[0])}%`],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const clauses = files.map(() => `${column} LIKE ? ESCAPE '\\'`);
|
|
105
|
+
return {
|
|
106
|
+
sql: ` AND (${clauses.join(' OR ')})`,
|
|
107
|
+
params: files.map((f) => `%${escapeLike(f)}%`),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Commander option accumulator for repeatable `--file` flag.
|
|
113
|
+
* Use as: `['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile]`
|
|
114
|
+
* @param {string} val - New value from Commander
|
|
115
|
+
* @param {string[]} acc - Accumulated values (undefined on first call)
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function collectFile(val, acc) {
|
|
119
|
+
acc = acc || [];
|
|
120
|
+
acc.push(val);
|
|
121
|
+
return acc;
|
|
122
|
+
}
|
|
123
|
+
|
|
75
124
|
// ─── Standalone Helpers ──────────────────────────────────────────────
|
|
76
125
|
|
|
77
126
|
/**
|
|
@@ -171,11 +220,18 @@ export class NodeQuery {
|
|
|
171
220
|
return this;
|
|
172
221
|
}
|
|
173
222
|
|
|
174
|
-
/** WHERE n.file LIKE ? (no-op if falsy).
|
|
223
|
+
/** WHERE n.file LIKE ? (no-op if falsy). Accepts a single string or string[]. */
|
|
175
224
|
fileFilter(file) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
225
|
+
const files = normalizeFileFilter(file);
|
|
226
|
+
if (files.length === 0) return this;
|
|
227
|
+
if (files.length === 1) {
|
|
228
|
+
this.#conditions.push("n.file LIKE ? ESCAPE '\\'");
|
|
229
|
+
this.#params.push(`%${escapeLike(files[0])}%`);
|
|
230
|
+
} else {
|
|
231
|
+
const clauses = files.map(() => "n.file LIKE ? ESCAPE '\\'");
|
|
232
|
+
this.#conditions.push(`(${clauses.join(' OR ')})`);
|
|
233
|
+
this.#params.push(...files.map((f) => `%${escapeLike(f)}%`));
|
|
234
|
+
}
|
|
179
235
|
return this;
|
|
180
236
|
}
|
|
181
237
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigError } from '../../shared/errors.js';
|
|
2
2
|
import { CORE_SYMBOL_KINDS, EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
|
|
3
|
-
import { escapeLike } from '../query-builder.js';
|
|
3
|
+
import { escapeLike, normalizeFileFilter } from '../query-builder.js';
|
|
4
4
|
import { Repository } from './base.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -27,6 +27,17 @@ function likeToRegex(pattern) {
|
|
|
27
27
|
return new RegExp(`^${regex}$`, 'i');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Build a filter predicate for file matching.
|
|
32
|
+
* Accepts string, string[], or falsy. Returns null when no filtering needed.
|
|
33
|
+
*/
|
|
34
|
+
function buildFileFilterFn(file) {
|
|
35
|
+
const files = normalizeFileFilter(file);
|
|
36
|
+
if (files.length === 0) return null;
|
|
37
|
+
const regexes = files.map((f) => likeToRegex(`%${escapeLike(f)}%`));
|
|
38
|
+
return (filePath) => regexes.some((re) => re.test(filePath));
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* In-memory Repository implementation backed by Maps.
|
|
32
43
|
* No SQLite dependency — suitable for fast unit tests.
|
|
@@ -121,9 +132,9 @@ export class InMemoryRepository extends Repository {
|
|
|
121
132
|
if (opts.kinds) {
|
|
122
133
|
nodes = nodes.filter((n) => opts.kinds.includes(n.kind));
|
|
123
134
|
}
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
nodes = nodes.filter((n) =>
|
|
135
|
+
{
|
|
136
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
137
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
127
138
|
}
|
|
128
139
|
|
|
129
140
|
// Compute fan-in per node
|
|
@@ -197,9 +208,9 @@ export class InMemoryRepository extends Repository {
|
|
|
197
208
|
if (opts.kind) {
|
|
198
209
|
nodes = nodes.filter((n) => n.kind === opts.kind);
|
|
199
210
|
}
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
nodes = nodes.filter((n) =>
|
|
211
|
+
{
|
|
212
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
213
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
@@ -208,9 +219,9 @@ export class InMemoryRepository extends Repository {
|
|
|
208
219
|
findNodeByQualifiedName(qualifiedName, opts = {}) {
|
|
209
220
|
let nodes = [...this.#nodes.values()].filter((n) => n.qualified_name === qualifiedName);
|
|
210
221
|
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
nodes = nodes.filter((n) =>
|
|
222
|
+
{
|
|
223
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
224
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
214
225
|
}
|
|
215
226
|
|
|
216
227
|
return nodes.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
@@ -248,9 +259,9 @@ export class InMemoryRepository extends Repository {
|
|
|
248
259
|
!n.file.includes('.stories.'),
|
|
249
260
|
);
|
|
250
261
|
}
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
nodes = nodes.filter((n) =>
|
|
262
|
+
{
|
|
263
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
264
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
254
265
|
}
|
|
255
266
|
if (opts.role) {
|
|
256
267
|
nodes = nodes.filter((n) => n.role === opts.role);
|
|
@@ -541,9 +552,9 @@ export class InMemoryRepository extends Repository {
|
|
|
541
552
|
['function', 'method', 'class'].includes(n.kind),
|
|
542
553
|
);
|
|
543
554
|
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
nodes = nodes.filter((n) =>
|
|
555
|
+
{
|
|
556
|
+
const fileFn = buildFileFilterFn(opts.file);
|
|
557
|
+
if (fileFn) nodes = nodes.filter((n) => fileFn(n.file));
|
|
547
558
|
}
|
|
548
559
|
if (opts.pattern) {
|
|
549
560
|
const patternRe = likeToRegex(`%${escapeLike(opts.pattern)}%`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigError } from '../../shared/errors.js';
|
|
2
2
|
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../shared/kinds.js';
|
|
3
|
-
import {
|
|
3
|
+
import { buildFileConditionSQL, NodeQuery } from '../query-builder.js';
|
|
4
4
|
import { cachedStmt } from './cached-stmt.js';
|
|
5
5
|
|
|
6
6
|
// ─── Query-builder based lookups (moved from src/db/repository.js) ─────
|
|
@@ -267,10 +267,9 @@ export function findNodesByScope(db, scopeName, opts = {}) {
|
|
|
267
267
|
sql += ' AND kind = ?';
|
|
268
268
|
params.push(opts.kind);
|
|
269
269
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
270
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
271
|
+
sql += fc.sql;
|
|
272
|
+
params.push(...fc.params);
|
|
274
273
|
sql += ' ORDER BY file, line';
|
|
275
274
|
return db.prepare(sql).all(...params);
|
|
276
275
|
}
|
|
@@ -286,12 +285,11 @@ export function findNodesByScope(db, scopeName, opts = {}) {
|
|
|
286
285
|
* @returns {object[]}
|
|
287
286
|
*/
|
|
288
287
|
export function findNodeByQualifiedName(db, qualifiedName, opts = {}) {
|
|
289
|
-
|
|
288
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
289
|
+
if (fc.sql) {
|
|
290
290
|
return db
|
|
291
|
-
.prepare(
|
|
292
|
-
|
|
293
|
-
)
|
|
294
|
-
.all(qualifiedName, `%${escapeLike(opts.file)}%`);
|
|
291
|
+
.prepare(`SELECT * FROM nodes WHERE qualified_name = ?${fc.sql} ORDER BY file, line`)
|
|
292
|
+
.all(qualifiedName, ...fc.params);
|
|
295
293
|
}
|
|
296
294
|
return cachedStmt(
|
|
297
295
|
_findNodeByQualifiedNameStmt,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findDistinctCallers,
|
|
3
|
+
findFileNodes,
|
|
4
|
+
findImportDependents,
|
|
5
|
+
findImportSources,
|
|
6
|
+
findImportTargets,
|
|
7
|
+
findNodesByFile,
|
|
8
|
+
openReadonlyOrFail,
|
|
9
|
+
} from '../../db/index.js';
|
|
10
|
+
import { isTestFile } from '../../infrastructure/test-filter.js';
|
|
11
|
+
|
|
12
|
+
/** Symbol kinds meaningful for a file brief — excludes parameters, properties, constants. */
|
|
13
|
+
const BRIEF_KINDS = new Set([
|
|
14
|
+
'function',
|
|
15
|
+
'method',
|
|
16
|
+
'class',
|
|
17
|
+
'interface',
|
|
18
|
+
'type',
|
|
19
|
+
'struct',
|
|
20
|
+
'enum',
|
|
21
|
+
'trait',
|
|
22
|
+
'record',
|
|
23
|
+
'module',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute file risk tier from symbol roles and max fan-in.
|
|
28
|
+
* @param {{ role: string|null, callerCount: number }[]} symbols
|
|
29
|
+
* @returns {'high'|'medium'|'low'}
|
|
30
|
+
*/
|
|
31
|
+
function computeRiskTier(symbols) {
|
|
32
|
+
let maxCallers = 0;
|
|
33
|
+
let hasCoreRole = false;
|
|
34
|
+
for (const s of symbols) {
|
|
35
|
+
if (s.callerCount > maxCallers) maxCallers = s.callerCount;
|
|
36
|
+
if (s.role === 'core') hasCoreRole = true;
|
|
37
|
+
}
|
|
38
|
+
if (maxCallers >= 10 || hasCoreRole) return 'high';
|
|
39
|
+
if (maxCallers >= 3) return 'medium';
|
|
40
|
+
return 'low';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* BFS to count transitive callers for a single node.
|
|
45
|
+
* Lightweight variant — only counts, does not collect details.
|
|
46
|
+
*/
|
|
47
|
+
function countTransitiveCallers(db, startId, noTests, maxDepth = 5) {
|
|
48
|
+
const visited = new Set([startId]);
|
|
49
|
+
let frontier = [startId];
|
|
50
|
+
|
|
51
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
52
|
+
const nextFrontier = [];
|
|
53
|
+
for (const fid of frontier) {
|
|
54
|
+
const callers = findDistinctCallers(db, fid);
|
|
55
|
+
for (const c of callers) {
|
|
56
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
57
|
+
visited.add(c.id);
|
|
58
|
+
nextFrontier.push(c.id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
frontier = nextFrontier;
|
|
63
|
+
if (frontier.length === 0) break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return visited.size - 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Count transitive file-level import dependents via BFS.
|
|
71
|
+
* Depth-bounded to match countTransitiveCallers and keep hook latency predictable.
|
|
72
|
+
*/
|
|
73
|
+
function countTransitiveImporters(db, fileNodeIds, noTests, maxDepth = 5) {
|
|
74
|
+
const visited = new Set(fileNodeIds);
|
|
75
|
+
let frontier = [...fileNodeIds];
|
|
76
|
+
|
|
77
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
78
|
+
const nextFrontier = [];
|
|
79
|
+
for (const current of frontier) {
|
|
80
|
+
const dependents = findImportDependents(db, current);
|
|
81
|
+
for (const dep of dependents) {
|
|
82
|
+
if (!visited.has(dep.id) && (!noTests || !isTestFile(dep.file))) {
|
|
83
|
+
visited.add(dep.id);
|
|
84
|
+
nextFrontier.push(dep.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
frontier = nextFrontier;
|
|
89
|
+
if (frontier.length === 0) break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return visited.size - fileNodeIds.length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Produce a token-efficient file brief: symbols with roles and caller counts,
|
|
97
|
+
* importer info with transitive count, and file risk tier.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} file - File path (partial match)
|
|
100
|
+
* @param {string} customDbPath - Path to graph.db
|
|
101
|
+
* @param {{ noTests?: boolean }} opts
|
|
102
|
+
* @returns {{ file: string, results: object[] }}
|
|
103
|
+
*/
|
|
104
|
+
export function briefData(file, customDbPath, opts = {}) {
|
|
105
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
106
|
+
try {
|
|
107
|
+
const noTests = opts.noTests || false;
|
|
108
|
+
const fileNodes = findFileNodes(db, `%${file}%`);
|
|
109
|
+
if (fileNodes.length === 0) {
|
|
110
|
+
return { file, results: [] };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results = fileNodes.map((fn) => {
|
|
114
|
+
// Direct importers
|
|
115
|
+
let importedBy = findImportSources(db, fn.id);
|
|
116
|
+
if (noTests) importedBy = importedBy.filter((i) => !isTestFile(i.file));
|
|
117
|
+
const directImporters = [...new Set(importedBy.map((i) => i.file))];
|
|
118
|
+
|
|
119
|
+
// Transitive importer count
|
|
120
|
+
const totalImporterCount = countTransitiveImporters(db, [fn.id], noTests);
|
|
121
|
+
|
|
122
|
+
// Direct imports
|
|
123
|
+
let importsTo = findImportTargets(db, fn.id);
|
|
124
|
+
if (noTests) importsTo = importsTo.filter((i) => !isTestFile(i.file));
|
|
125
|
+
|
|
126
|
+
// Symbol definitions with roles and caller counts
|
|
127
|
+
const defs = findNodesByFile(db, fn.file).filter((d) => BRIEF_KINDS.has(d.kind));
|
|
128
|
+
const symbols = defs.map((d) => {
|
|
129
|
+
const callerCount = countTransitiveCallers(db, d.id, noTests);
|
|
130
|
+
return {
|
|
131
|
+
name: d.name,
|
|
132
|
+
kind: d.kind,
|
|
133
|
+
line: d.line,
|
|
134
|
+
role: d.role || null,
|
|
135
|
+
callerCount,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const riskTier = computeRiskTier(symbols);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
file: fn.file,
|
|
143
|
+
risk: riskTier,
|
|
144
|
+
imports: importsTo.map((i) => i.file),
|
|
145
|
+
importedBy: directImporters,
|
|
146
|
+
totalImporterCount,
|
|
147
|
+
symbols,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return { file, results };
|
|
152
|
+
} finally {
|
|
153
|
+
db.close();
|
|
154
|
+
}
|
|
155
|
+
}
|