SqlCommand.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. //
  2. // System.Data.SqlClient.SqlCommand.cs
  3. //
  4. // Author:
  5. // Rodrigo Moya ([email protected])
  6. // Daniel Morgan ([email protected])
  7. // Tim Coleman ([email protected])
  8. //
  9. // (C) Ximian, Inc 2002 http://www.ximian.com/
  10. // (C) Daniel Morgan, 2002
  11. // (C) Copyright 2002 Tim Coleman
  12. //
  13. // Credits:
  14. // SQL and concepts were used from libgda 0.8.190 (GNOME Data Access)
  15. // http://www.gnome-db.org/
  16. // with permission from the authors of the
  17. // PostgreSQL provider in libgda:
  18. // Michael Lausch <[email protected]>
  19. // Rodrigo Moya <[email protected]>
  20. // Vivien Malerba <[email protected]>
  21. // Gonzalo Paniagua Javier <[email protected]>
  22. //
  23. // use #define DEBUG_SqlCommand if you want to spew debug messages
  24. // #define DEBUG_SqlCommand
  25. using System;
  26. using System.Collections;
  27. using System.ComponentModel;
  28. using System.Data;
  29. using System.Data.Common;
  30. using System.Runtime.InteropServices;
  31. using System.Text;
  32. using System.Xml;
  33. namespace System.Data.SqlClient {
  34. /// <summary>
  35. /// Represents a SQL statement that is executed
  36. /// while connected to a SQL database.
  37. /// </summary>
  38. // public sealed class SqlCommand : Component, IDbCommand, ICloneable
  39. public sealed class SqlCommand : IDbCommand {
  40. #region Fields
  41. private string sql = "";
  42. private int timeout = 30;
  43. // default is 30 seconds
  44. // for command execution
  45. private SqlConnection conn = null;
  46. private SqlTransaction trans = null;
  47. private CommandType cmdType = CommandType.Text;
  48. private bool designTime = false;
  49. private SqlParameterCollection parmCollection = new
  50. SqlParameterCollection();
  51. // SqlDataReader state data for ExecuteReader()
  52. private SqlDataReader dataReader = null;
  53. private string[] queries = null;
  54. private int currentQuery;
  55. private CommandBehavior cmdBehavior = CommandBehavior.Default;
  56. private ParmUtil parmUtil = null;
  57. #endregion // Fields
  58. #region Constructors
  59. public SqlCommand() {
  60. sql = "";
  61. }
  62. public SqlCommand (string cmdText) {
  63. sql = cmdText;
  64. }
  65. public SqlCommand (string cmdText, SqlConnection connection) {
  66. sql = cmdText;
  67. conn = connection;
  68. }
  69. public SqlCommand (string cmdText, SqlConnection connection,
  70. SqlTransaction transaction) {
  71. sql = cmdText;
  72. conn = connection;
  73. trans = transaction;
  74. }
  75. #endregion // Constructors
  76. #region Methods
  77. [MonoTODO]
  78. public void Cancel () {
  79. // FIXME: use non-blocking Exec for this
  80. throw new NotImplementedException ();
  81. }
  82. // FIXME: is this the correct way to return a stronger type?
  83. [MonoTODO]
  84. IDbDataParameter IDbCommand.CreateParameter () {
  85. return CreateParameter ();
  86. }
  87. [MonoTODO]
  88. public SqlParameter CreateParameter () {
  89. return new SqlParameter ();
  90. }
  91. public int ExecuteNonQuery () {
  92. IntPtr pgResult; // PGresult
  93. int rowsAffected = -1;
  94. ExecStatusType execStatus;
  95. String rowsAffectedString;
  96. string query;
  97. if(conn.State != ConnectionState.Open)
  98. throw new InvalidOperationException(
  99. "ConnnectionState is not Open");
  100. query = TweakQuery(sql, cmdType);
  101. // FIXME: PQexec blocks
  102. // while PQsendQuery is non-blocking
  103. // which is better to use?
  104. // int PQsendQuery(PGconn *conn,
  105. // const char *query);
  106. // execute SQL command
  107. // uses internal property to get the PGConn IntPtr
  108. pgResult = PostgresLibrary.
  109. PQexec (conn.PostgresConnection, query);
  110. execStatus = PostgresLibrary.
  111. PQresultStatus (pgResult);
  112. if(execStatus == ExecStatusType.PGRES_COMMAND_OK) {
  113. rowsAffectedString = PostgresLibrary.
  114. PQcmdTuples (pgResult);
  115. if(rowsAffectedString != null)
  116. if(rowsAffectedString.Equals("") == false)
  117. rowsAffected = int.Parse(rowsAffectedString);
  118. PostgresLibrary.PQclear (pgResult);
  119. pgResult = IntPtr.Zero;
  120. }
  121. else {
  122. String errorMessage;
  123. errorMessage = PostgresLibrary.
  124. PQresStatus(execStatus);
  125. errorMessage += " " + PostgresLibrary.
  126. PQresultErrorMessage(pgResult);
  127. PostgresLibrary.PQclear (pgResult);
  128. pgResult = IntPtr.Zero;
  129. throw new SqlException(0, 0,
  130. errorMessage, 0, "",
  131. conn.DataSource, "SqlCommand", 0);
  132. }
  133. return rowsAffected;
  134. }
  135. [MonoTODO]
  136. IDataReader IDbCommand.ExecuteReader () {
  137. return ExecuteReader ();
  138. }
  139. [MonoTODO]
  140. public SqlDataReader ExecuteReader () {
  141. return ExecuteReader(CommandBehavior.Default);
  142. }
  143. [MonoTODO]
  144. IDataReader IDbCommand.ExecuteReader (
  145. CommandBehavior behavior) {
  146. return ExecuteReader (behavior);
  147. }
  148. [MonoTODO]
  149. public SqlDataReader ExecuteReader (CommandBehavior behavior)
  150. {
  151. if(conn.State != ConnectionState.Open)
  152. throw new InvalidOperationException(
  153. "ConnectionState is not Open");
  154. cmdBehavior = behavior;
  155. queries = null;
  156. currentQuery = -1;
  157. dataReader = new SqlDataReader(this);
  158. if((behavior & CommandBehavior.SingleResult) == CommandBehavior.SingleResult) {
  159. queries = new String[1];
  160. queries[0] = sql;
  161. }
  162. else {
  163. queries = sql.Split(new Char[] {';'});
  164. }
  165. dataReader.NextResult();
  166. return dataReader;
  167. }
  168. internal SqlResult NextResult()
  169. {
  170. SqlResult res = new SqlResult();
  171. res.Connection = this.Connection;
  172. string statement;
  173. currentQuery++;
  174. if(currentQuery < queries.Length && queries[currentQuery].Equals("") == false) {
  175. statement = TweakQuery(queries[currentQuery], cmdType);
  176. ExecuteQuery(statement, res);
  177. res.ResultReturned = true;
  178. }
  179. else {
  180. res.ResultReturned = false;
  181. }
  182. return res;
  183. }
  184. private string TweakQuery(string query, CommandType commandType) {
  185. string statement = "";
  186. StringBuilder td;
  187. #if DEBUG_SqlCommand
  188. Console.WriteLine("---------[][] TweakQuery() [][]--------");
  189. Console.WriteLine("CommandType: " + commandType + " CommandBehavior: " + cmdBehavior);
  190. Console.WriteLine("SQL before command type: " + query);
  191. #endif
  192. // finish building SQL based on CommandType
  193. switch(commandType) {
  194. case CommandType.Text:
  195. statement = query;
  196. break;
  197. case CommandType.StoredProcedure:
  198. statement =
  199. "SELECT " + query + "()";
  200. break;
  201. case CommandType.TableDirect:
  202. // NOTE: this is for the PostgreSQL provider
  203. // and for OleDb, according to the docs,
  204. // an exception is thrown if you try to use
  205. // this with SqlCommand
  206. string[] directTables = query.Split(
  207. new Char[] {','});
  208. td = new StringBuilder("SELECT * FROM ");
  209. for(int tab = 0; tab < directTables.Length; tab++) {
  210. if(tab > 0)
  211. td.Append(',');
  212. td.Append(directTables[tab]);
  213. // FIXME: if multipe tables, how do we
  214. // join? based on Primary/Foreign Keys?
  215. // Otherwise, a Cartesian Product happens
  216. }
  217. statement = td.ToString();
  218. break;
  219. default:
  220. // FIXME: throw an exception?
  221. statement = query;
  222. break;
  223. }
  224. #if DEBUG_SqlCommand
  225. Console.WriteLine("SQL after command type: " + statement);
  226. #endif
  227. // TODO: this parameters utility
  228. // currently only support input variables
  229. // need todo output, input/output, and return.
  230. #if DEBUG_SqlCommand
  231. Console.WriteLine("using ParmUtil in TweakQuery()...");
  232. #endif
  233. parmUtil = new ParmUtil(statement, parmCollection);
  234. #if DEBUG_SqlCommand
  235. Console.WriteLine("ReplaceWithParms...");
  236. #endif
  237. statement = parmUtil.ReplaceWithParms();
  238. #if DEBUG_SqlCommand
  239. Console.WriteLine("SQL after ParmUtil: " + statement);
  240. #endif
  241. return statement;
  242. }
  243. private void ExecuteQuery (string query, SqlResult res)
  244. {
  245. IntPtr pgResult;
  246. ExecStatusType execStatus;
  247. if(conn.State != ConnectionState.Open)
  248. throw new InvalidOperationException(
  249. "ConnectionState is not Open");
  250. // FIXME: PQexec blocks
  251. // while PQsendQuery is non-blocking
  252. // which is better to use?
  253. // int PQsendQuery(PGconn *conn,
  254. // const char *query);
  255. // execute SQL command
  256. // uses internal property to get the PGConn IntPtr
  257. pgResult = PostgresLibrary.
  258. PQexec (conn.PostgresConnection, query);
  259. execStatus = PostgresLibrary.
  260. PQresultStatus (pgResult);
  261. if(execStatus == ExecStatusType.PGRES_TUPLES_OK) {
  262. res.BuildTableSchema(pgResult);
  263. }
  264. else {
  265. String errorMessage;
  266. errorMessage = PostgresLibrary.
  267. PQresStatus(execStatus);
  268. errorMessage += " " + PostgresLibrary.
  269. PQresultErrorMessage(pgResult);
  270. PostgresLibrary.PQclear (pgResult);
  271. pgResult = IntPtr.Zero;
  272. throw new SqlException(0, 0,
  273. errorMessage, 0, "",
  274. conn.DataSource, "SqlCommand", 0);
  275. }
  276. }
  277. // since SqlCommand has resources so SqlDataReader
  278. // can do Read() and NextResult(), need to free
  279. // those resources. Also, need to allow this SqlCommand
  280. // and this SqlConnection to do things again.
  281. internal void CloseReader() {
  282. conn.OpenReader = false;
  283. dataReader = null;
  284. queries = null;
  285. }
  286. /// <summary>
  287. /// ExecuteScalar is used to retrieve one object
  288. /// from one result set
  289. /// that has one row and one column.
  290. /// It is lightweight compared to ExecuteReader.
  291. /// </summary>
  292. [MonoTODO]
  293. public object ExecuteScalar () {
  294. IntPtr pgResult; // PGresult
  295. ExecStatusType execStatus;
  296. object obj = null; // return
  297. int nRow = 0; // first row
  298. int nCol = 0; // first column
  299. String value;
  300. int nRows;
  301. int nFields;
  302. string query;
  303. if(conn.State != ConnectionState.Open)
  304. throw new InvalidOperationException(
  305. "ConnnectionState is not Open");
  306. query = TweakQuery(sql, cmdType);
  307. // FIXME: PQexec blocks
  308. // while PQsendQuery is non-blocking
  309. // which is better to use?
  310. // int PQsendQuery(PGconn *conn,
  311. // const char *query);
  312. // execute SQL command
  313. // uses internal property to get the PGConn IntPtr
  314. pgResult = PostgresLibrary.
  315. PQexec (conn.PostgresConnection, query);
  316. execStatus = PostgresLibrary.
  317. PQresultStatus (pgResult);
  318. if(execStatus == ExecStatusType.PGRES_TUPLES_OK) {
  319. nRows = PostgresLibrary.
  320. PQntuples(pgResult);
  321. nFields = PostgresLibrary.
  322. PQnfields(pgResult);
  323. if(nRows > 0 && nFields > 0) {
  324. // get column name
  325. //String fieldName;
  326. //fieldName = PostgresLibrary.
  327. // PQfname(pgResult, nCol);
  328. int oid;
  329. string sType;
  330. DbType dbType;
  331. // get PostgreSQL data type (OID)
  332. oid = PostgresLibrary.
  333. PQftype(pgResult, nCol);
  334. sType = PostgresHelper.
  335. OidToTypname (oid, conn.Types);
  336. dbType = PostgresHelper.
  337. TypnameToSqlDbType(sType);
  338. int definedSize;
  339. // get defined size of column
  340. definedSize = PostgresLibrary.
  341. PQfsize(pgResult, nCol);
  342. // get data value
  343. value = PostgresLibrary.
  344. PQgetvalue(
  345. pgResult,
  346. nRow, nCol);
  347. int columnIsNull;
  348. // is column NULL?
  349. columnIsNull = PostgresLibrary.
  350. PQgetisnull(pgResult,
  351. nRow, nCol);
  352. int actualLength;
  353. // get Actual Length
  354. actualLength = PostgresLibrary.
  355. PQgetlength(pgResult,
  356. nRow, nCol);
  357. obj = PostgresHelper.
  358. ConvertDbTypeToSystem (
  359. dbType,
  360. value);
  361. }
  362. // close result set
  363. PostgresLibrary.PQclear (pgResult);
  364. pgResult = IntPtr.Zero;
  365. }
  366. else {
  367. String errorMessage;
  368. errorMessage = PostgresLibrary.
  369. PQresStatus(execStatus);
  370. errorMessage += " " + PostgresLibrary.
  371. PQresultErrorMessage(pgResult);
  372. PostgresLibrary.PQclear (pgResult);
  373. pgResult = IntPtr.Zero;
  374. throw new SqlException(0, 0,
  375. errorMessage, 0, "",
  376. conn.DataSource, "SqlCommand", 0);
  377. }
  378. return obj;
  379. }
  380. [MonoTODO]
  381. public XmlReader ExecuteXmlReader () {
  382. throw new NotImplementedException ();
  383. }
  384. [MonoTODO]
  385. public void Prepare () {
  386. // FIXME: parameters have to be implemented for this
  387. throw new NotImplementedException ();
  388. }
  389. [MonoTODO]
  390. public SqlCommand Clone () {
  391. throw new NotImplementedException ();
  392. }
  393. #endregion // Methods
  394. #region Properties
  395. public string CommandText {
  396. get {
  397. return sql;
  398. }
  399. set {
  400. sql = value;
  401. }
  402. }
  403. public int CommandTimeout {
  404. get {
  405. return timeout;
  406. }
  407. set {
  408. // FIXME: if value < 0, throw
  409. // ArgumentException
  410. // if (value < 0)
  411. // throw ArgumentException;
  412. timeout = value;
  413. }
  414. }
  415. public CommandType CommandType {
  416. get {
  417. return cmdType;
  418. }
  419. set {
  420. cmdType = value;
  421. }
  422. }
  423. // FIXME: for property Connection, is this the correct
  424. // way to handle a return of a stronger type?
  425. IDbConnection IDbCommand.Connection {
  426. get {
  427. return Connection;
  428. }
  429. set {
  430. // FIXME: throw an InvalidOperationException
  431. // if the change was during a
  432. // transaction in progress
  433. // csc
  434. Connection = (SqlConnection) value;
  435. // mcs
  436. // Connection = value;
  437. // FIXME: set Transaction property to null
  438. }
  439. }
  440. public SqlConnection Connection {
  441. get {
  442. // conn defaults to null
  443. return conn;
  444. }
  445. set {
  446. // FIXME: throw an InvalidOperationException
  447. // if the change was during
  448. // a transaction in progress
  449. conn = value;
  450. // FIXME: set Transaction property to null
  451. }
  452. }
  453. public bool DesignTimeVisible {
  454. get {
  455. return designTime;
  456. }
  457. set{
  458. designTime = value;
  459. }
  460. }
  461. // FIXME; for property Parameters, is this the correct
  462. // way to handle a stronger return type?
  463. IDataParameterCollection IDbCommand.Parameters {
  464. get {
  465. return Parameters;
  466. }
  467. }
  468. public SqlParameterCollection Parameters {
  469. get {
  470. return parmCollection;
  471. }
  472. }
  473. // FIXME: for property Transaction, is this the correct
  474. // way to handle a return of a stronger type?
  475. IDbTransaction IDbCommand.Transaction {
  476. get {
  477. return Transaction;
  478. }
  479. set {
  480. // FIXME: error handling - do not allow
  481. // setting of transaction if transaction
  482. // has already begun
  483. // csc
  484. Transaction = (SqlTransaction) value;
  485. // mcs
  486. // Transaction = value;
  487. }
  488. }
  489. public SqlTransaction Transaction {
  490. get {
  491. return trans;
  492. }
  493. set {
  494. // FIXME: error handling
  495. trans = value;
  496. }
  497. }
  498. [MonoTODO]
  499. public UpdateRowSource UpdatedRowSource {
  500. // FIXME: do this once DbDataAdaptor
  501. // and DataRow are done
  502. get {
  503. throw new NotImplementedException ();
  504. }
  505. set {
  506. throw new NotImplementedException ();
  507. }
  508. }
  509. #endregion // Properties
  510. #region Inner Classes
  511. #endregion // Inner Classes
  512. #region Destructors
  513. [MonoTODO]
  514. public void Dispose() {
  515. // FIXME: need proper way to release resources
  516. // Dispose(true);
  517. }
  518. [MonoTODO]
  519. ~SqlCommand() {
  520. // FIXME: need proper way to release resources
  521. // Dispose(false);
  522. }
  523. #endregion //Destructors
  524. }
  525. // SqlResult is used for passing Result Set data
  526. // from SqlCommand to SqlDataReader
  527. internal class SqlResult {
  528. private DataTable dataTableSchema; // only will contain the schema
  529. private IntPtr pg_result; // native PostgreSQL PGresult
  530. private int rowCount;
  531. private int fieldCount;
  532. private string[] pgtypes; // PostgreSQL types (typname)
  533. private bool resultReturned = false;
  534. private SqlConnection con;
  535. internal SqlConnection Connection {
  536. set {
  537. con = value;
  538. }
  539. }
  540. internal bool ResultReturned {
  541. get {
  542. return resultReturned;
  543. }
  544. set {
  545. resultReturned = value;
  546. }
  547. }
  548. internal DataTable Table {
  549. get {
  550. return dataTableSchema;
  551. }
  552. }
  553. internal IntPtr PgResult {
  554. get {
  555. return pg_result;
  556. }
  557. }
  558. internal int RowCount {
  559. get {
  560. return rowCount;
  561. }
  562. }
  563. internal int FieldCount {
  564. get {
  565. return fieldCount;
  566. }
  567. }
  568. internal string[] PgTypes {
  569. get {
  570. return pgtypes;
  571. }
  572. }
  573. internal void BuildTableSchema (IntPtr pgResult)
  574. {
  575. dataTableSchema = new DataTable ();
  576. dataTableSchema.Columns.Add ("ColumnName", typeof (string));
  577. dataTableSchema.Columns.Add ("ColumnOrdinal", typeof (int));
  578. dataTableSchema.Columns.Add ("ColumnSize", typeof (int));
  579. dataTableSchema.Columns.Add ("NumericPrecision", typeof (int));
  580. dataTableSchema.Columns.Add ("NumericScale", typeof (int));
  581. dataTableSchema.Columns.Add ("IsUnique", typeof (bool));
  582. dataTableSchema.Columns.Add ("IsKey", typeof (bool));
  583. dataTableSchema.Columns.Add ("BaseCatalogName", typeof (string));
  584. dataTableSchema.Columns.Add ("BaseColumnName", typeof (string));
  585. dataTableSchema.Columns.Add ("BaseSchemaName", typeof (string));
  586. dataTableSchema.Columns.Add ("BaseTableName", typeof (string));
  587. dataTableSchema.Columns.Add ("DataType", typeof (System.Type));
  588. dataTableSchema.Columns.Add ("AllowDBNull", typeof (bool));
  589. dataTableSchema.Columns.Add ("ProviderType", typeof (string));
  590. dataTableSchema.Columns.Add ("IsAliased", typeof (bool));
  591. dataTableSchema.Columns.Add ("IsExpression", typeof (bool));
  592. dataTableSchema.Columns.Add ("IsIdentity", typeof (bool));
  593. dataTableSchema.Columns.Add ("IsAutoIncrement", typeof (bool));
  594. dataTableSchema.Columns.Add ("IsRowVersion", typeof (bool));
  595. dataTableSchema.Columns.Add ("IsHidden", typeof (bool));
  596. dataTableSchema.Columns.Add ("IsLong", typeof (bool));
  597. dataTableSchema.Columns.Add ("IsReadOnly", typeof (bool));
  598. int fieldCount = PostgresLibrary.PQnfields (pgResult);
  599. DataRow schemaRow;
  600. int oid;
  601. string pgType;
  602. DbType dbType;
  603. for (int i = 0; i < fieldCount; i += 1 )
  604. {
  605. oid = PostgresLibrary.PQftype (pgResult, i);
  606. schemaRow = dataTableSchema.NewRow ();
  607. schemaRow["ColumnName"] = PostgresLibrary.PQfname (pgResult, i);
  608. schemaRow["ColumnOrdinal"] = i+1;
  609. schemaRow["ColumnSize"] = PostgresLibrary.PQfsize (pgResult, i);
  610. schemaRow["NumericPrecision"] = 0; // ? tim
  611. schemaRow["NumericScale"] = 0; // ? tim
  612. schemaRow["IsUnique"] = false; // ? tim
  613. schemaRow["IsKey"] = false; // ? tim
  614. schemaRow["BaseCatalogName"] = ""; // ? tim
  615. schemaRow["BaseSchemaName"] = ""; // ? tim
  616. schemaRow["BaseTableName"] = ""; // ? tim
  617. pgType = PostgresHelper.OidToTypname (oid, con.Types);
  618. dbType = PostgresHelper.TypnameToSqlDbType (pgType);
  619. //schemaRow["DataType"] = PostgresHelper.DbTypeToSystemType (dbType); ??? this gives a bad cast.
  620. schemaRow["AllowDBNull"] = false; // ? tim
  621. schemaRow["ProviderType"] = ""; // ? tim
  622. schemaRow["IsAliased"] = false; // ? tim
  623. schemaRow["IsExpression"] = false; // ? tim
  624. schemaRow["IsIdentity"] = false; // ? tim
  625. schemaRow["IsAutoIncrement"] = false; // ? tim
  626. schemaRow["IsRowVersion"] = false; // ? tim
  627. schemaRow["IsHidden"] = false; // ? tim
  628. schemaRow["IsLong"] = false; // ? tim
  629. schemaRow["IsReadOnly"] = false; // ? tim
  630. dataTableSchema.Rows.Add (schemaRow);
  631. }
  632. }
  633. }
  634. }