From: Hamatoma Date: Thu, 1 Jul 2021 20:22:28 +0000 (+0200) Subject: new: sql_storage X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=125fdb9ce63a4cd17c288bec78c3020a7a03b4eb;p=exhibition.git new: sql_storage --- diff --git a/rest_server/data/sql/users.sql.yaml b/rest_server/data/sql/users.sql.yaml index 12bda83..e87e0b8 100644 --- a/rest_server/data/sql/users.sql.yaml +++ b/rest_server/data/sql/users.sql.yaml @@ -3,22 +3,23 @@ module: Users list: type: list - params: [] + parameters: [] sql: select * from loginusers; byId: - type: query - params: [ ":id" ] - sql: "select * from loginusers where user_id=?;" + type: record + parameters: [ ":id" ] + sql: "select * from loginusers where user_id=:id;" update: type: update - params: [":name", ":displayname", ":email", ":changedby"] - sql: "UPDATE loginusers SET user_name=?, user_displayname=?, user_email=?, user_changed=NOW(), user_changedby=? - WHERE user_id=?;" + parameters: [":id", ":name", ":displayname", ":email", ":changedby"] + sql: "UPDATE loginusers SET + user_name=:name, user_displayname=:displayname, user_email=:email, user_changed=NOW(), user_changedby=:changedby + WHERE user_id=:id;" insert: type: insert - params: [":name", ":displayname", ":email", ":createdby"] + parameters: [":name", ":displayname", ":email", ":createdby"] sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby) - VALUES(?, ?, ?, NOW(), ?);" + VALUES(:name, :displayname, :email, NOW(), :createdby);" diff --git a/rest_server/lib/rest_server.dart b/rest_server/lib/rest_server.dart index 03f9b16..3a28566 100644 --- a/rest_server/lib/rest_server.dart +++ b/rest_server/lib/rest_server.dart @@ -8,6 +8,7 @@ import 'package:http/http.dart' as http; import 'package:crypto/crypto.dart'; import 'package:path/path.dart' as path; import 'package:args/args.dart'; +import 'package:rest_server/sql_storage.dart'; const forbidden = 'forbidden'; const wrongData = 'wrong data'; @@ -28,7 +29,7 @@ void runIsolate(SendPort isolateToMainStream) { if (data is String && data.startsWith('WorkerParameter')) { final params = WorkerParameters.fromString(data); final worker = - ServiceWorker(params.id, params.configuration, params.serviceName); + ServiceWorker(params.id, params.configuration, params.serviceName); if (params.id == 1) { worker.observe(); } else { @@ -51,22 +52,23 @@ class RestServer { var clientSessionTimeout = 15 * 60; String serviceName = 'exhibition'; RestServer( - int port, - this.logger, { - String address = 'localhost', - int sessionTimeout = 300, - int answerDumpLength = 80, - int dataDumpLength = 200, - String db = 'appexhibition', - String dbUser = 'exhibition', - String dbCode = 'TopSecret', - String dbHost = 'localhost', - int dbPort = 3306, - int watchDogPause = 60, - int traceDataLength = 80, - clientSessionTimeout = 15 * 60, - String dataDirectory = 'data', - }) { + int port, + this.logger, { + String address = 'localhost', + int sessionTimeout = 300, + int answerDumpLength = 80, + int dataDumpLength = 200, + String db = 'appexhibition', + String dbUser = 'exhibition', + String dbCode = 'TopSecret', + String dbHost = 'localhost', + int dbPort = 3306, + int watchDogPause = 60, + int traceDataLength = 80, + clientSessionTimeout = 15 * 60, + String dataDirectory = 'data', + String sqlDirectory = 'sql', + }) { if (unittestLogger != null) { logger = unittestLogger!; } @@ -76,6 +78,7 @@ class RestServer { 'watchDogPause': watchDogPause, 'address': address, 'dataDirectory': dataDirectory, + 'sqlDirectory': sqlDirectory, 'threads': Platform.numberOfProcessors, }, 'trace': { @@ -101,8 +104,8 @@ class RestServer { final logger2 = unittestLogger ?? MemoryLogger(); configuration = Configuration.fromFile(filename, logger2); final logFile = configuration.asString('logFile', - section: 'service', - defaultValue: '/var/log/local/$serviceName.log') ?? + section: 'service', + defaultValue: '/var/log/local/$serviceName.log') ?? ''; final level = configuration.asInt('logLevel', section: 'service') ?? LEVEL_SUMMERY; @@ -171,6 +174,7 @@ service: address: 0.0.0.0 port: 58021 dataDirectory: /var/cache/rest_server/data + sqlDirectory: /etc/rest_server/sql.d threads: 2 watchDogPause: 60 # logFile: /var/log/local/pollsam.log @@ -202,7 +206,7 @@ clientSessionTimeout: 900 } else { final appName = args.length >= 2 ? args[1] : 'pollsam'; var executable = - path.absolute(args.length > 1 ? args[0] : Platform.executable); + path.absolute(args.length > 1 ? args[0] : Platform.executable); if (!executable.startsWith(Platform.pathSeparator)) { executable = processSync.executeToString('which', [executable]).trim(); } @@ -252,7 +256,7 @@ class ServiceWorker { MySqlDb? db; String restVersion = ''; FileSync? _fileSync = FileSync(); - + SqlStorage sqlStorage = SqlStorage(globalLogger); ServiceWorker(this.threadId, this.configuration, this.serviceName) { final fnLog = '/var/log/local/$serviceName.$threadId.log'; logger = RestServer.unittestLogger ?? @@ -262,6 +266,7 @@ class ServiceWorker { db = MySqlDb.fromConfiguration(configuration, logger); clientSessionTimeout = configuration.asInt('clientSessionTimeout') ?? 30; _fileSync = FileSync(logger); + sqlStorage = SqlStorage(logger); } /// Checks whether a valid connection is available. If not a reconnection is @@ -360,7 +365,7 @@ WHERE /// Bearbeitet die Anforderung 'hives': /// Liefert Map mit Bienenstockinfo oder [forbidden], wenn Session-ID unbekannt. - Future queryData(Map parameters) async { + Future queryBySql(Map parameters) async { String rc; final sql = '''SELECT * FROM hives @@ -380,11 +385,11 @@ ORDER BY hive_name } else { final list = >[]; records.forEach((record) => list.add({ - 'hiveid': record['hive_id'], - 'name': record['hive_name'], - 'lat': record['hive_latitude'], - 'long': record['hive_longitude'] - })); + 'hiveid': record['hive_id'], + 'name': record['hive_name'], + 'lat': record['hive_latitude'], + 'long': record['hive_longitude'] + })); rc = convert.jsonEncode({'list': list}); } return rc; @@ -414,11 +419,11 @@ WHERE apiarist_token=?; /// Use only 31 bit (non negativ numbers on 32 bit clients): final sessionId = int.parse( - buildMd5Hash( - token + DateTime.now().microsecondsSinceEpoch.toString()) - .substring(0, 8), - radix: 16) & - 0x7fffffff; + buildMd5Hash( + token + DateTime.now().microsecondsSinceEpoch.toString()) + .substring(0, 8), + radix: 16) & + 0x7fffffff; final params2 = [sessionId, apiaristId]; final id = await db!.insertOne(sql, params: params2); if (id <= 0) { @@ -498,7 +503,8 @@ WHERE apiarist_registername=?; await checkConnection(); } try { - if (withSession && what != 'sessionid' && + if (withSession && + what != 'sessionid' && what != 'register' && what != 'watchdog' && !await checkSession(parameters)) { @@ -512,10 +518,10 @@ WHERE apiarist_registername=?; rc = await getSessionId(parameters); break; case 'store': - rc = await storeData(parameters); + rc = await storeBySql(parameters); break; case 'query': - rc = await queryData(parameters); + rc = await queryBySql(parameters); break; case 'register': rc = await getToken(parameters); @@ -556,6 +562,10 @@ WHERE apiarist_registername=?; /// @precondition: Must be called after the constructor! Future initAsync() async { await db!.connect(); + final directory = + configuration.asString('sqlDirectory', section: 'service') ?? + '/etc/rest_server/sql.d'; + sqlStorage.read(directory); } /// Connects to a listening address/port and waits for requests. @@ -596,7 +606,7 @@ WHERE apiarist_registername=?; Future observe() async { final duration = Duration( seconds: - configuration.asInt('watchDogPause', section: 'service') ?? 60); + configuration.asInt('watchDogPause', section: 'service') ?? 60); logger.log('watchdog pause: ${duration.inSeconds}', LEVEL_DETAIL); var counter = 0; while (true) { @@ -640,7 +650,7 @@ WHERE apiarist_registername=?; final port = configuration.asInt('port', section: 'service') ?? 58011; var rc = ''; final uri = - Uri(scheme: 'http', host: 'localhost', port: port, path: '/$what/1.0'); + Uri(scheme: 'http', host: 'localhost', port: port, path: '/$what/1.0'); http.Response response; // logger.log('request: POST $uri', LEVEL_LOOP); try { @@ -676,56 +686,44 @@ WHERE apiarist_registername=?; } } - /// Bearbeitet die Anforderung 'sample': Speichern einer Probe. - Future storeData(Map parameters) async { - String rc; - if (!testParam( - ['module', 'sql', 'sampleid', 'sessionid'], parameters, 'storeData')) { + /// Executes a SQL statement without returning a value, e.g. an update. + /// [parameters] is a map containing all named parameters of the SQL statement + /// with its values. + /// Returns 'OK' on success. + Future storeBySql(Map parameters) async { + String rc = 'OK'; + if (!testParam(['module', 'sql'], parameters, + 'storeBySql')) { rc = wrongParameters; } else { - - final sql = '''INSERT - INTO samples - (sample_code, sample_timestamp, sample_hiveid, sample_uuid, sample_connection, - sample_apiaristid, sample_raw, sample_encoding, created, createdby) - VALUES (?, ?, ?, ?, ?, - (SELECT MAX(connection_apiaristid) FROM connections WHERE connection_name=?), - ?, ?, NOW(), 'POLLSAM'); - '''; - final connection = parameters['sessionid']; - final params = [ - parameters['sampleid'], - parameters['time'], - parameters['hiveid'], - parameters['uuid'], - connection, - connection, - parameters.containsKey('raw') ? parameters['raw'] : null, - parameters.containsKey('encoding') ? parameters['encoding'] : null, - ]; - final id = await db!.insertOne(sql, params: params); - if (id <= 0) { - logger.error('insert into samples failed: ${parameters['uuid']}'); - } else { - logger.log('sample: $id', LEVEL_DETAIL); + final sqlStatement = sqlStorage.sqlStatement(parameters['module'], + parameters['sql']); + final positionalParameters = []; + final sql = sqlStatement.sqlStatement(parameters, positionalParameters); + var rc = 'OK'; + switch(sqlStatement.type){ + case SqlStatementType.execute: + case SqlStatementType.delete: + await db!.execute(sql, params: positionalParameters); + break; + case SqlStatementType.insert: + final id = await db!.insertOne(sql, params: positionalParameters); + if (id <= 0) { + logger.error('insert failed: ${parameters['module']}.${parameters['sql']}:' + '\nsql: $sql\nparams: ${positionalParameters.join('|')}'); + rc = 'ERROR'; + } else { + rc = 'id:$id'; + } + break; + case SqlStatementType.update: + final success = await db!.updateOne(sql, params: positionalParameters); + rc = success ? 'OK' : 'FAILED'; + break; + default: + logger.error('unexpected type ${sqlStatement.type} in storeBySql'); + rc = 'ERROR'; } - final hive = parameters['hiveid'].toString(); - final data = { - 'uuid': parameters['uuid'], - 'time': parameters['time'], - 'sampleid': parameters['sampleid'], - 'hiveid': hive, - }; - final content = convert.jsonEncode(data); - await storeFile( - configuration.asString( - 'dataDirectory', - section: 'service', - ) ?? - '', - parameters['uuid'] as String, - content); - rc = 'OK'; } return rc; } @@ -813,7 +811,7 @@ class WorkerParameters { factory WorkerParameters.fromString(String data) { final parts = data.split('\t'); const baseService = 2; - const baseTrace = baseService + 4; + const baseTrace = baseService + 5; const baseDb = baseTrace + 1; const baseRest = baseDb + 7; final configuration = BaseConfiguration({ @@ -821,7 +819,8 @@ class WorkerParameters { 'address': parts[baseService], 'port': parts[baseService + 1], 'watchDogPause': parts[baseService + 2], - 'dataDirectory': parts[baseService + 3] + 'dataDirectory': parts[baseService + 3], + 'sqlDirectory': parts[baseService + 4] }, 'trace': { 'answerLength': int.parse(parts[baseTrace]), @@ -854,6 +853,7 @@ class WorkerParameters { (configuration.asInt('port', section: section) ?? 58011).toString(), (configuration.asInt('watchDogPause', section: section) ?? 60).toString(), configuration.asString('dataDirectory', section: section) ?? 'data', + configuration.asString('sqlDirectory', section: section) ?? 'sql', configuration .asInt('answerLength', section: section2, defaultValue: 200) .toString(), @@ -876,113 +876,3 @@ class WorkerParameters { return rc; } } -enum SqlStatementType { - insert, list, query, update, delete -} -class SqlException extends FormatException{ - final String data; - SqlException(this.data); - @override - String toString() => 'SqlException: $data'; -} -class SqlStatement { - final String name; - final List parameters; - final String sql; - final SqlStatementType type; - final SqlModule parent; - SqlStatement(this.name, this.parameters, this.sql, this.type, this.parent); - /// Returns a SQL statement and a parameter list (positional parameters). - /// [map]: the current parameters as named parameters: : - /// [parameters]: OUT the positional parameters - String sqlStatement(Map map, List parameters){ - for (var parameter in parameters){ - if (! map.containsKey(parameter)) { - throw SqlException('${toString()}: missing parameter "$parameter"'); - } else { - parameters.add(map[parameter]); - } - } - return sql; - } - @override - String toString(){ - String rc = '${parent.name}.$name'; - return rc; - } -} -class SqlModule{ - final String name; - final Map sqlStatements = {}; - final SqlStorage parent; - SqlModule(this.name, this.parent); - /// Adds a statement to the map. - void add(SqlStatement statement) { - if (sqlStatements.containsKey(statement.name)){ - throw SqlException('module $name contains already a statement "${statement.name}"'); - } - sqlStatements[statement.name] = statement; - } -} -class SqlStorage{ - final BaseLogger logger; - Map modules = {}; - SqlStorage(this.logger); - void readModule(Map map, String filename){ - String moduleName = ''; - if (map.containsKey('module')){ - moduleName = map['module']; - } else { - logger.error('$filename: missing "module"'); - } - if (! modules.containsKey(moduleName)){ - modules[moduleName] = SqlModule(moduleName, this); - } - final module = modules[moduleName]; - for (var name in map.keys){ - switch(name){ - case 'module': - // already done. - break; - default: - final map2 = map[name]; - if (map2 is! Map){ - logger.error('$filename: "$name" is not a map'); - } else if (! map2.containsKey('type')){ - logger.error('$filename: "$name": missing type'); - } else if (! map2.containsKey('parameters')){ - logger.error('$filename: "$name": missing parameters'); - } else if (! map2.containsKey('sql')){ - logger.error('$filename: "$name": missing sql'); - } - final type = map2['type']; - final parameters = map2['parameters']; - final sql = map2['sql']; - if (type is! String){ - logger.error('$filename: "$name": type is not a string'); - } else if (parameters is! Iterable){ - logger.error('$filename: "$name": type is not an array'); - } else if (sql is! String){ - logger.error('$filename: "$name": type is not a string'); - } else { - final parameters2 = []; - int no = -1; - for (var item in parameters){ - no++; - if (item is! String){ - logger.error('$filename: "$name": parameter[$no] is not a string'); - } else { - - } - } - var type2 = stringToEnum(type, SqlStatementType.values); - if (type2 == null){ - logger.error('$filename: "$name": unknown type: $type. Using "query"'); - type2 = SqlStatementType.query; - } - modules[moduleName]!.add(SqlStatement(name, parameters2, sql, type2, module!)); - } - } - } - } -} diff --git a/rest_server/lib/sql_storage.dart b/rest_server/lib/sql_storage.dart new file mode 100644 index 0000000..8a9ab20 --- /dev/null +++ b/rest_server/lib/sql_storage.dart @@ -0,0 +1,187 @@ +import 'dart:io'; + +import 'package:dart_bones/dart_bones.dart'; +import 'package:path/path.dart'; +import 'package:yaml/yaml.dart'; + +class SqlException extends FormatException { + final String data; + SqlException(this.data); + @override + String toString() => 'SqlException: $data'; +} + +/// Stores all sql statements of a module. +class SqlModule { + final String name; + final Map sqlStatements = {}; + final SqlStorage sqlStorage; + SqlModule(this.name, this.sqlStorage); + + /// Adds a statement to the map. + void add(SqlStatement statement) { + if (sqlStatements.containsKey(statement.name)) { + throw SqlException( + 'module $name contains already a statement "${statement.name}"'); + } + sqlStatements[statement.name] = statement; + } + + /// Returns the [SqlStatement] from the map given by [name]. + SqlStatement sqlByName(String name) { + if (!sqlStatements.containsKey(name)) { + throw SqlException('missing statement "$name" in module "${this.name}"'); + } + final rc = sqlStatements[name]; + return rc!; + } +} + +/// Stores a SQL statement and things that allow named parameters to be used, +/// although only positional parameters can be processed (restriction of the +/// mysql package). +/// Note: Named parameters: :id or :date +/// Positional parameters: ? (question mark only) +class SqlStatement { + final String name; + final List parameters; + + /// The SQL statement with named parameters. + final String sql; + + /// The Sql statement with positional parameters. + String sqlPrepared = ''; + + /// The named parameters in the order of [sql] (multiple occurrences are possible). + final List orderOfParameters = []; + final SqlStatementType type; + final SqlModule sqlModule; + SqlStatement( + this.name, this.parameters, this.sql, this.type, this.sqlModule) { + var parameters2 = parameters.toList(); + parameters2.sort(); + // revers sorting: if a member is a prefix of another member then it is + // positioned behind: example [':id1', ':id' ] + // The longer member is found first. + parameters2 = parameters2.reversed.toList(); + RegExp regExp = RegExp('(' + parameters2.join('|') + r')\b'); + for (var match in regExp.allMatches(sql)) { + orderOfParameters.add(match.group(0)!); + } + sqlPrepared = sql.replaceAll(regExp, '?'); + } + + /// Returns a SQL statement and a parameter list (positional parameters). + /// [map]: the current parameters as named parameters: : + /// [parameters]: OUT the positional parameters + String sqlStatement(Map map, List parameters) { + for (var parameter in orderOfParameters) { + if (!map.containsKey(parameter)) { + throw SqlException('${toString()}: missing parameter "$parameter"'); + } else { + parameters.add(map[parameter]); + } + } + return sqlPrepared; + } + + @override + String toString() { + String rc = '${sqlModule.name}.$name'; + return rc; + } +} + +enum SqlStatementType { delete, execute, insert, list, record, update } + +/// Stores all sql modules. +class SqlStorage { + final BaseLogger logger; + Map modules = {}; + SqlStorage(this.logger); + + /// Reads multiple module data from a directory. + /// Only files ending with '.yaml' and containing '.sql.' are respected. + void read(String path) { + for (var entry in Directory(path).listSync()) { + final full = entry.path; + final node = basename(full); + if (node.endsWith('.yaml') && node.contains('.sql.') && entry is File) { + final contents = entry.readAsStringSync(); + final map = loadYaml(contents); + readModule(map, node); + } + } + } + + void readModule(Map map, String filename) { + String moduleName = ''; + if (map.containsKey('module')) { + moduleName = map['module']; + } else { + logger.error('$filename: missing "module"'); + } + if (!modules.containsKey(moduleName)) { + modules[moduleName] = SqlModule(moduleName, this); + } + final module = modules[moduleName]; + for (var name in map.keys) { + switch (name) { + case 'module': + // already done. + break; + default: + final map2 = map[name]; + if (map2 is! Map) { + logger.error('$filename: "$name" is not a map'); + } else if (!map2.containsKey('type')) { + logger.error('$filename: "$name": missing type'); + } else if (!map2.containsKey('parameters')) { + logger.error('$filename: "$name": missing parameters'); + } else if (!map2.containsKey('sql')) { + logger.error('$filename: "$name": missing sql'); + } else { + final type = map2['type']; + final parameters = map2['parameters']; + final sql = map2['sql']; + if (type is! String) { + logger.error('$filename: "$name": type is not a string'); + } else if (parameters is! Iterable) { + logger.error('$filename: "$name": type is not an array'); + } else if (sql is! String) { + logger.error('$filename: "$name": type is not a string'); + } else { + final parameters2 = []; + int no = -1; + for (var item in parameters) { + no++; + if (item is! String) { + logger.error( + '$filename: "$name": parameter[$no] is not a string'); + } else { + parameters2.add(item); + } + } + var type2 = stringToEnum(type, SqlStatementType.values); + if (type2 == null) { + logger.error( + '$filename: "$name": unknown type: $type. Using "query"'); + type2 = SqlStatementType.record; + } + modules[moduleName]! + .add(SqlStatement(name, parameters2, sql, type2, module!)); + } + } + } + } + } + + /// Returns the [SqlStatement] from the map given by [name]. + SqlStatement sqlStatement(String module, String name) { + if (!modules.containsKey(module)) { + throw SqlException('unknown module: "$module"'); + } + final rc = modules[module]!.sqlByName(name); + return rc; + } +} diff --git a/rest_server/pubspec.yaml b/rest_server/pubspec.yaml index 328aa2a..c7c4f54 100644 --- a/rest_server/pubspec.yaml +++ b/rest_server/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: http: ^0.13.0 args: ^2.1.0 path: ^1.8.0 + yaml: ^3.1.0 dart_bones: ^1.1.1 dev_dependencies: diff --git a/rest_server/test/sql_storage_test.dart b/rest_server/test/sql_storage_test.dart new file mode 100644 index 0000000..d47f6c3 --- /dev/null +++ b/rest_server/test/sql_storage_test.dart @@ -0,0 +1,105 @@ +import 'package:test/test.dart'; +import 'package:rest_server/sql_storage.dart'; +import 'package:dart_bones/dart_bones.dart'; + +void main() { + final logger = MemoryLogger(LEVEL_DETAIL); + + group('SqlStorage', () { + test('read()', () { + final sqlStorage = SqlStorage(logger); + //print('current directory: ${Directory.current.path}'); + sqlStorage.read('data/sql'); + expect(sqlStorage.sqlStatement('Users', 'list'), isNotNull); + expect(sqlStorage.sqlStatement('Users', 'byId'), isNotNull); + SqlStatement update; + expect(update = sqlStorage.sqlStatement('Users', 'update'), isNotNull); + expect(update.orderOfParameters, + ':name,:displayname,:email,:changedby,:id'.split(',')); + expect( + update.sqlPrepared, + 'UPDATE loginusers SET user_name=?, user_displayname=?, ' + 'user_email=?, user_changed=NOW(), user_changedby=? WHERE user_id=?;'); + expect(update.type, SqlStatementType.update); + expect(sqlStorage.sqlStatement('Users', 'insert'), isNotNull); + }); + }); + group('SqlModule', () { + final sqlStorage = SqlStorage(logger); + test('basic', () { + final sqlModule = SqlModule('users', sqlStorage); + sqlModule.add(SqlStatement( + 'record', + [':id'], + 'select * from users where id=:id', + SqlStatementType.record, + sqlModule)); + sqlModule.add(SqlStatement( + 'insert', + [':name', ':role'], + 'insert into users (name, role) values (:name, :role);', + SqlStatementType.insert, + sqlModule)); + SqlStatement statement; + expect(statement = sqlModule.sqlByName('record'), isNotNull); + expect(statement.name, 'record'); + expect(statement.sqlPrepared, 'select * from users where id=?'); + expect(statement = sqlModule.sqlByName('insert'), isNotNull); + expect(statement.name, 'insert'); + expect(statement.sqlPrepared, + 'insert into users (name, role) values (?, ?);'); + expect(statement.orderOfParameters, [':name', ':role']); + }); + }); + + group('SqlStatement', () { + final sqlStorage = SqlStorage(logger); + final sqlModule = SqlModule('standard', sqlStorage); + test('one parameter', () { + final sqlStatement = SqlStatement('list', [':id'], + 'select * from x where id=:id;', SqlStatementType.record, sqlModule); + expect(sqlStatement.parameters, [':id']); + expect(sqlStatement.orderOfParameters, [':id']); + expect(sqlStatement.sqlPrepared, 'select * from x where id=?;'); + expect(sqlStatement.type, SqlStatementType.record); + final parameters = []; + final map = {':id': '223'}; + expect(sqlStatement.sqlStatement(map, parameters), + 'select * from x where id=?;'); + expect(parameters, ['223']); + }); + test('many parameters multiple occurrences', () { + final sql = '''select +from users uu + left join role rr ON rr.id=uu.role +where + u.name like :name AND uu.created >= :from + AND rr.name like :nameRole AND rr.created >= :from +; +'''; + final expectedOrder = ':name,:from,:nameRole,:from'.split(','); + final sqlExpected = '''select +from users uu + left join role rr ON rr.id=uu.role +where + u.name like ? AND uu.created >= ? + AND rr.name like ? AND rr.created >= ? +; +'''; + final sqlStatement = SqlStatement('list', [':name', ':nameRole', ':from'], + sql, SqlStatementType.list, sqlModule); + expect(sqlStatement.parameters, [':name', ':nameRole', ':from']); + expect(sqlStatement.sqlPrepared, sqlExpected); + expect(sqlStatement.orderOfParameters, expectedOrder); + expect(sqlStatement.type, SqlStatementType.list); + final parameters = []; + final map = { + ':name': 'a%', + ':nameRole': 'b%', + ':from': '2021-06-01' + }; + expect(sqlStatement.sqlStatement(map, parameters), sqlExpected); + expect(parameters, ['a%', '2021-06-01', 'b%', '2021-06-01']); + }); + }); +}