Browse Source

Merge pull request #4645 from nocodb/feat/snowflake-client

feat: Snowflake support
pull/4690/head
navi 2 years ago committed by GitHub
parent
commit
86eddb3f48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 46
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  2. 46
      packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue
  3. 3
      packages/nc-gui/components/general/BaseLogo.vue
  4. 1
      packages/nc-gui/lib/enums.ts
  5. 46
      packages/nc-gui/pages/index/index/create-external.vue
  6. 31
      packages/nc-gui/utils/projectCreateUtils.ts
  7. 1015
      packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts
  8. 5
      packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts
  9. 1
      packages/nocodb-sdk/src/lib/sqlUi/index.ts
  10. 1981
      packages/nocodb/package-lock.json
  11. 4
      packages/nocodb/package.json
  12. 5
      packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts
  13. 2609
      packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts
  14. 5
      packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts
  15. 75
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts
  16. 7
      packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts
  17. 3
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts
  18. 975
      packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts

46
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -83,6 +83,15 @@ const validators = computed(() => {
? { ? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()], 'dataSource.connection.connection.filename': [fieldRequiredValidator()],
} }
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: { : {
'dataSource.connection.host': [fieldRequiredValidator()], 'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()], 'dataSource.connection.port': [fieldRequiredValidator()],
@ -383,6 +392,43 @@ watch(
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" /> <a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item> </a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else> <template v-else>
<!-- Host Address --> <!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']"> <a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

46
packages/nc-gui/components/dashboard/settings/data-sources/EditBase.vue

@ -84,6 +84,15 @@ const validators = computed(() => {
? { ? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()], 'dataSource.connection.connection.filename': [fieldRequiredValidator()],
} }
: formState.value.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: { : {
'dataSource.connection.host': [fieldRequiredValidator()], 'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()], 'dataSource.connection.port': [fieldRequiredValidator()],
@ -376,6 +385,43 @@ onMounted(async () => {
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" /> <a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item> </a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else> <template v-else>
<!-- Host Address --> <!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']"> <a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

3
packages/nc-gui/components/general/BaseLogo.vue

@ -3,6 +3,7 @@ import LogosMysqlIcon from '~icons/logos/mysql-icon'
import LogosPostgresql from '~icons/logos/postgresql' import LogosPostgresql from '~icons/logos/postgresql'
import VscodeIconsFileTypeSqlite from '~icons/vscode-icons/file-type-sqlite' import VscodeIconsFileTypeSqlite from '~icons/vscode-icons/file-type-sqlite'
import SimpleIconsMicrosoftsqlserver from '~icons/simple-icons/microsoftsqlserver' import SimpleIconsMicrosoftsqlserver from '~icons/simple-icons/microsoftsqlserver'
import LogosSnowflakeIcon from '~icons/logos/snowflake-icon'
import MdiDatabaseOutline from '~icons/mdi/database-outline' import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { baseType } = defineProps<{ baseType?: string }>() const { baseType } = defineProps<{ baseType?: string }>()
@ -17,6 +18,8 @@ const baseIcon = computed(() => {
return VscodeIconsFileTypeSqlite return VscodeIconsFileTypeSqlite
case ClientType.MSSQL: case ClientType.MSSQL:
return SimpleIconsMicrosoftsqlserver return SimpleIconsMicrosoftsqlserver
case ClientType.SNOWFLAKE:
return LogosSnowflakeIcon
default: default:
return MdiDatabaseOutline return MdiDatabaseOutline
} }

1
packages/nc-gui/lib/enums.ts

@ -20,6 +20,7 @@ export enum ClientType {
PG = 'pg', PG = 'pg',
SQLITE = 'sqlite3', SQLITE = 'sqlite3',
VITESS = 'vitess', VITESS = 'vitess',
SNOWFLAKE = 'snowflake',
} }
export enum Language { export enum Language {

46
packages/nc-gui/pages/index/index/create-external.vue

@ -79,6 +79,15 @@ const validators = computed(() => {
? { ? {
'dataSource.connection.connection.filename': [fieldRequiredValidator()], 'dataSource.connection.connection.filename': [fieldRequiredValidator()],
} }
: formState.dataSource.client === ClientType.SNOWFLAKE
? {
'dataSource.connection.account': [fieldRequiredValidator()],
'dataSource.connection.user': [fieldRequiredValidator()],
'dataSource.connection.password': [fieldRequiredValidator()],
'dataSource.connection.warehouse': [fieldRequiredValidator()],
'dataSource.connection.database': [fieldRequiredValidator()],
'dataSource.connection.schema': [fieldRequiredValidator()],
}
: { : {
'dataSource.connection.host': [fieldRequiredValidator()], 'dataSource.connection.host': [fieldRequiredValidator()],
'dataSource.connection.port': [fieldRequiredValidator()], 'dataSource.connection.port': [fieldRequiredValidator()],
@ -385,6 +394,43 @@ onMounted(async () => {
<a-input v-model:value="formState.dataSource.connection.connection.filename" /> <a-input v-model:value="formState.dataSource.connection.connection.filename" />
</a-form-item> </a-form-item>
<template v-else-if="formState.dataSource.client === ClientType.SNOWFLAKE">
<!-- Account -->
<a-form-item label="Account" v-bind="validateInfos['dataSource.connection.account']">
<a-input v-model:value="formState.dataSource.connection.account" class="nc-extdb-account" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="formState.dataSource.connection.username" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')" v-bind="validateInfos['dataSource.connection.password']">
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Warehouse -->
<a-form-item label="Warehouse" v-bind="validateInfos['dataSource.connection.warehouse']">
<a-input v-model:value="formState.dataSource.connection.warehouse" />
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item :label="$t('labels.schemaName')" v-bind="validateInfos['dataSource.connection.schema']">
<a-input v-model:value="formState.dataSource.connection.schema" />
</a-form-item>
</template>
<template v-else> <template v-else>
<!-- Host Address --> <!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']"> <a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">

31
packages/nc-gui/utils/projectCreateUtils.ts

@ -4,7 +4,7 @@ interface ProjectCreateForm {
title: string title: string
dataSource: { dataSource: {
client: ClientType client: ClientType
connection: DefaultConnection | SQLiteConnection connection: DefaultConnection | SQLiteConnection | SnowflakeConnection
searchPath?: string[] searchPath?: string[]
} }
inflection: { inflection: {
@ -33,6 +33,15 @@ interface SQLiteConnection {
useNullAsDefault?: boolean useNullAsDefault?: boolean
} }
export interface SnowflakeConnection {
account: string
username: string
password: string
warehouse: string
database: string
schema: string
}
const defaultHost = 'localhost' const defaultHost = 'localhost'
const testDataBaseNames = { const testDataBaseNames = {
@ -45,7 +54,7 @@ const testDataBaseNames = {
} }
export const getTestDatabaseName = (db: { client: ClientType; connection?: { database?: string } }) => { export const getTestDatabaseName = (db: { client: ClientType; connection?: { database?: string } }) => {
if (db.client === ClientType.PG) return db.connection?.database if (db.client === ClientType.PG || db.client === ClientType.SNOWFLAKE) return db.connection?.database
return testDataBaseNames[db.client as keyof typeof testDataBaseNames] return testDataBaseNames[db.client as keyof typeof testDataBaseNames]
} }
@ -66,12 +75,16 @@ export const clientTypes = [
text: 'SQLite', text: 'SQLite',
value: ClientType.SQLITE, value: ClientType.SQLITE,
}, },
{
text: 'SnowFlake',
value: ClientType.SNOWFLAKE,
},
] ]
const homeDir = '' const homeDir = ''
type ConnectionClientType = type ConnectionClientType =
| Exclude<ClientType, ClientType.SQLITE> | Exclude<ClientType, ClientType.SQLITE | ClientType.SNOWFLAKE>
| 'tidb' | 'tidb'
| 'yugabyte' | 'yugabyte'
| 'citusdb' | 'citusdb'
@ -79,7 +92,9 @@ type ConnectionClientType =
| 'oracledb' | 'oracledb'
| 'greenplum' | 'greenplum'
const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection } & { [ClientType.SQLITE]: SQLiteConnection } = { const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection } & { [ClientType.SQLITE]: SQLiteConnection } & {
[ClientType.SNOWFLAKE]: SnowflakeConnection
} = {
[ClientType.PG]: { [ClientType.PG]: {
host: defaultHost, host: defaultHost,
port: '5432', port: '5432',
@ -116,6 +131,14 @@ const sampleConnectionData: { [key in ConnectionClientType]: DefaultConnection }
}, },
useNullAsDefault: true, useNullAsDefault: true,
}, },
[ClientType.SNOWFLAKE]: {
account: 'account',
username: 'username',
password: 'password',
warehouse: 'warehouse',
database: 'database',
schema: 'schema',
},
tidb: { tidb: {
host: defaultHost, host: defaultHost,
port: '4000', port: '4000',

1015
packages/nocodb-sdk/src/lib/sqlUi/SnowflakeUi.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb-sdk/src/lib/sqlUi/SqlUiFactory.ts

@ -5,6 +5,7 @@ import { MysqlUi } from './MysqlUi';
import { OracleUi } from './OracleUi'; import { OracleUi } from './OracleUi';
import { PgUi } from './PgUi'; import { PgUi } from './PgUi';
import { SqliteUi } from './SqliteUi'; import { SqliteUi } from './SqliteUi';
import { SnowflakeUi } from './SnowflakeUi';
// import {YugabyteUi} from "./YugabyteUi"; // import {YugabyteUi} from "./YugabyteUi";
// import {TidbUi} from "./TidbUi"; // import {TidbUi} from "./TidbUi";
@ -42,6 +43,10 @@ export class SqlUiFactory {
return PgUi; return PgUi;
} }
if (connectionConfig.client === 'snowflake') {
return SnowflakeUi;
}
throw new Error('Database not supported'); throw new Error('Database not supported');
} }
} }

1
packages/nocodb-sdk/src/lib/sqlUi/index.ts

@ -5,4 +5,5 @@ export * from './PgUi';
export * from './MssqlUi'; export * from './MssqlUi';
export * from './OracleUi'; export * from './OracleUi';
export * from './SqliteUi'; export * from './SqliteUi';
export * from './SnowflakeUi';
export * from './SqlUiFactory'; export * from './SqlUiFactory';

1981
packages/nocodb/package-lock.json generated

File diff suppressed because it is too large Load Diff

4
packages/nocodb/package.json

@ -93,7 +93,7 @@
"jsep": "^1.3.6", "jsep": "^1.3.6",
"jsonfile": "^6.1.0", "jsonfile": "^6.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^2.2.0", "knex": "2.2.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"mailersend": "^1.1.0", "mailersend": "^1.1.0",
@ -104,7 +104,7 @@
"multer": "^1.4.2", "multer": "^1.4.2",
"mysql2": "^2.2.5", "mysql2": "^2.2.5",
"nanoid": "^3.1.20", "nanoid": "^3.1.20",
"nc-help": "0.2.82", "nc-help": "0.2.85",
"nc-lib-gui": "0.100.2", "nc-lib-gui": "0.100.2",
"nc-plugin": "0.1.2", "nc-plugin": "0.1.2",
"ncp": "^2.0.0", "ncp": "^2.0.0",

5
packages/nocodb/src/lib/db/sql-client/lib/SqlClientFactory.ts

@ -6,6 +6,8 @@ import PgClient from './pg/PgClient';
import YugabyteClient from './pg/YugabyteClient'; import YugabyteClient from './pg/YugabyteClient';
import TidbClient from './mysql/TidbClient'; import TidbClient from './mysql/TidbClient';
import VitessClient from './mysql/VitessClient'; import VitessClient from './mysql/VitessClient';
import SfClient from './snowflake/SnowflakeClient';
import { SnowflakeClient } from 'nc-help';
class SqlClientFactory { class SqlClientFactory {
static create(connectionConfig) { static create(connectionConfig) {
@ -31,6 +33,9 @@ class SqlClientFactory {
if (connectionConfig.meta.dbtype === 'yugabyte') if (connectionConfig.meta.dbtype === 'yugabyte')
return new YugabyteClient(connectionConfig); return new YugabyteClient(connectionConfig);
return new PgClient(connectionConfig); return new PgClient(connectionConfig);
} else if (connectionConfig.client === 'snowflake') {
connectionConfig.client = SnowflakeClient;
return new SfClient(connectionConfig);
} }
throw new Error('Database not supported'); throw new Error('Database not supported');

2609
packages/nocodb/src/lib/db/sql-client/lib/snowflake/SnowflakeClient.ts

File diff suppressed because it is too large Load Diff

5
packages/nocodb/src/lib/db/sql-client/lib/snowflake/snowflake.queries.ts

@ -0,0 +1,5 @@
// Snowflake queries
const sfQueries = {};
export default sfQueries;

75
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/BaseModelSqlv2.ts

@ -320,7 +320,7 @@ class BaseModelSqlv2 {
as: 'count', as: 'count',
}).first(); }).first();
const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any; const res = (await this.dbDriver.raw(unsanitize(qb.toQuery()))) as any;
return (this.isPg ? res.rows[0] : res[0][0] ?? res[0]).count; return ((this.isPg || this.isSnowflake) ? res.rows[0] : res[0][0] ?? res[0]).count;
} }
// todo: add support for sortArrJson and filterArrJson // todo: add support for sortArrJson and filterArrJson
@ -1557,6 +1557,11 @@ class BaseModelSqlv2 {
.select(ai.column_name) .select(ai.column_name)
.max(ai.column_name, { as: 'id' }) .max(ai.column_name, { as: 'id' })
)[0].id; )[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any)[0].id;
} }
response = await this.readByPk(id); response = await this.readByPk(id);
} else { } else {
@ -1669,11 +1674,13 @@ class BaseModelSqlv2 {
private getTnPath(tb: Model) { private getTnPath(tb: Model) {
const schema = (this.dbDriver as any).searchPath?.(); const schema = (this.dbDriver as any).searchPath?.();
const table = if (this.isMssql && schema) {
this.isMssql && schema return this.dbDriver.raw('??.??', [schema, tb.table_name]);
? this.dbDriver.raw('??.??', [schema, tb.table_name]) } else if (this.isSnowflake) {
: tb.table_name; return [this.dbDriver.client.config.connection.database, this.dbDriver.client.config.connection.schema, tb.table_name].join('.');
return table; } else {
return tb.table_name;
}
} }
public get tnPath() { public get tnPath() {
@ -1696,6 +1703,10 @@ class BaseModelSqlv2 {
return this.clientType === 'mysql2' || this.clientType === 'mysql'; return this.clientType === 'mysql2' || this.clientType === 'mysql';
} }
get isSnowflake() {
return this.clientType === 'snowflake';
}
get clientType() { get clientType() {
return this.dbDriver.clientType(); return this.dbDriver.clientType();
} }
@ -1798,7 +1809,19 @@ class BaseModelSqlv2 {
} }
if (ai) { if (ai) {
// response = await this.readByPk(id) if (this.isSqlite) {
// sqlite doesnt return id after insert
id = (
await this.dbDriver(this.tnPath)
.select(ai.column_name)
.max(ai.column_name, { as: 'id' })
)[0].id;
} else if (this.isSnowflake) {
id = ((
await this.dbDriver(this.tnPath)
.max(ai.column_name, { as: 'id' })
) as any).rows[0].id;
}
response = await this.readByPk(id); response = await this.readByPk(id);
} else { } else {
response = data; response = data;
@ -1871,10 +1894,10 @@ class BaseModelSqlv2 {
const response = const response =
this.isPg || this.isMssql this.isPg || this.isMssql
? await this.dbDriver ? await this.dbDriver
.batchInsert(this.model.table_name, insertDatas, chunkSize) .batchInsert(this.tnPath, insertDatas, chunkSize)
.returning(this.model.primaryKey?.column_name) .returning(this.model.primaryKey?.column_name)
: await this.dbDriver.batchInsert( : await this.dbDriver.batchInsert(
this.model.table_name, this.tnPath,
insertDatas, insertDatas,
chunkSize chunkSize
); );
@ -1907,7 +1930,7 @@ class BaseModelSqlv2 {
continue; continue;
} }
const wherePk = await this._wherePk(pkValues); const wherePk = await this._wherePk(pkValues);
const response = await transaction(this.model.table_name) const response = await transaction(this.tnPath)
.update(d) .update(d)
.where(wherePk); .where(wherePk);
res.push(response); res.push(response);
@ -1987,7 +2010,7 @@ class BaseModelSqlv2 {
const res = []; const res = [];
for (const d of deleteIds) { for (const d of deleteIds) {
if (Object.keys(d).length) { if (Object.keys(d).length) {
const response = await transaction(this.model.table_name) const response = await transaction(this.tnPath)
.del() .del()
.where(d); .where(d);
res.push(response); res.push(response);
@ -2236,7 +2259,7 @@ class BaseModelSqlv2 {
subject: 'NocoDB Form', subject: 'NocoDB Form',
html: ejs.render(formSubmissionEmailTemplate, { html: ejs.render(formSubmissionEmailTemplate, {
data: transformedData, data: transformedData,
tn: this.model.table_name, tn: this.tnPath,
_tn: this.model.title, _tn: this.model.title,
}), }),
}); });
@ -2358,6 +2381,23 @@ class BaseModelSqlv2 {
const vTn = this.getTnPath(vTable); const vTn = this.getTnPath(vTable);
if (this.isSnowflake) {
const parentPK = this.dbDriver(parentTn)
.select(parentColumn.column_name)
.where(_wherePk(parentTable.primaryKeys, childId))
.first();
const childPK = this.dbDriver(childTn)
.select(childColumn.column_name)
.where(_wherePk(childTable.primaryKeys, rowId))
.first();
await this.dbDriver.raw(`INSERT INTO ?? (??, ??) SELECT (${parentPK.toQuery()}), (${childPK.toQuery()})`, [
vTn,
vParentCol.column_name,
vChildCol.column_name,
])
} else {
await this.dbDriver(vTn).insert({ await this.dbDriver(vTn).insert({
[vParentCol.column_name]: this.dbDriver(parentTn) [vParentCol.column_name]: this.dbDriver(parentTn)
.select(parentColumn.column_name) .select(parentColumn.column_name)
@ -2369,6 +2409,7 @@ class BaseModelSqlv2 {
.first(), .first(),
}); });
} }
}
break; break;
case RelationTypes.HAS_MANY: case RelationTypes.HAS_MANY:
{ {
@ -2555,7 +2596,7 @@ class BaseModelSqlv2 {
} else { } else {
groupingValues = new Set( groupingValues = new Set(
( (
await this.dbDriver(this.model.table_name) await this.dbDriver(this.tnPath)
.select(column.column_name) .select(column.column_name)
.distinct() .distinct()
).map((row) => row[column.column_name]) ).map((row) => row[column.column_name])
@ -2563,7 +2604,7 @@ class BaseModelSqlv2 {
groupingValues.add(null); groupingValues.add(null);
} }
const qb = this.dbDriver(this.model.table_name); const qb = this.dbDriver(this.tnPath);
qb.limit(+rest?.limit || 25); qb.limit(+rest?.limit || 25);
qb.offset(+rest?.offset || 0); qb.offset(+rest?.offset || 0);
@ -2701,7 +2742,7 @@ class BaseModelSqlv2 {
if (isVirtualCol(column)) if (isVirtualCol(column))
NcError.notImplemented('Grouping for virtual columns not implemented'); NcError.notImplemented('Grouping for virtual columns not implemented');
const qb = this.dbDriver(this.model.table_name) const qb = this.dbDriver(this.tnPath)
.count('*', { as: 'count' }) .count('*', { as: 'count' })
.groupBy(column.column_name); .groupBy(column.column_name);
@ -2764,13 +2805,13 @@ class BaseModelSqlv2 {
childTable?: Model childTable?: Model
) { ) {
let query = qb.toQuery(); let query = qb.toQuery();
if (!this.isPg && !this.isMssql) { if (!this.isPg && !this.isMssql && !this.isSnowflake) {
query = unsanitize(qb.toQuery()); query = unsanitize(qb.toQuery());
} else { } else {
query = sanitize(query); query = sanitize(query);
} }
return this.convertAttachmentType( return this.convertAttachmentType(
this.isPg this.isPg || this.isSnowflake
? (await this.dbDriver.raw(query))?.rows ? (await this.dbDriver.raw(query))?.rows
: query.slice(0, 6) === 'select' && !this.isMssql : query.slice(0, 6) === 'select' && !this.isMssql
? await this.dbDriver.from( ? await this.dbDriver.from(

7
packages/nocodb/src/lib/db/sql-data-mapper/lib/sql/CustomKnex.ts

@ -1,4 +1,5 @@
import { Knex, knex } from 'knex'; import { Knex, knex } from 'knex';
import { SnowflakeClient } from 'nc-help';
const types = require('pg').types; const types = require('pg').types;
// override parsing date column to Date() // override parsing date column to Date()
@ -993,6 +994,8 @@ function CustomKnex(arg: string | Knex.Config<any> | any): CustomKnex {
arg.useNullAsDefault = true; arg.useNullAsDefault = true;
} }
if (arg?.client === 'snowflake') arg.client = SnowflakeClient;
const kn: any = knex(arg); const kn: any = knex(arg);
const knexRaw = kn.raw; const knexRaw = kn.raw;
@ -1019,7 +1022,9 @@ function CustomKnex(arg: string | Knex.Config<any> | any): CustomKnex {
value: () => { value: () => {
return typeof arg === 'string' return typeof arg === 'string'
? arg.match(/^(\w+):/) ?? [1] ? arg.match(/^(\w+):/) ?? [1]
: arg.client; : (typeof arg.client === 'string')
? arg.client
: arg.client?.prototype?.dialect || arg.client?.prototype?.driverName;
}, },
}, },
searchPath: { searchPath: {

3
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaFactory.ts

@ -4,6 +4,7 @@ import ModelXcMetaMysql from './ModelXcMetaMysql';
import ModelXcMetaOracle from './ModelXcMetaOracle'; import ModelXcMetaOracle from './ModelXcMetaOracle';
import ModelXcMetaPg from './ModelXcMetaPg'; import ModelXcMetaPg from './ModelXcMetaPg';
import ModelXcMetaSqlite from './ModelXcMetaSqlite'; import ModelXcMetaSqlite from './ModelXcMetaSqlite';
import ModelXcMetaSnowflake from './ModelXcMetaSnowflake';
class ModelXcMetaFactory { class ModelXcMetaFactory {
public static create(connectionConfig, args): BaseModelXcMeta { public static create(connectionConfig, args): BaseModelXcMeta {
@ -20,6 +21,8 @@ class ModelXcMetaFactory {
return new ModelXcMetaPg(args); return new ModelXcMetaPg(args);
} else if (connectionConfig.client === 'oracledb') { } else if (connectionConfig.client === 'oracledb') {
return new ModelXcMetaOracle(args); return new ModelXcMetaOracle(args);
} else if (connectionConfig.client === 'snowflake') {
return new ModelXcMetaSnowflake(args);
} }
throw new Error('Database not supported'); throw new Error('Database not supported');

975
packages/nocodb/src/lib/db/sql-mgr/code/models/xc/ModelXcMetaSnowflake.ts

@ -0,0 +1,975 @@
import BaseModelXcMeta from './BaseModelXcMeta';
class ModelXcMetaSnowflake extends BaseModelXcMeta {
/**
* @param dir
* @param filename
* @param ctx
* @param ctx.tn
* @param ctx.columns
* @param ctx.relations
*/
constructor({ dir, filename, ctx }) {
super({ dir, filename, ctx });
}
/**
* Prepare variables used in code template
*/
prepare() {
const data: any = {};
/* run of simple variable */
data.tn = this.ctx.tn;
data.dbType = this.ctx.dbType;
/* for complex code provide a func and args - do derivation within the func cbk */
data.columns = {
func: this._renderXcColumns.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
relations: this.ctx.relations,
},
};
/* for complex code provide a func and args - do derivation within the func cbk */
data.hasMany = {
func: this._renderXcHasMany.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
hasMany: this.ctx.hasMany,
},
};
/* for complex code provide a func and args - do derivation within the func cbk */
data.belongsTo = {
func: this._renderXcBelongsTo.bind(this),
args: {
tn: this.ctx.tn,
columns: this.ctx.columns,
belongsTo: this.ctx.belongsTo,
},
};
return data;
}
_renderXcHasMany(args) {
return JSON.stringify(args.hasMany);
}
_renderXcBelongsTo(args) {
return JSON.stringify(args.belongsTo);
}
/**
*
* @param args
* @param args.columns
* @param args.relations
* @returns {string}
* @private
*/
_renderXcColumns(args) {
let str = '[\r\n';
for (let i = 0; i < args.columns.length; ++i) {
str += `{\r\n`;
str += `cn: '${args.columns[i].cn}',\r\n`;
str += `type: '${this._getAbstractType(args.columns[i])}',\r\n`;
str += `dt: '${args.columns[i].dt}',\r\n`;
if (args.columns[i].rqd) str += `rqd: ${args.columns[i].rqd},\r\n`;
if (args.columns[i].cdf) {
str += `default: "${args.columns[i].cdf}",\r\n`;
str += `columnDefault: "${args.columns[i].cdf}",\r\n`;
}
if (args.columns[i].un) str += `un: ${args.columns[i].un},\r\n`;
if (args.columns[i].pk) str += `pk: ${args.columns[i].pk},\r\n`;
if (args.columns[i].ai) str += `ai: ${args.columns[i].ai},\r\n`;
if (args.columns[i].dtxp) str += `dtxp: "${args.columns[i].dtxp}",\r\n`;
if (args.columns[i].dtxs) str += `dtxs: ${args.columns[i].dtxs},\r\n`;
str += `validate: {
func: [],
args: [],
msg: []
},`;
str += `},\r\n`;
}
str += ']\r\n';
return str;
}
_getAbstractType(column) {
let str = '';
switch (column.dt) {
case 'int':
str = 'integer';
break;
case 'integer':
str = 'integer';
break;
case 'bigint':
str = 'bigInteger';
break;
case 'bigserial':
str = 'bigserial';
break;
case 'char':
str = 'string';
break;
case 'int2':
str = 'integer';
break;
case 'int4':
str = 'integer';
break;
case 'int8':
str = 'integer';
break;
case 'int4range':
str = 'int4range';
break;
case 'int8range':
str = 'int8range';
break;
case 'serial':
str = 'serial';
break;
case 'serial2':
str = 'serial2';
break;
case 'serial8':
str = 'serial8';
break;
case 'character':
str = 'string';
break;
case 'bit':
str = 'bit';
break;
case 'bool':
str = 'boolean';
break;
case 'boolean':
str = 'boolean';
break;
case 'date':
str = 'date';
break;
case 'double precision':
str = 'double';
break;
case 'event_trigger':
str = 'event_trigger';
break;
case 'fdw_handler':
str = 'fdw_handler';
break;
case 'float4':
str = 'float';
break;
case 'float8':
str = 'float';
break;
case 'uuid':
str = 'uuid';
break;
case 'smallint':
str = 'integer';
break;
case 'smallserial':
str = 'smallserial';
break;
case 'character varying':
str = 'string';
break;
case 'text':
str = 'text';
break;
case 'real':
str = 'float';
break;
case 'time':
str = 'time';
break;
case 'time without time zone':
str = 'time';
break;
case 'timestamp':
str = 'timestamp';
break;
case 'timestamp without time zone':
str = 'timestamp';
break;
case 'timestamptz':
str = 'timestampt';
break;
case 'timestamp with time zone':
str = 'timestamp';
break;
case 'timetz':
str = 'time';
break;
case 'time with time zone':
str = 'time';
break;
case 'daterange':
str = 'daterange';
break;
case 'json':
str = 'json';
break;
case 'jsonb':
str = 'jsonb';
break;
case 'gtsvector':
str = 'gtsvector';
break;
case 'index_am_handler':
str = 'index_am_handler';
break;
case 'anyenum':
str = 'enum';
break;
case 'anynonarray':
str = 'anynonarray';
break;
case 'anyrange':
str = 'anyrange';
break;
case 'box':
str = 'box';
break;
case 'bpchar':
str = 'bpchar';
break;
case 'bytea':
str = 'bytea';
break;
case 'cid':
str = 'cid';
break;
case 'cidr':
str = 'cidr';
break;
case 'circle':
str = 'circle';
break;
case 'cstring':
str = 'cstring';
break;
case 'inet':
str = 'inet';
break;
case 'internal':
str = 'internal';
break;
case 'interval':
str = 'interval';
break;
case 'language_handler':
str = 'language_handler';
break;
case 'line':
str = 'line';
break;
case 'lsec':
str = 'lsec';
break;
case 'macaddr':
str = 'macaddr';
break;
case 'money':
str = 'float';
break;
case 'name':
str = 'name';
break;
case 'numeric':
str = 'numeric';
break;
case 'numrange':
str = 'numrange';
break;
case 'oid':
str = 'oid';
break;
case 'opaque':
str = 'opaque';
break;
case 'path':
str = 'path';
break;
case 'pg_ddl_command':
str = 'pg_ddl_command';
break;
case 'pg_lsn':
str = 'pg_lsn';
break;
case 'pg_node_tree':
str = 'pg_node_tree';
break;
case 'point':
str = 'point';
break;
case 'polygon':
str = 'polygon';
break;
case 'record':
str = 'record';
break;
case 'refcursor':
str = 'refcursor';
break;
case 'regclass':
str = 'regclass';
break;
case 'regconfig':
str = 'regconfig';
break;
case 'regdictionary':
str = 'regdictionary';
break;
case 'regnamespace':
str = 'regnamespace';
break;
case 'regoper':
str = 'regoper';
break;
case 'regoperator':
str = 'regoperator';
break;
case 'regproc':
str = 'regproc';
break;
case 'regpreocedure':
str = 'regpreocedure';
break;
case 'regrole':
str = 'regrole';
break;
case 'regtype':
str = 'regtype';
break;
case 'reltime':
str = 'reltime';
break;
case 'smgr':
str = 'smgr';
break;
case 'tid':
str = 'tid';
break;
case 'tinterval':
str = 'tinterval';
break;
case 'trigger':
str = 'trigger';
break;
case 'tsm_handler':
str = 'tsm_handler';
break;
case 'tsquery':
str = 'tsquery';
break;
case 'tsrange':
str = 'tsrange';
break;
case 'tstzrange':
str = 'tstzrange';
break;
case 'tsvector':
str = 'tsvector';
break;
case 'txid_snapshot':
str = 'txid_snapshot';
break;
case 'unknown':
str = 'unknown';
break;
case 'void':
str = 'void';
break;
case 'xid':
str = 'xid';
break;
case 'xml':
str = 'xml';
break;
default:
str += `"${column.dt}"`;
break;
}
return str;
}
getUIDataType(col): any {
switch (this.getAbstractType(col)) {
case 'integer':
return 'Number';
case 'boolean':
return 'Checkbox';
case 'float':
return 'Decimal';
case 'date':
return 'Date';
case 'datetime':
return 'DateTime';
case 'time':
return 'Time';
case 'year':
return 'Year';
case 'string':
return 'SingleLineText';
case 'text':
return 'LongText';
case 'enum':
return 'SingleSelect';
case 'set':
return 'MultiSelect';
case 'json':
return 'JSON';
case 'blob':
return 'LongText';
case 'geometry':
return 'Geometry';
default:
return 'SpecificDBType';
}
}
getAbstractType(col): any {
const dt = col.dt.toLowerCase();
switch (dt) {
case 'anyenum':
return 'enum';
case 'anynonarray':
case 'anyrange':
return dt;
case 'bit':
return 'integer';
case 'bigint':
case 'bigserial':
return 'integer';
case 'bool':
return 'boolean';
case 'bpchar':
case 'bytea':
return dt;
case 'char':
case 'character':
case 'character varying':
return 'string';
case 'cid':
case 'cidr':
case 'cstring':
return dt;
case 'date':
return 'date';
case 'daterange':
return 'string';
case 'double precision':
return 'string';
case 'event_trigger':
case 'fdw_handler':
return dt;
case 'float4':
case 'float8':
return 'float';
case 'gtsvector':
case 'index_am_handler':
case 'inet':
return dt;
case 'int':
case 'int2':
case 'int4':
case 'int8':
case 'integer':
return 'integer';
case 'int4range':
case 'int8range':
case 'internal':
case 'interval':
return 'string';
case 'json':
case 'jsonb':
return 'json';
case 'language_handler':
case 'lsec':
case 'macaddr':
case 'money':
case 'name':
case 'numeric':
case 'numrange':
case 'oid':
case 'opaque':
case 'path':
case 'pg_ddl_command':
case 'pg_lsn':
case 'pg_node_tree':
return dt;
case 'real':
return 'float';
case 'record':
case 'refcursor':
case 'regclass':
case 'regconfig':
case 'regdictionary':
case 'regnamespace':
case 'regoper':
case 'regoperator':
case 'regproc':
case 'regpreocedure':
case 'regrole':
case 'regtype':
case 'reltime':
return dt;
case 'serial':
case 'serial2':
case 'serial8':
case 'smallint':
case 'smallserial':
return 'integer';
case 'smgr':
return dt;
case 'text':
return 'text';
case 'tid':
return dt;
case 'time':
case 'time without time zone':
return 'time';
case 'timestamp':
case 'timestamp without time zone':
case 'timestamptz':
case 'timestamp with time zone':
return 'datetime';
case 'timetz':
case 'time with time zone':
return 'time';
case 'tinterval':
case 'trigger':
case 'tsm_handler':
case 'tsquery':
case 'tsrange':
case 'tstzrange':
case 'tsvector':
case 'txid_snapshot':
case 'unknown':
case 'void':
case 'xid':
case 'xml':
return dt;
case 'tinyint':
case 'mediumint':
return 'integer';
case 'float':
case 'decimal':
case 'double':
return 'float';
case 'boolean':
return 'boolean';
case 'datetime':
return 'datetime';
case 'uuid':
case 'year':
case 'varchar':
case 'nchar':
return 'string';
case 'tinytext':
case 'mediumtext':
case 'longtext':
return 'text';
case 'binary':
case 'varbinary':
return 'text';
case 'blob':
case 'tinyblob':
case 'mediumblob':
case 'longblob':
return 'blob';
case 'enum':
return 'enum';
case 'set':
return 'set';
case 'line':
case 'point':
case 'polygon':
case 'circle':
case 'box':
case 'geometry':
case 'linestring':
case 'multipoint':
case 'multilinestring':
case 'multipolygon':
return 'geometry';
}
}
_sequelizeGetType(column) {
let str = '';
switch (column.dt) {
case 'int':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'tinyint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'smallint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'mediumint':
str += `DataTypes.INTEGER(${column.dtxp})`;
if (column.un) str += `.UNSIGNED`;
break;
case 'bigint':
str += `DataTypes.BIGINT`;
if (column.un) str += `.UNSIGNED`;
break;
case 'float':
str += `DataTypes.FLOAT`;
break;
case 'decimal':
str += `DataTypes.DECIMAL`;
break;
case 'double':
str += `"DOUBLE(${column.dtxp},${column.ns})"`;
break;
case 'real':
str += `DataTypes.FLOAT`;
break;
case 'bit':
str += `DataTypes.BOOLEAN`;
break;
case 'boolean':
str += `DataTypes.STRING(45)`;
break;
case 'serial':
str += `DataTypes.BIGINT`;
break;
case 'date':
str += `DataTypes.DATEONLY`;
break;
case 'datetime':
str += `DataTypes.DATE`;
break;
case 'timestamp':
str += `DataTypes.DATE`;
break;
case 'time':
str += `DataTypes.TIME`;
break;
case 'year':
str += `"YEAR"`;
break;
case 'char':
str += `DataTypes.CHAR(${column.dtxp})`;
break;
case 'varchar':
str += `DataTypes.STRING(${column.dtxp})`;
break;
case 'nchar':
str += `DataTypes.CHAR(${column.dtxp})`;
break;
case 'text':
str += `DataTypes.TEXT`;
break;
case 'tinytext':
str += `DataTypes.TEXT`;
break;
case 'mediumtext':
str += `DataTypes.TEXT`;
break;
case 'longtext':
str += `DataTypes.TEXT`;
break;
case 'binary':
str += `"BINARY(${column.dtxp})"`;
break;
case 'varbinary':
str += `"VARBINARY(${column.dtxp})"`;
break;
case 'blob':
str += `"BLOB"`;
break;
case 'tinyblob':
str += `"TINYBLOB"`;
break;
case 'mediumblob':
str += `"MEDIUMBLOB"`;
break;
case 'longblob':
str += `"LONGBLOB"`;
break;
case 'enum':
str += `DataTypes.ENUM(${column.dtxp})`;
break;
case 'set':
str += `"SET(${column.dtxp})"`;
break;
case 'geometry':
str += `DataTypes.GEOMETRY`;
break;
case 'point':
str += `"POINT"`;
break;
case 'linestring':
str += `"LINESTRING"`;
break;
case 'polygon':
str += `"POLYGON"`;
break;
case 'multipoint':
str += `"MULTIPOINT"`;
break;
case 'multilinestring':
str += `"MULTILINESTRING"`;
break;
case 'multipolygon':
str += `"MULTIPOLYGON"`;
break;
case 'json':
str += `DataTypes.JSON`;
break;
default:
str += `"${column.dt}"`;
break;
}
return str;
}
_sequelizeGetDefault(column) {
let str = '';
switch (column.dt) {
case 'int':
str += `'${column.cdf}'`;
break;
case 'tinyint':
str += `'${column.cdf}'`;
break;
case 'smallint':
str += `'${column.cdf}'`;
break;
case 'mediumint':
str += `'${column.cdf}'`;
break;
case 'bigint':
str += `'${column.cdf}'`;
break;
case 'float':
str += `'${column.cdf}'`;
break;
case 'decimal':
str += `'${column.cdf}'`;
break;
case 'double':
str += `'${column.cdf}'`;
break;
case 'real':
str += `'${column.cdf}'`;
break;
case 'bit':
str += column.cdf ? column.cdf.split('b')[1] : column.cdf;
break;
case 'boolean':
str += column.cdf;
break;
case 'serial':
str += column.cdf;
break;
case 'date':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'datetime':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'timestamp':
str += `sequelize.literal('${column.cdf_sequelize}')`;
break;
case 'time':
str += `'${column.cdf}'`;
break;
case 'year':
str += `'${column.cdf}'`;
break;
case 'char':
str += `'${column.cdf}'`;
break;
case 'varchar':
str += `'${column.cdf}'`;
break;
case 'nchar':
str += `'${column.cdf}'`;
break;
case 'text':
str += column.cdf;
break;
case 'tinytext':
str += column.cdf;
break;
case 'mediumtext':
str += column.cdf;
break;
case 'longtext':
str += column.cdf;
break;
case 'binary':
str += column.cdf;
break;
case 'varbinary':
str += column.cdf;
break;
case 'blob':
str += column.cdf;
break;
case 'tinyblob':
str += column.cdf;
break;
case 'mediumblob':
str += column.cdf;
break;
case 'longblob':
str += column.cdf;
break;
case 'enum':
str += `'${column.cdf}'`;
break;
case 'set':
str += `'${column.cdf}'`;
break;
case 'geometry':
str += `'${column.cdf}'`;
break;
case 'point':
str += `'${column.cdf}'`;
break;
case 'linestring':
str += `'${column.cdf}'`;
break;
case 'polygon':
str += `'${column.cdf}'`;
break;
case 'multipoint':
str += `'${column.cdf}'`;
break;
case 'multilinestring':
str += `'${column.cdf}'`;
break;
case 'multipolygon':
str += `'${column.cdf}'`;
break;
case 'json':
str += column.cdf;
break;
}
return str;
}
/* getXcColumnsObject(args) {
const columnsArr = [];
for (const column of args.columns) {
const columnObj = {
validate: {
func: [],
args: [],
msg: []
},
cn: column.cn,
_cn: column._cn || column.cn,
type: this._getAbstractType(column),
dt: column.dt,
uidt: column.uidt || this._getUIDataType(column),
uip: column.uip,
uicn: column.uicn,
...column
};
if (column.rqd) {
columnObj.rqd = column.rqd;
}
if (column.cdf) {
columnObj.default = column.cdf;
columnObj.columnDefault = column.cdf;
}
if (column.un) {
columnObj.un = column.un;
}
if (column.pk) {
columnObj.pk = column.pk;
}
if (column.ai) {
columnObj.ai = column.ai;
}
if (column.dtxp) {
columnObj.dtxp = column.dtxp;
}
if (column.dtxs) {
columnObj.dtxs = column.dtxs;
}
columnsArr.push(columnObj);
}
this.mapDefaultPrimaryValue(columnsArr);
return columnsArr;
}*/
/* getObject() {
return {
tn: this.ctx.tn,
_tn: this.ctx._tn,
columns: this.getXcColumnsObject(this.ctx),
pks: [],
hasMany: this.ctx.hasMany,
belongsTo: this.ctx.belongsTo,
dbType: this.ctx.dbType,
type: this.ctx.type,
}
}*/
}
export default ModelXcMetaSnowflake;
Loading…
Cancel
Save