Skip to content

Commit

Permalink
Support Cockroach db (#1521)
Browse files Browse the repository at this point in the history
* support cockroach db

* Update comment
  • Loading branch information
jiqiang90 authored Feb 19, 2023
1 parent 48ec07b commit 28407f5
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 105 deletions.
6 changes: 6 additions & 0 deletions packages/common/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ export const runnerMapping = {
'@subql/node-flare': NETWORK_FAMILY.flare,
'@subql/node-near': NETWORK_FAMILY.near,
};

// DATABASE TYPE
export enum SUPPORT_DB {
cockRoach = 'CockroachDB',
postgres = 'PostgreSQL',
}
18 changes: 18 additions & 0 deletions packages/common/src/project/database/databaseUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2020-2022 OnFinality Limited authors & contributors
// SPDX-License-Identifier: Apache-2.0

import {SUPPORT_DB} from '@subql/common';
import {Pool} from 'pg';
import {Sequelize} from 'sequelize';

export async function getDbType(queryFrom: Sequelize | Pool): Promise<SUPPORT_DB> {
const result = await (queryFrom as any).query('select version()');
// sequelize return an array, Promise<[unknown[], unknown]
// pgPool return a single string object with rows
const cleanResult = result instanceof Array ? result[0][0] : result.rows[0];
const matchDB = Object.values(SUPPORT_DB).find((db) => (cleanResult as {version: string}).version.includes(db));
if (!matchDB) {
throw new Error(`Database type not supported, got ${result}`);
}
return matchDB;
}
4 changes: 4 additions & 0 deletions packages/common/src/project/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright 2020-2022 OnFinality Limited authors & contributors
// SPDX-License-Identifier: Apache-2.0

export * from './databaseUtil';
1 change: 1 addition & 0 deletions packages/common/src/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './load';
export * from './types';
export * from './versioned';
export * from './readers';
export * from './database';
export * from './utils';
2 changes: 1 addition & 1 deletion packages/node-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@willsoto/nestjs-prometheus": "^4.4.0",
"lodash": "^4.17.21",
"prom-client": "^14.0.1",
"sequelize": "6.23.0",
"sequelize": "6.28.0",
"vm2": "^3.9.9",
"yargs": "^16.2.0"
},
Expand Down
21 changes: 16 additions & 5 deletions packages/node-core/src/db/db.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,33 @@ const DEFAULT_DB_OPTION: DbOption = {
database: process.env.DB_DATABASE ?? 'postgres',
};

async function establishConnection(sequelize: Sequelize, numRetries: number): Promise<void> {
const CONNECTION_SSL_ERROR_REGEX = 'not support SSL';

async function establishConnectionSequelize(option: SequelizeOption, numRetries: number): Promise<Sequelize> {
const uri = `postgresql://${option.username}:${option.password}@${option.host}:${option.port}/${option.database}`;

const sequelize = new Sequelize(uri, option);
try {
await sequelize.authenticate();
} catch (error) {
logger.error(error, 'Unable to connect to the database');
if (JSON.stringify(error.message).includes(CONNECTION_SSL_ERROR_REGEX)) {
logger.warn('Database does not support SSL connection, will try to connect without it');
option.dialectOptions = undefined;
}
if (numRetries > 0) {
await delay(3);
void (await establishConnection(sequelize, numRetries - 1));
return establishConnectionSequelize(option, numRetries - 1);
} else {
logger.error(error, 'Unable to connect to the database');
process.exit(1);
}
}
return sequelize;
}

const sequelizeFactory = (option: SequelizeOption) => async () => {
const sequelize = new Sequelize(option);
const numRetries = 5;
await establishConnection(sequelize, numRetries);
const sequelize = await establishConnectionSequelize(option, numRetries);
await sequelize.sync();
return sequelize;
};
Expand All @@ -57,6 +66,7 @@ export class DbModule {
ssl: nodeConfig.isPostgresSecureConnection,
dialectOptions: {
ssl: {
rejectUnauthorized: false,
ca: nodeConfig.postgresCACert,
key: nodeConfig.postgresClientKey ?? '',
cert: nodeConfig.postgresClientCert ?? '',
Expand Down Expand Up @@ -95,6 +105,7 @@ export class DbModule {
ssl: nodeConfig.isPostgresSecureConnection,
dialectOptions: {
ssl: {
rejectUnauthorized: false,
ca: nodeConfig.postgresCACert,
key: nodeConfig.postgresClientKey,
cert: nodeConfig.postgresClientCert,
Expand Down
71 changes: 49 additions & 22 deletions packages/node-core/src/indexer/store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import assert from 'assert';
import {Inject, Injectable} from '@nestjs/common';
import {hexToU8a, u8aToBuffer} from '@polkadot/util';
import {getDbType, SUPPORT_DB} from '@subql/common';
import {Entity, Store} from '@subql/types';
import {
GraphQLModelsRelationsEnums,
Expand All @@ -29,31 +30,30 @@ import {
UpsertOptions,
Utils,
} from 'sequelize';
import {Attributes, CountOptions, CountWithOptions} from 'sequelize/types/model';
import {CountOptions} from 'sequelize/types/model';
import {NodeConfig} from '../configure';
import {getLogger} from '../logger';
import {
commentTableQuery,
addTagsToForeignKeyMap,
BTREE_GIST_EXTENSION_EXIST_QUERY,
camelCaseObjectKey,
commentConstraintQuery,
commentTableQuery,
createExcludeConstraintQuery,
createNotifyTrigger,
createSchemaTrigger,
createSchemaTriggerFunction,
createSendNotificationTriggerFunction,
createUniqueIndexQuery,
dropNotifyFunction,
dropNotifyTrigger,
enumNameToHash,
getFkConstraint,
getTriggers,
SmartTags,
smartTags,
getVirtualFkTag,
addTagsToForeignKeyMap,
createExcludeConstraintQuery,
BTREE_GIST_EXTENSION_EXIST_QUERY,
modelsTypeToModelAttributes,
camelCaseObjectKey,
createSchemaTriggerFunction,
createSchemaTrigger,
enumNameToHash,
dropNotifyFunction,
getFunctions,
SmartTags,
smartTags,
} from '../utils';
import {Metadata, MetadataFactory, MetadataRepo, PoiFactory, PoiRepo, ProofOfIndex} from './entities';
import {StoreOperations} from './StoreOperations';
Expand Down Expand Up @@ -89,6 +89,8 @@ export class StoreService {
@Inject('ISubqueryProject') private subqueryProject: ISubqueryProject<IProjectNetworkConfig>;
private blockHeight: number;
historical: boolean;
private dbType: SUPPORT_DB;
private useSubscription: boolean;

constructor(private sequelize: Sequelize, private config: NodeConfig) {}

Expand All @@ -97,7 +99,17 @@ export class StoreService {
this.modelsRelations = modelsRelations;
this.historical = await this.getHistoricalStateEnabled();
logger.info(`Historical state is ${this.historical ? 'enabled' : 'disabled'}`);
this.dbType = await getDbType(this.sequelize);

this.useSubscription = this.config.subscription;
if (this.useSubscription && this.dbType === SUPPORT_DB.cockRoach) {
this.useSubscription = false;
logger.warn(`Subscription is not support with ${this.dbType}`);
}
if (this.historical && this.dbType === SUPPORT_DB.cockRoach) {
this.historical = false;
logger.warn(`Historical feature is not support with ${this.dbType}`);
}
try {
await this.syncSchema(this.schema);
} catch (e) {
Expand All @@ -120,6 +132,11 @@ export class StoreService {
}

async initHotSchemaReloadQueries(schema: string): Promise<void> {
if (this.dbType === SUPPORT_DB.cockRoach) {
logger.warn(`Hot schema reload feature is not supported with ${this.dbType}`);
return;
}

/* These SQL queries are to allow hot-schema reload on query service */
const schemaTriggerName = hashName(schema, 'schema_trigger', this.metaDataRepo.tableName);
const schemaTriggers = await getTriggers(this.sequelize, schemaTriggerName);
Expand Down Expand Up @@ -183,16 +200,24 @@ export class StoreService {
// Ref: https://www.graphile.org/postgraphile/enums/
// Example query for enum name: COMMENT ON TYPE "polkadot-starter_enum_a40fe73329" IS E'@enum\n@enumName TestEnum'
// It is difficult for sequelize use replacement, instead we use escape to avoid injection
// UPDATE: this comment got syntax error with cockroach db, disable it for now. Waiting to be fixed.
// See https://github.com/cockroachdb/cockroach/issues/44135

const comment = this.sequelize.escape(
`@enum\\n@enumName ${e.name}${e.description ? `\\n ${e.description}` : ''}`
);
await this.sequelize.query(`COMMENT ON TYPE "${enumTypeName}" IS E${comment}`);
if (this.dbType === SUPPORT_DB.cockRoach) {
logger.warn(
`Comment on enum ${e.description} is not supported with ${this.dbType}, enum name may display incorrectly in query service`
);
} else {
const comment = this.sequelize.escape(
`@enum\\n@enumName ${e.name}${e.description ? `\\n ${e.description}` : ''}`
);
await this.sequelize.query(`COMMENT ON TYPE "${enumTypeName}" IS E${comment}`);
}
enumTypeMap.set(e.name, `"${enumTypeName}"`);
}
const extraQueries = [];
// Function need to create ahead of triggers
if (this.config.subscription) {
if (this.useSubscription) {
extraQueries.push(createSendNotificationTriggerFunction(schema));
}
for (const model of this.modelsRelations.models) {
Expand Down Expand Up @@ -223,22 +248,24 @@ export class StoreService {
extraQueries.push(createExcludeConstraintQuery(schema, sequelizeModel.tableName));
}

if (this.config.subscription) {
if (this.useSubscription) {
const triggerName = hashName(schema, 'notify_trigger', sequelizeModel.tableName);
const notifyTriggers = await getTriggers(this.sequelize, triggerName);

// Triggers not been found
if (notifyTriggers.length === 0) {
extraQueries.push(createNotifyTrigger(schema, sequelizeModel.tableName));
} else {
this.validateNotifyTriggers(triggerName, notifyTriggers as NotifyTriggerPayload[]);
}
} else {
extraQueries.push(dropNotifyTrigger(schema, sequelizeModel.tableName));
//TODO: DROP TRIGGER IF EXIST is not valid syntax for cockroach, better check trigger exist at first.
if (this.dbType !== SUPPORT_DB.cockRoach) {
extraQueries.push(dropNotifyTrigger(schema, sequelizeModel.tableName));
}
}
}
// We have to drop the function after all triggers depend on it are removed
if (!this.config.subscription) {
if (!this.useSubscription && this.dbType !== SUPPORT_DB.cockRoach) {
extraQueries.push(dropNotifyFunction(schema));
}

Expand Down
11 changes: 11 additions & 0 deletions packages/node/docker/cockroach-db/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: '3.5'

services:
crdb:
image: cockroachdb/cockroach:latest-v22.1
ports:
- '26257:26257'
- '8080:8080'
command: start-single-node --insecure
volumes:
- '${PWD}/cockroach-data/crdb:/cockroach/cockroach-data'
2 changes: 1 addition & 1 deletion packages/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.5.2",
"sequelize": "6.23.0",
"sequelize": "^6.28.0",
"tar": "^6.1.11",
"typescript": "^4.4.4",
"vm2": "^3.9.9",
Expand Down
2 changes: 1 addition & 1 deletion packages/query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@nestjs/platform-express": "^8.2.6",
"@subql/common": "workspace:*",
"@subql/utils": "workspace:*",
"@subql/x-graphile-build-pg": "4.13.0-0.2.1",
"@subql/x-graphile-build-pg": "4.13.0-0.2.2",
"@subql/x-postgraphile-core": "4.13.0-0.2.0",
"apollo-server-express": "^3.6.2",
"express-pino-logger": "^6.0.0",
Expand Down
10 changes: 8 additions & 2 deletions packages/query/src/graphql/graphql.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import PgPubSub from '@graphile/pg-pubsub';
import {Module, OnModuleDestroy, OnModuleInit} from '@nestjs/common';
import {HttpAdapterHost} from '@nestjs/core';
import {delay} from '@subql/common';
import {delay, getDbType, SUPPORT_DB} from '@subql/common';
import {hashName} from '@subql/utils';
import {getPostGraphileBuilder, PostGraphileCoreOptions} from '@subql/x-postgraphile-core';
import {
Expand Down Expand Up @@ -38,7 +38,7 @@ const SCHEMA_RETRY_NUMBER = 5;
})
export class GraphqlModule implements OnModuleInit, OnModuleDestroy {
private apolloServer: ApolloServer;

private dbType: SUPPORT_DB;
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
private readonly config: Config,
Expand All @@ -55,6 +55,12 @@ export class GraphqlModule implements OnModuleInit, OnModuleDestroy {
} catch (e) {
throw new Error(`create apollo server failed, ${e.message}`);
}
this.dbType = await getDbType(this.pgPool);
if (this.dbType === SUPPORT_DB.cockRoach) {
logger.info(`Using Cockroach database, subscription and hot-schema functions are not supported`);
argv.subscription = false;
argv['disable-hot-schema'] = true;
}
}

async schemaListener(dbSchema: string, options: PostGraphileCoreOptions): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"pino": "^6.13.3",
"rotating-file-stream": "^3.0.2",
"semver": "^7.3.7",
"sequelize": "6.23.0",
"sequelize": "6.28.0",
"tar": "^6.1.11"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 28407f5

Please sign in to comment.