diff --git a/example/http/insert_search_data.sh b/example/http/insert_search_data.sh
index e4e2fed55b..90af2b39e2 100755
--- a/example/http/insert_search_data.sh
+++ b/example/http/insert_search_data.sh
@@ -104,6 +104,30 @@ curl --request GET \
]
} '
+# select num and year of 'tbl1' and order by num descending, year ascending
+echo -e '\n\n-- select num and year of tbl1 and order by num descending, year ascending'
+curl --request GET \
+ --url http://localhost:23820/databases/default_db/tables/tbl1/docs \
+ --header 'accept: application/json' \
+ --header 'content-type: application/json' \
+ --data '
+ {
+ "output":
+ [
+ "num",
+ "year"
+ ],
+ "sort" :
+ [
+ {
+ "num": "desc"
+ },
+ {
+ "year": "asc"
+ }
+ ]
+ } '
+
# select num and year of 'tbl1' where num > 1 and year < 2023
echo -e '\n\n-- select num and year of tbl1 where num > 1 and year < 2023 offset 1 limit 1'
diff --git a/gui/app/(dashboard)/database/[dabataseId]/page.tsx b/gui/app/(dashboard)/database/[dabataseId]/page.tsx
new file mode 100644
index 0000000000..7a72089640
--- /dev/null
+++ b/gui/app/(dashboard)/database/[dabataseId]/page.tsx
@@ -0,0 +1,3 @@
+export default async function DatabasePage() {
+ return
DatabasePage id
;
+}
diff --git a/gui/app/(dashboard)/database/[dabataseId]/table/[tableId]/page.tsx b/gui/app/(dashboard)/database/[dabataseId]/table/[tableId]/page.tsx
new file mode 100644
index 0000000000..ffa195a421
--- /dev/null
+++ b/gui/app/(dashboard)/database/[dabataseId]/table/[tableId]/page.tsx
@@ -0,0 +1,3 @@
+export default async function DatabasePage() {
+ return table id
;
+}
diff --git a/gui/app/(dashboard)/database/[dabataseId]/table/page.tsx b/gui/app/(dashboard)/database/[dabataseId]/table/page.tsx
new file mode 100644
index 0000000000..1572b0df51
--- /dev/null
+++ b/gui/app/(dashboard)/database/[dabataseId]/table/page.tsx
@@ -0,0 +1,3 @@
+export default async function TablePage() {
+ return table
;
+}
diff --git a/gui/app/(dashboard)/database/context-menu.tsx b/gui/app/(dashboard)/database/context-menu.tsx
deleted file mode 100644
index 95c5b40c02..0000000000
--- a/gui/app/(dashboard)/database/context-menu.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-'use client';
-
-import {
- ContextMenuContent,
- ContextMenuItem
-} from '@/components/ui/context-menu';
-import { useSeDialogState } from '@/lib/hooks';
-import { TableCreatingDialog } from './table-creating-dialog';
-import AddIcon from '/public/add.svg';
-
-export function InfinityContextMenuContent({
- databaseName
-}: {
- databaseName: string;
-}) {
- const { showDialog, visible, hideDialog, switchVisible } = useSeDialogState();
- return (
- <>
-
-
-
-
-
-
- >
- );
-}
diff --git a/gui/app/(dashboard)/database/layout.tsx b/gui/app/(dashboard)/database/layout.tsx
new file mode 100644
index 0000000000..f382f067cd
--- /dev/null
+++ b/gui/app/(dashboard)/database/layout.tsx
@@ -0,0 +1,87 @@
+import SideMenu, { MenuItem } from '@/components/ui/side-menu';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from '@/components/ui/table';
+import { listDatabase, listTable } from '../actions';
+import { InfinityContextMenuContent } from '../tables/context-menu';
+
+async function InfinityTable() {
+ const tables = await listTable('default_db');
+ return (
+
+
+
+ Name
+
+
+
+ {tables.tables.map((table: string) => (
+
+ {table}
+
+ ))}
+
+
+ );
+}
+
+export default async function DatabaseLayout({
+ searchParams,
+ children
+}: {
+ searchParams: { q: string; offset: string };
+ children: React.ReactNode;
+}) {
+ const search = searchParams?.q ?? '';
+ const offset = searchParams?.offset ?? 0;
+
+ const items: MenuItem[] = [
+ {
+ key: 'sub1',
+ label: 'Navigation',
+ children: [
+ {
+ key: 'g1',
+ label: 'Item 1'
+ },
+ {
+ key: 'g2',
+ label: 'Item 2'
+ }
+ ]
+ }
+ ];
+
+ const ret = await listDatabase();
+ if (ret.databases.length > 1) {
+ const latestDatabase = ret.databases.at(-1);
+ const tables = await listTable(latestDatabase);
+ console.log('🚀 ~ ret:', tables);
+ items.push({
+ key: latestDatabase,
+ label: latestDatabase,
+ children: tables.tables
+ });
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/gui/app/(dashboard)/database/page.tsx b/gui/app/(dashboard)/database/page.tsx
index d0048320c1..3f9e71d12e 100644
--- a/gui/app/(dashboard)/database/page.tsx
+++ b/gui/app/(dashboard)/database/page.tsx
@@ -1,135 +1,3 @@
-import SideMenu, { MenuItem } from '@/components/ui/side-menu';
-import {
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow
-} from '@/components/ui/table';
-import { listDatabase, listTable } from '../actions';
-import { InfinityContextMenuContent } from './context-menu';
-
-const invoices = [
- {
- invoice: 'INV001',
- paymentStatus: 'Paid',
- totalAmount: '$250.00',
- paymentMethod: 'Credit Card'
- },
- {
- invoice: 'INV002',
- paymentStatus: 'Pending',
- totalAmount: '$150.00',
- paymentMethod: 'PayPal'
- },
- {
- invoice: 'INV003',
- paymentStatus: 'Unpaid',
- totalAmount: '$350.00',
- paymentMethod: 'Bank Transfer'
- },
- {
- invoice: 'INV004',
- paymentStatus: 'Paid',
- totalAmount: '$450.00',
- paymentMethod: 'Credit Card'
- },
- {
- invoice: 'INV005',
- paymentStatus: 'Paid',
- totalAmount: '$550.00',
- paymentMethod: 'PayPal'
- },
- {
- invoice: 'INV006',
- paymentStatus: 'Pending',
- totalAmount: '$200.00',
- paymentMethod: 'Bank Transfer'
- },
- {
- invoice: 'INV007',
- paymentStatus: 'Unpaid',
- totalAmount: '$300.00',
- paymentMethod: 'Credit Card'
- }
-];
-
-function InfinityTable() {
- return (
-
-
-
- Name
- Type
- Default
-
-
-
- {invoices.map((invoice) => (
-
- {invoice.invoice}
- {invoice.paymentStatus}
- {invoice.paymentMethod}
-
- ))}
-
-
- );
-}
-
-export default async function DatabasePage({
- searchParams
-}: {
- searchParams: { q: string; offset: string };
-}) {
- const search = searchParams.q ?? '';
- const offset = searchParams.offset ?? 0;
-
- const items: MenuItem[] = [
- {
- key: 'sub1',
- label: 'Navigation',
- children: [
- {
- key: 'g1',
- label: 'Item 1'
- },
- {
- key: 'g2',
- label: 'Item 2'
- }
- ]
- }
- ];
-
- const ret = await listDatabase();
- if (ret.databases.length > 1) {
- const latestDatabase = ret.databases.at(-1);
- const tables = await listTable(latestDatabase);
- console.log('🚀 ~ ret:', tables);
- items.push({
- key: latestDatabase,
- label: latestDatabase,
- children: tables.tables
- });
- }
-
- return (
-
-
-
-
- );
+export default async function DatabasePage() {
+ return DatabasePage
;
}
diff --git a/gui/app/(dashboard)/database/table-creating-dialog.tsx b/gui/app/(dashboard)/database/table-creating-dialog.tsx
deleted file mode 100644
index f99be95179..0000000000
--- a/gui/app/(dashboard)/database/table-creating-dialog.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import { Button } from '@/components/ui/button';
-import {
- Dialog,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger
-} from '@/components/ui/dialog';
-import { IDialogProps } from '@/lib/interfaces';
-import React from 'react';
-import { TableCreatingForm } from './table-creating-form';
-
-export function TableCreatingDialog({
- children,
- visible,
- switchVisible,
- hideDialog
-}: React.PropsWithChildren>) {
- return (
-
- {children}
-
-
- Create Table
-
-
-
-
- Save
-
-
-
-
- );
-}
diff --git a/gui/app/(dashboard)/database/table-creating-form.tsx b/gui/app/(dashboard)/database/table-creating-form.tsx
deleted file mode 100644
index d66302bfb8..0000000000
--- a/gui/app/(dashboard)/database/table-creating-form.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useRouter } from 'next/navigation';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
-
-import { toast } from '@/components/hooks/use-toast';
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage
-} from '@/components/ui/form';
-import { Input } from '@/components/ui/input';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue
-} from '@/components/ui/select';
-import { CreateOption } from '@/lib/constant/common';
-import { createDatabase } from '../actions';
-
-export const FormSchema = z.object({
- name: z
- .string({
- required_error: 'Please input name'
- })
- .trim(),
- fields: z.array(),
- create_option: z.nativeEnum(CreateOption)
-});
-
-interface IProps {
- hide(): void;
-}
-
-export function TableCreatingForm({ hide }: IProps) {
- const router = useRouter();
- const form = useForm>({
- resolver: zodResolver(FormSchema),
- defaultValues: {
- create_option: CreateOption.Error
- }
- });
-
- async function onSubmit(data: z.infer) {
- const ret = await createDatabase(data);
- console.log('🚀 ~ onSubmit ~ ret:', ret);
- if (ret.error_code === 0) {
- router.refresh();
- hide();
- toast({
- title: 'Create Success',
- description: ''
- });
- }
- }
-
- return (
-
-
- );
-}
diff --git a/gui/app/(dashboard)/page.tsx b/gui/app/(dashboard)/page.tsx
index 05c93eddc3..bed4ec14de 100644
--- a/gui/app/(dashboard)/page.tsx
+++ b/gui/app/(dashboard)/page.tsx
@@ -31,7 +31,7 @@ export default async function ProductsPage({
};
return (
-
+
Briefs
diff --git a/gui/app/(dashboard)/tables/page.tsx b/gui/app/(dashboard)/tables/page.tsx
index d0048320c1..374113d54d 100644
--- a/gui/app/(dashboard)/tables/page.tsx
+++ b/gui/app/(dashboard)/tables/page.tsx
@@ -10,67 +10,19 @@ import {
import { listDatabase, listTable } from '../actions';
import { InfinityContextMenuContent } from './context-menu';
-const invoices = [
- {
- invoice: 'INV001',
- paymentStatus: 'Paid',
- totalAmount: '$250.00',
- paymentMethod: 'Credit Card'
- },
- {
- invoice: 'INV002',
- paymentStatus: 'Pending',
- totalAmount: '$150.00',
- paymentMethod: 'PayPal'
- },
- {
- invoice: 'INV003',
- paymentStatus: 'Unpaid',
- totalAmount: '$350.00',
- paymentMethod: 'Bank Transfer'
- },
- {
- invoice: 'INV004',
- paymentStatus: 'Paid',
- totalAmount: '$450.00',
- paymentMethod: 'Credit Card'
- },
- {
- invoice: 'INV005',
- paymentStatus: 'Paid',
- totalAmount: '$550.00',
- paymentMethod: 'PayPal'
- },
- {
- invoice: 'INV006',
- paymentStatus: 'Pending',
- totalAmount: '$200.00',
- paymentMethod: 'Bank Transfer'
- },
- {
- invoice: 'INV007',
- paymentStatus: 'Unpaid',
- totalAmount: '$300.00',
- paymentMethod: 'Credit Card'
- }
-];
-
-function InfinityTable() {
+async function InfinityTable() {
+ const tables = await listTable('default_db');
return (
- Name
- Type
- Default
+ Name
- {invoices.map((invoice) => (
-
- {invoice.invoice}
- {invoice.paymentStatus}
- {invoice.paymentMethod}
+ {tables.tables.map((table: string) => (
+
+ {table}
))}
diff --git a/gui/lib/request.ts b/gui/lib/request.ts
index fe2ca7d95b..351a06eafc 100644
--- a/gui/lib/request.ts
+++ b/gui/lib/request.ts
@@ -1,4 +1,4 @@
-const baseUrl = 'http://127.0.0.1:23820/';
+const baseUrl = 'http://127.0.0.1:3000/';
export const request = async (
url: string,
diff --git a/python/infinity_http.py b/python/infinity_http.py
index 869dee7d66..776d15c8fa 100644
--- a/python/infinity_http.py
+++ b/python/infinity_http.py
@@ -4,7 +4,7 @@
import logging
import os
from test_pysdk.common.common_data import *
-from infinity.common import ConflictType, InfinityException, SparseVector
+from infinity.common import ConflictType, InfinityException, SparseVector, SortType
from test_pysdk.common import common_values
import infinity
from typing import Optional, Any
@@ -16,6 +16,7 @@
import pyarrow as pa
from infinity.table import ExplainType
from datetime import date, time, datetime
+from typing import Optional, Union, List, Any
class infinity_http:
@@ -476,6 +477,8 @@ def select(self):
tmp.update({"search": self._search_exprs})
if len(self._output):
tmp.update({"output":self._output})
+ if len(self._sort):
+ tmp.update({"sort":self._sort})
#print(tmp)
d = self.set_up_data([], tmp)
r = self.request(url, "get", h, d)
@@ -552,6 +555,21 @@ def output(
self._output = output
self._filter = ""
self._search_exprs = []
+ self._sort = []
+ return self
+
+ def sort(self, order_by_expr_list: Optional[List[list[str, SortType]]]):
+ for order_by_expr in order_by_expr_list:
+ tmp = {}
+ if len(order_by_expr) != 2:
+ raise InfinityException(ErrorCode.INVALID_PARAMETER, f"order_by_expr_list must be a list of [column_name, sort_type]")
+ if order_by_expr[1] not in [SortType.Asc, SortType.Desc]:
+ raise InfinityException(ErrorCode.INVALID_PARAMETER, f"sort_type must be SortType.Asc or SortType.Desc")
+ if order_by_expr[1] == SortType.Asc:
+ tmp[order_by_expr[0]] = "asc"
+ else:
+ tmp[order_by_expr[0]] = "desc"
+ self._sort.append(tmp)
return self
def match_text(self, fields: str, query: str, topn: int, opt_params: Optional[dict] = None):
@@ -713,7 +731,7 @@ def update(self, filter_str: str, update: dict[str, Any]):
class database_result(infinity_http):
def __init__(self, list = [], error_code = ErrorCode.OK, database_name = "" ,columns=[], table_name = "",
- index_list = [], output = ["*"], filter="", fusion=[], knn={}, match = {}, match_tensor = {}, match_sparse = {}, output_res = []):
+ index_list = [], output = ["*"], filter="", fusion=[], knn={}, match = {}, match_tensor = {}, match_sparse = {}, sort = [], output_res = []):
self.db_names = list
self.error_code = error_code
self.database_name = database_name # get database
@@ -728,6 +746,7 @@ def __init__(self, list = [], error_code = ErrorCode.OK, database_name = "" ,col
self._match = match
self._match_tensor = match_tensor
self._match_sparse = match_sparse
+ self._sort = sort
self.output_res = output_res
diff --git a/python/test_pysdk/test_select.py b/python/test_pysdk/test_select.py
index 84389836b4..4b3261b10d 100644
--- a/python/test_pysdk/test_select.py
+++ b/python/test_pysdk/test_select.py
@@ -752,7 +752,6 @@ def test_neg_func(self, suffix):
res = db_obj.drop_table("test_neg_func" + suffix, ConflictType.Error)
assert res.error_code == ErrorCode.OK
- @pytest.mark.usefixtures("skip_if_http")
def test_sort(self, suffix):
db_obj = self.infinity_obj.get_database("default_db")
@@ -785,5 +784,10 @@ def test_sort(self, suffix):
'c2': (0, 1, 1, 2, 2, 3, 3, 6, 7, 7, 8, 8, 9)})
.astype({'c1': dtype('int32'), 'c2': dtype('int32')}))
+ res = table_obj.output(["_row_id"]).sort([["_row_id", SortType.Desc]]).to_df()
+ #pd.testing.assert_frame_equal(res, pd.DataFrame({'ROW_ID': (12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)})
+ # .astype({'ROW_ID': dtype('int64')}))
+ print(res)
+
res = db_obj.drop_table("test_sort"+suffix, ConflictType.Error)
assert res.error_code == ErrorCode.OK
\ No newline at end of file
diff --git a/src/network/http/http_search.cpp b/src/network/http/http_search.cpp
index 614413f0da..09557fb020 100644
--- a/src/network/http/http_search.cpp
+++ b/src/network/http/http_search.cpp
@@ -42,6 +42,7 @@ import value;
import physical_import;
import explain_statement;
import internal_types;
+import select_statement;
namespace infinity {
@@ -63,6 +64,7 @@ void HTTPSearch::Process(Infinity *infinity_ptr,
UniquePtr offset{};
UniquePtr search_expr{};
Vector *output_columns{nullptr};
+ Vector *order_by_list{nullptr};
DeferFn defer_fn([&]() {
if (output_columns != nullptr) {
for (auto &expr : *output_columns) {
@@ -72,6 +74,17 @@ void HTTPSearch::Process(Infinity *infinity_ptr,
output_columns = nullptr;
}
});
+
+ DeferFn defer_fn_order([&]() {
+ if (order_by_list != nullptr) {
+ for (auto &expr : *order_by_list) {
+ delete expr;
+ }
+ delete order_by_list;
+ order_by_list = nullptr;
+ }
+ });
+
for (const auto &elem : input_json.items()) {
String key = elem.key();
ToLower(key);
@@ -92,6 +105,24 @@ void HTTPSearch::Process(Infinity *infinity_ptr,
if (output_columns == nullptr) {
return;
}
+ } else if (IsEqual(key, "sort")) {
+ if (order_by_list != nullptr) {
+ response["error_code"] = ErrorCode::kInvalidExpression;
+ response["error_message"] = "More than one sort field.";
+ return;
+ }
+
+ auto &list = elem.value();
+ if (!list.is_array()) {
+ response["error_code"] = ErrorCode::kInvalidExpression;
+ response["error_message"] = "Sort field should be array";
+ return;
+ }
+
+ order_by_list = ParseSort(list, http_status, response);
+ if (order_by_list == nullptr) {
+ return;
+ }
} else if (IsEqual(key, "filter")) {
if (filter) {
@@ -143,9 +174,10 @@ void HTTPSearch::Process(Infinity *infinity_ptr,
}
const QueryResult result =
- infinity_ptr->Search(db_name, table_name, search_expr.release(), filter.release(), limit.release(), offset.release(), output_columns, nullptr);
+ infinity_ptr->Search(db_name, table_name, search_expr.release(), filter.release(), limit.release(), offset.release(), output_columns, order_by_list);
output_columns = nullptr;
+ order_by_list = nullptr;
if (result.IsOk()) {
SizeT block_rows = result.result_table_->DataBlockCount();
for (SizeT block_id = 0; block_id < block_rows; ++block_id) {
@@ -415,6 +447,72 @@ Vector *HTTPSearch::ParseOutput(const nlohmann::json &output_list,
return res;
}
+Vector *HTTPSearch::ParseSort(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response) {
+ Vector *order_by_list = new Vector();
+ DeferFn defer_fn([&]() {
+ if (order_by_list != nullptr) {
+ for (auto &expr : *order_by_list) {
+ delete expr;
+ }
+ delete order_by_list;
+ order_by_list = nullptr;
+ }
+ });
+
+ for(const auto &order_expr : json_object) {
+ for (const auto &expression : order_expr.items()) {
+ String key = expression.key();
+ ToLower(key);
+ auto order_by_expr = MakeUnique();
+ if (key == "_row_id" or key == "_similarity" or key == "_distance" or key == "_score") {
+ auto parsed_expr = new FunctionExpr();
+ if (key == "_row_id") {
+ parsed_expr->func_name_ = "row_id";
+ } else if (key == "_similarity") {
+ parsed_expr->func_name_ = "similarity";
+ } else if (key == "_distance") {
+ parsed_expr->func_name_ = "distance";
+ } else if (key == "_score") {
+ parsed_expr->func_name_ = "score";
+ }
+ order_by_expr->expr_ = parsed_expr;
+ parsed_expr = nullptr;
+ } else {
+ UniquePtr expr_parsed_result = MakeUnique();
+ ExprParser expr_parser;
+ expr_parser.Parse(key, expr_parsed_result.get());
+ if (expr_parsed_result->IsError() || expr_parsed_result->exprs_ptr_->size() == 0) {
+ response["error_code"] = ErrorCode::kInvalidExpression;
+ response["error_message"] = fmt::format("Invalid expression: {}", key);
+ return nullptr;
+ }
+
+ order_by_expr->expr_ = expr_parsed_result->exprs_ptr_->at(0);
+ expr_parsed_result->exprs_ptr_->at(0) = nullptr;
+ }
+
+ String value = expression.value();
+ ToLower(value);
+ if (value == "asc") {
+ order_by_expr->type_ = OrderType::kAsc;
+ } else if (value == "desc") {
+ order_by_expr->type_ = OrderType::kDesc;
+ } else {
+ response["error_code"] = ErrorCode::kInvalidExpression;
+ response["error_message"] = fmt::format("Invalid expression: {}", value);
+ return nullptr;
+ }
+
+ order_by_list->emplace_back(order_by_expr.release());
+ }
+ }
+
+ // Avoiding DeferFN auto free the output expressions
+ Vector *res = order_by_list;
+ order_by_list = nullptr;
+ return res;
+}
+
UniquePtr HTTPSearch::ParseSearchExpr(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response) {
if (json_object.type() != nlohmann::json::value_t::array) {
response["error_code"] = ErrorCode::kInvalidExpression;
diff --git a/src/network/http/http_search.cppm b/src/network/http/http_search.cppm
index 93f4862d1e..0a9cbb8acb 100644
--- a/src/network/http/http_search.cppm
+++ b/src/network/http/http_search.cppm
@@ -29,6 +29,7 @@ import infinity;
import internal_types;
import constant_expr;
import search_expr;
+import select_statement;
namespace infinity {
@@ -48,6 +49,7 @@ public:
nlohmann::json &response);
static Vector *ParseOutput(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response);
+ static Vector *ParseSort(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response);
static UniquePtr ParseFilter(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response);
static UniquePtr ParseSearchExpr(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response);
static UniquePtr ParseFusion(const nlohmann::json &json_object, HTTPStatus &http_status, nlohmann::json &response);