mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-12-23 11:12:51 +00:00
Merge branch 'main' into alter-operator
This commit is contained in:
commit
699ce46560
12 changed files with 213 additions and 10 deletions
|
|
@ -18,7 +18,7 @@
|
|||
[package]
|
||||
name = "sqlparser"
|
||||
description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011"
|
||||
version = "0.59.0"
|
||||
version = "0.60.0"
|
||||
authors = ["Apache DataFusion <dev@datafusion.apache.org>"]
|
||||
homepage = "https://github.com/apache/datafusion-sqlparser-rs"
|
||||
documentation = "https://docs.rs/sqlparser/"
|
||||
|
|
|
|||
98
changelog/0.60.0.md
Normal file
98
changelog/0.60.0.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
-->
|
||||
|
||||
# sqlparser-rs 0.60.0 Changelog
|
||||
|
||||
This release consists of 37 commits from 20 contributors. See credits at the end of this changelog for more information.
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- feat: Add RESET to the base dialect #2078 [#2079](https://github.com/apache/datafusion-sqlparser-rs/pull/2079) (watford-ep)
|
||||
- feat: Add support for SET SESSION AUTHORIZATION #2086 [#2087](https://github.com/apache/datafusion-sqlparser-rs/pull/2087) (watford-ep)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix: parse error on unnamed arg with default syntax [#2091](https://github.com/apache/datafusion-sqlparser-rs/pull/2091) (r1b)
|
||||
|
||||
**Other:**
|
||||
|
||||
- Add support for INVISIBLE columns in MySQL [#2033](https://github.com/apache/datafusion-sqlparser-rs/pull/2033) (altmannmarcelo)
|
||||
- Link to actual change logs in CHANGELOG.md [#2040](https://github.com/apache/datafusion-sqlparser-rs/pull/2040) (lovasoa)
|
||||
- Snowflake: ALTER USER and KeyValueOptions Refactoring [#2035](https://github.com/apache/datafusion-sqlparser-rs/pull/2035) (yoavcloud)
|
||||
- Correctly tokenize nested comments in Databricks, ClickHouse, and ANSI [#2044](https://github.com/apache/datafusion-sqlparser-rs/pull/2044) (jmhain)
|
||||
- MySQL: `CREATE INDEX`: allow `USING` clause before `ON` [#2029](https://github.com/apache/datafusion-sqlparser-rs/pull/2029) (MohamedAbdeen21)
|
||||
- [databricks] update dialect to support grouping by with modifier [#2047](https://github.com/apache/datafusion-sqlparser-rs/pull/2047) (n-young)
|
||||
- Moved constraint variant outside of `TableConstraint` enum [#2054](https://github.com/apache/datafusion-sqlparser-rs/pull/2054) (LucaCappelletti94)
|
||||
- Add support for procedure parameter default values [#2041](https://github.com/apache/datafusion-sqlparser-rs/pull/2041) (aharpervc)
|
||||
- Support updating PRs using github UI [#2052](https://github.com/apache/datafusion-sqlparser-rs/pull/2052) (blaginin)
|
||||
- Increase version of sqlparser_derive from 0.3.0 to 0.4.0 [#2060](https://github.com/apache/datafusion-sqlparser-rs/pull/2060) (jjbayer)
|
||||
- Moving several struct variants out of `Statement` enum to allow for trait impls for specific sub-variants [#2057](https://github.com/apache/datafusion-sqlparser-rs/pull/2057) (LucaCappelletti94)
|
||||
- Added support for SQLite triggers [#2037](https://github.com/apache/datafusion-sqlparser-rs/pull/2037) (LucaCappelletti94)
|
||||
- Added support for MATCH syntax and unified column option ForeignKey [#2062](https://github.com/apache/datafusion-sqlparser-rs/pull/2062) (LucaCappelletti94)
|
||||
- chore: add stack overflow warning for Visitor and VisitorMut [#2068](https://github.com/apache/datafusion-sqlparser-rs/pull/2068) (niebayes)
|
||||
- Reused `CheckConstraint` in `ColumnOption` [#2063](https://github.com/apache/datafusion-sqlparser-rs/pull/2063) (LucaCappelletti94)
|
||||
- Refactored `ColumnOption::Unique` to reuse `UniqueConstraint` and `PrimaryKeyConstraint` [#2064](https://github.com/apache/datafusion-sqlparser-rs/pull/2064) (LucaCappelletti94)
|
||||
- Redshift: more copy options [#2072](https://github.com/apache/datafusion-sqlparser-rs/pull/2072) (yoavcloud)
|
||||
- SQLite: make period optional for CREATE TRIGGER [#2071](https://github.com/apache/datafusion-sqlparser-rs/pull/2071) (takluyver)
|
||||
- Added TIMESTAMP_NTZ type support with precision to Snowflake dialect [#2080](https://github.com/apache/datafusion-sqlparser-rs/pull/2080) (romanoff)
|
||||
- Make `BitwiseNot` ("~") available for all dialects, not just PostgreSQL [#2081](https://github.com/apache/datafusion-sqlparser-rs/pull/2081) (alexander-beedie)
|
||||
- Add snowflake dynamic table parsing [#2083](https://github.com/apache/datafusion-sqlparser-rs/pull/2083) (romanoff)
|
||||
- Add support for `INSERT INTO VALUE` [#2085](https://github.com/apache/datafusion-sqlparser-rs/pull/2085) (etgarperets)
|
||||
- Complete PostgreSQL `CREATE TYPE` Support [#2094](https://github.com/apache/datafusion-sqlparser-rs/pull/2094) (LucaCappelletti94)
|
||||
- Include DML keyword in statement span [#2090](https://github.com/apache/datafusion-sqlparser-rs/pull/2090) (xitep)
|
||||
- Add PostgreSQL Operator DDL Support [#2096](https://github.com/apache/datafusion-sqlparser-rs/pull/2096) (LucaCappelletti94)
|
||||
- impl `Spanned` for MERGE statements [#2100](https://github.com/apache/datafusion-sqlparser-rs/pull/2100) (xitep)
|
||||
- Preserve optional `AS` keyword in aliases [#2103](https://github.com/apache/datafusion-sqlparser-rs/pull/2103) (xitep)
|
||||
- Added support for `DROP OPERATOR` syntax [#2102](https://github.com/apache/datafusion-sqlparser-rs/pull/2102) (LucaCappelletti94)
|
||||
- Only set `hive_formats` on `CreateTable` if formats are present [#2105](https://github.com/apache/datafusion-sqlparser-rs/pull/2105) (mvzink)
|
||||
- Support PostgreSQL C Functions with Multiple AS Parameters [#2095](https://github.com/apache/datafusion-sqlparser-rs/pull/2095) (LucaCappelletti94)
|
||||
- Added support for `DROP OPERATOR FAMILY` [#2106](https://github.com/apache/datafusion-sqlparser-rs/pull/2106) (LucaCappelletti94)
|
||||
- Update criterion requirement from 0.7 to 0.8 in /sqlparser_bench [#2111](https://github.com/apache/datafusion-sqlparser-rs/pull/2111) (dependabot[bot])
|
||||
- Added support for `DROP OPERATOR CLASS` syntax [#2109](https://github.com/apache/datafusion-sqlparser-rs/pull/2109) (LucaCappelletti94)
|
||||
- Introduce Oracle dialect [#2113](https://github.com/apache/datafusion-sqlparser-rs/pull/2113) (xitep)
|
||||
|
||||
## Credits
|
||||
|
||||
Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor.
|
||||
|
||||
```
|
||||
12 Luca Cappelletti
|
||||
4 xitep
|
||||
2 Andriy Romanov
|
||||
2 Christopher Watford
|
||||
2 Yoav Cohen
|
||||
1 Alexander Beedie
|
||||
1 Andrew Harper
|
||||
1 Dmitrii Blaginin
|
||||
1 Joey Hain
|
||||
1 Joris Bayer
|
||||
1 Marcelo Altmann
|
||||
1 Michael Victor Zink
|
||||
1 Mohamed Abdeen
|
||||
1 Ophir LOJKINE
|
||||
1 Thomas Kluyver
|
||||
1 dependabot[bot]
|
||||
1 etgarperets
|
||||
1 nick young
|
||||
1 niebayes
|
||||
1 r1b
|
||||
```
|
||||
|
||||
Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release.
|
||||
|
||||
|
|
@ -124,13 +124,15 @@ test_source_distribution() {
|
|||
cargo build
|
||||
cargo test --all-features
|
||||
|
||||
if ( find -iname 'Cargo.toml' | xargs grep SNAPSHOT ); then
|
||||
if ( find . -iname 'Cargo.toml' | xargs grep SNAPSHOT ); then
|
||||
echo "Cargo.toml version should not contain SNAPSHOT for releases"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Can't test using dry-run because sqlparser depends on sqlparser_derive
|
||||
# see https://github.com/crate-ci/cargo-release/issues/691#issuecomment-2059866021
|
||||
# Check that publish works
|
||||
cargo publish --dry-run
|
||||
# cargo publish --dry-run
|
||||
}
|
||||
|
||||
TEST_SUCCESS=no
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ $ cargo run --example cli - [--dialectname]
|
|||
"--clickhouse" => Box::new(ClickHouseDialect {}),
|
||||
"--duckdb" => Box::new(DuckDbDialect {}),
|
||||
"--sqlite" => Box::new(SQLiteDialect {}),
|
||||
"--oracle" => Box::new(OracleDialect {}),
|
||||
"--generic" | "" => Box::new(GenericDialect {}),
|
||||
s => panic!("Unexpected parameter: {s}"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2900,7 +2900,9 @@ impl fmt::Display for CreateTable {
|
|||
if let Some(file_format) = self.file_format {
|
||||
write!(f, " STORED AS {file_format}")?;
|
||||
}
|
||||
write!(f, " LOCATION '{}'", self.location.as_ref().unwrap())?;
|
||||
if let Some(location) = &self.location {
|
||||
write!(f, " LOCATION '{location}'")?;
|
||||
}
|
||||
}
|
||||
|
||||
match &self.table_options {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ mod generic;
|
|||
mod hive;
|
||||
mod mssql;
|
||||
mod mysql;
|
||||
mod oracle;
|
||||
mod postgresql;
|
||||
mod redshift;
|
||||
mod snowflake;
|
||||
|
|
@ -45,6 +46,7 @@ pub use self::generic::GenericDialect;
|
|||
pub use self::hive::HiveDialect;
|
||||
pub use self::mssql::MsSqlDialect;
|
||||
pub use self::mysql::MySqlDialect;
|
||||
pub use self::oracle::OracleDialect;
|
||||
pub use self::postgresql::PostgreSqlDialect;
|
||||
pub use self::redshift::RedshiftSqlDialect;
|
||||
pub use self::snowflake::SnowflakeDialect;
|
||||
|
|
@ -1260,6 +1262,7 @@ pub fn dialect_from_str(dialect_name: impl AsRef<str>) -> Option<Box<dyn Dialect
|
|||
"ansi" => Some(Box::new(AnsiDialect {})),
|
||||
"duckdb" => Some(Box::new(DuckDbDialect {})),
|
||||
"databricks" => Some(Box::new(DatabricksDialect {})),
|
||||
"oracle" => Some(Box::new(OracleDialect {})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/dialect/oracle.rs
Normal file
81
src/dialect/oracle.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
use super::Dialect;
|
||||
|
||||
/// A [`Dialect`] for [Oracle Databases](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/index.html)
|
||||
#[derive(Debug)]
|
||||
pub struct OracleDialect;
|
||||
|
||||
impl Dialect for OracleDialect {
|
||||
// ~ appears not to be called anywhere
|
||||
fn identifier_quote_style(&self, _identifier: &str) -> Option<char> {
|
||||
Some('"')
|
||||
}
|
||||
|
||||
fn is_delimited_identifier_start(&self, ch: char) -> bool {
|
||||
ch == '"'
|
||||
}
|
||||
|
||||
fn is_identifier_start(&self, ch: char) -> bool {
|
||||
ch.is_alphabetic()
|
||||
}
|
||||
|
||||
fn is_identifier_part(&self, ch: char) -> bool {
|
||||
ch.is_alphanumeric() || ch == '_' || ch == '$' || ch == '#' || ch == '@'
|
||||
}
|
||||
|
||||
fn supports_outer_join_operator(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_connect_by(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_execute_immediate(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_match_recognize(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_window_function_null_treatment_arg(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_boolean_literals(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_comment_on(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_create_table_select(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_set_stmt_without_operator(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_group_by_expr(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -229,6 +229,7 @@ define_keywords!(
|
|||
COMMITTED,
|
||||
COMMUTATOR,
|
||||
COMPATIBLE,
|
||||
COMPRESS,
|
||||
COMPRESSION,
|
||||
COMPUPDATE,
|
||||
COMPUTE,
|
||||
|
|
@ -473,6 +474,7 @@ define_keywords!(
|
|||
IAM_ROLE,
|
||||
ICEBERG,
|
||||
ID,
|
||||
IDENTIFIED,
|
||||
IDENTITY,
|
||||
IDENTITY_INSERT,
|
||||
IF,
|
||||
|
|
@ -576,6 +578,7 @@ define_keywords!(
|
|||
LOG,
|
||||
LOGIN,
|
||||
LOGS,
|
||||
LONG,
|
||||
LONGBLOB,
|
||||
LONGTEXT,
|
||||
LOWCARDINALITY,
|
||||
|
|
@ -661,6 +664,7 @@ define_keywords!(
|
|||
NFKD,
|
||||
NO,
|
||||
NOBYPASSRLS,
|
||||
NOCOMPRESS,
|
||||
NOCREATEDB,
|
||||
NOCREATEROLE,
|
||||
NOINHERIT,
|
||||
|
|
@ -684,6 +688,7 @@ define_keywords!(
|
|||
NULLABLE,
|
||||
NULLIF,
|
||||
NULLS,
|
||||
NUMBER,
|
||||
NUMERIC,
|
||||
NVARCHAR,
|
||||
OBJECT,
|
||||
|
|
@ -750,6 +755,7 @@ define_keywords!(
|
|||
PAST,
|
||||
PATH,
|
||||
PATTERN,
|
||||
PCTFREE,
|
||||
PER,
|
||||
PERCENT,
|
||||
PERCENTILE_CONT,
|
||||
|
|
@ -922,6 +928,7 @@ define_keywords!(
|
|||
SIGNED,
|
||||
SIMILAR,
|
||||
SIMPLE,
|
||||
SIZE,
|
||||
SKIP,
|
||||
SLOW,
|
||||
SMALLINT,
|
||||
|
|
@ -983,6 +990,7 @@ define_keywords!(
|
|||
SWAP,
|
||||
SYMMETRIC,
|
||||
SYNC,
|
||||
SYNONYM,
|
||||
SYSTEM,
|
||||
SYSTEM_TIME,
|
||||
SYSTEM_USER,
|
||||
|
|
@ -1094,6 +1102,7 @@ define_keywords!(
|
|||
VARBINARY,
|
||||
VARBIT,
|
||||
VARCHAR,
|
||||
VARCHAR2,
|
||||
VARIABLE,
|
||||
VARIABLES,
|
||||
VARYING,
|
||||
|
|
|
|||
|
|
@ -12400,7 +12400,7 @@ impl<'a> Parser<'a> {
|
|||
let (tables, with_from_keyword) = if !self.parse_keyword(Keyword::FROM) {
|
||||
// `FROM` keyword is optional in BigQuery SQL.
|
||||
// https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#delete_statement
|
||||
if dialect_of!(self is BigQueryDialect | GenericDialect) {
|
||||
if dialect_of!(self is BigQueryDialect | OracleDialect | GenericDialect) {
|
||||
(vec![], false)
|
||||
} else {
|
||||
let tables = self.parse_comma_separated(|p| p.parse_object_name(false))?;
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ pub fn all_dialects() -> TestedDialects {
|
|||
Box::new(DuckDbDialect {}),
|
||||
Box::new(DatabricksDialect {}),
|
||||
Box::new(ClickHouseDialect {}),
|
||||
Box::new(OracleDialect {}),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ use sqlparser::ast::TableFactor::{Pivot, Unpivot};
|
|||
use sqlparser::ast::*;
|
||||
use sqlparser::dialect::{
|
||||
AnsiDialect, BigQueryDialect, ClickHouseDialect, DatabricksDialect, Dialect, DuckDbDialect,
|
||||
GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, PostgreSqlDialect, RedshiftSqlDialect,
|
||||
SQLiteDialect, SnowflakeDialect,
|
||||
GenericDialect, HiveDialect, MsSqlDialect, MySqlDialect, OracleDialect, PostgreSqlDialect,
|
||||
RedshiftSqlDialect, SQLiteDialect, SnowflakeDialect,
|
||||
};
|
||||
use sqlparser::keywords::{Keyword, ALL_KEYWORDS};
|
||||
use sqlparser::parser::{Parser, ParserError, ParserOptions};
|
||||
|
|
@ -712,7 +712,9 @@ fn parse_delete_statement() {
|
|||
fn parse_delete_without_from_error() {
|
||||
let sql = "DELETE \"table\" WHERE 1";
|
||||
|
||||
let dialects = all_dialects_except(|d| d.is::<BigQueryDialect>() || d.is::<GenericDialect>());
|
||||
let dialects = all_dialects_except(|d| {
|
||||
d.is::<BigQueryDialect>() || d.is::<OracleDialect>() || d.is::<GenericDialect>()
|
||||
});
|
||||
let res = dialects.parse_sql_statements(sql);
|
||||
assert_eq!(
|
||||
ParserError::ParserError("Expected: FROM, found: WHERE".to_string()),
|
||||
|
|
@ -723,7 +725,9 @@ fn parse_delete_without_from_error() {
|
|||
#[test]
|
||||
fn parse_delete_statement_for_multi_tables() {
|
||||
let sql = "DELETE schema1.table1, schema2.table2 FROM schema1.table1 JOIN schema2.table2 ON schema2.table2.col1 = schema1.table1.col1 WHERE schema2.table2.col2 = 1";
|
||||
let dialects = all_dialects_except(|d| d.is::<BigQueryDialect>() || d.is::<GenericDialect>());
|
||||
let dialects = all_dialects_except(|d| {
|
||||
d.is::<BigQueryDialect>() || d.is::<OracleDialect>() || d.is::<GenericDialect>()
|
||||
});
|
||||
match dialects.verified_stmt(sql) {
|
||||
Statement::Delete(Delete {
|
||||
tables,
|
||||
|
|
@ -12943,7 +12947,7 @@ fn test_match_recognize_patterns() {
|
|||
fn check(pattern: &str, expect: MatchRecognizePattern) {
|
||||
let select =
|
||||
all_dialects_where(|d| d.supports_match_recognize()).verified_only_select(&format!(
|
||||
"SELECT * FROM my_table MATCH_RECOGNIZE(PATTERN ({pattern}) DEFINE DUMMY AS true)" // "select * from my_table match_recognize ("
|
||||
"SELECT * FROM my_table MATCH_RECOGNIZE(PATTERN ({pattern}) DEFINE DUMMY AS 1 = 1)" // "select * from my_table match_recognize ("
|
||||
));
|
||||
let TableFactor::MatchRecognize {
|
||||
pattern: actual, ..
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ fn parse_table_create() {
|
|||
let sql = r#"CREATE TABLE IF NOT EXISTS db.table (a BIGINT, b STRING, c TIMESTAMP) PARTITIONED BY (d STRING, e TIMESTAMP) STORED AS ORC LOCATION 's3://...' TBLPROPERTIES ("prop" = "2", "asdf" = '1234', 'asdf' = "1234", "asdf" = 2)"#;
|
||||
let iof = r#"CREATE TABLE IF NOT EXISTS db.table (a BIGINT, b STRING, c TIMESTAMP) PARTITIONED BY (d STRING, e TIMESTAMP) STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.orc.OrcInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat' LOCATION 's3://...'"#;
|
||||
let serdeproperties = r#"CREATE EXTERNAL TABLE IF NOT EXISTS db.table (a STRING, b STRING, c STRING) PARTITIONED BY (d STRING, e STRING) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde.config' WITH SERDEPROPERTIES ('prop_a' = 'a', 'prop_b' = 'b') STORED AS TEXTFILE LOCATION 's3://...' TBLPROPERTIES ('prop_c' = 'c')"#;
|
||||
let externaltable = r#"CREATE EXTERNAL TABLE t (c INT)"#;
|
||||
|
||||
hive().verified_stmt(sql);
|
||||
hive().verified_stmt(iof);
|
||||
hive().verified_stmt(serdeproperties);
|
||||
hive().verified_stmt(externaltable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue