From 3e5f9b99f57318f77fa76af55f6828c8297c978c Mon Sep 17 00:00:00 2001 From: taozhi8833998 Date: Tue, 26 Dec 2023 09:27:12 +0800 Subject: [PATCH 1/2] feat: support create user in mysql --- pegjs/flinksql.pegjs | 12 --- pegjs/mariadb.pegjs | 168 +++++++++++++++++++++++++++++++++++++ pegjs/mysql.pegjs | 168 +++++++++++++++++++++++++++++++++++++ src/aggregation.js | 6 +- src/command.js | 1 + src/create.js | 29 +++++++ src/func.js | 3 +- test/mysql-mariadb.spec.js | 27 ++++++ test/postgres.spec.js | 2 +- 9 files changed, 396 insertions(+), 20 deletions(-) diff --git a/pegjs/flinksql.pegjs b/pegjs/flinksql.pegjs index 28f167e5..234ebaa9 100644 --- a/pegjs/flinksql.pegjs +++ b/pegjs/flinksql.pegjs @@ -3025,14 +3025,6 @@ scalar_func cast_expr = e:(literal / aggr_func / func_call / case_expr / interval_expr / column_ref / param) s:KW_DOUBLE_COLON t:data_type { - /* => { - type: 'cast'; - expr: expr | literal | aggr_func | func_call | case_expr | interval_expr | column_ref | param - | expr; - symbol: '::' | 'as', - target: data_type; - } - */ return { type: 'cast', keyword: 'cast', @@ -3042,7 +3034,6 @@ cast_expr } } / c:(KW_CAST / KW_TRY_CAST) __ LPAREN __ e:expr __ KW_AS __ t:data_type __ RPAREN { - // => IGNORE return { type: 'cast', keyword: c.toLowerCase(), @@ -3052,7 +3043,6 @@ cast_expr }; } / c:(KW_CAST / KW_TRY_CAST) __ LPAREN __ e:expr __ KW_AS __ KW_DECIMAL __ LPAREN __ precision:int __ RPAREN __ RPAREN { - // => IGNORE return { type: 'cast', keyword: c.toLowerCase(), @@ -3064,7 +3054,6 @@ cast_expr }; } / c:(KW_CAST / KW_TRY_CAST) __ LPAREN __ e:expr __ KW_AS __ KW_DECIMAL __ LPAREN __ precision:int __ COMMA __ scale:int __ RPAREN __ RPAREN { - // => IGNORE return { type: 'cast', keyword: c.toLowerCase(), @@ -3076,7 +3065,6 @@ cast_expr }; } / c:(KW_CAST / KW_TRY_CAST) __ LPAREN __ e:expr __ KW_AS __ s:signedness __ t:KW_INTEGER? __ RPAREN { /* MySQL cast to un-/signed integer */ - // => IGNORE return { type: 'cast', keyword: c.toLowerCase(), diff --git a/pegjs/mariadb.pegjs b/pegjs/mariadb.pegjs index 807dd8b9..4e324a0b 100644 --- a/pegjs/mariadb.pegjs +++ b/pegjs/mariadb.pegjs @@ -251,6 +251,7 @@ create_stmt / create_index_stmt / create_db_stmt / create_view_stmt + / create_user_stmt alter_stmt = alter_table_stmt @@ -354,6 +355,173 @@ create_db_stmt } } +auth_option + = 'IDENTIFIED' __ ap:('WITH'i __ ident)? __ 'BY'i __ 'RANDOM'i __ 'PASSWORD'i { + const value = { + prefix: 'by', + type: 'origin', + value: 'random password', + } + return { + keyword: ['identified', ap && ap[0].toLowerCase()].filter(v => v).join(' '), + auth_plugin: ap && ap[2], + value + } + } + / 'IDENTIFIED' __ ap:('WITH'i __ ident)? __ 'BY'i __ v:literal_string { + v.prefix = 'by' + return { + keyword: ['identified', ap && ap[0].toLowerCase()].filter(v => v).join(' '), + auth_plugin: ap && ap[2], + value: v + } + } + / 'IDENTIFIED' __ 'WITH'i __ ap:ident __ 'AS'i __ v:literal_string { + v.prefix = 'as' + return { + keyword: 'identified with', + auth_plugin: ap && ap[2], + value: v + } + } +user_auth_option + = u:user_or_role __ ap:(auth_option)? { + return { + user: u, + auth_option: ap + } + } +user_auth_option_list + = head:user_auth_option tail:(__ COMMA __ user_auth_option)* { + return createList(head, tail) + } +default_role + = KW_DEFAULT __ 'role'i __ r:user_or_role_list { + return { + keyword: 'default role', + value: r + } + } +tls_option + = v:('NONE'i / 'SSL'i / 'X509'i) { + return{ + type: 'origin', + value: v + } + } + / k:('CIPHER'i / 'ISSUER'i / 'SUBJECT'i) __ v:literal_string { + v.prefix = k.toLowerCase() + return v + } +tls_option_list + = head:tls_option tail:(__ KW_AND __ tls_option)* { + return createBinaryExprChain(head, tail) + } +require_options + = 'REQUIRE'i __ t:tls_option_list { + return { + keyword: 'require', + value: t + } + } + +resource_option + = k:('MAX_QUERIES_PER_HOUR'i / 'MAX_UPDATES_PER_HOUR'i / 'MAX_CONNECTIONS_PER_HOUR'i / 'MAX_USER_CONNECTIONS'i) __ v:literal_numeric { + v.prefix = k.toLowerCase() + return v + } +with_resource_option + = KW_WITH __ r:resource_option t:(__ resource_option)* { + const resourceOptions = [r] + if (t) { + for (const item of t) { + resourceOptions.push(item[1]) + } + } + return { + keyword: 'with', + value: resourceOptions + } + } +password_option + = 'PASSWORD'i __ 'EXPIRE'i __ v:('DEFAULT'i / 'NEVER'i / interval_expr) { + return { + keyword: 'password expire', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + / 'PASSWORD'i __ 'HISTORY'i __ v:('DEFAULT'i / literal_numeric) { + return { + keyword: 'password history', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + / 'PASSWORD'i __ 'REUSE' __ v:interval_expr { + return { + keyword: 'password reuse', + value: v + } + } + / 'PASSWORD'i __ 'REQUIRE'i __ 'CURRENT'i __ v:('DEFAULT'i / 'OPTIONAL'i) { + return { + keyword: 'password require current', + value: { type: 'origin', value: v } + } + } + / 'FAILED_LOGIN_ATTEMPTS'i __ v:literal_numeric { + return { + keyword: 'failed_login_attempts', + value: v + } + } + / 'PASSWORD_LOCK_TIME'i __ v:(literal_numeric / 'UNBOUNDED'i) { + return { + keyword: 'password_lock_time', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + +password_option_list + = head:password_option tail:(__ password_option)* { + return createList(head, tail, 1) + } +user_lock_option + = 'ACCOUNT'i __ v:('LOCK'i / 'UNLOCK'i) { + const value = { + type: 'origin', + value: v.toLowerCase() + } + value.prefix = 'account' + return value + } +attribute + = 'ATTRIBUTE'i __ v:literal_string { + v.prefix = 'attribute' + return v + } +create_user_stmt + = a:KW_CREATE __ k:KW_USER __ ife:if_not_exists_stmt? __ t:user_auth_option_list __ + d:default_role? __ r:require_options? __ wr:with_resource_option? __ p:password_option_list? __ + l:user_lock_option? __ c:keyword_comment? __ attr:attribute? { + return { + tableList: Array.from(tableList), + columnList: columnListTableAlias(columnList), + ast: { + type: a[0].toLowerCase(), + keyword: 'user', + if_not_exists: ife, + user: t, + default_role: d, + require: r, + resource_options: wr, + password_options: p, + lock_option: l, + comment: c, + attribute: attr + } + } + } + view_with = KW_WITH __ c:("CASCADED"i / "LOCAL"i) __ "CHECK"i __ "OPTION" { return `with ${c.toLowerCase()} check option` diff --git a/pegjs/mysql.pegjs b/pegjs/mysql.pegjs index 11f8ba8d..3e5d3e18 100644 --- a/pegjs/mysql.pegjs +++ b/pegjs/mysql.pegjs @@ -446,6 +446,7 @@ create_stmt / create_index_stmt / create_db_stmt / create_view_stmt + / create_user_stmt alter_stmt = alter_table_stmt @@ -548,6 +549,173 @@ create_db_stmt } } +auth_option + = 'IDENTIFIED' __ ap:('WITH'i __ ident)? __ 'BY'i __ 'RANDOM'i __ 'PASSWORD'i { + const value = { + prefix: 'by', + type: 'origin', + value: 'random password', + } + return { + keyword: ['identified', ap && ap[0].toLowerCase()].filter(v => v).join(' '), + auth_plugin: ap && ap[2], + value + } + } + / 'IDENTIFIED' __ ap:('WITH'i __ ident)? __ 'BY'i __ v:literal_string { + v.prefix = 'by' + return { + keyword: ['identified', ap && ap[0].toLowerCase()].filter(v => v).join(' '), + auth_plugin: ap && ap[2], + value: v + } + } + / 'IDENTIFIED' __ 'WITH'i __ ap:ident __ 'AS'i __ v:literal_string { + v.prefix = 'as' + return { + keyword: 'identified with', + auth_plugin: ap && ap[2], + value: v + } + } +user_auth_option + = u:user_or_role __ ap:(auth_option)? { + return { + user: u, + auth_option: ap + } + } +user_auth_option_list + = head:user_auth_option tail:(__ COMMA __ user_auth_option)* { + return createList(head, tail) + } +default_role + = KW_DEFAULT __ 'role'i __ r:user_or_role_list { + return { + keyword: 'default role', + value: r + } + } +tls_option + = v:('NONE'i / 'SSL'i / 'X509'i) { + return{ + type: 'origin', + value: v + } + } + / k:('CIPHER'i / 'ISSUER'i / 'SUBJECT'i) __ v:literal_string { + v.prefix = k.toLowerCase() + return v + } +tls_option_list + = head:tls_option tail:(__ KW_AND __ tls_option)* { + return createBinaryExprChain(head, tail) + } +require_options + = 'REQUIRE'i __ t:tls_option_list { + return { + keyword: 'require', + value: t + } + } + +resource_option + = k:('MAX_QUERIES_PER_HOUR'i / 'MAX_UPDATES_PER_HOUR'i / 'MAX_CONNECTIONS_PER_HOUR'i / 'MAX_USER_CONNECTIONS'i) __ v:literal_numeric { + v.prefix = k.toLowerCase() + return v + } +with_resource_option + = KW_WITH __ r:resource_option t:(__ resource_option)* { + const resourceOptions = [r] + if (t) { + for (const item of t) { + resourceOptions.push(item[1]) + } + } + return { + keyword: 'with', + value: resourceOptions + } + } +password_option + = 'PASSWORD'i __ 'EXPIRE'i __ v:('DEFAULT'i / 'NEVER'i / interval_expr) { + return { + keyword: 'password expire', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + / 'PASSWORD'i __ 'HISTORY'i __ v:('DEFAULT'i / literal_numeric) { + return { + keyword: 'password history', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + / 'PASSWORD'i __ 'REUSE' __ v:interval_expr { + return { + keyword: 'password reuse', + value: v + } + } + / 'PASSWORD'i __ 'REQUIRE'i __ 'CURRENT'i __ v:('DEFAULT'i / 'OPTIONAL'i) { + return { + keyword: 'password require current', + value: { type: 'origin', value: v } + } + } + / 'FAILED_LOGIN_ATTEMPTS'i __ v:literal_numeric { + return { + keyword: 'failed_login_attempts', + value: v + } + } + / 'PASSWORD_LOCK_TIME'i __ v:(literal_numeric / 'UNBOUNDED'i) { + return { + keyword: 'password_lock_time', + value: typeof v === 'string' ? { type: 'origin', value: v } : v + } + } + +password_option_list + = head:password_option tail:(__ password_option)* { + return createList(head, tail, 1) + } +user_lock_option + = 'ACCOUNT'i __ v:('LOCK'i / 'UNLOCK'i) { + const value = { + type: 'origin', + value: v.toLowerCase() + } + value.prefix = 'account' + return value + } +attribute + = 'ATTRIBUTE'i __ v:literal_string { + v.prefix = 'attribute' + return v + } +create_user_stmt + = a:KW_CREATE __ k:KW_USER __ ife:if_not_exists_stmt? __ t:user_auth_option_list __ + d:default_role? __ r:require_options? __ wr:with_resource_option? __ p:password_option_list? __ + l:user_lock_option? __ c:keyword_comment? __ attr:attribute? { + return { + tableList: Array.from(tableList), + columnList: columnListTableAlias(columnList), + ast: { + type: a[0].toLowerCase(), + keyword: 'user', + if_not_exists: ife, + user: t, + default_role: d, + require: r, + resource_options: wr, + password_options: p, + lock_option: l, + comment: c, + attribute: attr + } + } + } + view_with = KW_WITH __ c:("CASCADED"i / "LOCAL"i) __ "CHECK"i __ "OPTION" { return `with ${c.toLowerCase()} check option` diff --git a/src/aggregation.js b/src/aggregation.js index 804f4a01..9c4910f1 100644 --- a/src/aggregation.js +++ b/src/aggregation.js @@ -8,11 +8,7 @@ function aggrToSQL(expr) { let str = exprToSQL(args.expr) const fnName = expr.name const overStr = overToSQL(over) - let separator = ' ' - if (args.parentheses) { - separator = '' - str = `(${str})` - } + const separator = ' ' if (args.distinct) str = ['DISTINCT', str].join(separator) if (args.orderby) str = `${str} ${orderOrPartitionByToSQL(args.orderby, 'order by')}` if (args.separator) str = [str, toUpper(args.separator.keyword), literalToSQL(args.separator.value)].filter(hasVal).join(' ') diff --git a/src/command.js b/src/command.js index c6e7fea3..6c455427 100644 --- a/src/command.js +++ b/src/command.js @@ -225,6 +225,7 @@ export { executeToSQL, forLoopToSQL, grantAndRevokeToSQL, + grantUserOrRoleToSQL, ifToSQL, raiseToSQL, renameToSQL, diff --git a/src/create.js b/src/create.js index 9f046086..eee30d7e 100644 --- a/src/create.js +++ b/src/create.js @@ -2,6 +2,7 @@ import { alterArgsToSQL, alterExprToSQL } from './alter' import { exprToSQL } from './expr' import { indexDefinitionToSQL, indexOptionListToSQL, indexTypeToSQL } from './index-definition' import { columnDefinitionToSQL, columnRefToSQL } from './column' +import { grantUserOrRoleToSQL } from './command' import { constraintDefinitionToSQL } from './constrain' import { funcToSQL } from './func' import { tablesToSQL, tableOptionToSQL, tableToSQL } from './tables' @@ -12,6 +13,7 @@ import { columnOrderListToSQL, commonOptionConnector, commonKeywordArgsToSQL, + commentToSQL, commonTypeValue, dataTypeToSQL, toUpper, @@ -338,6 +340,30 @@ function createAggregateToSQL(stmt) { sql.push(`${functionName}(${argsSQL})`, `(${options.map(aggregateOptionToSQL).join(', ')})`) return sql.filter(hasVal).join(' ') } +function createUserToSQL(stmt) { + const { + attribute, comment, default_role: defaultRole, if_not_exists: ifNotExists, keyword, lock_option: lockOption, + password_options: passwordOptions, require: requireOption, resource_options: resourceOptions, type, user, + } = stmt + const userAuthOptions = user.map(userAuthOption => { + const { user: userInfo, auth_option } = userAuthOption + const result = [grantUserOrRoleToSQL(userInfo)] + if (auth_option) result.push(toUpper(auth_option.keyword), auth_option.auth_plugin, literalToSQL(auth_option.value)) + return result.filter(hasVal).join(' ') + }).join(', ') + const sql = [ + toUpper(type), + toUpper(keyword), + toUpper(ifNotExists), + userAuthOptions, + ] + if (defaultRole) sql.push(toUpper(defaultRole.keyword), defaultRole.value.map(grantUserOrRoleToSQL).join(', ')) + if (requireOption) sql.push(commonOptionConnector(requireOption.keyword, exprToSQL, requireOption.value)) + if (resourceOptions) sql.push(toUpper(resourceOptions.keyword), resourceOptions.value.map(resourceOption => exprToSQL(resourceOption)).join(' ')) + if (passwordOptions) passwordOptions.forEach(passwordOption => sql.push(commonOptionConnector(passwordOption.keyword, exprToSQL, passwordOption.value))) + sql.push(literalToSQL(lockOption), commentToSQL(comment), literalToSQL(attribute)) + return sql.filter(hasVal).join(' ') +} function createToSQL(stmt) { const { keyword } = stmt let sql = '' @@ -375,6 +401,9 @@ function createToSQL(stmt) { case 'type': sql = createTypeToSQL(stmt) break + case 'user': + sql = createUserToSQL(stmt) + break default: throw new Error(`unknown create resource ${keyword}`) } diff --git a/src/func.js b/src/func.js index 94618fc8..514d8f19 100644 --- a/src/func.js +++ b/src/func.js @@ -23,7 +23,7 @@ function arrayDimensionToSymbol(target) { } function castToSQL(expr) { - const { arrows = [], collate, target, expr: expression, keyword, symbol, as: alias, tail, properties = [] } = expr + const { arrows = [], collate, target, expr: expression, keyword, symbol, as: alias, properties = [] } = expr const { length, dataType, parentheses, quoted, scale, suffix: dataTypeSuffix } = target let str = '' if (length != null) str = scale ? `${length}, ${scale}` : length @@ -38,7 +38,6 @@ function castToSQL(expr) { symbolChar = ` ${symbol.toUpperCase()} ` } suffix += arrows.map((arrow, index) => commonOptionConnector(arrow, literalToSQL, properties[index])).join(' ') - if (tail) suffix += ` ${tail.operator} ${exprToSQL(tail.expr)}` if (alias) suffix += ` AS ${identifierToSql(alias)}` if (collate) suffix += ` ${commonTypeValue(collate).join(' ')}` const arrayDimension = arrayDimensionToSymbol(target) diff --git a/test/mysql-mariadb.spec.js b/test/mysql-mariadb.spec.js index 816d2b95..fd8070fc 100644 --- a/test/mysql-mariadb.spec.js +++ b/test/mysql-mariadb.spec.js @@ -900,6 +900,33 @@ describe('mysql', () => { 'SELECT COUNT((`A`.`col_1` = "03" AND `A`.`col_2` = "") OR NULL) FROM `sample_table` AS `A`' ] }, + { + title: 'create user', + sql: [ + "CREATE USER 'john'@'localhost' IDENTIFIED BY 'johnDoe1@'", + "CREATE USER 'john'@'localhost' IDENTIFIED BY 'johnDoe1@'" + ] + }, + { + title: 'cc', + sql: [ + "CREATE USER 'joe'@'10.0.0.1' DEFAULT ROLE administrator, developer;", + "CREATE USER 'joe'@'10.0.0.1' DEFAULT ROLE 'administrator', 'developer'" + ] + }, + { + title: 'create user with password option', + sql: [ + `CREATE USER 'jeffrey'@'localhost' + IDENTIFIED WITH caching_sha2_password BY 'new_password' + default role administrator, developer + require ssl and x509 + with max_queries_per_hour 100 + PASSWORD EXPIRE INTERVAL 180 DAY + FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2 account lock comment 'test comment' attribute '{"fname": "James", "lname": "Scott", "phone": "123-456-7890"}';`, + `CREATE USER 'jeffrey'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'new_password' DEFAULT ROLE 'administrator', 'developer' REQUIRE SSL AND X509 WITH MAX_QUERIES_PER_HOUR 100 PASSWORD EXPIRE INTERVAL 180 DAY FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 2 ACCOUNT LOCK COMMENT 'test comment' ATTRIBUTE '{"fname": "James", "lname": "Scott", "phone": "123-456-7890"}'` + ] + }, ] SQL_LIST.forEach(sqlInfo => { const { title, sql } = sqlInfo diff --git a/test/postgres.spec.js b/test/postgres.spec.js index 629c6ab4..65138219 100644 --- a/test/postgres.spec.js +++ b/test/postgres.spec.js @@ -1594,7 +1594,7 @@ describe('Postgres', () => { ] }, { - title: 'cast when expr is additive_ expr', + title: 'cast when expr is additive expr', sql: [ `SELECT CASE From 048c83ff87f5bb49febb5fc9943efdb144c81222 Mon Sep 17 00:00:00 2001 From: taozhi8833998 Date: Tue, 26 Dec 2023 09:34:38 +0800 Subject: [PATCH 2/2] refactor: format codes --- src/create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/create.js b/src/create.js index eee30d7e..d7e56943 100644 --- a/src/create.js +++ b/src/create.js @@ -358,7 +358,7 @@ function createUserToSQL(stmt) { userAuthOptions, ] if (defaultRole) sql.push(toUpper(defaultRole.keyword), defaultRole.value.map(grantUserOrRoleToSQL).join(', ')) - if (requireOption) sql.push(commonOptionConnector(requireOption.keyword, exprToSQL, requireOption.value)) + sql.push(commonOptionConnector(requireOption && requireOption.keyword, exprToSQL, requireOption && requireOption.value)) if (resourceOptions) sql.push(toUpper(resourceOptions.keyword), resourceOptions.value.map(resourceOption => exprToSQL(resourceOption)).join(' ')) if (passwordOptions) passwordOptions.forEach(passwordOption => sql.push(commonOptionConnector(passwordOption.keyword, exprToSQL, passwordOption.value))) sql.push(literalToSQL(lockOption), commentToSQL(comment), literalToSQL(attribute))