sonamu 0.9.17 → 0.9.19
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/dist/ai/providers/rtzr/api.d.ts +4 -0
- package/dist/ai/providers/rtzr/api.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/api.js +5 -1
- package/dist/ai/providers/rtzr/error.d.ts +15 -6
- package/dist/ai/providers/rtzr/error.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/error.js +88 -11
- package/dist/ai/providers/rtzr/index.js +2 -2
- package/dist/ai/providers/rtzr/model.d.ts.map +1 -1
- package/dist/ai/providers/rtzr/model.js +20 -9
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +18 -61
- package/dist/migration/index-where-predicate.d.ts +11 -0
- package/dist/migration/index-where-predicate.d.ts.map +1 -0
- package/dist/migration/index-where-predicate.js +305 -0
- package/package.json +4 -4
- package/src/ai/providers/rtzr/__tests__/model.test.ts +284 -0
- package/src/ai/providers/rtzr/api.ts +6 -0
- package/src/ai/providers/rtzr/error.ts +122 -9
- package/src/ai/providers/rtzr/model.ts +39 -8
- package/src/migration/__tests__/code-generation.search-text.test.ts +40 -0
- package/src/migration/code-generation.ts +36 -72
- package/src/migration/index-where-predicate.ts +392 -0
|
@@ -1,22 +1,135 @@
|
|
|
1
|
-
import { AISDKError } from "@ai-sdk/provider";
|
|
2
|
-
import {
|
|
1
|
+
import { AISDKError, APICallError } from "@ai-sdk/provider";
|
|
2
|
+
import { extractResponseHeaders, safeParseJSON, zodSchema } from "@ai-sdk/provider-utils";
|
|
3
|
+
import { type ResponseHandler } from "@ai-sdk/provider-utils";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const rtzrFlatErrorDataSchema = z.object({
|
|
7
|
+
code: z.union([z.string(), z.number()]).nullish(),
|
|
8
|
+
msg: z.string().nullish(),
|
|
9
|
+
type: z.string().nullish(),
|
|
10
|
+
param: z.unknown().nullish(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const rtzrNestedErrorDataSchema = z.object({
|
|
6
14
|
error: z.object({
|
|
7
|
-
msg: z.string(),
|
|
15
|
+
msg: z.string().nullish(),
|
|
16
|
+
message: z.string().nullish(),
|
|
8
17
|
type: z.string().nullish(),
|
|
9
|
-
param: z.
|
|
18
|
+
param: z.unknown().nullish(),
|
|
10
19
|
code: z.union([z.string(), z.number()]).nullish(),
|
|
11
20
|
}),
|
|
12
21
|
});
|
|
13
22
|
|
|
23
|
+
// RTZR returns flat HTTP errors, but async transcription failures wrap details in error.
|
|
24
|
+
export const rtzrErrorDataSchema = z.union([rtzrFlatErrorDataSchema, rtzrNestedErrorDataSchema]);
|
|
25
|
+
|
|
14
26
|
export type RtzrErrorData = z.infer<typeof rtzrErrorDataSchema>;
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
type NormalizedRtzrError = {
|
|
29
|
+
code?: string | number | null;
|
|
30
|
+
message?: string | null;
|
|
31
|
+
msg?: string | null;
|
|
32
|
+
type?: string | null;
|
|
33
|
+
param?: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function isNullish(value: unknown): value is null | undefined {
|
|
37
|
+
return value === null || value === undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeRtzrErrorData(data: RtzrErrorData): NormalizedRtzrError {
|
|
41
|
+
if ("error" in data) {
|
|
42
|
+
return data.error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatParam(param: unknown): string | undefined {
|
|
49
|
+
if (isNullish(param)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof param === "string") {
|
|
54
|
+
return param;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(param);
|
|
59
|
+
} catch {
|
|
60
|
+
return String(param);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatRtzrErrorData(data: RtzrErrorData): string {
|
|
65
|
+
const error = normalizeRtzrErrorData(data);
|
|
66
|
+
const param = formatParam(error.param);
|
|
67
|
+
const parts = [
|
|
68
|
+
isNullish(error.code) ? undefined : `code ${error.code}`,
|
|
69
|
+
isNullish(error.type) ? undefined : `type ${error.type}`,
|
|
70
|
+
isNullish(param) ? undefined : `param ${param}`,
|
|
71
|
+
].filter((item): item is string => !isNullish(item) && item.length > 0);
|
|
72
|
+
const message = error.msg ?? error.message;
|
|
73
|
+
|
|
74
|
+
if (!isNullish(message) && message.length > 0) {
|
|
75
|
+
return [...parts, message].join(": ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (parts.length > 0) {
|
|
79
|
+
return parts.join(": ");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return "unknown RTZR error";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function formatRtzrErrorResponseBody(
|
|
86
|
+
responseBody: string,
|
|
87
|
+
): Promise<string | undefined> {
|
|
88
|
+
if (responseBody.trim().length === 0) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const parsedError = await safeParseJSON({
|
|
93
|
+
text: responseBody,
|
|
94
|
+
schema: zodSchema(rtzrErrorDataSchema),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!parsedError.success) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return formatRtzrErrorData(parsedError.value);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const rtzrFailedResponseHandler: ResponseHandler<APICallError> = async ({
|
|
105
|
+
response,
|
|
106
|
+
url,
|
|
107
|
+
requestBodyValues,
|
|
108
|
+
}) => {
|
|
109
|
+
const responseBody = await response.text();
|
|
110
|
+
const responseHeaders = extractResponseHeaders(response);
|
|
111
|
+
const detail =
|
|
112
|
+
(await formatRtzrErrorResponseBody(responseBody)) ??
|
|
113
|
+
response.statusText ??
|
|
114
|
+
"unparseable RTZR error response";
|
|
115
|
+
const parsedError = await safeParseJSON({
|
|
116
|
+
text: responseBody,
|
|
117
|
+
schema: zodSchema(rtzrErrorDataSchema),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
responseHeaders,
|
|
122
|
+
value: new APICallError({
|
|
123
|
+
message: `HTTP ${response.status}: ${detail}`,
|
|
124
|
+
url,
|
|
125
|
+
requestBodyValues,
|
|
126
|
+
statusCode: response.status,
|
|
127
|
+
responseHeaders,
|
|
128
|
+
responseBody,
|
|
129
|
+
data: parsedError.success ? parsedError.value : undefined,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
};
|
|
20
133
|
|
|
21
134
|
export class RtzrClientError extends AISDKError {
|
|
22
135
|
constructor(
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
mediaTypeToExtension,
|
|
12
12
|
parseProviderOptions,
|
|
13
13
|
postFormDataToApi,
|
|
14
|
+
safeParseJSON,
|
|
15
|
+
zodSchema,
|
|
14
16
|
} from "@ai-sdk/provider-utils";
|
|
15
17
|
import { type FetchFunction } from "@ai-sdk/provider-utils";
|
|
16
18
|
import { isEmpty } from "radashi";
|
|
@@ -20,10 +22,19 @@ import {
|
|
|
20
22
|
rtzrTranscriptionResponseSchema,
|
|
21
23
|
rtzrTranscriptionResultResponseSchema,
|
|
22
24
|
} from "./api";
|
|
23
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
formatRtzrErrorData,
|
|
27
|
+
formatRtzrErrorResponseBody,
|
|
28
|
+
RtzrClientError,
|
|
29
|
+
rtzrFailedResponseHandler,
|
|
30
|
+
} from "./error";
|
|
24
31
|
import { rtzrTranscriptionProviderOptions } from "./options";
|
|
25
32
|
import { type RtzrTranscriptionModelId, type RtzrTranscriptionProviderOptions } from "./options";
|
|
26
33
|
|
|
34
|
+
function isNullish(value: unknown): value is null | undefined {
|
|
35
|
+
return value === null || value === undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export type RtzrTranscriptionCallOptions = Omit<
|
|
28
39
|
TranscriptionModelV3CallOptions,
|
|
29
40
|
"providerOptions"
|
|
@@ -78,17 +89,25 @@ export class RtzrTranscriptionModel implements TranscriptionModelV3 {
|
|
|
78
89
|
},
|
|
79
90
|
);
|
|
80
91
|
|
|
92
|
+
const responseBody = await response.text();
|
|
81
93
|
if (!response.ok) {
|
|
82
|
-
|
|
94
|
+
const detail = await formatRtzrErrorResponseBody(responseBody);
|
|
95
|
+
throw new RtzrClientError(
|
|
96
|
+
`Failed to authorize RTZR: HTTP ${response.status}${
|
|
97
|
+
isNullish(detail) ? "" : `: ${detail}`
|
|
98
|
+
}`,
|
|
99
|
+
);
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
const
|
|
86
|
-
|
|
102
|
+
const parsedData = await safeParseJSON({
|
|
103
|
+
text: responseBody,
|
|
104
|
+
schema: zodSchema(rtzrAuthResponseSchema),
|
|
105
|
+
});
|
|
87
106
|
if (!parsedData.success) {
|
|
88
|
-
throw new RtzrClientError(`
|
|
107
|
+
throw new RtzrClientError(`Invalid RTZR auth response: ${parsedData.error.message}`);
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
return parsedData.
|
|
110
|
+
return parsedData.value.access_token;
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
private async getArgs({ audio, mediaType, providerOptions }: RtzrTranscriptionCallOptions) {
|
|
@@ -144,10 +163,10 @@ export class RtzrTranscriptionModel implements TranscriptionModelV3 {
|
|
|
144
163
|
});
|
|
145
164
|
|
|
146
165
|
const { value: response, rawValue } = await (async () => {
|
|
147
|
-
// transcription이 끝날
|
|
166
|
+
// transcription이 끝날 때까지 RTZR 권장 간격인 5초마다 체크.
|
|
148
167
|
// timeout을 따로 지정하지 않는 이유는 애초에 AbortSignal을 사용하고 있음.
|
|
149
168
|
while (true) {
|
|
150
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
151
170
|
const data = await getFromApi({
|
|
152
171
|
url: this.config.url({
|
|
153
172
|
path: `/transcribe/${transcriptionId}`,
|
|
@@ -167,6 +186,18 @@ export class RtzrTranscriptionModel implements TranscriptionModelV3 {
|
|
|
167
186
|
}
|
|
168
187
|
})();
|
|
169
188
|
|
|
189
|
+
if (response.status === "failed") {
|
|
190
|
+
const detail = isNullish(response.error)
|
|
191
|
+
? "unknown RTZR transcription error"
|
|
192
|
+
: formatRtzrErrorData({
|
|
193
|
+
error: response.error,
|
|
194
|
+
});
|
|
195
|
+
throw new RtzrClientError(
|
|
196
|
+
`RTZR transcription failed for id ${response.id}: status ${response.status}: ` +
|
|
197
|
+
`provider ${this.provider}: model ${this.modelId}: ${detail}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
170
201
|
const segments = response.results?.utterances ?? [];
|
|
171
202
|
|
|
172
203
|
const languages = new Set(segments.map((item) => item.lang).filter((item) => !isEmpty(item)));
|
|
@@ -488,6 +488,46 @@ describe("code-generation partial index DDL", () => {
|
|
|
488
488
|
expect(alterIndexesTo.drop).toHaveLength(0);
|
|
489
489
|
});
|
|
490
490
|
|
|
491
|
+
test("partial index predicate의 text cast 위치 차이는 alter diff에서 no-op이어야 한다", () => {
|
|
492
|
+
const entityIndex: MigrationIndex = {
|
|
493
|
+
type: "index",
|
|
494
|
+
name: "file_descriptions_idx_context_created_at",
|
|
495
|
+
using: "btree",
|
|
496
|
+
columns: [{ name: "context" }, { name: "created_at" }],
|
|
497
|
+
where:
|
|
498
|
+
"(context)::text = ANY ((ARRAY['ast_result'::character varying, 'culture_result'::character varying])::text[])",
|
|
499
|
+
};
|
|
500
|
+
const dbIndex: MigrationIndex = {
|
|
501
|
+
...setMigrationIndexDefaults(entityIndex),
|
|
502
|
+
where:
|
|
503
|
+
"context = ANY (ARRAY[('ast_result'::character varying)::text, ('culture_result'::character varying)::text])",
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const alterIndexesTo = getAlterIndexesTo([entityIndex], [dbIndex]);
|
|
507
|
+
|
|
508
|
+
expect(alterIndexesTo.add).toHaveLength(0);
|
|
509
|
+
expect(alterIndexesTo.drop).toHaveLength(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("partial index predicate의 IN과 ANY 동치 표현은 alter diff에서 no-op이어야 한다", () => {
|
|
513
|
+
const entityIndex: MigrationIndex = {
|
|
514
|
+
type: "index",
|
|
515
|
+
name: "file_descriptions_idx_context_created_at",
|
|
516
|
+
columns: [{ name: "context" }, { name: "created_at" }],
|
|
517
|
+
where: "context IN ('ast_result', 'culture_result')",
|
|
518
|
+
};
|
|
519
|
+
const dbIndex: MigrationIndex = {
|
|
520
|
+
...setMigrationIndexDefaults(entityIndex),
|
|
521
|
+
where:
|
|
522
|
+
"context = ANY (ARRAY[('ast_result'::character varying)::text, ('culture_result'::character varying)::text])",
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const alterIndexesTo = getAlterIndexesTo([entityIndex], [dbIndex]);
|
|
526
|
+
|
|
527
|
+
expect(alterIndexesTo.add).toHaveLength(0);
|
|
528
|
+
expect(alterIndexesTo.drop).toHaveLength(0);
|
|
529
|
+
});
|
|
530
|
+
|
|
491
531
|
test("partial index predicate 변경은 alter diff에서 drop/add 대상이어야 한다", async () => {
|
|
492
532
|
const previousIndex: MigrationIndex = {
|
|
493
533
|
type: "index",
|
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
import { isSearchTextProp } from "../types/types";
|
|
16
16
|
import { formatCode } from "../utils/formatter";
|
|
17
17
|
import { differenceWith, intersectionBy } from "../utils/utils";
|
|
18
|
+
import {
|
|
19
|
+
normalizeIndexWherePredicate,
|
|
20
|
+
normalizeIndexWherePredicateForComparison,
|
|
21
|
+
} from "./index-where-predicate";
|
|
18
22
|
import { PostgreSQLSchemaReader } from "./postgresql-schema-reader";
|
|
19
23
|
|
|
20
24
|
/**
|
|
@@ -1612,15 +1616,28 @@ export function getAlterIndexesTo(entityIndexes: MigrationIndex[], dbIndexes: Mi
|
|
|
1612
1616
|
|
|
1613
1617
|
const normalizedEntityIndexes = entityIndexes.map(setMigrationIndexDefaults);
|
|
1614
1618
|
const normalizedDbIndexes = dbIndexes.map(setMigrationIndexDefaults);
|
|
1619
|
+
// 비교에는 정규화된 predicate를 쓰되, 실제 migration 출력은 원본 SQL을 보존한다.
|
|
1620
|
+
const comparisonEntityIndexes = entityIndexes.map(setMigrationIndexComparisonDefaults);
|
|
1621
|
+
const comparisonDbIndexes = dbIndexes.map(setMigrationIndexComparisonDefaults);
|
|
1615
1622
|
const extraIndexes = {
|
|
1616
|
-
db: diff(
|
|
1617
|
-
entity: diff(
|
|
1623
|
+
db: diff(comparisonDbIndexes, comparisonEntityIndexes, identity),
|
|
1624
|
+
entity: diff(comparisonEntityIndexes, comparisonDbIndexes, identity),
|
|
1618
1625
|
};
|
|
1619
1626
|
if (extraIndexes.entity.length > 0) {
|
|
1620
|
-
|
|
1627
|
+
const addIdentities = new Set(extraIndexes.entity.map(identity));
|
|
1628
|
+
indexesTo.add = indexesTo.add.concat(
|
|
1629
|
+
normalizedEntityIndexes.filter((_, index) =>
|
|
1630
|
+
addIdentities.has(identity(comparisonEntityIndexes[index])),
|
|
1631
|
+
),
|
|
1632
|
+
);
|
|
1621
1633
|
}
|
|
1622
1634
|
if (extraIndexes.db.length > 0) {
|
|
1623
|
-
|
|
1635
|
+
const dropIdentities = new Set(extraIndexes.db.map(identity));
|
|
1636
|
+
indexesTo.drop = indexesTo.drop.concat(
|
|
1637
|
+
normalizedDbIndexes.filter((_, index) =>
|
|
1638
|
+
dropIdentities.has(identity(comparisonDbIndexes[index])),
|
|
1639
|
+
),
|
|
1640
|
+
);
|
|
1624
1641
|
}
|
|
1625
1642
|
|
|
1626
1643
|
return indexesTo;
|
|
@@ -1639,10 +1656,24 @@ function genIndexDropDefinition(index: MigrationIndex) {
|
|
|
1639
1656
|
* DB 조회 결과와 비교하기 위한 인덱스 기본값 설정
|
|
1640
1657
|
*/
|
|
1641
1658
|
export function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex {
|
|
1659
|
+
return setMigrationIndexDefaultsBase(index, false);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function setMigrationIndexComparisonDefaults(index: MigrationIndex): MigrationIndex {
|
|
1663
|
+
// DB canonical SQL과 entity SQL의 표현 차이를 diff identity에서만 흡수한다.
|
|
1664
|
+
return setMigrationIndexDefaultsBase(index, true);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function setMigrationIndexDefaultsBase(
|
|
1668
|
+
index: MigrationIndex,
|
|
1669
|
+
normalizeWhereForComparison: boolean,
|
|
1670
|
+
): MigrationIndex {
|
|
1642
1671
|
const isVectorIndex = index.type === "hnsw" || index.type === "ivfflat";
|
|
1643
1672
|
const supportsOrdering = !isVectorIndex && (!index.using || index.using === "btree");
|
|
1644
1673
|
const normalizedUsing = isVectorIndex ? index.using : (index.using ?? "btree");
|
|
1645
|
-
const normalizedWhere =
|
|
1674
|
+
const normalizedWhere = normalizeWhereForComparison
|
|
1675
|
+
? normalizeIndexWherePredicateForComparison(index.where)
|
|
1676
|
+
: normalizeIndexWherePredicate(index.where);
|
|
1646
1677
|
|
|
1647
1678
|
return {
|
|
1648
1679
|
...index,
|
|
@@ -1662,73 +1693,6 @@ export function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex
|
|
|
1662
1693
|
};
|
|
1663
1694
|
}
|
|
1664
1695
|
|
|
1665
|
-
function normalizeIndexWherePredicate(where: string | undefined): string | undefined {
|
|
1666
|
-
if (!where) {
|
|
1667
|
-
return undefined;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
const trimmed = where.trim();
|
|
1671
|
-
if (trimmed.length === 0) {
|
|
1672
|
-
return undefined;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
|
|
1676
|
-
const closeIndex = findMatchingParenthesisInSql(trimmed, 0);
|
|
1677
|
-
if (closeIndex === trimmed.length - 1) {
|
|
1678
|
-
return trimmed.slice(1, -1).trim();
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
return trimmed;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function findMatchingParenthesisInSql(source: string, openIndex: number): number {
|
|
1686
|
-
let depth = 0;
|
|
1687
|
-
let inSingleQuote = false;
|
|
1688
|
-
let inDoubleQuote = false;
|
|
1689
|
-
|
|
1690
|
-
for (let index = openIndex; index < source.length; index += 1) {
|
|
1691
|
-
const char = source[index];
|
|
1692
|
-
const nextChar = source[index + 1];
|
|
1693
|
-
|
|
1694
|
-
if (char === "'" && !inDoubleQuote) {
|
|
1695
|
-
if (inSingleQuote && nextChar === "'") {
|
|
1696
|
-
index += 1;
|
|
1697
|
-
continue;
|
|
1698
|
-
}
|
|
1699
|
-
inSingleQuote = !inSingleQuote;
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
if (char === '"' && !inSingleQuote) {
|
|
1704
|
-
if (inDoubleQuote && nextChar === '"') {
|
|
1705
|
-
index += 1;
|
|
1706
|
-
continue;
|
|
1707
|
-
}
|
|
1708
|
-
inDoubleQuote = !inDoubleQuote;
|
|
1709
|
-
continue;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
if (inSingleQuote || inDoubleQuote) {
|
|
1713
|
-
continue;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
if (char === "(") {
|
|
1717
|
-
depth += 1;
|
|
1718
|
-
continue;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
if (char === ")") {
|
|
1722
|
-
depth -= 1;
|
|
1723
|
-
if (depth === 0) {
|
|
1724
|
-
return index;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
return -1;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
1696
|
/**
|
|
1733
1697
|
* 테이블 변경 케이스 - Foreign Key 변경
|
|
1734
1698
|
*/
|