1use std::collections::VecDeque;
4use std::io::Cursor;
5use std::sync::Arc;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use arrow::ipc::reader::StreamReader;
9use arrow::record_batch::RecordBatch;
10use mongreldb_protocol::native;
11use mongreldb_protocol::native_transport::{NativeRpcClientConfig, NativeRpcClientPool};
12use mongreldb_protocol::{NATIVE_API_MAJOR, NATIVE_API_MINOR};
13use mongreldb_query::QueryId;
14use prost::Message;
15
16use crate::{ClientError, ClientResult, SecretString};
17
18pub use mongreldb_protocol::native_transport::NativeRpcClientConfig as Config;
19
20#[derive(Clone)]
21pub struct NativeClient {
22 pool: Arc<NativeRpcClientPool>,
23 database_id: [u8; 16],
24 request_timeout: Duration,
25 max_retries: usize,
26}
27
28impl NativeClient {
29 pub async fn connect(
30 config: NativeRpcClientConfig,
31 connections: usize,
32 database_id: [u8; 16],
33 ) -> ClientResult<Self> {
34 let request_timeout = config.request_timeout;
35 let pool = NativeRpcClientPool::connect(config, connections)
36 .await
37 .map_err(|error| ClientError::Transport(error.to_string()))?;
38 Ok(Self {
39 pool,
40 database_id,
41 request_timeout,
42 max_retries: 2,
43 })
44 }
45
46 pub fn with_max_retries(mut self, max_retries: usize) -> Self {
47 self.max_retries = max_retries;
48 self
49 }
50
51 pub async fn authenticate_password(
52 &self,
53 username: impl Into<String>,
54 password: &SecretString,
55 ) -> ClientResult<NativeSession> {
56 let username = username.into();
57 let client_nonce = QueryId::random()
58 .map_err(|error| ClientError::Transport(error.to_string()))?
59 .to_string();
60 let mut exchange = mongreldb_core::ScramClientSession::begin(
61 &username,
62 secrecy::ExposeSecret::expose_secret(password),
63 &client_nonce,
64 )
65 .map_err(|error| ClientError::Transport(error.to_string()))?;
66 let begin = self
67 .pool
68 .client()
69 .auth()
70 .begin_scram(native::BeginScramRequest {
71 context: Some(context(self.request_timeout, None)?),
72 username,
73 client_first_bare: exchange.client_first_bare().into(),
74 client_nonce,
75 })
76 .await
77 .map_err(native_error)?
78 .into_inner();
79 let (client_final_without_proof, client_proof) = exchange
80 .respond(&begin.server_first)
81 .map_err(|error| ClientError::Transport(error.to_string()))?;
82 let finish = self
83 .pool
84 .client()
85 .auth()
86 .finish_scram(native::FinishScramRequest {
87 context: Some(context(self.request_timeout, None)?),
88 exchange_id: begin.exchange_id,
89 client_final_without_proof,
90 client_proof,
91 })
92 .await
93 .map_err(native_error)?
94 .into_inner();
95 exchange
96 .verify_server_final(&finish.server_final)
97 .map_err(|error| ClientError::Transport(error.to_string()))?;
98 self.open_session(
99 finish
100 .authentication
101 .ok_or_else(|| ClientError::Decode("SCRAM authentication missing".into()))?,
102 )
103 .await
104 }
105
106 pub async fn authenticate_anonymous(&self) -> ClientResult<NativeSession> {
107 self.authenticate(None).await
108 }
109
110 pub async fn authenticate_mysql_caching_sha2(
114 &self,
115 username: impl Into<String>,
116 nonce: &[u8],
117 proof: &[u8],
118 ) -> ClientResult<NativeSession> {
119 self.authenticate(Some(
120 native::authenticate_request::Credential::MysqlCachingSha2(
121 native::MysqlCachingSha2Credential {
122 username: username.into(),
123 nonce: nonce.to_vec(),
124 proof: proof.to_vec(),
125 },
126 ),
127 ))
128 .await
129 }
130
131 async fn authenticate(
132 &self,
133 credential: Option<native::authenticate_request::Credential>,
134 ) -> ClientResult<NativeSession> {
135 let auth = self
136 .pool
137 .client()
138 .auth()
139 .authenticate(native::AuthenticateRequest {
140 context: Some(context(self.request_timeout, None)?),
141 credential,
142 })
143 .await
144 .map_err(native_error)?
145 .into_inner();
146 self.open_session(auth).await
147 }
148
149 async fn open_session(
150 &self,
151 auth: native::AuthenticateResponse,
152 ) -> ClientResult<NativeSession> {
153 let session_id = self
154 .pool
155 .client()
156 .session()
157 .open_session(native::OpenSessionRequest {
158 context: Some(context(self.request_timeout, None)?),
159 identity: auth.identity,
160 database_id: self.database_id.to_vec(),
161 auth_token: auth.auth_token,
162 })
163 .await
164 .map_err(native_error)?
165 .into_inner()
166 .session_id;
167 Ok(NativeSession {
168 pool: Arc::clone(&self.pool),
169 session_id,
170 database_id: self.database_id,
171 request_timeout: self.request_timeout,
172 max_retries: self.max_retries,
173 })
174 }
175}
176
177#[derive(Clone)]
178pub struct NativeSession {
179 pool: Arc<NativeRpcClientPool>,
180 session_id: Vec<u8>,
181 database_id: [u8; 16],
182 request_timeout: Duration,
183 max_retries: usize,
184}
185
186impl NativeSession {
187 pub fn id(&self) -> &[u8] {
188 &self.session_id
189 }
190
191 pub async fn close(self) -> ClientResult<()> {
192 self.pool
193 .client()
194 .session()
195 .close_session(native::CloseSessionRequest {
196 context: Some(context(self.request_timeout, None)?),
197 session_id: self.session_id,
198 })
199 .await
200 .map_err(native_error)?;
201 Ok(())
202 }
203
204 pub async fn create_table(
205 &self,
206 table: impl Into<String>,
207 schema: &mongreldb_core::Schema,
208 ) -> ClientResult<native::CreateTableResponse> {
209 if schema.columns.iter().any(|column| {
210 matches!(
211 column.embedding_source.as_ref(),
212 Some(mongreldb_core::EmbeddingSource::LocalModel { .. })
213 )
214 }) {
215 return Err(ClientError::Decode(
216 "native CreateTable cannot transport node-local model paths; use ConfiguredModel or GeneratedColumnSpec"
217 .into(),
218 ));
219 }
220 let full_schema_required = !schema.indexes.is_empty()
221 || !schema.constraints.checks.is_empty()
222 || schema
223 .columns
224 .iter()
225 .any(|column| column.default_value.is_some() || column.embedding_source.is_some());
226 let legacy_columns = schema
227 .columns
228 .iter()
229 .map(native_column)
230 .collect::<ClientResult<Vec<_>>>();
231 let use_legacy_fields = !full_schema_required && legacy_columns.is_ok();
232 let schema_json = serde_json::to_vec(schema).map_err(|error| {
233 ClientError::Decode(format!("native schema encode failed: {error}"))
234 })?;
235 let response = self
236 .pool
237 .client()
238 .catalog()
239 .create_table(native::CreateTableRequest {
240 context: Some(context(self.request_timeout, None)?),
241 session_id: self.session_id.clone(),
242 table: table.into(),
243 schema_id: schema.schema_id,
244 columns: if use_legacy_fields {
245 legacy_columns.unwrap_or_default()
246 } else {
247 Vec::new()
248 },
249 uniques: if use_legacy_fields {
250 schema
251 .constraints
252 .uniques
253 .iter()
254 .map(|constraint| native::UniqueConstraint {
255 id: u32::from(constraint.id),
256 name: constraint.name.clone(),
257 columns: constraint.columns.iter().map(|id| u32::from(*id)).collect(),
258 })
259 .collect()
260 } else {
261 Vec::new()
262 },
263 foreign_keys: if use_legacy_fields {
264 schema
265 .constraints
266 .foreign_keys
267 .iter()
268 .map(native_foreign_key)
269 .collect()
270 } else {
271 Vec::new()
272 },
273 schema_json,
274 })
275 .await
276 .map_err(native_error)?
277 .into_inner();
278 Ok(response)
279 }
280
281 pub async fn schema(&self, table: impl Into<String>) -> ClientResult<mongreldb_core::Schema> {
282 let response = self
283 .pool
284 .client()
285 .catalog()
286 .get_schema(native::GetSchemaRequest {
287 context: Some(context(self.request_timeout, None)?),
288 database_id: self.database_id.to_vec(),
289 table: table.into(),
290 session_id: self.session_id.clone(),
291 })
292 .await
293 .map_err(native_error)?
294 .into_inner();
295 if response.schema_json.is_empty() {
296 return Err(ClientError::Decode(
297 "native schema response omitted complete schema_json".into(),
298 ));
299 }
300 serde_json::from_slice(&response.schema_json)
301 .map_err(|error| ClientError::Decode(format!("native schema decode failed: {error}")))
302 }
303
304 pub async fn prepare(&self, sql: impl Into<String>) -> ClientResult<NativePrepared> {
305 let response = self
306 .pool
307 .client()
308 .query()
309 .prepare(native::PrepareRequest {
310 context: Some(context(self.request_timeout, None)?),
311 session_id: self.session_id.clone(),
312 sql: sql.into(),
313 })
314 .await
315 .map_err(native_error)?
316 .into_inner();
317 Ok(NativePrepared {
318 statement_id: response.statement_id,
319 schema_version: response.schema_version,
320 })
321 }
322
323 pub async fn execute(
324 &self,
325 sql: impl Into<String>,
326 idempotency_key: Option<&str>,
327 ) -> ClientResult<NativeExecuteResult> {
328 let sql = sql.into();
329 let retryable = idempotency_key.is_some() || read_only_sql(&sql);
330 self.execute_request(
331 native::execute_request::Command::Sql(sql),
332 Vec::new(),
333 idempotency_key,
334 retryable,
335 )
336 .await
337 }
338
339 pub async fn execute_prepared(
340 &self,
341 statement: NativePrepared,
342 parameters: &[mongreldb_protocol::request::ParameterValue],
343 ) -> ClientResult<NativeExecuteResult> {
344 let parameters = parameters
345 .iter()
346 .map(|parameter| {
347 bincode::serialize(parameter)
348 .map_err(|error| ClientError::Decode(error.to_string()))
349 })
350 .collect::<ClientResult<Vec<_>>>()?;
351 self.execute_request(
352 native::execute_request::Command::PreparedStatementId(statement.statement_id),
353 parameters,
354 None,
355 true,
356 )
357 .await
358 }
359
360 async fn execute_request(
361 &self,
362 command: native::execute_request::Command,
363 parameters: Vec<Vec<u8>>,
364 idempotency_key: Option<&str>,
365 retryable: bool,
366 ) -> ClientResult<NativeExecuteResult> {
367 let mut attempt = 0;
368 loop {
369 let query_id = random_query_id()?;
370 let request = native::ExecuteRequest {
371 context: Some(context(self.request_timeout, idempotency_key)?),
372 session_id: self.session_id.clone(),
373 query_id: query_id.clone(),
374 command: Some(command.clone()),
375 parameters: parameters.clone(),
376 };
377 match self.pool.client().query().execute(request).await {
378 Ok(response) => return decode_execute(response.into_inner()),
379 Err(error)
380 if retryable && attempt < self.max_retries && native_retryable(&error) =>
381 {
382 attempt += 1;
383 }
384 Err(error) => return Err(native_error(error)),
385 }
386 }
387 }
388
389 pub async fn execute_stream(&self, sql: impl Into<String>) -> ClientResult<NativeArrowStream> {
390 let sql = sql.into();
391 let mut attempt = 0;
392 loop {
393 let query_id = random_query_id()?;
394 let request = native::ExecuteRequest {
395 context: Some(context(self.request_timeout, None)?),
396 session_id: self.session_id.clone(),
397 query_id: query_id.clone(),
398 command: Some(native::execute_request::Command::Sql(sql.clone())),
399 parameters: Vec::new(),
400 };
401 match self.pool.client().query().execute_stream(request).await {
402 Ok(response) => {
403 return Ok(NativeArrowStream {
404 query_id,
405 inner: response.into_inner(),
406 buffered: VecDeque::new(),
407 finished: false,
408 })
409 }
410 Err(error)
411 if read_only_sql(&sql)
412 && attempt < self.max_retries
413 && native_retryable(&error) =>
414 {
415 attempt += 1;
416 }
417 Err(error) => return Err(native_error(error)),
418 }
419 }
420 }
421
422 pub async fn cancel(&self, query_id: &[u8]) -> ClientResult<NativeCancelResult> {
423 let response = self
424 .pool
425 .client()
426 .query()
427 .cancel_query(native::CancelQueryRequest {
428 context: Some(context(self.request_timeout, None)?),
429 query_id: query_id.to_vec(),
430 session_id: self.session_id.clone(),
431 })
432 .await
433 .map(tonic::Response::into_inner)
434 .map_err(native_error)?;
435 Ok(NativeCancelResult {
436 outcome: native::CancelOutcome::try_from(response.outcome)
437 .unwrap_or(native::CancelOutcome::Unspecified),
438 durable: response.durable.unwrap_or_default(),
439 })
440 }
441
442 pub async fn query_status(&self, query_id: &[u8]) -> ClientResult<NativeQueryStatus> {
443 let response = self
444 .pool
445 .client()
446 .query()
447 .get_query_status(native::GetQueryStatusRequest {
448 context: Some(context(self.request_timeout, None)?),
449 query_id: query_id.to_vec(),
450 session_id: self.session_id.clone(),
451 })
452 .await
453 .map(tonic::Response::into_inner)
454 .map_err(native_error)?;
455 Ok(NativeQueryStatus {
456 query_id: response.query_id,
457 phase: response.phase,
458 error: response.error,
459 durable: response.durable.unwrap_or_default(),
460 })
461 }
462
463 pub async fn begin(&self, isolation: native::IsolationLevel) -> ClientResult<Vec<u8>> {
464 self.pool
465 .client()
466 .transaction()
467 .begin(native::BeginTransactionRequest {
468 context: Some(context(self.request_timeout, None)?),
469 session_id: self.session_id.clone(),
470 isolation: isolation as i32,
471 })
472 .await
473 .map(|response| response.into_inner().transaction_id)
474 .map_err(native_error)
475 }
476
477 pub async fn commit(&self) -> ClientResult<()> {
478 self.transaction("commit").await
479 }
480
481 pub async fn rollback(&self) -> ClientResult<()> {
482 self.transaction("rollback").await
483 }
484
485 async fn transaction(&self, operation: &str) -> ClientResult<()> {
486 let request = native::TransactionRequest {
487 context: Some(context(self.request_timeout, None)?),
488 session_id: self.session_id.clone(),
489 };
490 let mut client = self.pool.client().transaction();
491 match operation {
492 "commit" => client.commit(request).await,
493 _ => client.rollback(request).await,
494 }
495 .map_err(native_error)?;
496 Ok(())
497 }
498}
499
500fn native_column(column: &mongreldb_core::ColumnDef) -> ClientResult<native::CreateColumn> {
501 use mongreldb_core::TypeId;
502 let (data_type, decimal_precision, decimal_scale) = match &column.ty {
503 TypeId::Bool => (native::ColumnType::Bool, 0, 0),
504 TypeId::Int8 => (native::ColumnType::Int8, 0, 0),
505 TypeId::Int16 => (native::ColumnType::Int16, 0, 0),
506 TypeId::Int32 => (native::ColumnType::Int32, 0, 0),
507 TypeId::Int64 => (native::ColumnType::Int64, 0, 0),
508 TypeId::UInt8 => (native::ColumnType::Uint8, 0, 0),
509 TypeId::UInt16 => (native::ColumnType::Uint16, 0, 0),
510 TypeId::UInt32 => (native::ColumnType::Uint32, 0, 0),
511 TypeId::UInt64 => (native::ColumnType::Uint64, 0, 0),
512 TypeId::Float32 => (native::ColumnType::Float32, 0, 0),
513 TypeId::Float64 => (native::ColumnType::Float64, 0, 0),
514 TypeId::TimestampNanos => (native::ColumnType::TimestampNanos, 0, 0),
515 TypeId::Date32 => (native::ColumnType::Date32, 0, 0),
516 TypeId::Date64 => (native::ColumnType::Date64, 0, 0),
517 TypeId::Time64 => (native::ColumnType::Time64, 0, 0),
518 TypeId::Bytes => (native::ColumnType::Bytes, 0, 0),
519 TypeId::Json => (native::ColumnType::Json, 0, 0),
520 TypeId::Decimal128 { precision, scale } => (
521 native::ColumnType::Decimal128,
522 u32::from(*precision),
523 i32::from(*scale),
524 ),
525 _ => {
526 return Err(ClientError::Decode(format!(
527 "native CreateTable does not support column type {:?}",
528 column.ty
529 )))
530 }
531 };
532 let allowed_flags = mongreldb_core::ColumnFlags::NULLABLE
533 | mongreldb_core::ColumnFlags::PRIMARY_KEY
534 | mongreldb_core::ColumnFlags::AUTO_INCREMENT;
535 if column.flags.bits() & !allowed_flags != 0 {
536 return Err(ClientError::Decode(format!(
537 "native CreateTable does not support flags on column {:?}",
538 column.name
539 )));
540 }
541 Ok(native::CreateColumn {
542 id: u32::from(column.id),
543 name: column.name.clone(),
544 data_type: data_type as i32,
545 nullable: column.flags.contains(mongreldb_core::ColumnFlags::NULLABLE),
546 primary_key: column
547 .flags
548 .contains(mongreldb_core::ColumnFlags::PRIMARY_KEY),
549 decimal_precision,
550 decimal_scale,
551 auto_increment: column
552 .flags
553 .contains(mongreldb_core::ColumnFlags::AUTO_INCREMENT),
554 })
555}
556
557fn native_foreign_key(constraint: &mongreldb_core::constraint::ForeignKey) -> native::ForeignKey {
558 let action = |action| match action {
559 mongreldb_core::constraint::FkAction::Restrict => native::ForeignKeyAction::Restrict,
560 mongreldb_core::constraint::FkAction::Cascade => native::ForeignKeyAction::Cascade,
561 mongreldb_core::constraint::FkAction::SetNull => native::ForeignKeyAction::SetNull,
562 };
563 native::ForeignKey {
564 id: u32::from(constraint.id),
565 name: constraint.name.clone(),
566 columns: constraint.columns.iter().map(|id| u32::from(*id)).collect(),
567 referenced_table: constraint.ref_table.clone(),
568 referenced_columns: constraint
569 .ref_columns
570 .iter()
571 .map(|id| u32::from(*id))
572 .collect(),
573 on_delete: action(constraint.on_delete) as i32,
574 on_update: action(constraint.on_update) as i32,
575 }
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub struct NativePrepared {
580 pub statement_id: u64,
581 pub schema_version: u64,
582}
583
584pub type NativeDurableOutcome = native::DurableOutcome;
586
587#[derive(Debug, Clone)]
588pub struct NativeCancelResult {
589 pub outcome: native::CancelOutcome,
590 pub durable: NativeDurableOutcome,
591}
592
593#[derive(Debug, Clone)]
594pub struct NativeQueryStatus {
595 pub query_id: Vec<u8>,
596 pub phase: i32,
597 pub error: Option<native::ErrorDetail>,
598 pub durable: NativeDurableOutcome,
599}
600
601pub struct NativeExecuteResult {
602 pub query_id: Vec<u8>,
603 pub batches: Vec<RecordBatch>,
604 pub rows_affected: u64,
605 pub committed: bool,
606 pub commit_epoch: Option<u64>,
607 pub idempotency_replayed: bool,
608 pub original_query_id: Vec<u8>,
609 pub durable: NativeDurableOutcome,
611}
612
613pub struct NativeArrowStream {
614 query_id: Vec<u8>,
615 inner: tonic::Streaming<native::ArrowFrame>,
616 buffered: VecDeque<RecordBatch>,
617 finished: bool,
618}
619
620impl NativeArrowStream {
621 pub fn query_id(&self) -> &[u8] {
622 &self.query_id
623 }
624
625 pub async fn next_batch(&mut self) -> ClientResult<Option<RecordBatch>> {
626 if let Some(batch) = self.buffered.pop_front() {
627 return Ok(Some(batch));
628 }
629 while !self.finished {
630 let Some(frame) = self.inner.message().await.map_err(native_error)? else {
631 self.finished = true;
632 return Ok(None);
633 };
634 if frame.end_of_stream {
635 self.finished = true;
636 }
637 if frame.ipc.is_empty() {
638 continue;
639 }
640 self.buffered.extend(decode_ipc(&frame.ipc)?);
641 if let Some(batch) = self.buffered.pop_front() {
642 return Ok(Some(batch));
643 }
644 }
645 Ok(None)
646 }
647}
648
649fn decode_execute(response: native::ExecuteResponse) -> ClientResult<NativeExecuteResult> {
650 let mut batches = Vec::new();
651 for frame in &response.frames {
652 batches.extend(decode_ipc(&frame.ipc)?);
653 }
654 let durable = response.durable.unwrap_or_else(|| native::DurableOutcome {
655 committed: response.committed,
656 committed_statements: 0,
657 last_commit_hlc: Vec::new(),
658 first_commit_statement_index: None,
659 last_commit_statement_index: None,
660 completed_statements: 0,
661 current_statement_index: 0,
662 terminal_state: String::new(),
663 serialization_state: String::new(),
664 last_commit_epoch: (response.commit_epoch != 0).then_some(response.commit_epoch),
665 });
666 Ok(NativeExecuteResult {
667 query_id: response.query_id,
668 batches,
669 rows_affected: response.rows_affected,
670 committed: durable.committed || response.committed,
671 commit_epoch: durable
672 .last_commit_epoch
673 .or((response.commit_epoch != 0).then_some(response.commit_epoch)),
674 idempotency_replayed: response.idempotency_replayed,
675 original_query_id: response.original_query_id,
676 durable,
677 })
678}
679
680fn decode_ipc(ipc: &[u8]) -> ClientResult<Vec<RecordBatch>> {
681 if ipc.is_empty() {
682 return Ok(Vec::new());
683 }
684 StreamReader::try_new(Cursor::new(ipc), None)
685 .map_err(|error| ClientError::Decode(error.to_string()))?
686 .collect::<Result<Vec<_>, _>>()
687 .map_err(|error| ClientError::Decode(error.to_string()))
688}
689
690fn context(
691 timeout: Duration,
692 idempotency_key: Option<&str>,
693) -> ClientResult<native::RequestContext> {
694 let request_id = QueryId::random()
695 .map_err(|error| ClientError::Transport(error.to_string()))?
696 .to_string();
697 Ok(native::RequestContext {
698 version: Some(native::ApiVersion {
699 major: NATIVE_API_MAJOR,
700 minor: NATIVE_API_MINOR,
701 }),
702 request_id,
703 deadline_unix_micros: now_unix_micros()
704 .saturating_add(timeout.as_micros().min(u128::from(u64::MAX)) as u64),
705 idempotency_key: idempotency_key.unwrap_or_default().into(),
706 })
707}
708
709fn random_query_id() -> ClientResult<Vec<u8>> {
710 QueryId::random()
711 .map(|id| id.as_bytes().to_vec())
712 .map_err(|error| ClientError::Transport(error.to_string()))
713}
714
715fn now_unix_micros() -> u64 {
716 SystemTime::now()
717 .duration_since(UNIX_EPOCH)
718 .unwrap_or_default()
719 .as_micros()
720 .min(u128::from(u64::MAX)) as u64
721}
722
723fn read_only_sql(sql: &str) -> bool {
724 matches!(
725 mongreldb_query::classify_sql_idempotency(sql),
726 mongreldb_query::SqlIdempotencyClass::ReadOnly
727 )
728}
729
730fn native_retryable(status: &tonic::Status) -> bool {
731 if matches!(status.code(), tonic::Code::Unavailable) {
732 return true;
733 }
734 native_detail(status).is_some_and(|detail| detail.retryable)
735}
736
737fn native_error(status: tonic::Status) -> ClientError {
738 match native_detail(&status) {
739 Some(detail) => ClientError::Native {
740 code: format!("{:?}", status.code()),
741 category_code: Some(detail.category_code),
742 category: detail.category,
743 message: detail.message,
744 retryable: detail.retryable,
745 },
746 None => ClientError::Native {
747 code: format!("{:?}", status.code()),
748 category_code: None,
749 category: "transport".into(),
750 message: status.message().into(),
751 retryable: status.code() == tonic::Code::Unavailable,
752 },
753 }
754}
755
756fn native_detail(status: &tonic::Status) -> Option<native::ErrorDetail> {
757 (!status.details().is_empty())
758 .then(|| native::ErrorDetail::decode(status.details()).ok())
759 .flatten()
760}