47 Commits

Author SHA1 Message Date
Pijus Kamandulis
887d456ad4 Return error code if server fails to start 2025-02-03 22:58:45 +02:00
Pijus Kamandulis
da1566875b Wait for server shutdown when stopping server 2025-02-03 22:21:54 +02:00
Pijus Kamandulis
3fee3bc816 Fix ARRAY_CONTAINS partial matches for nested objects 2025-02-03 19:29:29 +02:00
Pijus Kamandulis
8657c48fc8 Added support for table alias; Make AS keyword optional #9 2025-02-03 19:02:12 +02:00
Pijus Kamandulis
e080888c20 Add shared-libraries to release 2025-01-28 23:28:23 +02:00
Pijus Kamandulis
b8d79fd945 Upgrade upload-artifact pipeline action 2025-01-28 21:31:05 +02:00
Pijus Kamandulis
f25cb7fb03 Stamp binaries with version control information 2025-01-28 21:15:49 +02:00
Pijus Kamandulis
125f10d8a2 Add more error handling and mutex guards 2025-01-27 21:09:37 +02:00
Pijus Kamandulis
d6b816b55a Fix docker tag 2025-01-25 21:17:32 +02:00
Pijus Kamandulis
12215fba76 Update dependancies 2025-01-25 21:11:42 +02:00
Pijus Kamandulis
a1793c17ab Added docker image with explorer included 2025-01-25 20:20:55 +02:00
Pijus Kamandulis
96d3a0a7ae Handle server close; Update logger 2025-01-15 00:26:15 +02:00
Pijus Kamandulis
8b8b087aab Added setting for LogLevel 2025-01-09 21:07:41 +02:00
Pijus Kamandulis
c2c9dc03b3 Fixed issue with wrong signature generation for pkrange requests 2025-01-09 20:19:28 +02:00
Pijus Kamandulis
d86bac7d79 Implement CRUD for UDFs, SPs and Triggers 2025-01-06 20:45:43 +02:00
Pijus Kamandulis
69b76c1c3e Simplify constant initialization in unit tests 2024-12-26 20:43:57 +02:00
Pijus Kamandulis
8e3db3e44d Added support for 'ARRAY_CONTAINS', 'ARRAY_CONTAINS_ANY' and 'ARRAY_CONTAINS_ALL' functions 2024-12-26 20:27:59 +02:00
Pijus Kamandulis
f5b8453995 Support patch operations 'set' and 'incr' #7 2024-12-25 23:32:50 +02:00
Pijus Kamandulis
928ca29fe4 Support parameter in bracket #8 2024-12-25 21:28:42 +02:00
Pijus Kamandulis
39cd9e2357 Update dependancies 2024-12-20 20:27:42 +02:00
Pijus Kamandulis
bcf4b513b6 Expose repository functions to sharedlibs 2024-12-20 20:25:32 +02:00
Pijus Kamandulis
363f822e5a Added some tests for sharedlibrary 2024-12-19 23:21:45 +02:00
Pijus Kamandulis
be7a615931 Cross-Compile Shared Libraries 2024-12-19 00:48:17 +02:00
Pijus Kamandulis
83f086a2dc Configuration fixes 2024-12-18 23:28:04 +02:00
Pijus Kamandulis
777034181f Refactor to support multiple server instances in shared library 2024-12-18 19:39:57 +02:00
Pijus Kamandulis
84c33e3c8e Upgrade dependancies 2024-12-18 00:34:10 +02:00
Pijus Kamandulis
5e677431a3 Prepare for sharedlibrary builds 2024-12-18 00:28:59 +02:00
Pijus Kamandulis
a4659d90a9 Enable multi-platform docker builds 2024-12-08 18:55:20 +02:00
Pijus Kamandulis
503e6bb8ad Update compatibility matrix 2024-12-08 18:17:37 +02:00
Pijus Kamandulis
e5ddc143f0 Improved concurrency handling 2024-12-08 17:54:58 +02:00
Pijus Kamandulis
66ea859f34 Add support for subqueries 2024-12-07 22:29:26 +02:00
Pijus Kamandulis
3584f9b5ce Enable ARM builds for Windows and Linux 2024-11-16 20:09:24 +02:00
Pijus Kamandulis
c7d01b4593 Fix cosmos explorer incorrect redirect 2024-11-14 18:42:17 +02:00
erikzeneco
2834f3f641 check isUpsert header in POST document request (#5)
* check isUpsert header in POST document request

* Verify response code on "CreateItem that already exists" test

---------

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>
2024-11-01 21:11:59 +02:00
Pijus Kamandulis
a6b5d32ff7 Merge pull request #4 from pikami/erikzeneco/serve_request_paths_with_trailing_slashes
Erikzeneco/serve request paths with trailing slashes
2024-10-29 18:06:56 +02:00
Pijus Kamandulis
0e98e3481a Strip trailing slash using middleware 2024-10-28 20:20:52 +02:00
Erik Zentveld
827046f634 re-add removed blank lines 2024-10-28 16:18:49 +01:00
Erik Zentveld
475d586dc5 Merge branch 'master' into serve_request_paths_with_trailing_slashes 2024-10-28 14:37:01 +01:00
Erik Zentveld
9abef691d6 serve request paths with trailing slashes, as sent by python client 2024-10-28 13:29:26 +01:00
Pijus Kamandulis
62dcbc1f2b Merge pull request #1 from erikzeneco/master
Update README.md
2024-10-16 18:34:14 +03:00
erikzeneco
2f42651fb7 Update README.md
Use envPrefix for parameter passed in as environment variable with docker.
2024-10-16 16:24:53 +02:00
Pijus Kamandulis
20af73ee9c Partial JOIN implementation 2024-07-17 21:56:17 +03:00
Pijus Kamandulis
3bdff9b643 Implement Mathematical Functions 2024-06-19 00:44:46 +03:00
Pijus Kamandulis
b808e97c72 Fix array access 2024-06-03 19:00:52 +03:00
Pijus Kamandulis
e623a563f4 Update dependencies 2024-06-01 19:55:06 +03:00
Pijus Kamandulis
2cd61aa620 Implement document PATCH operation 2024-06-01 19:52:07 +03:00
Pijus Kamandulis
0cec7816c1 Fixed authentication key generation for partition key ranges
Fixed collection rid generation

Improved compatibility with SDKs
2024-06-01 02:32:52 +03:00
97 changed files with 11770 additions and 2654 deletions

View File

@@ -0,0 +1,31 @@
name: Cross-Compile Shared Libraries
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@v3.1.0
with:
xgo_version: latest
go_version: 1.22.0
dest: dist
pkg: sharedlibrary
prefix: cosmium
targets: linux/amd64,linux/arm64,windows/amd64,windows/arm64,darwin/amd64,darwin/arm64
v: true
buildmode: c-shared
buildvcs: true
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: shared-libraries
path: dist/*

View File

@@ -17,16 +17,32 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: 1.21.6 go-version: 1.22.0
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@v3.1.0
with:
xgo_version: latest
go_version: 1.22.0
dest: sharedlibrary_dist
pkg: sharedlibrary
prefix: cosmium
targets: linux/amd64,linux/arm64,windows/amd64,windows/arm64,darwin/amd64,darwin/arm64
v: true
buildmode: c-shared
buildvcs: true
- name: Docker Login - name: Docker Login
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v5
with: with:

5
.gitignore vendored
View File

@@ -1,2 +1,7 @@
dist/ dist/
sharedlibrary_dist/
ignored/ ignored/
explorer_www/
main
save.json
.vscode/

View File

@@ -1,5 +1,6 @@
builds: builds:
- binary: cosmium - binary: cosmium
main: ./cmd/server
goos: goos:
- darwin - darwin
- linux - linux
@@ -9,11 +10,6 @@ builds:
- arm64 - arm64
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ignore:
- goos: linux
goarch: arm64
- goos: windows
goarch: arm64
release: release:
prerelease: auto prerelease: auto
@@ -30,13 +26,32 @@ brews:
commit_author: commit_author:
name: pikami name: pikami
email: git@pikami.org email: git@pikami.org
skip_upload: auto
archives:
- id: bundle
format: tar.gz
format_overrides:
- goos: windows
format: zip
- id: shared-libraries
meta: true
format: "tar.gz"
wrap_in_directory: true
name_template: "{{ .ProjectName }}_{{ .Version }}_shared-libraries"
files:
- LICENSE
- README.md
- sharedlibrary_dist/**
dockers: dockers:
- image_templates: - id: docker-linux-amd64
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}" goos: linux
- "ghcr.io/pikami/{{ .ProjectName }}:latest" goarch: amd64
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-amd64"
dockerfile: Dockerfile dockerfile: Dockerfile
use: docker use: buildx
build_flag_templates: build_flag_templates:
- "--platform=linux/amd64" - "--platform=linux/amd64"
- "--pull" - "--pull"
@@ -47,6 +62,79 @@ dockers:
- "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.version={{.Version}}"
- id: docker-linux-arm64
goos: linux
goarch: arm64
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64v8"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64"
- "--pull"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.description=Lightweight Cosmos DB emulator"
- "--label=org.opencontainers.image.url=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.source=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- id: docker-explorer-linux-amd64
goos: linux
goarch: amd64
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-amd64"
dockerfile: Explorer.Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--pull"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.description=Lightweight Cosmos DB emulator"
- "--label=org.opencontainers.image.url=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.source=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- id: docker-explorer-linux-arm64
goos: linux
goarch: arm64
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-arm64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-arm64v8"
dockerfile: Explorer.Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64"
- "--pull"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.description=Lightweight Cosmos DB emulator"
- "--label=org.opencontainers.image.url=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.source=https://github.com/pikami/cosmium"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
docker_manifests:
- name_template: 'ghcr.io/pikami/{{ .ProjectName }}:latest'
skip_push: auto
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-amd64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64v8"
- name_template: 'ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}'
skip_push: auto
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-amd64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-arm64v8"
- name_template: 'ghcr.io/pikami/{{ .ProjectName }}:explorer'
skip_push: auto
image_templates:
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-amd64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-arm64"
- "ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-explorer-arm64v8"
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'

View File

@@ -1,4 +1,4 @@
FROM scratch FROM alpine:latest
WORKDIR /app WORKDIR /app
COPY cosmium /app/cosmium COPY cosmium /app/cosmium

9
Explorer.Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM ghcr.io/cosmiumdev/cosmos-explorer-base:latest AS explorer-base
FROM alpine:latest
COPY --from=explorer-base /cosmos-explorer /cosmos-explorer
WORKDIR /app
COPY cosmium /app/cosmium
ENTRYPOINT ["/app/cosmium", "-ExplorerDir", "/cosmos-explorer"]

View File

@@ -4,28 +4,65 @@ GOTEST=$(GOCMD) test
GOCLEAN=$(GOCMD) clean GOCLEAN=$(GOCMD) clean
BINARY_NAME=cosmium BINARY_NAME=cosmium
SERVER_LOCATION=./cmd/server
SHARED_LIB_LOCATION=./sharedlibrary
SHARED_LIB_OPT=-buildmode=c-shared
XGO_TARGETS=linux/amd64,linux/arm64,windows/amd64,windows/arm64,darwin/amd64,darwin/arm64
GOVERSION=1.22.0
DIST_DIR=dist DIST_DIR=dist
SHARED_LIB_TEST_CC=gcc
SHARED_LIB_TEST_CFLAGS=-Wall -ldl
SHARED_LIB_TEST_TARGET=$(DIST_DIR)/sharedlibrary_test
SHARED_LIB_TEST_DIR=./sharedlibrary/tests
SHARED_LIB_TEST_SOURCES=$(wildcard $(SHARED_LIB_TEST_DIR)/*.c)
all: test build-all all: test build-all
build-all: build-darwin-arm64 build-darwin-amd64 build-linux-amd64 build-windows-amd64 build-all: build-darwin-arm64 build-darwin-amd64 build-linux-amd64 build-linux-arm64 build-windows-amd64 build-windows-arm64
build-darwin-arm64: build-darwin-arm64:
@echo "Building macOS ARM binary..." @echo "Building macOS ARM binary..."
@GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64 . @GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64 $(SERVER_LOCATION)
build-darwin-amd64: build-darwin-amd64:
@echo "Building macOS x64 binary..." @echo "Building macOS x64 binary..."
@GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-amd64 . @GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-amd64 $(SERVER_LOCATION)
build-linux-amd64: build-linux-amd64:
@echo "Building Linux x64 binary..." @echo "Building Linux x64 binary..."
@GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 . @GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64 $(SERVER_LOCATION)
build-linux-arm64:
@echo "Building Linux ARM binary..."
@GOOS=linux GOARCH=arm64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-linux-arm64 $(SERVER_LOCATION)
build-windows-amd64: build-windows-amd64:
@echo "Building Windows x64 binary..." @echo "Building Windows x64 binary..."
@GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe . @GOOS=windows GOARCH=amd64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-windows-amd64.exe $(SERVER_LOCATION)
build-windows-arm64:
@echo "Building Windows ARM binary..."
@GOOS=windows GOARCH=arm64 $(GOBUILD) -o $(DIST_DIR)/$(BINARY_NAME)-windows-arm64.exe $(SERVER_LOCATION)
build-sharedlib-linux-amd64:
@echo "Building shared library for Linux x64..."
@GOOS=linux GOARCH=amd64 $(GOBUILD) $(SHARED_LIB_OPT) -o $(DIST_DIR)/$(BINARY_NAME)-linux-amd64.so $(SHARED_LIB_LOCATION)
build-sharedlib-tests: build-sharedlib-linux-amd64
@echo "Building shared library tests..."
@$(SHARED_LIB_TEST_CC) $(SHARED_LIB_TEST_CFLAGS) -o $(SHARED_LIB_TEST_TARGET) $(SHARED_LIB_TEST_SOURCES)
run-sharedlib-tests: build-sharedlib-tests
@echo "Running shared library tests..."
@$(SHARED_LIB_TEST_TARGET) $(DIST_DIR)/$(BINARY_NAME)-linux-amd64.so
xgo-compile-sharedlib:
@echo "Building shared libraries using xgo..."
@mkdir -p $(DIST_DIR)
@xgo -targets=$(XGO_TARGETS) -go $(GOVERSION) -buildmode=c-shared -dest=$(DIST_DIR) -out=$(BINARY_NAME) -pkg=$(SHARED_LIB_LOCATION) .
generate-parser-nosql: generate-parser-nosql:
pigeon -o ./parsers/nosql/nosql.go ./parsers/nosql/nosql.peg pigeon -o ./parsers/nosql/nosql.go ./parsers/nosql/nosql.peg

View File

@@ -1,11 +1,13 @@
# Cosmium # Cosmium
Cosmium is a lightweight Cosmos DB emulator designed to facilitate local development and testing. While it aims to provide developers with a solution for running a local database during development, it's important to note that it's not 100% compatible with Cosmos DB. However, it serves as a convenient tool for E2E or integration tests during the CI/CD pipeline. Read more about compatibility [here](docs/compatibility.md). Cosmium is a lightweight Cosmos DB emulator designed to facilitate local development and testing. While it aims to provide developers with a solution for running a local database during development, it's important to note that it's not 100% compatible with Cosmos DB. However, it serves as a convenient tool for E2E or integration tests during the CI/CD pipeline. Read more about compatibility [here](./docs/COMPATIBILITY.md).
One of Cosmium's notable features is its ability to save and load state to a single JSON file. This feature makes it easy to load different test cases or share state with other developers, enhancing collaboration and efficiency in development workflows. One of Cosmium's notable features is its ability to save and load state to a single JSON file. This feature makes it easy to load different test cases or share state with other developers, enhancing collaboration and efficiency in development workflows.
# Getting Started # Getting Started
### Installation via Homebrew ### Installation via Homebrew
You can install Cosmium using Homebrew by adding the `pikami/brew` tap and then installing the package. You can install Cosmium using Homebrew by adding the `pikami/brew` tap and then installing the package.
```sh ```sh
@@ -23,10 +25,12 @@ You can download the latest version of Cosmium from the [GitHub Releases page](h
Cosmium is available for the following platforms: Cosmium is available for the following platforms:
* **Linux**: cosmium-linux-amd64 - **Linux**: cosmium-linux-amd64
* **macOS**: cosmium-darwin-amd64 - **Linux on ARM**: cosmium-linux-arm64
* **macOS on Apple Silicon**: cosmium-darwin-arm64 - **macOS**: cosmium-darwin-amd64
* **Windows**: cosmium-windows-amd64.exe - **macOS on Apple Silicon**: cosmium-darwin-arm64
- **Windows**: cosmium-windows-amd64.exe
- **Windows on ARM**: cosmium-windows-arm64.exe
### Running Cosmium ### Running Cosmium
@@ -37,25 +41,34 @@ cosmium -Persist "./save.json"
``` ```
Connection String Example: Connection String Example:
``` ```
AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==; AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;
``` ```
### Running Cosmos DB Explorer ### Running Cosmos DB Explorer
If you want to run Cosmos DB Explorer alongside Cosmium, you'll need to build it yourself and point the `-ExplorerDir` argument to the dist directory. Please refer to the [Cosmos DB Explorer repository](https://github.com/Azure/cosmos-explorer) for instructions on building the application. If you want to run Cosmos DB Explorer alongside Cosmium, you'll need to build it yourself and point the `-ExplorerDir` argument to the dist directory. Please refer to the [Cosmos DB Explorer repository](https://github.com/Azure/cosmos-explorer) for instructions on building the application.
There's also a prebuilt docker image that includes the explorer: `ghcr.io/pikami/cosmium:explorer`
Once running, the explorer can be reached by navigating following URL: `https://127.0.0.1:8081/_explorer/` (might be different depending on your configuration). Once running, the explorer can be reached by navigating following URL: `https://127.0.0.1:8081/_explorer/` (might be different depending on your configuration).
### Running with docker (optional) ### Running with docker (optional)
There are two docker tags available:
- ghcr.io/pikami/cosmium:latest - Cosmium core service
- ghcr.io/pikami/cosmium:explorer - Cosmium with database explorer available on `https://127.0.0.1:8081/_explorer/`
If you wan to run the application using docker, configure it using environment variables see example: If you wan to run the application using docker, configure it using environment variables see example:
```sh ```sh
docker run --rm \ docker run --rm \
-e Persist=/save.json \ -e COSMIUM_PERSIST=/save.json \
-v ./save.json:/save.json \ -v ./save.json:/save.json \
-p 8081:8081 \ -p 8081:8081 \
ghcr.io/pikami/cosmium ghcr.io/pikami/cosmium # or `ghcr.io/pikami/cosmium:explorer`
``` ```
### SSL Certificate ### SSL Certificate
@@ -66,24 +79,26 @@ To disable SSL and run Cosmium on HTTP instead, you can use the `-DisableTls` fl
### Other Available Arguments ### Other Available Arguments
* **-AccountKey**: Account key for authentication (default "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==") - **-AccountKey**: Account key for authentication (default "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==")
* **-DisableAuth**: Disable authentication - **-DisableAuth**: Disable authentication
* **-Host**: Hostname (default "localhost") - **-Host**: Hostname (default "localhost")
* **-InitialData**: Path to JSON containing initial state - **-InitialData**: Path to JSON containing initial state
* **-Persist**: Saves data to the given path on application exit (When `-InitialData` argument is not supplied, it will try to load data from path supplied in `-Persist`) - **-Persist**: Saves data to the given path on application exit (When `-InitialData` argument is not supplied, it will try to load data from path supplied in `-Persist`)
* **-Port**: Listen port (default 8081) - **-Port**: Listen port (default 8081)
* **-Debug**: Runs application in debug mode, this provides additional logging - **-LogLevel**: Sets the logging level (one of: debug, info, error, silent) (default info)
These arguments allow you to configure various aspects of Cosmium's behavior according to your requirements. These arguments allow you to configure various aspects of Cosmium's behavior according to your requirements.
All mentioned arguments can also be set using environment variables: All mentioned arguments can also be set using environment variables:
* **COSMIUM_ACCOUNTKEY** for `-AccountKey`
* **COSMIUM_DISABLEAUTH** for `-DisableAuth` - **COSMIUM_ACCOUNTKEY** for `-AccountKey`
* **COSMIUM_HOST** for `-Host` - **COSMIUM_DISABLEAUTH** for `-DisableAuth`
* **COSMIUM_INITIALDATA** for `-InitialData` - **COSMIUM_HOST** for `-Host`
* **COSMIUM_PERSIST** for `-Persist` - **COSMIUM_INITIALDATA** for `-InitialData`
* **COSMIUM_PORT** for `-Port` - **COSMIUM_PERSIST** for `-Persist`
* **COSMIUM_DEBUG** for `-Debug` - **COSMIUM_PORT** for `-Port`
- **COSMIUM_LOGLEVEL** for `-LogLevel`
# License # License
This project is [MIT licensed](./LICENSE). This project is [MIT licensed](./LICENSE).

39
api/api_server.go Normal file
View File

@@ -0,0 +1,39 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
)
type ApiServer struct {
stopServer chan interface{}
onServerShutdown chan interface{}
isActive bool
router *gin.Engine
config config.ServerConfig
}
func NewApiServer(dataRepository *repositories.DataRepository, config config.ServerConfig) *ApiServer {
stopChan := make(chan interface{})
onServerShutdownChan := make(chan interface{})
apiServer := &ApiServer{
stopServer: stopChan,
onServerShutdown: onServerShutdownChan,
config: config,
}
apiServer.CreateRouter(dataRepository)
return apiServer
}
func (s *ApiServer) GetRouter() *gin.Engine {
return s.router
}
func (s *ApiServer) Stop() {
s.stopServer <- true
<-s.onServerShutdown
}

View File

@@ -5,16 +5,17 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/pikami/cosmium/internal/logger"
) )
const ( const (
DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==" DefaultAccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="
EnvPrefix = "COSMIUM_" EnvPrefix = "COSMIUM_"
ExplorerBaseUrlLocation = "/_explorer"
) )
var Config = ServerConfig{} func ParseFlags() ServerConfig {
func ParseFlags() {
host := flag.String("Host", "localhost", "Hostname") host := flag.String("Host", "localhost", "Hostname")
port := flag.Int("Port", 8081, "Listen port") port := flag.Int("Port", 8081, "Listen port")
explorerPath := flag.String("ExplorerDir", "", "Path to cosmos-explorer files") explorerPath := flag.String("ExplorerDir", "", "Path to cosmos-explorer files")
@@ -25,26 +26,60 @@ func ParseFlags() {
disableAuthentication := flag.Bool("DisableAuth", false, "Disable authentication") disableAuthentication := flag.Bool("DisableAuth", false, "Disable authentication")
disableTls := flag.Bool("DisableTls", false, "Disable TLS, serve over HTTP") disableTls := flag.Bool("DisableTls", false, "Disable TLS, serve over HTTP")
persistDataPath := flag.String("Persist", "", "Saves data to given path on application exit") persistDataPath := flag.String("Persist", "", "Saves data to given path on application exit")
debug := flag.Bool("Debug", false, "Runs application in debug mode, this provides additional logging") logLevel := NewEnumValue("info", []string{"debug", "info", "error", "silent"})
flag.Var(logLevel, "LogLevel", fmt.Sprintf("Sets the logging level %s", logLevel.AllowedValuesList()))
flag.Parse() flag.Parse()
setFlagsFromEnvironment() setFlagsFromEnvironment()
Config.Host = *host config := ServerConfig{}
Config.Port = *port config.Host = *host
Config.ExplorerPath = *explorerPath config.Port = *port
Config.TLS_CertificatePath = *tlsCertificatePath config.ExplorerPath = *explorerPath
Config.TLS_CertificateKey = *tlsCertificateKey config.TLS_CertificatePath = *tlsCertificatePath
Config.InitialDataFilePath = *initialDataPath config.TLS_CertificateKey = *tlsCertificateKey
Config.PersistDataFilePath = *persistDataPath config.InitialDataFilePath = *initialDataPath
Config.DisableAuth = *disableAuthentication config.PersistDataFilePath = *persistDataPath
Config.DisableTls = *disableTls config.DisableAuth = *disableAuthentication
Config.Debug = *debug config.DisableTls = *disableTls
config.AccountKey = *accountKey
config.LogLevel = logLevel.value
Config.DatabaseAccount = Config.Host config.PopulateCalculatedFields()
Config.DatabaseDomain = Config.Host
Config.DatabaseEndpoint = fmt.Sprintf("https://%s:%d/", Config.Host, Config.Port) return config
Config.AccountKey = *accountKey }
func (c *ServerConfig) PopulateCalculatedFields() {
c.DatabaseAccount = c.Host
c.DatabaseDomain = c.Host
c.DatabaseEndpoint = fmt.Sprintf("https://%s:%d/", c.Host, c.Port)
c.ExplorerBaseUrlLocation = ExplorerBaseUrlLocation
switch c.LogLevel {
case "debug":
logger.SetLogLevel(logger.LogLevelDebug)
case "info":
logger.SetLogLevel(logger.LogLevelInfo)
case "error":
logger.SetLogLevel(logger.LogLevelError)
case "silent":
logger.SetLogLevel(logger.LogLevelSilent)
default:
logger.SetLogLevel(logger.LogLevelInfo)
}
}
func (c *ServerConfig) ApplyDefaultsToEmptyFields() {
if c.Host == "" {
c.Host = "localhost"
}
if c.Port == 0 {
c.Port = 8081
}
if c.AccountKey == "" {
c.AccountKey = DefaultAccountKey
}
} }
func setFlagsFromEnvironment() (err error) { func setFlagsFromEnvironment() (err error) {

36
api/config/enumFlag.go Normal file
View File

@@ -0,0 +1,36 @@
package config
import (
"fmt"
"strings"
)
type EnumValue struct {
allowedValues []string
value string
}
func (e *EnumValue) String() string {
return e.value
}
func (e *EnumValue) Set(v string) error {
for _, allowed := range e.allowedValues {
if v == allowed {
e.value = v
return nil
}
}
return fmt.Errorf("invalid value %q, must be one of: %s", v, strings.Join(e.allowedValues, ", "))
}
func NewEnumValue(defaultValue string, allowedValues []string) *EnumValue {
return &EnumValue{
allowedValues: allowedValues,
value: defaultValue,
}
}
func (e *EnumValue) AllowedValuesList() string {
return fmt.Sprintf("(one of: %s)", strings.Join(e.allowedValues, ", "))
}

View File

@@ -1,19 +1,20 @@
package config package config
type ServerConfig struct { type ServerConfig struct {
DatabaseAccount string DatabaseAccount string `json:"databaseAccount"`
DatabaseDomain string DatabaseDomain string `json:"databaseDomain"`
DatabaseEndpoint string DatabaseEndpoint string `json:"databaseEndpoint"`
AccountKey string AccountKey string `json:"accountKey"`
ExplorerPath string ExplorerPath string `json:"explorerPath"`
Port int Port int `json:"port"`
Host string Host string `json:"host"`
TLS_CertificatePath string TLS_CertificatePath string `json:"tlsCertificatePath"`
TLS_CertificateKey string TLS_CertificateKey string `json:"tlsCertificateKey"`
InitialDataFilePath string InitialDataFilePath string `json:"initialDataFilePath"`
PersistDataFilePath string PersistDataFilePath string `json:"persistDataFilePath"`
DisableAuth bool DisableAuth bool `json:"disableAuth"`
DisableTls bool DisableTls bool `json:"disableTls"`
Debug bool LogLevel string `json:"logLevel"`
ExplorerBaseUrlLocation string `json:"explorerBaseUrlLocation"`
} }

View File

@@ -1,20 +1,21 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllCollections(c *gin.Context) { func (h *Handlers) GetAllCollections(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collections, status := repositories.GetAllCollections(databaseId) collections, status := h.repository.GetAllCollections(databaseId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
database, _ := repositories.GetDatabase(databaseId) database, _ := h.repository.GetDatabase(databaseId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(collections)))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": database.ResourceID, "_rid": database.ResourceID,
"DocumentCollections": collections, "DocumentCollections": collections,
@@ -26,11 +27,11 @@ func GetAllCollections(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func GetCollection(c *gin.Context) { func (h *Handlers) GetCollection(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
id := c.Param("collId") id := c.Param("collId")
collection, status := repositories.GetCollection(databaseId, id) collection, status := h.repository.GetCollection(databaseId, id)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, collection) c.IndentedJSON(http.StatusOK, collection)
return return
@@ -44,11 +45,11 @@ func GetCollection(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func DeleteCollection(c *gin.Context) { func (h *Handlers) DeleteCollection(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
id := c.Param("collId") id := c.Param("collId")
status := repositories.DeleteCollection(databaseId, id) status := h.repository.DeleteCollection(databaseId, id)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
@@ -62,7 +63,7 @@ func DeleteCollection(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func CreateCollection(c *gin.Context) { func (h *Handlers) CreateCollection(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
var newCollection repositorymodels.Collection var newCollection repositorymodels.Collection
@@ -76,7 +77,7 @@ func CreateCollection(c *gin.Context) {
return return
} }
createdCollection, status := repositories.CreateCollection(databaseId, newCollection) createdCollection, status := h.repository.CreateCollection(databaseId, newCollection)
if status == repositorymodels.Conflict { if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return return

View File

@@ -4,9 +4,14 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
) )
func CosmiumExport(c *gin.Context) { func (h *Handlers) CosmiumExport(c *gin.Context) {
c.IndentedJSON(http.StatusOK, repositories.GetState()) repositoryState, err := h.repository.GetState()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, "application/json", []byte(repositoryState))
} }

View File

@@ -1,16 +1,17 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllDatabases(c *gin.Context) { func (h *Handlers) GetAllDatabases(c *gin.Context) {
databases, status := repositories.GetAllDatabases() databases, status := h.repository.GetAllDatabases()
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(databases)))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "", "_rid": "",
"Databases": databases, "Databases": databases,
@@ -22,10 +23,10 @@ func GetAllDatabases(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func GetDatabase(c *gin.Context) { func (h *Handlers) GetDatabase(c *gin.Context) {
id := c.Param("databaseId") id := c.Param("databaseId")
database, status := repositories.GetDatabase(id) database, status := h.repository.GetDatabase(id)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, database) c.IndentedJSON(http.StatusOK, database)
return return
@@ -39,10 +40,10 @@ func GetDatabase(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func DeleteDatabase(c *gin.Context) { func (h *Handlers) DeleteDatabase(c *gin.Context) {
id := c.Param("databaseId") id := c.Param("databaseId")
status := repositories.DeleteDatabase(id) status := h.repository.DeleteDatabase(id)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
@@ -56,7 +57,7 @@ func DeleteDatabase(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func CreateDatabase(c *gin.Context) { func (h *Handlers) CreateDatabase(c *gin.Context) {
var newDatabase repositorymodels.Database var newDatabase repositorymodels.Database
if err := c.BindJSON(&newDatabase); err != nil { if err := c.BindJSON(&newDatabase); err != nil {
@@ -69,7 +70,7 @@ func CreateDatabase(c *gin.Context) {
return return
} }
createdDatabase, status := repositories.CreateDatabase(newDatabase) createdDatabase, status := h.repository.CreateDatabase(newDatabase)
if status == repositorymodels.Conflict { if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return return

View File

@@ -1,22 +1,27 @@
package handlers package handlers
import ( import (
"encoding/json"
"fmt"
"net/http" "net/http"
"strconv"
jsonpatch "github.com/cosmiumdev/json-patch/v5"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/constants" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/internal/logger"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllDocuments(c *gin.Context) { func (h *Handlers) GetAllDocuments(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
documents, status := repositories.GetAllDocuments(databaseId, collectionId) documents, status := h.repository.GetAllDocuments(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
collection, _ := repositories.GetCollection(databaseId, collectionId) collection, _ := h.repository.GetCollection(databaseId, collectionId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(documents)))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": collection.ID, "_rid": collection.ID,
"Documents": documents, "Documents": documents,
@@ -28,12 +33,12 @@ func GetAllDocuments(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func GetDocument(c *gin.Context) { func (h *Handlers) GetDocument(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
documentId := c.Param("docId") documentId := c.Param("docId")
document, status := repositories.GetDocument(databaseId, collectionId, documentId) document, status := h.repository.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, document) c.IndentedJSON(http.StatusOK, document)
return return
@@ -47,12 +52,12 @@ func GetDocument(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func DeleteDocument(c *gin.Context) { func (h *Handlers) DeleteDocument(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
documentId := c.Param("docId") documentId := c.Param("docId")
status := repositories.DeleteDocument(databaseId, collectionId, documentId) status := h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
@@ -67,7 +72,7 @@ func DeleteDocument(c *gin.Context) {
} }
// TODO: Maybe move "replace" logic to repository // TODO: Maybe move "replace" logic to repository
func ReplaceDocument(c *gin.Context) { func (h *Handlers) ReplaceDocument(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
documentId := c.Param("docId") documentId := c.Param("docId")
@@ -78,13 +83,13 @@ func ReplaceDocument(c *gin.Context) {
return return
} }
status := repositories.DeleteDocument(databaseId, collectionId, documentId) status := h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound { if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return return
} }
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody) createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict { if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return return
@@ -98,7 +103,83 @@ func ReplaceDocument(c *gin.Context) {
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func DocumentsPost(c *gin.Context) { func (h *Handlers) PatchDocument(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
documentId := c.Param("docId")
document, status := h.repository.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
var requestBody map[string]interface{}
if err := c.BindJSON(&requestBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
operations := requestBody["operations"]
operationsBytes, err := json.Marshal(operations)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "Could not decode operations"})
return
}
patch, err := jsonpatch.DecodePatch(operationsBytes)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
currentDocumentBytes, err := json.Marshal(document)
if err != nil {
logger.ErrorLn("Failed to marshal existing document:", err)
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to marshal existing document"})
return
}
modifiedDocumentBytes, err := patch.Apply(currentDocumentBytes)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
var modifiedDocument map[string]interface{}
err = json.Unmarshal(modifiedDocumentBytes, &modifiedDocument)
if err != nil {
logger.ErrorLn("Failed to unmarshal modified document:", err)
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to unmarshal modified document"})
return
}
if modifiedDocument["id"] != document["id"] {
c.JSON(http.StatusUnprocessableEntity, gin.H{"message": "The ID field cannot be modified"})
return
}
status = h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, modifiedDocument)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) DocumentsPost(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
@@ -120,14 +201,15 @@ func DocumentsPost(c *gin.Context) {
queryParameters = parametersToMap(paramsArray) queryParameters = parametersToMap(paramsArray)
} }
docs, status := repositories.ExecuteQueryDocuments(databaseId, collectionId, query.(string), queryParameters) docs, status := h.repository.ExecuteQueryDocuments(databaseId, collectionId, query.(string), queryParameters)
if status != repositorymodels.StatusOk { if status != repositorymodels.StatusOk {
// TODO: Currently we return everything if the query fails // TODO: Currently we return everything if the query fails
GetAllDocuments(c) h.GetAllDocuments(c)
return return
} }
collection, _ := repositories.GetCollection(databaseId, collectionId) collection, _ := h.repository.GetCollection(databaseId, collectionId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(docs)))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": collection.ResourceID, "_rid": collection.ResourceID,
"Documents": docs, "Documents": docs,
@@ -141,7 +223,12 @@ func DocumentsPost(c *gin.Context) {
return return
} }
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody) isUpsert, _ := strconv.ParseBool(c.GetHeader("x-ms-documentdb-is-upsert"))
if isUpsert {
h.repository.DeleteDocument(databaseId, collectionId, requestBody["id"].(string))
}
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict { if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return return

View File

@@ -4,15 +4,14 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
) )
func RegisterExplorerHandlers(router *gin.Engine) { func (h *Handlers) RegisterExplorerHandlers(router *gin.Engine) {
explorer := router.Group("/_explorer") explorer := router.Group(h.config.ExplorerBaseUrlLocation)
{ {
explorer.Use(func(ctx *gin.Context) { explorer.Use(func(ctx *gin.Context) {
if ctx.Param("filepath") == "/config.json" { if ctx.Param("filepath") == "/config.json" {
endpoint := fmt.Sprintf("https://%s:%d", config.Config.Host, config.Config.Port) endpoint := fmt.Sprintf("https://%s:%d", h.config.Host, h.config.Port)
ctx.JSON(200, gin.H{ ctx.JSON(200, gin.H{
"BACKEND_ENDPOINT": endpoint, "BACKEND_ENDPOINT": endpoint,
"MONGO_BACKEND_ENDPOINT": endpoint, "MONGO_BACKEND_ENDPOINT": endpoint,
@@ -25,8 +24,8 @@ func RegisterExplorerHandlers(router *gin.Engine) {
} }
}) })
if config.Config.ExplorerPath != "" { if h.config.ExplorerPath != "" {
explorer.Static("/", config.Config.ExplorerPath) explorer.Static("/", h.config.ExplorerPath)
} }
} }
} }

18
api/handlers/handlers.go Normal file
View File

@@ -0,0 +1,18 @@
package handlers
import (
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
)
type Handlers struct {
repository *repositories.DataRepository
config config.ServerConfig
}
func NewHandlers(dataRepository *repositories.DataRepository, config config.ServerConfig) *Handlers {
return &Handlers{
repository: dataRepository,
config: config,
}
}

View File

@@ -10,44 +10,22 @@ import (
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
) )
func Authentication() gin.HandlerFunc { func Authentication(config config.ServerConfig) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
requestUrl := c.Request.URL.String() requestUrl := c.Request.URL.String()
if config.Config.DisableAuth || if config.DisableAuth ||
strings.HasPrefix(requestUrl, "/_explorer") || strings.HasPrefix(requestUrl, config.ExplorerBaseUrlLocation) ||
strings.HasPrefix(requestUrl, "/cosmium") { strings.HasPrefix(requestUrl, "/cosmium") {
return return
} }
var resourceType string resourceType := urlToResourceType(requestUrl)
parts := strings.Split(requestUrl, "/") resourceId := requestToResourceId(c)
switch len(parts) {
case 2, 3:
resourceType = parts[1]
case 4, 5:
resourceType = parts[3]
case 6, 7:
resourceType = parts[5]
}
databaseId, _ := c.Params.Get("databaseId")
collId, _ := c.Params.Get("collId")
docId, _ := c.Params.Get("docId")
var resourceId string
if databaseId != "" {
resourceId += "dbs/" + databaseId
}
if collId != "" {
resourceId += "/colls/" + collId
}
if docId != "" {
resourceId += "/docs/" + docId
}
authHeader := c.Request.Header.Get("authorization") authHeader := c.Request.Header.Get("authorization")
date := c.Request.Header.Get("x-ms-date") date := c.Request.Header.Get("x-ms-date")
expectedSignature := authentication.GenerateSignature( expectedSignature := authentication.GenerateSignature(
c.Request.Method, resourceType, resourceId, date, config.Config.AccountKey) c.Request.Method, resourceType, resourceId, date, config.AccountKey)
decoded, _ := url.QueryUnescape(authHeader) decoded, _ := url.QueryUnescape(authHeader)
params, _ := url.ParseQuery(decoded) params, _ := url.ParseQuery(decoded)
@@ -62,3 +40,44 @@ func Authentication() gin.HandlerFunc {
} }
} }
} }
func urlToResourceType(requestUrl string) string {
var resourceType string
parts := strings.Split(requestUrl, "/")
switch len(parts) {
case 2, 3:
resourceType = parts[1]
case 4, 5:
resourceType = parts[3]
case 6, 7:
resourceType = parts[5]
}
return resourceType
}
func requestToResourceId(c *gin.Context) string {
databaseId, _ := c.Params.Get("databaseId")
collId, _ := c.Params.Get("collId")
docId, _ := c.Params.Get("docId")
resourceType := urlToResourceType(c.Request.URL.String())
var resourceId string
if databaseId != "" {
resourceId += "dbs/" + databaseId
}
if collId != "" {
resourceId += "/colls/" + collId
}
if docId != "" {
resourceId += "/docs/" + docId
}
isFeed := c.Request.Header.Get("A-Im") == "Incremental Feed"
if resourceType == "pkranges" && isFeed {
// CosmosSDK replaces '/' with '-' in resource id requests
resourceId = strings.Replace(collId, "-", "/", -1)
}
return resourceId
}

View File

@@ -16,7 +16,7 @@ func RequestLogger() gin.HandlerFunc {
bodyStr := readBody(rdr1) bodyStr := readBody(rdr1)
if bodyStr != "" { if bodyStr != "" {
logger.Debug(bodyStr) logger.DebugLn(bodyStr)
} }
c.Request.Body = rdr2 c.Request.Body = rdr2

View File

@@ -0,0 +1,21 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
)
func StripTrailingSlashes(r *gin.Engine, config config.ServerConfig) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if len(path) > 1 && path[len(path)-1] == '/' && !strings.Contains(path, config.ExplorerBaseUrlLocation) {
c.Request.URL.Path = path[:len(path)-1]
r.HandleContext(c)
c.Abort()
return
}
c.Next()
}
}

View File

@@ -7,6 +7,7 @@ import (
) )
func GetOffers(c *gin.Context) { func GetOffers(c *gin.Context) {
c.Header("x-ms-item-count", "0")
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "", "_rid": "",
"_count": 0, "_count": 0,

View File

@@ -5,11 +5,10 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetPartitionKeyRanges(c *gin.Context) { func (h *Handlers) GetPartitionKeyRanges(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
@@ -18,7 +17,7 @@ func GetPartitionKeyRanges(c *gin.Context) {
return return
} }
partitionKeyRanges, status := repositories.GetPartitionKeyRanges(databaseId, collectionId) partitionKeyRanges, status := h.repository.GetPartitionKeyRanges(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Header("etag", "\"420\"") c.Header("etag", "\"420\"")
c.Header("lsn", "420") c.Header("lsn", "420")
@@ -27,7 +26,7 @@ func GetPartitionKeyRanges(c *gin.Context) {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(partitionKeyRanges))) c.Header("x-ms-item-count", fmt.Sprintf("%d", len(partitionKeyRanges)))
collectionRid := collectionId collectionRid := collectionId
collection, _ := repositories.GetCollection(databaseId, collectionId) collection, _ := h.repository.GetCollection(databaseId, collectionId)
if collection.ResourceID != "" { if collection.ResourceID != "" {
collectionRid = collection.ResourceID collectionRid = collection.ResourceID
} }

View File

@@ -5,27 +5,26 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
) )
func GetServerInfo(c *gin.Context) { func (h *Handlers) GetServerInfo(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_self": "", "_self": "",
"id": config.Config.DatabaseAccount, "id": h.config.DatabaseAccount,
"_rid": fmt.Sprintf("%s.%s", config.Config.DatabaseAccount, config.Config.DatabaseDomain), "_rid": fmt.Sprintf("%s.%s", h.config.DatabaseAccount, h.config.DatabaseDomain),
"media": "//media/", "media": "//media/",
"addresses": "//addresses/", "addresses": "//addresses/",
"_dbs": "//dbs/", "_dbs": "//dbs/",
"writableLocations": []map[string]interface{}{ "writableLocations": []map[string]interface{}{
{ {
"name": "South Central US", "name": "South Central US",
"databaseAccountEndpoint": config.Config.DatabaseEndpoint, "databaseAccountEndpoint": h.config.DatabaseEndpoint,
}, },
}, },
"readableLocations": []map[string]interface{}{ "readableLocations": []map[string]interface{}{
{ {
"name": "South Central US", "name": "South Central US",
"databaseAccountEndpoint": config.Config.DatabaseEndpoint, "databaseAccountEndpoint": h.config.DatabaseEndpoint,
}, },
}, },
"enableMultipleWriteLocations": false, "enableMultipleWriteLocations": false,

View File

@@ -1,23 +1,118 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllStoredProcedures(c *gin.Context) { func (h *Handlers) GetAllStoredProcedures(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
sps, status := repositories.GetAllStoredProcedures(databaseId, collectionId) sps, status := h.repository.GetAllStoredProcedures(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(sps)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "StoredProcedures": sps, "_count": len(sps)}) c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "StoredProcedures": sps, "_count": len(sps)})
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func (h *Handlers) GetStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
sp, status := h.repository.GetStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, sp)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) DeleteStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
status := h.repository.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) ReplaceStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
var sp repositorymodels.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
status := h.repository.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
createdSP, status := h.repository.CreateStoredProcedure(databaseId, collectionId, sp)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) CreateStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var sp repositorymodels.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
createdSP, status := h.repository.CreateStoredProcedure(databaseId, collectionId, sp)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusCreated, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}

View File

@@ -1,23 +1,118 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllTriggers(c *gin.Context) { func (h *Handlers) GetAllTriggers(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
triggers, status := repositories.GetAllTriggers(databaseId, collectionId) triggers, status := h.repository.GetAllTriggers(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(triggers)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "Triggers": triggers, "_count": len(triggers)}) c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "Triggers": triggers, "_count": len(triggers)})
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func (h *Handlers) GetTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
trigger, status := h.repository.GetTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, trigger)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) DeleteTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
status := h.repository.DeleteTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) ReplaceTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
var trigger repositorymodels.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
status := h.repository.DeleteTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
createdTrigger, status := h.repository.CreateTrigger(databaseId, collectionId, trigger)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) CreateTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var trigger repositorymodels.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
createdTrigger, status := h.repository.CreateTrigger(databaseId, collectionId, trigger)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusCreated, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}

View File

@@ -1,23 +1,118 @@
package handlers package handlers
import ( import (
"fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
func GetAllUserDefinedFunctions(c *gin.Context) { func (h *Handlers) GetAllUserDefinedFunctions(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
udfs, status := repositories.GetAllUserDefinedFunctions(databaseId, collectionId) udfs, status := h.repository.GetAllUserDefinedFunctions(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(udfs)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "UserDefinedFunctions": udfs, "_count": len(udfs)}) c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "UserDefinedFunctions": udfs, "_count": len(udfs)})
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
} }
func (h *Handlers) GetUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
udf, status := h.repository.GetUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, udf)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) DeleteUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
status := h.repository.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) ReplaceUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
var udf repositorymodels.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
status := h.repository.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
return
}
createdUdf, status := h.repository.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusOK, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}
func (h *Handlers) CreateUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var udf repositorymodels.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
return
}
createdUdf, status := h.repository.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
return
}
if status == repositorymodels.StatusOk {
c.IndentedJSON(http.StatusCreated, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
}

View File

@@ -1,94 +1,144 @@
package api package api
import ( import (
"context"
"fmt" "fmt"
"net/http" "net/http"
"sync"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/api/handlers" "github.com/pikami/cosmium/api/handlers"
"github.com/pikami/cosmium/api/handlers/middleware" "github.com/pikami/cosmium/api/handlers/middleware"
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/repositories"
tlsprovider "github.com/pikami/cosmium/internal/tls_provider" tlsprovider "github.com/pikami/cosmium/internal/tls_provider"
) )
func CreateRouter() *gin.Engine { var ginMux sync.Mutex
router := gin.Default()
if config.Config.Debug { func (s *ApiServer) CreateRouter(repository *repositories.DataRepository) {
routeHandlers := handlers.NewHandlers(repository, s.config)
ginMux.Lock()
gin.DefaultWriter = logger.InfoWriter()
gin.DefaultErrorWriter = logger.ErrorWriter()
if s.config.LogLevel != "debug" {
gin.SetMode(gin.ReleaseMode)
}
ginMux.Unlock()
router := gin.Default(func(e *gin.Engine) {
e.RedirectTrailingSlash = false
})
if s.config.LogLevel == "debug" {
router.Use(middleware.RequestLogger()) router.Use(middleware.RequestLogger())
} }
router.Use(middleware.Authentication()) router.Use(middleware.StripTrailingSlashes(router, s.config))
router.Use(middleware.Authentication(s.config))
router.GET("/dbs/:databaseId/colls/:collId/pkranges", handlers.GetPartitionKeyRanges) router.GET("/dbs/:databaseId/colls/:collId/pkranges", routeHandlers.GetPartitionKeyRanges)
router.POST("/dbs/:databaseId/colls/:collId/docs", handlers.DocumentsPost) router.POST("/dbs/:databaseId/colls/:collId/docs", routeHandlers.DocumentsPost)
router.GET("/dbs/:databaseId/colls/:collId/docs", handlers.GetAllDocuments) router.GET("/dbs/:databaseId/colls/:collId/docs", routeHandlers.GetAllDocuments)
router.GET("/dbs/:databaseId/colls/:collId/docs/:docId", handlers.GetDocument) router.GET("/dbs/:databaseId/colls/:collId/docs/:docId", routeHandlers.GetDocument)
router.PUT("/dbs/:databaseId/colls/:collId/docs/:docId", handlers.ReplaceDocument) router.PUT("/dbs/:databaseId/colls/:collId/docs/:docId", routeHandlers.ReplaceDocument)
router.DELETE("/dbs/:databaseId/colls/:collId/docs/:docId", handlers.DeleteDocument) router.PATCH("/dbs/:databaseId/colls/:collId/docs/:docId", routeHandlers.PatchDocument)
router.DELETE("/dbs/:databaseId/colls/:collId/docs/:docId", routeHandlers.DeleteDocument)
router.POST("/dbs/:databaseId/colls", handlers.CreateCollection) router.POST("/dbs/:databaseId/colls", routeHandlers.CreateCollection)
router.GET("/dbs/:databaseId/colls", handlers.GetAllCollections) router.GET("/dbs/:databaseId/colls", routeHandlers.GetAllCollections)
router.GET("/dbs/:databaseId/colls/:collId", handlers.GetCollection) router.GET("/dbs/:databaseId/colls/:collId", routeHandlers.GetCollection)
router.DELETE("/dbs/:databaseId/colls/:collId", handlers.DeleteCollection) router.DELETE("/dbs/:databaseId/colls/:collId", routeHandlers.DeleteCollection)
router.POST("/dbs", handlers.CreateDatabase) router.POST("/dbs", routeHandlers.CreateDatabase)
router.GET("/dbs", handlers.GetAllDatabases) router.GET("/dbs", routeHandlers.GetAllDatabases)
router.GET("/dbs/:databaseId", handlers.GetDatabase) router.GET("/dbs/:databaseId", routeHandlers.GetDatabase)
router.DELETE("/dbs/:databaseId", handlers.DeleteDatabase) router.DELETE("/dbs/:databaseId", routeHandlers.DeleteDatabase)
router.GET("/dbs/:databaseId/colls/:collId/udfs", handlers.GetAllUserDefinedFunctions) router.POST("/dbs/:databaseId/colls/:collId/triggers", routeHandlers.CreateTrigger)
router.GET("/dbs/:databaseId/colls/:collId/sprocs", handlers.GetAllStoredProcedures) router.GET("/dbs/:databaseId/colls/:collId/triggers", routeHandlers.GetAllTriggers)
router.GET("/dbs/:databaseId/colls/:collId/triggers", handlers.GetAllTriggers) router.GET("/dbs/:databaseId/colls/:collId/triggers/:triggerId", routeHandlers.GetTrigger)
router.PUT("/dbs/:databaseId/colls/:collId/triggers/:triggerId", routeHandlers.ReplaceTrigger)
router.DELETE("/dbs/:databaseId/colls/:collId/triggers/:triggerId", routeHandlers.DeleteTrigger)
router.POST("/dbs/:databaseId/colls/:collId/sprocs", routeHandlers.CreateStoredProcedure)
router.GET("/dbs/:databaseId/colls/:collId/sprocs", routeHandlers.GetAllStoredProcedures)
router.GET("/dbs/:databaseId/colls/:collId/sprocs/:sprocId", routeHandlers.GetStoredProcedure)
router.PUT("/dbs/:databaseId/colls/:collId/sprocs/:sprocId", routeHandlers.ReplaceStoredProcedure)
router.DELETE("/dbs/:databaseId/colls/:collId/sprocs/:sprocId", routeHandlers.DeleteStoredProcedure)
router.POST("/dbs/:databaseId/colls/:collId/udfs", routeHandlers.CreateUserDefinedFunction)
router.GET("/dbs/:databaseId/colls/:collId/udfs", routeHandlers.GetAllUserDefinedFunctions)
router.GET("/dbs/:databaseId/colls/:collId/udfs/:udfId", routeHandlers.GetUserDefinedFunction)
router.PUT("/dbs/:databaseId/colls/:collId/udfs/:udfId", routeHandlers.ReplaceUserDefinedFunction)
router.DELETE("/dbs/:databaseId/colls/:collId/udfs/:udfId", routeHandlers.DeleteUserDefinedFunction)
router.GET("/offers", handlers.GetOffers) router.GET("/offers", handlers.GetOffers)
router.GET("/", handlers.GetServerInfo) router.GET("/", routeHandlers.GetServerInfo)
router.GET("/cosmium/export", handlers.CosmiumExport) router.GET("/cosmium/export", routeHandlers.CosmiumExport)
handlers.RegisterExplorerHandlers(router) routeHandlers.RegisterExplorerHandlers(router)
return router s.router = router
} }
func StartAPI() { func (s *ApiServer) Start() error {
if !config.Config.Debug { listenAddress := fmt.Sprintf(":%d", s.config.Port)
gin.SetMode(gin.ReleaseMode) s.isActive = true
server := &http.Server{
Addr: listenAddress,
Handler: s.router.Handler(),
} }
router := CreateRouter() errChan := make(chan error, 1)
listenAddress := fmt.Sprintf(":%d", config.Config.Port)
if config.Config.TLS_CertificatePath != "" && config.Config.TLS_CertificateKey != "" { go func() {
err := router.RunTLS( <-s.stopServer
listenAddress, logger.InfoLn("Shutting down server...")
config.Config.TLS_CertificatePath, err := server.Shutdown(context.TODO())
config.Config.TLS_CertificateKey)
if err != nil { if err != nil {
logger.Error("Failed to start HTTPS server:", err) logger.ErrorLn("Failed to shutdown server:", err)
}
s.onServerShutdown <- true
}()
go func() {
var err error
if s.config.DisableTls {
logger.Infof("Listening and serving HTTP on %s\n", server.Addr)
err = server.ListenAndServe()
} else if s.config.TLS_CertificatePath != "" && s.config.TLS_CertificateKey != "" {
logger.Infof("Listening and serving HTTPS on %s\n", server.Addr)
err = server.ListenAndServeTLS(
s.config.TLS_CertificatePath,
s.config.TLS_CertificateKey)
} else {
tlsConfig := tlsprovider.GetDefaultTlsConfig()
server.TLSConfig = tlsConfig
logger.Infof("Listening and serving HTTPS on %s\n", server.Addr)
err = server.ListenAndServeTLS("", "")
} }
return if err != nil && err != http.ErrServerClosed {
} logger.ErrorLn("Failed to start server:", err)
errChan <- err
} else {
errChan <- nil
}
s.isActive = false
}()
if config.Config.DisableTls { select {
router.Run(listenAddress) case err := <-errChan:
return err
case <-time.After(50 * time.Millisecond):
return nil
} }
tlsConfig := tlsprovider.GetDefaultTlsConfig()
server := &http.Server{
Addr: listenAddress,
Handler: router.Handler(),
TLSConfig: tlsConfig,
}
logger.Infof("Listening and serving HTTPS on %s\n", server.Addr)
err := server.ListenAndServeTLS("", "")
if err != nil {
logger.Error("Failed to start HTTPS server:", err)
}
router.Run()
} }

View File

@@ -11,16 +11,15 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_Authentication(t *testing.T) { func Test_Authentication(t *testing.T) {
ts := runTestServer() ts := runTestServer()
defer ts.Close() defer ts.Server.Close()
t.Run("Should get 200 when correct account key is used", func(t *testing.T) { t.Run("Should get 200 when correct account key is used", func(t *testing.T) {
repositories.DeleteDatabase(testDatabaseName) ts.Repository.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey), fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
@@ -35,26 +34,8 @@ func Test_Authentication(t *testing.T) {
assert.Equal(t, createResponse.DatabaseProperties.ID, testDatabaseName) assert.Equal(t, createResponse.DatabaseProperties.ID, testDatabaseName)
}) })
t.Run("Should get 200 when wrong account key is used, but authentication is dissabled", func(t *testing.T) {
config.Config.DisableAuth = true
repositories.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
createResponse, err := client.CreateDatabase(
context.TODO(),
azcosmos.DatabaseProperties{ID: testDatabaseName},
&azcosmos.CreateDatabaseOptions{})
assert.Nil(t, err)
assert.Equal(t, createResponse.DatabaseProperties.ID, testDatabaseName)
config.Config.DisableAuth = false
})
t.Run("Should get 401 when wrong account key is used", func(t *testing.T) { t.Run("Should get 401 when wrong account key is used", func(t *testing.T) {
repositories.DeleteDatabase(testDatabaseName) ts.Repository.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"), fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
@@ -85,3 +66,29 @@ func Test_Authentication(t *testing.T) {
assert.Contains(t, string(responseBody), "BACKEND_ENDPOINT") assert.Contains(t, string(responseBody), "BACKEND_ENDPOINT")
}) })
} }
func Test_Authentication_Disabled(t *testing.T) {
ts := runTestServerCustomConfig(config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
DisableAuth: true,
})
defer ts.Server.Close()
t.Run("Should get 200 when wrong account key is used, but authentication is dissabled", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
createResponse, err := client.CreateDatabase(
context.TODO(),
azcosmos.DatabaseProperties{ID: testDatabaseName},
&azcosmos.CreateDatabaseOptions{})
assert.Nil(t, err)
assert.Equal(t, createResponse.DatabaseProperties.ID, testDatabaseName)
})
}

View File

@@ -10,22 +10,21 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_Collections(t *testing.T) { func Test_Collections(t *testing.T) {
ts := runTestServer() ts := runTestServer()
defer ts.Close() defer ts.Server.Close()
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.Config.AccountKey), fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
) )
assert.Nil(t, err) assert.Nil(t, err)
repositories.CreateDatabase(repositorymodels.Database{ID: testDatabaseName}) ts.Repository.CreateDatabase(repositorymodels.Database{ID: testDatabaseName})
databaseClient, err := client.NewDatabase(testDatabaseName) databaseClient, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -40,7 +39,7 @@ func Test_Collections(t *testing.T) {
}) })
t.Run("Should return conflict when collection exists", func(t *testing.T) { t.Run("Should return conflict when collection exists", func(t *testing.T) {
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -60,7 +59,7 @@ func Test_Collections(t *testing.T) {
t.Run("Collection Read", func(t *testing.T) { t.Run("Collection Read", func(t *testing.T) {
t.Run("Should read collection", func(t *testing.T) { t.Run("Should read collection", func(t *testing.T) {
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -74,7 +73,7 @@ func Test_Collections(t *testing.T) {
}) })
t.Run("Should return not found when collection does not exist", func(t *testing.T) { t.Run("Should return not found when collection does not exist", func(t *testing.T) {
repositories.DeleteCollection(testDatabaseName, testCollectionName) ts.Repository.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName) collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -93,7 +92,7 @@ func Test_Collections(t *testing.T) {
t.Run("Collection Delete", func(t *testing.T) { t.Run("Collection Delete", func(t *testing.T) {
t.Run("Should delete collection", func(t *testing.T) { t.Run("Should delete collection", func(t *testing.T) {
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -106,7 +105,7 @@ func Test_Collections(t *testing.T) {
}) })
t.Run("Should return not found when collection does not exist", func(t *testing.T) { t.Run("Should return not found when collection does not exist", func(t *testing.T) {
repositories.DeleteCollection(testDatabaseName, testCollectionName) ts.Repository.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName) collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)

View File

@@ -5,13 +5,37 @@ import (
"github.com/pikami/cosmium/api" "github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
) )
func runTestServer() *httptest.Server { type TestServer struct {
config.Config.AccountKey = config.DefaultAccountKey Server *httptest.Server
config.Config.ExplorerPath = "/tmp/nothing" Repository *repositories.DataRepository
URL string
}
return httptest.NewServer(api.CreateRouter()) func runTestServerCustomConfig(config config.ServerConfig) *TestServer {
repository := repositories.NewDataRepository(repositories.RepositoryOptions{})
api := api.NewApiServer(repository, config)
server := httptest.NewServer(api.GetRouter())
return &TestServer{
Server: server,
Repository: repository,
URL: server.URL,
}
}
func runTestServer() *TestServer {
config := config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
}
return runTestServerCustomConfig(config)
} }
const ( const (

View File

@@ -10,24 +10,23 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_Databases(t *testing.T) { func Test_Databases(t *testing.T) {
ts := runTestServer() ts := runTestServer()
defer ts.Close() defer ts.Server.Close()
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.Config.AccountKey), fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
) )
assert.Nil(t, err) assert.Nil(t, err)
t.Run("Database Create", func(t *testing.T) { t.Run("Database Create", func(t *testing.T) {
t.Run("Should create database", func(t *testing.T) { t.Run("Should create database", func(t *testing.T) {
repositories.DeleteDatabase(testDatabaseName) ts.Repository.DeleteDatabase(testDatabaseName)
createResponse, err := client.CreateDatabase(context.TODO(), azcosmos.DatabaseProperties{ createResponse, err := client.CreateDatabase(context.TODO(), azcosmos.DatabaseProperties{
ID: testDatabaseName, ID: testDatabaseName,
@@ -38,7 +37,7 @@ func Test_Databases(t *testing.T) {
}) })
t.Run("Should return conflict when database exists", func(t *testing.T) { t.Run("Should return conflict when database exists", func(t *testing.T) {
repositories.CreateDatabase(repositorymodels.Database{ ts.Repository.CreateDatabase(repositorymodels.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -58,7 +57,7 @@ func Test_Databases(t *testing.T) {
t.Run("Database Read", func(t *testing.T) { t.Run("Database Read", func(t *testing.T) {
t.Run("Should read database", func(t *testing.T) { t.Run("Should read database", func(t *testing.T) {
repositories.CreateDatabase(repositorymodels.Database{ ts.Repository.CreateDatabase(repositorymodels.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -72,7 +71,7 @@ func Test_Databases(t *testing.T) {
}) })
t.Run("Should return not found when database does not exist", func(t *testing.T) { t.Run("Should return not found when database does not exist", func(t *testing.T) {
repositories.DeleteDatabase(testDatabaseName) ts.Repository.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName) databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -91,7 +90,7 @@ func Test_Databases(t *testing.T) {
t.Run("Database Delete", func(t *testing.T) { t.Run("Database Delete", func(t *testing.T) {
t.Run("Should delete database", func(t *testing.T) { t.Run("Should delete database", func(t *testing.T) {
repositories.CreateDatabase(repositorymodels.Database{ ts.Repository.CreateDatabase(repositorymodels.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -104,7 +103,7 @@ func Test_Databases(t *testing.T) {
}) })
t.Run("Should return not found when database does not exist", func(t *testing.T) { t.Run("Should return not found when database does not exist", func(t *testing.T) {
repositories.DeleteDatabase(testDatabaseName) ts.Repository.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName) databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err) assert.Nil(t, err)

View File

@@ -3,13 +3,17 @@ package tests_test
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"sync"
"testing" "testing"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos" "github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -49,9 +53,11 @@ func testCosmosQuery(t *testing.T,
} }
} }
func Test_Documents(t *testing.T) { func documents_InitializeDb(t *testing.T) (*TestServer, *azcosmos.ContainerClient) {
repositories.CreateDatabase(repositorymodels.Database{ID: testDatabaseName}) ts := runTestServer()
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{
ts.Repository.CreateDatabase(repositorymodels.Database{ID: testDatabaseName})
ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ID: testCollectionName, ID: testCollectionName,
PartitionKey: struct { PartitionKey: struct {
Paths []string "json:\"paths\"" Paths []string "json:\"paths\""
@@ -61,14 +67,11 @@ func Test_Documents(t *testing.T) {
Paths: []string{"/pk"}, Paths: []string{"/pk"},
}, },
}) })
repositories.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "12345", "pk": "123", "isCool": false}) ts.Repository.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3}})
repositories.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "67890", "pk": "456", "isCool": true}) ts.Repository.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "67890", "pk": "456", "isCool": true, "arr": []int{6, 7, 8}})
ts := runTestServer()
defer ts.Close()
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.Config.AccountKey), fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
) )
assert.Nil(t, err) assert.Nil(t, err)
@@ -76,6 +79,13 @@ func Test_Documents(t *testing.T) {
collectionClient, err := client.NewContainer(testDatabaseName, testCollectionName) collectionClient, err := client.NewContainer(testDatabaseName, testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)
return ts, collectionClient
}
func Test_Documents(t *testing.T) {
ts, collectionClient := documents_InitializeDb(t)
defer ts.Server.Close()
t.Run("Should query document", func(t *testing.T) { t.Run("Should query document", func(t *testing.T) {
testCosmosQuery(t, collectionClient, testCosmosQuery(t, collectionClient,
"SELECT c.id, c[\"pk\"] FROM c ORDER BY c.id", "SELECT c.id, c[\"pk\"] FROM c ORDER BY c.id",
@@ -136,4 +146,236 @@ func Test_Documents(t *testing.T) {
}, },
) )
}) })
t.Run("Should query document with query parameters as accessor", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c[@param]="67890"
ORDER BY c.id`,
[]azcosmos.QueryParameter{
{Name: "@param", Value: "id"},
},
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should query array accessor", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT c.id,
c["arr"][0] AS arr0,
c["arr"][1] AS arr1,
c["arr"][2] AS arr2,
c["arr"][3] AS arr3
FROM c ORDER BY c.id`,
nil,
[]interface{}{
map[string]interface{}{"id": "12345", "arr0": 1.0, "arr1": 2.0, "arr2": 3.0, "arr3": nil},
map[string]interface{}{"id": "67890", "arr0": 6.0, "arr1": 7.0, "arr2": 8.0, "arr3": nil},
},
)
})
t.Run("Should handle parallel writes", func(t *testing.T) {
var wg sync.WaitGroup
rutineCount := 100
results := make(chan error, rutineCount)
createCall := func(i int) {
defer wg.Done()
item := map[string]interface{}{
"id": fmt.Sprintf("id-%d", i),
"pk": fmt.Sprintf("pk-%d", i),
"val": i,
}
bytes, err := json.Marshal(item)
if err != nil {
results <- err
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err = collectionClient.CreateItem(
ctx,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
results <- err
collectionClient.ReadItem(ctx, azcosmos.PartitionKey{}, fmt.Sprintf("id-%d", i), nil)
collectionClient.DeleteItem(ctx, azcosmos.PartitionKey{}, fmt.Sprintf("id-%d", i), nil)
}
for i := 0; i < rutineCount; i++ {
wg.Add(1)
go createCall(i)
}
wg.Wait()
close(results)
for err := range results {
if err != nil {
t.Errorf("Error creating item: %v", err)
}
}
})
}
func Test_Documents_Patch(t *testing.T) {
ts, collectionClient := documents_InitializeDb(t)
defer ts.Server.Close()
t.Run("Should PATCH document", func(t *testing.T) {
context := context.TODO()
expectedData := map[string]interface{}{"id": "67890", "pk": "666", "newField": "newValue", "incr": 15., "setted": "isSet"}
patch := azcosmos.PatchOperations{}
patch.AppendAdd("/newField", "newValue")
patch.AppendIncrement("/incr", 15)
patch.AppendRemove("/isCool")
patch.AppendReplace("/pk", "666")
patch.AppendSet("/setted", "isSet")
itemResponse, err := collectionClient.PatchItem(
context,
azcosmos.PartitionKey{},
"67890",
patch,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.Nil(t, err)
var itemResponseBody map[string]interface{}
json.Unmarshal(itemResponse.Value, &itemResponseBody)
assert.Equal(t, expectedData["id"], itemResponseBody["id"])
assert.Equal(t, expectedData["pk"], itemResponseBody["pk"])
assert.Empty(t, itemResponseBody["isCool"])
assert.Equal(t, expectedData["newField"], itemResponseBody["newField"])
assert.Equal(t, expectedData["incr"], itemResponseBody["incr"])
assert.Equal(t, expectedData["setted"], itemResponseBody["setted"])
})
t.Run("Should not allow to PATCH document ID", func(t *testing.T) {
context := context.TODO()
patch := azcosmos.PatchOperations{}
patch.AppendReplace("/id", "newValue")
_, err := collectionClient.PatchItem(
context,
azcosmos.PartitionKey{},
"67890",
patch,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, err)
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
assert.Equal(t, http.StatusUnprocessableEntity, respErr.StatusCode)
} else {
panic(err)
}
})
t.Run("CreateItem", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{
"Id": "6789011",
"pk": "456",
"newField": "newValue2",
}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.CreateItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
t.Run("CreateItem that already exists", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err := collectionClient.CreateItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.NotNil(t, err)
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
assert.Equal(t, http.StatusConflict, respErr.StatusCode)
} else {
panic(err)
}
})
t.Run("UpsertItem new", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "123456", "pk": "1234", "isCool": false, "arr": []int{1, 2, 3}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.UpsertItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
t.Run("UpsertItem that already exists", func(t *testing.T) {
context := context.TODO()
item := map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3, 4}}
bytes, err := json.Marshal(item)
assert.Nil(t, err)
r, err2 := collectionClient.UpsertItem(
context,
azcosmos.PartitionKey{},
bytes,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.NotNil(t, r)
assert.Nil(t, err2)
})
} }

View File

@@ -0,0 +1,41 @@
package tests_test
import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/authentication"
"github.com/stretchr/testify/assert"
)
// Request document with trailing slash like python cosmosdb client does.
func Test_Documents_Read_Trailing_Slash(t *testing.T) {
ts, _ := documents_InitializeDb(t)
defer ts.Server.Close()
t.Run("Read doc with client that appends slash to path", func(t *testing.T) {
resourceIdTemplate := "dbs/%s/colls/%s/docs/%s"
path := fmt.Sprintf(resourceIdTemplate, testDatabaseName, testCollectionName, "12345")
testUrl := ts.URL + "/" + path + "/"
date := time.Now().Format(time.RFC1123)
signature := authentication.GenerateSignature("GET", "docs", path, date, config.DefaultAccountKey)
httpClient := &http.Client{}
req, _ := http.NewRequest("GET", testUrl, nil)
req.Header.Add("x-ms-date", date)
req.Header.Add("authorization", "sig="+url.QueryEscape(signature))
res, err := httpClient.Do(req)
assert.Nil(t, err)
if res != nil {
defer res.Body.Close()
assert.Equal(t, http.StatusOK, res.StatusCode, "Expected HTTP status 200 OK")
} else {
t.FailNow()
}
})
}

43
cmd/server/server.go Normal file
View File

@@ -0,0 +1,43 @@
package main
import (
"os"
"os/signal"
"syscall"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
)
func main() {
configuration := config.ParseFlags()
repository := repositories.NewDataRepository(repositories.RepositoryOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
PersistDataFilePath: configuration.PersistDataFilePath,
})
server := api.NewApiServer(repository, configuration)
err := server.Start()
if err != nil {
panic(err)
}
waitForExit(server, repository, configuration)
}
func waitForExit(server *api.ApiServer, repository *repositories.DataRepository, config config.ServerConfig) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// Block until a exit signal is received
<-sigs
// Stop the server
server.Stop()
if config.PersistDataFilePath != "" {
repository.SaveStateFS(config.PersistDataFilePath)
}
}

View File

@@ -15,10 +15,11 @@ Cosmium strives to support the core features of Cosmos DB, including:
## Compatibility Matrix ## Compatibility Matrix
### Features ### Features
| Feature | Implemented | | Feature | Implemented |
|-------------------------------|-------------| | ----------------------------- | ----------- |
| Subqueries | No | | Subqueries | Yes |
| Joins | No | | Joins | Yes |
| Computed properties | No | | Computed properties | No |
| Coalesce operators | No | | Coalesce operators | No |
| Bitwise operators | No | | Bitwise operators | No |
@@ -29,8 +30,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| User-defined functions (UDFs) | No | | User-defined functions (UDFs) | No |
### Clauses ### Clauses
| Clause | Implemented | | Clause | Implemented |
|--------------|-------------| | ------------ | ----------- |
| SELECT | Yes | | SELECT | Yes |
| FROM | Yes | | FROM | Yes |
| WHERE | Yes | | WHERE | Yes |
@@ -39,8 +41,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| OFFSET LIMIT | Yes | | OFFSET LIMIT | Yes |
### Keywords ### Keywords
| Keyword | Implemented | | Keyword | Implemented |
|----------|-------------| | -------- | ----------- |
| BETWEEN | No | | BETWEEN | No |
| DISTINCT | Yes | | DISTINCT | Yes |
| LIKE | No | | LIKE | No |
@@ -48,8 +51,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| TOP | Yes | | TOP | Yes |
### Aggregate Functions ### Aggregate Functions
| Function | Implemented | | Function | Implemented |
|----------|-------------| | -------- | ----------- |
| AVG | Yes | | AVG | Yes |
| COUNT | Yes | | COUNT | Yes |
| MAX | Yes | | MAX | Yes |
@@ -57,25 +61,30 @@ Cosmium strives to support the core features of Cosmos DB, including:
| SUM | Yes | | SUM | Yes |
### Array Functions ### Array Functions
| Function | Implemented |
|----------------|-------------| | Function | Implemented |
| ARRAY_CONCAT | Yes | | ------------------ | ----------- |
| ARRAY_CONTAINS | No | | ARRAY_CONCAT | Yes |
| ARRAY_LENGTH | Yes | | ARRAY_CONTAINS | Yes |
| ARRAY_SLICE | Yes | | ARRAY_CONTAINS_ANY | Yes |
| CHOOSE | No | | ARRAY_CONTAINS_ALL | Yes |
| ObjectToArray | No | | ARRAY_LENGTH | Yes |
| SetIntersect | Yes | | ARRAY_SLICE | Yes |
| SetUnion | Yes | | CHOOSE | No |
| ObjectToArray | No |
| SetIntersect | Yes |
| SetUnion | Yes |
### Conditional Functions ### Conditional Functions
| Function | Implemented | | Function | Implemented |
|----------|-------------| | -------- | ----------- |
| IIF | No | | IIF | No |
### Date and time Functions ### Date and time Functions
| Function | Implemented | | Function | Implemented |
|---------------------------|-------------| | ------------------------- | ----------- |
| DateTimeAdd | No | | DateTimeAdd | No |
| DateTimeBin | No | | DateTimeBin | No |
| DateTimeDiff | No | | DateTimeDiff | No |
@@ -93,53 +102,56 @@ Cosmium strives to support the core features of Cosmos DB, including:
| TimestampToDateTime | No | | TimestampToDateTime | No |
### Item Functions ### Item Functions
| Function | Implemented | | Function | Implemented |
|------------|-------------| | ---------- | ----------- |
| DocumentId | No | | DocumentId | No |
### Mathematical Functions ### Mathematical Functions
| Function | Implemented | | Function | Implemented |
|------------------|-------------| | ---------------- | ----------- |
| ABS | No | | ABS | Yes |
| ACOS | No | | ACOS | Yes |
| ASIN | No | | ASIN | Yes |
| ATAN | No | | ATAN | Yes |
| ATN2 | No | | ATN2 | Yes |
| CEILING | No | | CEILING | Yes |
| COS | No | | COS | Yes |
| COT | No | | COT | Yes |
| DEGREES | No | | DEGREES | Yes |
| EXP | No | | EXP | Yes |
| FLOOR | No | | FLOOR | Yes |
| IntAdd | No | | IntAdd | Yes |
| IntBitAnd | No | | IntBitAnd | Yes |
| IntBitLeftShift | No | | IntBitLeftShift | Yes |
| IntBitNot | No | | IntBitNot | Yes |
| IntBitOr | No | | IntBitOr | Yes |
| IntBitRightShift | No | | IntBitRightShift | Yes |
| IntBitXor | No | | IntBitXor | Yes |
| IntDiv | No | | IntDiv | Yes |
| IntMod | No | | IntMod | Yes |
| IntMul | No | | IntMul | Yes |
| IntSub | No | | IntSub | Yes |
| LOG | No | | LOG | Yes |
| LOG10 | No | | LOG10 | Yes |
| NumberBin | No | | NumberBin | Yes |
| PI | No | | PI | Yes |
| POWER | No | | POWER | Yes |
| RADIANS | No | | RADIANS | Yes |
| RAND | No | | RAND | Yes |
| ROUND | No | | ROUND | Yes |
| SIGN | No | | SIGN | Yes |
| SIN | No | | SIN | Yes |
| SQRT | No | | SQRT | Yes |
| SQUARE | No | | SQUARE | Yes |
| TAN | No | | TAN | Yes |
| TRUNC | No | | TRUNC | Yes |
### Spatial Functions ### Spatial Functions
| Function | Implemented | | Function | Implemented |
|--------------------|-------------| | ------------------ | ----------- |
| ST_AREA | No | | ST_AREA | No |
| ST_DISTANCE | No | | ST_DISTANCE | No |
| ST_WITHIN | No | | ST_WITHIN | No |
@@ -148,8 +160,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| ST_ISVALIDDETAILED | No | | ST_ISVALIDDETAILED | No |
### String Functions ### String Functions
| Function | Implemented | | Function | Implemented |
|-----------------|-------------| | --------------- | ----------- |
| CONCAT | Yes | | CONCAT | Yes |
| CONTAINS | Yes | | CONTAINS | Yes |
| ENDSWITH | Yes | | ENDSWITH | Yes |
@@ -177,8 +190,9 @@ Cosmium strives to support the core features of Cosmos DB, including:
| UPPER | Yes | | UPPER | Yes |
### Type checking Functions ### Type checking Functions
| Function | Implemented | | Function | Implemented |
|------------------|-------------| | ---------------- | ----------- |
| IS_ARRAY | Yes | | IS_ARRAY | Yes |
| IS_BOOL | Yes | | IS_BOOL | Yes |
| IS_DEFINED | Yes | | IS_DEFINED | Yes |
@@ -190,6 +204,15 @@ Cosmium strives to support the core features of Cosmos DB, including:
| IS_PRIMITIVE | Yes | | IS_PRIMITIVE | Yes |
| IS_STRING | Yes | | IS_STRING | Yes |
### Document Batch Requests
| Operation | Implemented |
| --------- | ----------- |
| Create | No |
| Update | No |
| Delete | No |
| Read | No |
## Known Differences ## Known Differences
While Cosmium aims to replicate the behavior of Cosmos DB as closely as possible, there are certain differences and limitations to be aware of: While Cosmium aims to replicate the behavior of Cosmos DB as closely as possible, there are certain differences and limitations to be aware of:

51
go.mod
View File

@@ -1,43 +1,46 @@
module github.com/pikami/cosmium module github.com/pikami/cosmium
go 1.21.6 go 1.22.0
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6
github.com/gin-gonic/gin v1.9.1 github.com/cosmiumdev/json-patch/v5 v5.9.3
github.com/google/uuid v1.1.1 github.com/gin-gonic/gin v1.10.0
github.com/stretchr/testify v1.8.4 github.com/google/uuid v1.6.0
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
) )
require ( require (
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.12.7 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.4 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.13.0 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

141
go.sum
View File

@@ -1,109 +1,118 @@
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 h1:c4k2FIYIh4xtwqrQwV0Ct1v5+ehlNXj5NI/MWVsiTkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2/go.mod h1:5FDJtLEO/GxwNgUxbwrY3LP0pEoThTQJtk2oysdXHxM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0/go.mod h1:+6sju8gk8FRmSajX3Oz4G5Gm7P+mbqE9FVaXXFYTkCM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 h1:oBqQLSI1pZwGOdXJAoJJSzmff9tlfD4KroVfjQQmd0g= github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 h1:oBqQLSI1pZwGOdXJAoJJSzmff9tlfD4KroVfjQQmd0g=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6/go.mod h1:Beh5cHIXJ0oWEDWk9lNFtuklCojLLQ5hl+LqSNTTs0I= github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6/go.mod h1:Beh5cHIXJ0oWEDWk9lNFtuklCojLLQ5hl+LqSNTTs0I=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cosmiumdev/json-patch/v5 v5.9.3 h1:l+Og3+5edqV2NHDo58sz72eS733lbXVYP61seYK43Do=
github.com/cosmiumdev/json-patch/v5 v5.9.3/go.mod h1:WzSTCdia0WrlZtjnL19P4RiwWtfdyArm/E7stgEeP5g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@@ -10,6 +10,11 @@ import (
// https://learn.microsoft.com/en-us/rest/api/cosmos-db/access-control-on-cosmosdb-resources // https://learn.microsoft.com/en-us/rest/api/cosmos-db/access-control-on-cosmosdb-resources
func GenerateSignature(verb string, resourceType string, resourceId string, date string, masterKey string) string { func GenerateSignature(verb string, resourceType string, resourceId string, date string, masterKey string) string {
isNameBased := resourceId != "" && ((len(resourceId) > 4 && resourceId[3] == '/') || strings.HasPrefix(strings.ToLower(resourceId), "interopusers"))
if !isNameBased {
resourceId = strings.ToLower(resourceId)
}
payload := fmt.Sprintf( payload := fmt.Sprintf(
"%s\n%s\n%s\n%s\n%s\n", "%s\n%s\n%s\n%s\n%s\n",
strings.ToLower(verb), strings.ToLower(verb),

View File

@@ -27,4 +27,14 @@ func Test_GenerateSignature(t *testing.T) {
signature := authentication.GenerateSignature("DELETE", "dbs", "dbs/Test Database", testDate, config.DefaultAccountKey) signature := authentication.GenerateSignature("DELETE", "dbs", "dbs/Test Database", testDate, config.DefaultAccountKey)
assert.Equal(t, "LcuXXg0TcXxZG0kUCj9tZIWRy2yCzim3oiqGiHpRqGs=", signature) assert.Equal(t, "LcuXXg0TcXxZG0kUCj9tZIWRy2yCzim3oiqGiHpRqGs=", signature)
}) })
t.Run("Should generate PKRANGES signature", func(t *testing.T) {
signature := authentication.GenerateSignature("GET", "pkranges", "m4d+xG08uVM=", testDate, config.DefaultAccountKey)
assert.Equal(t, "6S5ceZsl2EXWB3Jo5bJcK7zv8NxXnsxWPWD9TH3nNMo=", signature)
})
t.Run("Should generate PATCH signature", func(t *testing.T) {
signature := authentication.GenerateSignature("PATCH", "docs", "dbs/test-db/colls/test-coll/docs/67890", testDate, config.DefaultAccountKey)
assert.Equal(t, "VR1ddfxKBXnoaT+b3WkhyYVc9JmGNpTnaRmyDM44398=", signature)
})
} }

View File

@@ -1,40 +1,140 @@
package logger package logger
import ( import (
"fmt"
"log" "log"
"os" "os"
"runtime"
"github.com/pikami/cosmium/api/config" "strings"
"sync"
) )
var DebugLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) type LogLevelType int
var (
LogLevelDebug LogLevelType = 0
LogLevelInfo LogLevelType = 1
LogLevelError LogLevelType = 2
LogLevelSilent LogLevelType = 10
)
type LogWriter struct {
WriterLevel LogLevelType
}
var logLevelMutex sync.RWMutex
var logLevel = LogLevelInfo
var DebugLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
var InfoLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) var InfoLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
var ErrorLogger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile) var ErrorLogger = log.New(os.Stderr, "", log.Ldate|log.Ltime)
func DebugLn(v ...any) {
if GetLogLevel() <= LogLevelDebug {
prefix := getCallerPrefix()
DebugLogger.Println(append([]interface{}{prefix}, v...)...)
}
}
func Debug(v ...any) { func Debug(v ...any) {
if config.Config.Debug { if GetLogLevel() <= LogLevelDebug {
DebugLogger.Println(v...) prefix := getCallerPrefix()
DebugLogger.Println(append([]interface{}{prefix}, v...)...)
} }
} }
func Debugf(format string, v ...any) { func Debugf(format string, v ...any) {
if config.Config.Debug { if GetLogLevel() <= LogLevelDebug {
DebugLogger.Printf(format, v...) prefix := getCallerPrefix()
DebugLogger.Printf(prefix+format, v...)
}
}
func InfoLn(v ...any) {
if GetLogLevel() <= LogLevelInfo {
InfoLogger.Println(v...)
} }
} }
func Info(v ...any) { func Info(v ...any) {
InfoLogger.Println(v...) if GetLogLevel() <= LogLevelInfo {
InfoLogger.Print(v...)
}
} }
func Infof(format string, v ...any) { func Infof(format string, v ...any) {
InfoLogger.Printf(format, v...) if GetLogLevel() <= LogLevelInfo {
InfoLogger.Printf(format, v...)
}
}
func ErrorLn(v ...any) {
if GetLogLevel() <= LogLevelError {
prefix := getCallerPrefix()
ErrorLogger.Println(append([]interface{}{prefix}, v...)...)
}
} }
func Error(v ...any) { func Error(v ...any) {
ErrorLogger.Println(v...) if GetLogLevel() <= LogLevelError {
prefix := getCallerPrefix()
ErrorLogger.Print(append([]interface{}{prefix}, v...)...)
}
} }
func Errorf(format string, v ...any) { func Errorf(format string, v ...any) {
ErrorLogger.Printf(format, v...) if GetLogLevel() <= LogLevelError {
prefix := getCallerPrefix()
ErrorLogger.Printf(prefix+format, v...)
}
}
func (lw *LogWriter) Write(p []byte) (n int, err error) {
switch lw.WriterLevel {
case LogLevelDebug:
Debug(string(p))
case LogLevelInfo:
Info(string(p))
case LogLevelError:
Error(string(p))
}
return len(p), nil
}
func ErrorWriter() *LogWriter {
return &LogWriter{WriterLevel: LogLevelError}
}
func InfoWriter() *LogWriter {
return &LogWriter{WriterLevel: LogLevelInfo}
}
func DebugWriter() *LogWriter {
return &LogWriter{WriterLevel: LogLevelDebug}
}
func SetLogLevel(level LogLevelType) {
logLevelMutex.Lock()
defer logLevelMutex.Unlock()
logLevel = level
}
func GetLogLevel() LogLevelType {
logLevelMutex.RLock()
defer logLevelMutex.RUnlock()
return logLevel
}
func getCallerPrefix() string {
_, file, line, ok := runtime.Caller(2)
if ok {
parts := strings.Split(file, "/")
if len(parts) > 0 {
file = parts[len(parts)-1]
}
return fmt.Sprintf("%s:%d - ", file, line)
}
return ""
} }

View File

@@ -11,48 +11,60 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func GetAllCollections(databaseId string) ([]repositorymodels.Collection, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetAllCollections(databaseId string) ([]repositorymodels.Collection, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return make([]repositorymodels.Collection, 0), repositorymodels.StatusNotFound return make([]repositorymodels.Collection, 0), repositorymodels.StatusNotFound
} }
return maps.Values(storeState.Collections[databaseId]), repositorymodels.StatusOk return maps.Values(r.storeState.Collections[databaseId]), repositorymodels.StatusOk
} }
func GetCollection(databaseId string, collectionId string) (repositorymodels.Collection, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetCollection(databaseId string, collectionId string) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound return repositorymodels.Collection{}, repositorymodels.StatusNotFound
} }
if _, ok := storeState.Collections[databaseId][collectionId]; !ok { if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound return repositorymodels.Collection{}, repositorymodels.StatusNotFound
} }
return storeState.Collections[databaseId][collectionId], repositorymodels.StatusOk return r.storeState.Collections[databaseId][collectionId], repositorymodels.StatusOk
} }
func DeleteCollection(databaseId string, collectionId string) repositorymodels.RepositoryStatus { func (r *DataRepository) DeleteCollection(databaseId string, collectionId string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
if _, ok := storeState.Collections[databaseId][collectionId]; !ok { if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
delete(storeState.Collections[databaseId], collectionId) delete(r.storeState.Collections[databaseId], collectionId)
return repositorymodels.StatusOk return repositorymodels.StatusOk
} }
func CreateCollection(databaseId string, newCollection repositorymodels.Collection) (repositorymodels.Collection, repositorymodels.RepositoryStatus) { func (r *DataRepository) CreateCollection(databaseId string, newCollection repositorymodels.Collection) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool var ok bool
var database repositorymodels.Database var database repositorymodels.Database
if database, ok = storeState.Databases[databaseId]; !ok { if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound return repositorymodels.Collection{}, repositorymodels.StatusNotFound
} }
if _, ok = storeState.Collections[databaseId][newCollection.ID]; ok { if _, ok = r.storeState.Collections[databaseId][newCollection.ID]; ok {
return repositorymodels.Collection{}, repositorymodels.Conflict return repositorymodels.Collection{}, repositorymodels.Conflict
} }
@@ -63,8 +75,11 @@ func CreateCollection(databaseId string, newCollection repositorymodels.Collecti
newCollection.ETag = fmt.Sprintf("\"%s\"", uuid.New()) newCollection.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newCollection.Self = fmt.Sprintf("dbs/%s/colls/%s/", database.ResourceID, newCollection.ResourceID) newCollection.Self = fmt.Sprintf("dbs/%s/colls/%s/", database.ResourceID, newCollection.ResourceID)
storeState.Collections[databaseId][newCollection.ID] = newCollection r.storeState.Collections[databaseId][newCollection.ID] = newCollection
storeState.Documents[databaseId][newCollection.ID] = make(map[string]repositorymodels.Document) r.storeState.Documents[databaseId][newCollection.ID] = make(map[string]repositorymodels.Document)
r.storeState.Triggers[databaseId][newCollection.ID] = make(map[string]repositorymodels.Trigger)
r.storeState.StoredProcedures[databaseId][newCollection.ID] = make(map[string]repositorymodels.StoredProcedure)
r.storeState.UserDefinedFunctions[databaseId][newCollection.ID] = make(map[string]repositorymodels.UserDefinedFunction)
return newCollection, repositorymodels.StatusOk return newCollection, repositorymodels.StatusOk
} }

View File

@@ -10,30 +10,42 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func GetAllDatabases() ([]repositorymodels.Database, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetAllDatabases() ([]repositorymodels.Database, repositorymodels.RepositoryStatus) {
return maps.Values(storeState.Databases), repositorymodels.StatusOk r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.Databases), repositorymodels.StatusOk
} }
func GetDatabase(id string) (repositorymodels.Database, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetDatabase(id string) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
if database, ok := storeState.Databases[id]; ok { r.storeState.RLock()
defer r.storeState.RUnlock()
if database, ok := r.storeState.Databases[id]; ok {
return database, repositorymodels.StatusOk return database, repositorymodels.StatusOk
} }
return repositorymodels.Database{}, repositorymodels.StatusNotFound return repositorymodels.Database{}, repositorymodels.StatusNotFound
} }
func DeleteDatabase(id string) repositorymodels.RepositoryStatus { func (r *DataRepository) DeleteDatabase(id string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[id]; !ok { r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[id]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
delete(storeState.Databases, id) delete(r.storeState.Databases, id)
return repositorymodels.StatusOk return repositorymodels.StatusOk
} }
func CreateDatabase(newDatabase repositorymodels.Database) (repositorymodels.Database, repositorymodels.RepositoryStatus) { func (r *DataRepository) CreateDatabase(newDatabase repositorymodels.Database) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[newDatabase.ID]; ok { r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[newDatabase.ID]; ok {
return repositorymodels.Database{}, repositorymodels.Conflict return repositorymodels.Database{}, repositorymodels.Conflict
} }
@@ -42,9 +54,12 @@ func CreateDatabase(newDatabase repositorymodels.Database) (repositorymodels.Dat
newDatabase.ETag = fmt.Sprintf("\"%s\"", uuid.New()) newDatabase.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID) newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID)
storeState.Databases[newDatabase.ID] = newDatabase r.storeState.Databases[newDatabase.ID] = newDatabase
storeState.Collections[newDatabase.ID] = make(map[string]repositorymodels.Collection) r.storeState.Collections[newDatabase.ID] = make(map[string]repositorymodels.Collection)
storeState.Documents[newDatabase.ID] = make(map[string]map[string]repositorymodels.Document) r.storeState.Documents[newDatabase.ID] = make(map[string]map[string]repositorymodels.Document)
r.storeState.Triggers[newDatabase.ID] = make(map[string]map[string]repositorymodels.Trigger)
r.storeState.StoredProcedures[newDatabase.ID] = make(map[string]map[string]repositorymodels.StoredProcedure)
r.storeState.UserDefinedFunctions[newDatabase.ID] = make(map[string]map[string]repositorymodels.UserDefinedFunction)
return newDatabase, repositorymodels.StatusOk return newDatabase, repositorymodels.StatusOk
} }

View File

@@ -14,70 +14,83 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func GetAllDocuments(databaseId string, collectionId string) ([]repositorymodels.Document, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetAllDocuments(databaseId string, collectionId string) ([]repositorymodels.Document, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
} }
if _, ok := storeState.Collections[databaseId][collectionId]; !ok { if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
} }
return maps.Values(storeState.Documents[databaseId][collectionId]), repositorymodels.StatusOk return maps.Values(r.storeState.Documents[databaseId][collectionId]), repositorymodels.StatusOk
} }
func GetDocument(databaseId string, collectionId string, documentId string) (repositorymodels.Document, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetDocument(databaseId string, collectionId string, documentId string) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound return repositorymodels.Document{}, repositorymodels.StatusNotFound
} }
if _, ok := storeState.Collections[databaseId][collectionId]; !ok { if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound return repositorymodels.Document{}, repositorymodels.StatusNotFound
} }
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; !ok { if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound return repositorymodels.Document{}, repositorymodels.StatusNotFound
} }
return storeState.Documents[databaseId][collectionId][documentId], repositorymodels.StatusOk return r.storeState.Documents[databaseId][collectionId][documentId], repositorymodels.StatusOk
} }
func DeleteDocument(databaseId string, collectionId string, documentId string) repositorymodels.RepositoryStatus { func (r *DataRepository) DeleteDocument(databaseId string, collectionId string, documentId string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[databaseId]; !ok { r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
if _, ok := storeState.Collections[databaseId][collectionId]; !ok { if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; !ok { if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.StatusNotFound return repositorymodels.StatusNotFound
} }
delete(storeState.Documents[databaseId][collectionId], documentId) delete(r.storeState.Documents[databaseId][collectionId], documentId)
return repositorymodels.StatusOk return repositorymodels.StatusOk
} }
func CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (repositorymodels.Document, repositorymodels.RepositoryStatus) { func (r *DataRepository) CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool var ok bool
var documentId string var documentId string
var database repositorymodels.Database var database repositorymodels.Database
var collection repositorymodels.Collection var collection repositorymodels.Collection
if documentId, ok = document["id"].(string); !ok || documentId == "" { if documentId, ok = document["id"].(string); !ok || documentId == "" {
return repositorymodels.Document{}, repositorymodels.BadRequest documentId = fmt.Sprint(uuid.New())
document["id"] = documentId
} }
if database, ok = storeState.Databases[databaseId]; !ok { if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound return repositorymodels.Document{}, repositorymodels.StatusNotFound
} }
if collection, ok = storeState.Collections[databaseId][collectionId]; !ok { if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound return repositorymodels.Document{}, repositorymodels.StatusNotFound
} }
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; ok { if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; ok {
return repositorymodels.Document{}, repositorymodels.Conflict return repositorymodels.Document{}, repositorymodels.Conflict
} }
@@ -86,19 +99,19 @@ func CreateDocument(databaseId string, collectionId string, document map[string]
document["_etag"] = fmt.Sprintf("\"%s\"", uuid.New()) document["_etag"] = fmt.Sprintf("\"%s\"", uuid.New())
document["_self"] = fmt.Sprintf("dbs/%s/colls/%s/docs/%s/", database.ResourceID, collection.ResourceID, document["_rid"]) document["_self"] = fmt.Sprintf("dbs/%s/colls/%s/docs/%s/", database.ResourceID, collection.ResourceID, document["_rid"])
storeState.Documents[databaseId][collectionId][documentId] = document r.storeState.Documents[databaseId][collectionId][documentId] = document
return document, repositorymodels.StatusOk return document, repositorymodels.StatusOk
} }
func ExecuteQueryDocuments(databaseId string, collectionId string, query string, queryParameters map[string]interface{}) ([]memoryexecutor.RowType, repositorymodels.RepositoryStatus) { func (r *DataRepository) ExecuteQueryDocuments(databaseId string, collectionId string, query string, queryParameters map[string]interface{}) ([]memoryexecutor.RowType, repositorymodels.RepositoryStatus) {
parsedQuery, err := nosql.Parse("", []byte(query)) parsedQuery, err := nosql.Parse("", []byte(query))
if err != nil { if err != nil {
log.Printf("Failed to parse query: %s\nerr: %v", query, err) log.Printf("Failed to parse query: %s\nerr: %v", query, err)
return nil, repositorymodels.BadRequest return nil, repositorymodels.BadRequest
} }
collectionDocuments, status := GetAllDocuments(databaseId, collectionId) collectionDocuments, status := r.GetAllDocuments(databaseId, collectionId)
if status != repositorymodels.StatusOk { if status != repositorymodels.StatusOk {
return nil, status return nil, status
} }
@@ -110,7 +123,7 @@ func ExecuteQueryDocuments(databaseId string, collectionId string, query string,
if typedQuery, ok := parsedQuery.(parsers.SelectStmt); ok { if typedQuery, ok := parsedQuery.(parsers.SelectStmt); ok {
typedQuery.Parameters = queryParameters typedQuery.Parameters = queryParameters
return memoryexecutor.Execute(typedQuery, covDocs), repositorymodels.StatusOk return memoryexecutor.ExecuteQuery(typedQuery, covDocs), repositorymodels.StatusOk
} }
return nil, repositorymodels.BadRequest return nil, repositorymodels.BadRequest

View File

@@ -9,16 +9,19 @@ import (
) )
// I have no idea what this is tbh // I have no idea what this is tbh
func GetPartitionKeyRanges(databaseId string, collectionId string) ([]repositorymodels.PartitionKeyRange, repositorymodels.RepositoryStatus) { func (r *DataRepository) GetPartitionKeyRanges(databaseId string, collectionId string) ([]repositorymodels.PartitionKeyRange, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
databaseRid := databaseId databaseRid := databaseId
collectionRid := collectionId collectionRid := collectionId
var timestamp int64 = 0 var timestamp int64 = 0
if database, ok := storeState.Databases[databaseId]; !ok { if database, ok := r.storeState.Databases[databaseId]; !ok {
databaseRid = database.ResourceID databaseRid = database.ResourceID
} }
if collection, ok := storeState.Collections[databaseId][collectionId]; !ok { if collection, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
collectionRid = collection.ResourceID collectionRid = collection.ResourceID
timestamp = collection.TimeStamp timestamp = collection.TimeStamp
} }

View File

@@ -0,0 +1,34 @@
package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models"
type DataRepository struct {
storeState repositorymodels.State
initialDataFilePath string
persistDataFilePath string
}
type RepositoryOptions struct {
InitialDataFilePath string
PersistDataFilePath string
}
func NewDataRepository(options RepositoryOptions) *DataRepository {
repository := &DataRepository{
storeState: repositorymodels.State{
Databases: make(map[string]repositorymodels.Database),
Collections: make(map[string]map[string]repositorymodels.Collection),
Documents: make(map[string]map[string]map[string]repositorymodels.Document),
Triggers: make(map[string]map[string]map[string]repositorymodels.Trigger),
StoredProcedures: make(map[string]map[string]map[string]repositorymodels.StoredProcedure),
UserDefinedFunctions: make(map[string]map[string]map[string]repositorymodels.UserDefinedFunction),
},
initialDataFilePath: options.InitialDataFilePath,
persistDataFilePath: options.PersistDataFilePath,
}
repository.InitializeRepository()
return repository
}

View File

@@ -6,67 +6,76 @@ import (
"os" "os"
"reflect" "reflect"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" repositorymodels "github.com/pikami/cosmium/internal/repository_models"
) )
var storedProcedures = []repositorymodels.StoredProcedure{} func (r *DataRepository) InitializeRepository() {
var triggers = []repositorymodels.Trigger{} if r.initialDataFilePath != "" {
var userDefinedFunctions = []repositorymodels.UserDefinedFunction{} r.LoadStateFS(r.initialDataFilePath)
var storeState = repositorymodels.State{
Databases: make(map[string]repositorymodels.Database),
Collections: make(map[string]map[string]repositorymodels.Collection),
Documents: make(map[string]map[string]map[string]repositorymodels.Document),
}
func InitializeRepository() {
if config.Config.InitialDataFilePath != "" {
LoadStateFS(config.Config.InitialDataFilePath)
return return
} }
if config.Config.PersistDataFilePath != "" { if r.persistDataFilePath != "" {
stat, err := os.Stat(config.Config.PersistDataFilePath) stat, err := os.Stat(r.persistDataFilePath)
if err != nil { if err != nil {
return return
} }
if stat.IsDir() { if stat.IsDir() {
logger.Error("Argument '-Persist' must be a path to file, not a directory.") logger.ErrorLn("Argument '-Persist' must be a path to file, not a directory.")
os.Exit(1) os.Exit(1)
} }
LoadStateFS(config.Config.PersistDataFilePath) r.LoadStateFS(r.persistDataFilePath)
return return
} }
} }
func LoadStateFS(filePath string) { func (r *DataRepository) LoadStateFS(filePath string) {
data, err := os.ReadFile(filePath) data, err := os.ReadFile(filePath)
if err != nil { if err != nil {
log.Fatalf("Error reading state JSON file: %v", err) log.Fatalf("Error reading state JSON file: %v", err)
return return
} }
var state repositorymodels.State err = r.LoadStateJSON(string(data))
if err := json.Unmarshal(data, &state); err != nil { if err != nil {
log.Fatalf("Error unmarshalling state JSON: %v", err) log.Fatalf("Error unmarshalling state JSON: %v", err)
return
} }
logger.Info("Loaded state:")
logger.Infof("Databases: %d\n", getLength(state.Databases))
logger.Infof("Collections: %d\n", getLength(state.Collections))
logger.Infof("Documents: %d\n", getLength(state.Documents))
storeState = state
ensureStoreStateNoNullReferences()
} }
func SaveStateFS(filePath string) { func (r *DataRepository) LoadStateJSON(jsonData string) error {
data, err := json.MarshalIndent(storeState, "", "\t") r.storeState.Lock()
defer r.storeState.Unlock()
var state repositorymodels.State
if err := json.Unmarshal([]byte(jsonData), &state); err != nil {
return err
}
r.storeState.Collections = state.Collections
r.storeState.Databases = state.Databases
r.storeState.Documents = state.Documents
r.ensureStoreStateNoNullReferences()
logger.InfoLn("Loaded state:")
logger.Infof("Databases: %d\n", getLength(r.storeState.Databases))
logger.Infof("Collections: %d\n", getLength(r.storeState.Collections))
logger.Infof("Documents: %d\n", getLength(r.storeState.Documents))
logger.Infof("Triggers: %d\n", getLength(r.storeState.Triggers))
logger.Infof("Stored procedures: %d\n", getLength(r.storeState.StoredProcedures))
logger.Infof("User defined functions: %d\n", getLength(r.storeState.UserDefinedFunctions))
return nil
}
func (r *DataRepository) SaveStateFS(filePath string) {
r.storeState.RLock()
defer r.storeState.RUnlock()
data, err := json.MarshalIndent(r.storeState, "", "\t")
if err != nil { if err != nil {
logger.Errorf("Failed to save state: %v\n", err) logger.Errorf("Failed to save state: %v\n", err)
return return
@@ -74,21 +83,36 @@ func SaveStateFS(filePath string) {
os.WriteFile(filePath, data, os.ModePerm) os.WriteFile(filePath, data, os.ModePerm)
logger.Info("Saved state:") logger.InfoLn("Saved state:")
logger.Infof("Databases: %d\n", getLength(storeState.Databases)) logger.Infof("Databases: %d\n", getLength(r.storeState.Databases))
logger.Infof("Collections: %d\n", getLength(storeState.Collections)) logger.Infof("Collections: %d\n", getLength(r.storeState.Collections))
logger.Infof("Documents: %d\n", getLength(storeState.Documents)) logger.Infof("Documents: %d\n", getLength(r.storeState.Documents))
logger.Infof("Triggers: %d\n", getLength(r.storeState.Triggers))
logger.Infof("Stored procedures: %d\n", getLength(r.storeState.StoredProcedures))
logger.Infof("User defined functions: %d\n", getLength(r.storeState.UserDefinedFunctions))
} }
func GetState() repositorymodels.State { func (r *DataRepository) GetState() (string, error) {
return storeState r.storeState.RLock()
defer r.storeState.RUnlock()
data, err := json.MarshalIndent(r.storeState, "", "\t")
if err != nil {
logger.Errorf("Failed to serialize state: %v\n", err)
return "", err
}
return string(data), nil
} }
func getLength(v interface{}) int { func getLength(v interface{}) int {
switch v.(type) { switch v.(type) {
case repositorymodels.Database, case repositorymodels.Database,
repositorymodels.Collection, repositorymodels.Collection,
repositorymodels.Document: repositorymodels.Document,
repositorymodels.Trigger,
repositorymodels.StoredProcedure,
repositorymodels.UserDefinedFunction:
return 1 return 1
} }
@@ -109,38 +133,74 @@ func getLength(v interface{}) int {
return count return count
} }
func ensureStoreStateNoNullReferences() { func (r *DataRepository) ensureStoreStateNoNullReferences() {
if storeState.Databases == nil { if r.storeState.Databases == nil {
storeState.Databases = make(map[string]repositorymodels.Database) r.storeState.Databases = make(map[string]repositorymodels.Database)
} }
if storeState.Collections == nil { if r.storeState.Collections == nil {
storeState.Collections = make(map[string]map[string]repositorymodels.Collection) r.storeState.Collections = make(map[string]map[string]repositorymodels.Collection)
} }
if storeState.Documents == nil { if r.storeState.Documents == nil {
storeState.Documents = make(map[string]map[string]map[string]repositorymodels.Document) r.storeState.Documents = make(map[string]map[string]map[string]repositorymodels.Document)
} }
for database := range storeState.Databases { if r.storeState.Triggers == nil {
if storeState.Collections[database] == nil { r.storeState.Triggers = make(map[string]map[string]map[string]repositorymodels.Trigger)
storeState.Collections[database] = make(map[string]repositorymodels.Collection) }
if r.storeState.StoredProcedures == nil {
r.storeState.StoredProcedures = make(map[string]map[string]map[string]repositorymodels.StoredProcedure)
}
if r.storeState.UserDefinedFunctions == nil {
r.storeState.UserDefinedFunctions = make(map[string]map[string]map[string]repositorymodels.UserDefinedFunction)
}
for database := range r.storeState.Databases {
if r.storeState.Collections[database] == nil {
r.storeState.Collections[database] = make(map[string]repositorymodels.Collection)
} }
if storeState.Documents[database] == nil { if r.storeState.Documents[database] == nil {
storeState.Documents[database] = make(map[string]map[string]repositorymodels.Document) r.storeState.Documents[database] = make(map[string]map[string]repositorymodels.Document)
} }
for collection := range storeState.Collections[database] { if r.storeState.Triggers[database] == nil {
if storeState.Documents[database][collection] == nil { r.storeState.Triggers[database] = make(map[string]map[string]repositorymodels.Trigger)
storeState.Documents[database][collection] = make(map[string]repositorymodels.Document) }
if r.storeState.StoredProcedures[database] == nil {
r.storeState.StoredProcedures[database] = make(map[string]map[string]repositorymodels.StoredProcedure)
}
if r.storeState.UserDefinedFunctions[database] == nil {
r.storeState.UserDefinedFunctions[database] = make(map[string]map[string]repositorymodels.UserDefinedFunction)
}
for collection := range r.storeState.Collections[database] {
if r.storeState.Documents[database][collection] == nil {
r.storeState.Documents[database][collection] = make(map[string]repositorymodels.Document)
} }
for document := range storeState.Documents[database][collection] { for document := range r.storeState.Documents[database][collection] {
if storeState.Documents[database][collection][document] == nil { if r.storeState.Documents[database][collection][document] == nil {
delete(storeState.Documents[database][collection], document) delete(r.storeState.Documents[database][collection], document)
} }
} }
if r.storeState.Triggers[database][collection] == nil {
r.storeState.Triggers[database][collection] = make(map[string]repositorymodels.Trigger)
}
if r.storeState.StoredProcedures[database][collection] == nil {
r.storeState.StoredProcedures[database][collection] = make(map[string]repositorymodels.StoredProcedure)
}
if r.storeState.UserDefinedFunctions[database][collection] == nil {
r.storeState.UserDefinedFunctions[database][collection] = make(map[string]repositorymodels.UserDefinedFunction)
}
} }
} }
} }

View File

@@ -1,7 +1,91 @@
package repositories package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models" import (
"fmt"
"time"
func GetAllStoredProcedures(databaseId string, collectionId string) ([]repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) { "github.com/google/uuid"
return storedProcedures, repositorymodels.StatusOk repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *DataRepository) GetAllStoredProcedures(databaseId string, collectionId string) ([]repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.StoredProcedures[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetStoredProcedure(databaseId string, collectionId string, spId string) (repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if sp, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; ok {
return sp, repositorymodels.StatusOk
}
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
func (r *DataRepository) DeleteStoredProcedure(databaseId string, collectionId string, spId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.StoredProcedures[databaseId][collectionId], spId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateStoredProcedure(databaseId string, collectionId string, sp repositorymodels.StoredProcedure) (repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database repositorymodels.Database
var collection repositorymodels.Collection
if sp.ID == "" {
return repositorymodels.StoredProcedure{}, repositorymodels.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if _, ok = r.storeState.StoredProcedures[databaseId][collectionId][sp.ID]; ok {
return repositorymodels.StoredProcedure{}, repositorymodels.Conflict
}
sp.TimeStamp = time.Now().Unix()
sp.ResourceID = resourceid.NewCombined(database.ResourceID, collection.ResourceID, resourceid.New())
sp.ETag = fmt.Sprintf("\"%s\"", uuid.New())
sp.Self = fmt.Sprintf("dbs/%s/colls/%s/sprocs/%s/", database.ResourceID, collection.ResourceID, sp.ResourceID)
r.storeState.StoredProcedures[databaseId][collectionId][sp.ID] = sp
return sp, repositorymodels.StatusOk
} }

View File

@@ -1,7 +1,91 @@
package repositories package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models" import (
"fmt"
"time"
func GetAllTriggers(databaseId string, collectionId string) ([]repositorymodels.Trigger, repositorymodels.RepositoryStatus) { "github.com/google/uuid"
return triggers, repositorymodels.StatusOk repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *DataRepository) GetAllTriggers(databaseId string, collectionId string) ([]repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.Triggers[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetTrigger(databaseId string, collectionId string, triggerId string) (repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
}
if trigger, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; ok {
return trigger, repositorymodels.StatusOk
}
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
}
func (r *DataRepository) DeleteTrigger(databaseId string, collectionId string, triggerId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.Triggers[databaseId][collectionId], triggerId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateTrigger(databaseId string, collectionId string, trigger repositorymodels.Trigger) (repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database repositorymodels.Database
var collection repositorymodels.Collection
if trigger.ID == "" {
return repositorymodels.Trigger{}, repositorymodels.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
}
if _, ok = r.storeState.Triggers[databaseId][collectionId][trigger.ID]; ok {
return repositorymodels.Trigger{}, repositorymodels.Conflict
}
trigger.TimeStamp = time.Now().Unix()
trigger.ResourceID = resourceid.NewCombined(database.ResourceID, collection.ResourceID, resourceid.New())
trigger.ETag = fmt.Sprintf("\"%s\"", uuid.New())
trigger.Self = fmt.Sprintf("dbs/%s/colls/%s/triggers/%s/", database.ResourceID, collection.ResourceID, trigger.ResourceID)
r.storeState.Triggers[databaseId][collectionId][trigger.ID] = trigger
return trigger, repositorymodels.StatusOk
} }

View File

@@ -1,7 +1,91 @@
package repositories package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models" import (
"fmt"
"time"
func GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) { "github.com/google/uuid"
return userDefinedFunctions, repositorymodels.StatusOk repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *DataRepository) GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.UserDefinedFunctions[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetUserDefinedFunction(databaseId string, collectionId string, udfId string) (repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if udf, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; ok {
return udf, repositorymodels.StatusOk
}
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
func (r *DataRepository) DeleteUserDefinedFunction(databaseId string, collectionId string, udfId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.UserDefinedFunctions[databaseId][collectionId], udfId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateUserDefinedFunction(databaseId string, collectionId string, udf repositorymodels.UserDefinedFunction) (repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database repositorymodels.Database
var collection repositorymodels.Collection
if udf.ID == "" {
return repositorymodels.UserDefinedFunction{}, repositorymodels.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udf.ID]; ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.Conflict
}
udf.TimeStamp = time.Now().Unix()
udf.ResourceID = resourceid.NewCombined(database.ResourceID, collection.ResourceID, resourceid.New())
udf.ETag = fmt.Sprintf("\"%s\"", uuid.New())
udf.Self = fmt.Sprintf("dbs/%s/colls/%s/udfs/%s/", database.ResourceID, collection.ResourceID, udf.ResourceID)
r.storeState.UserDefinedFunctions[databaseId][collectionId][udf.ID] = udf
return udf, repositorymodels.StatusOk
} }

View File

@@ -1,5 +1,7 @@
package repositorymodels package repositorymodels
import "sync"
type Database struct { type Database struct {
ID string `json:"id"` ID string `json:"id"`
TimeStamp int64 `json:"_ts"` TimeStamp int64 `json:"_ts"`
@@ -17,6 +19,22 @@ const (
BadRequest = 4 BadRequest = 4
) )
type TriggerOperation string
const (
All TriggerOperation = "All"
Create TriggerOperation = "Create"
Delete TriggerOperation = "Delete"
Replace TriggerOperation = "Replace"
)
type TriggerType string
const (
Pre TriggerType = "Pre"
Post TriggerType = "Post"
)
type Collection struct { type Collection struct {
ID string `json:"id"` ID string `json:"id"`
IndexingPolicy CollectionIndexingPolicy `json:"indexingPolicy"` IndexingPolicy CollectionIndexingPolicy `json:"indexingPolicy"`
@@ -58,29 +76,29 @@ type UserDefinedFunction struct {
Body string `json:"body"` Body string `json:"body"`
ID string `json:"id"` ID string `json:"id"`
ResourceID string `json:"_rid"` ResourceID string `json:"_rid"`
TimeStamp int `json:"_ts"` TimeStamp int64 `json:"_ts"`
Self string `json:"_self"` Self string `json:"_self"`
Etag string `json:"_etag"` ETag string `json:"_etag"`
} }
type StoredProcedure struct { type StoredProcedure struct {
Body string `json:"body"` Body string `json:"body"`
ID string `json:"id"` ID string `json:"id"`
ResourceID string `json:"_rid"` ResourceID string `json:"_rid"`
TimeStamp int `json:"_ts"` TimeStamp int64 `json:"_ts"`
Self string `json:"_self"` Self string `json:"_self"`
Etag string `json:"_etag"` ETag string `json:"_etag"`
} }
type Trigger struct { type Trigger struct {
Body string `json:"body"` Body string `json:"body"`
ID string `json:"id"` ID string `json:"id"`
TriggerOperation string `json:"triggerOperation"` TriggerOperation TriggerOperation `json:"triggerOperation"`
TriggerType string `json:"triggerType"` TriggerType TriggerType `json:"triggerType"`
ResourceID string `json:"_rid"` ResourceID string `json:"_rid"`
TimeStamp int `json:"_ts"` TimeStamp int64 `json:"_ts"`
Self string `json:"_self"` Self string `json:"_self"`
Etag string `json:"_etag"` ETag string `json:"_etag"`
} }
type Document map[string]interface{} type Document map[string]interface{}
@@ -101,6 +119,8 @@ type PartitionKeyRange struct {
} }
type State struct { type State struct {
sync.RWMutex
// Map databaseId -> Database // Map databaseId -> Database
Databases map[string]Database `json:"databases"` Databases map[string]Database `json:"databases"`
@@ -109,4 +129,13 @@ type State struct {
// Map databaseId -> collectionId -> documentId -> Documents // Map databaseId -> collectionId -> documentId -> Documents
Documents map[string]map[string]map[string]Document `json:"documents"` Documents map[string]map[string]map[string]Document `json:"documents"`
// Map databaseId -> collectionId -> triggerId -> Trigger
Triggers map[string]map[string]map[string]Trigger `json:"triggers"`
// Map databaseId -> collectionId -> spId -> StoredProcedure
StoredProcedures map[string]map[string]map[string]StoredProcedure `json:"sprocs"`
// Map databaseId -> collectionId -> udfId -> UserDefinedFunction
UserDefinedFunctions map[string]map[string]map[string]UserDefinedFunction `json:"udfs"`
} }

View File

@@ -2,6 +2,7 @@ package resourceid
import ( import (
"encoding/base64" "encoding/base64"
"math/rand"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -10,6 +11,12 @@ func New() string {
id := uuid.New().ID() id := uuid.New().ID()
idBytes := uintToBytes(id) idBytes := uintToBytes(id)
// first byte should be bigger than 0x80 for collection ids
// clients classify this id as "user" otherwise
if (idBytes[0] & 0x80) <= 0 {
idBytes[0] = byte(rand.Intn(0x80) + 0x80)
}
return base64.StdEncoding.EncodeToString(idBytes) return base64.StdEncoding.EncodeToString(idBytes)
} }

View File

@@ -9,7 +9,7 @@ import (
func GetDefaultTlsConfig() *tls.Config { func GetDefaultTlsConfig() *tls.Config {
cert, err := tls.X509KeyPair([]byte(certificate), []byte(certificateKey)) cert, err := tls.X509KeyPair([]byte(certificate), []byte(certificateKey))
if err != nil { if err != nil {
logger.Error("Failed to parse certificate and key:", err) logger.ErrorLn("Failed to parse certificate and key:", err)
return &tls.Config{} return &tls.Config{}
} }

33
main.go
View File

@@ -1,33 +0,0 @@
package main
import (
"os"
"os/signal"
"syscall"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
)
func main() {
config.ParseFlags()
repositories.InitializeRepository()
go api.StartAPI()
waitForExit()
}
func waitForExit() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// Block until a exit signal is received
<-sigs
if config.Config.PersistDataFilePath != "" {
repositories.SaveStateFS(config.Config.PersistDataFilePath)
}
}

View File

@@ -3,7 +3,9 @@ package parsers
type SelectStmt struct { type SelectStmt struct {
SelectItems []SelectItem SelectItems []SelectItem
Table Table Table Table
JoinItems []JoinItem
Filters interface{} Filters interface{}
Exists bool
Distinct bool Distinct bool
Count int Count int
Offset int Offset int
@@ -13,7 +15,14 @@ type SelectStmt struct {
} }
type Table struct { type Table struct {
Value string Value string
SelectItem SelectItem
IsInSelect bool
}
type JoinItem struct {
Table Table
SelectItem SelectItem
} }
type SelectItemType int type SelectItemType int
@@ -24,6 +33,7 @@ const (
SelectItemTypeArray SelectItemTypeArray
SelectItemTypeConstant SelectItemTypeConstant
SelectItemTypeFunctionCall SelectItemTypeFunctionCall
SelectItemTypeSubQuery
) )
type SelectItem struct { type SelectItem struct {
@@ -114,11 +124,51 @@ const (
FunctionCallIsPrimitive FunctionCallType = "IsPrimitive" FunctionCallIsPrimitive FunctionCallType = "IsPrimitive"
FunctionCallIsString FunctionCallType = "IsString" FunctionCallIsString FunctionCallType = "IsString"
FunctionCallArrayConcat FunctionCallType = "ArrayConcat" FunctionCallArrayConcat FunctionCallType = "ArrayConcat"
FunctionCallArrayLength FunctionCallType = "ArrayLength" FunctionCallArrayContains FunctionCallType = "ArrayContains"
FunctionCallArraySlice FunctionCallType = "ArraySlice" FunctionCallArrayContainsAny FunctionCallType = "ArrayContainsAny"
FunctionCallSetIntersect FunctionCallType = "SetIntersect" FunctionCallArrayContainsAll FunctionCallType = "ArrayContainsAll"
FunctionCallSetUnion FunctionCallType = "SetUnion" FunctionCallArrayLength FunctionCallType = "ArrayLength"
FunctionCallArraySlice FunctionCallType = "ArraySlice"
FunctionCallSetIntersect FunctionCallType = "SetIntersect"
FunctionCallSetUnion FunctionCallType = "SetUnion"
FunctionCallMathAbs FunctionCallType = "MathAbs"
FunctionCallMathAcos FunctionCallType = "MathAcos"
FunctionCallMathAsin FunctionCallType = "MathAsin"
FunctionCallMathAtan FunctionCallType = "MathAtan"
FunctionCallMathAtn2 FunctionCallType = "MathAtn2"
FunctionCallMathCeiling FunctionCallType = "MathCeiling"
FunctionCallMathCos FunctionCallType = "MathCos"
FunctionCallMathCot FunctionCallType = "MathCot"
FunctionCallMathDegrees FunctionCallType = "MathDegrees"
FunctionCallMathExp FunctionCallType = "MathExp"
FunctionCallMathFloor FunctionCallType = "MathFloor"
FunctionCallMathIntAdd FunctionCallType = "MathIntAdd"
FunctionCallMathIntBitAnd FunctionCallType = "MathIntBitAnd"
FunctionCallMathIntBitLeftShift FunctionCallType = "MathIntBitLeftShift"
FunctionCallMathIntBitNot FunctionCallType = "MathIntBitNot"
FunctionCallMathIntBitOr FunctionCallType = "MathIntBitOr"
FunctionCallMathIntBitRightShift FunctionCallType = "MathIntBitRightShift"
FunctionCallMathIntBitXor FunctionCallType = "MathIntBitXor"
FunctionCallMathIntDiv FunctionCallType = "MathIntDiv"
FunctionCallMathIntMod FunctionCallType = "MathIntMod"
FunctionCallMathIntMul FunctionCallType = "MathIntMul"
FunctionCallMathIntSub FunctionCallType = "MathIntSub"
FunctionCallMathLog FunctionCallType = "MathLog"
FunctionCallMathLog10 FunctionCallType = "MathLog10"
FunctionCallMathNumberBin FunctionCallType = "MathNumberBin"
FunctionCallMathPi FunctionCallType = "MathPi"
FunctionCallMathPower FunctionCallType = "MathPower"
FunctionCallMathRadians FunctionCallType = "MathRadians"
FunctionCallMathRand FunctionCallType = "MathRand"
FunctionCallMathRound FunctionCallType = "MathRound"
FunctionCallMathSign FunctionCallType = "MathSign"
FunctionCallMathSin FunctionCallType = "MathSin"
FunctionCallMathSqrt FunctionCallType = "MathSqrt"
FunctionCallMathSquare FunctionCallType = "MathSquare"
FunctionCallMathTan FunctionCallType = "MathTan"
FunctionCallMathTrunc FunctionCallType = "MathTrunc"
FunctionCallAggregateAvg FunctionCallType = "AggregateAvg" FunctionCallAggregateAvg FunctionCallType = "AggregateAvg"
FunctionCallAggregateCount FunctionCallType = "AggregateCount" FunctionCallAggregateCount FunctionCallType = "AggregateCount"

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Parse_AggregateFunctions(t *testing.T) { func Test_Parse_AggregateFunctions(t *testing.T) {
@@ -27,7 +28,7 @@ func Test_Parse_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -51,7 +52,7 @@ func Test_Parse_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -75,7 +76,7 @@ func Test_Parse_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -99,7 +100,7 @@ func Test_Parse_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -123,7 +124,7 @@ func Test_Parse_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Parse_ArrayFunctions(t *testing.T) { func Test_Parse_ArrayFunctions(t *testing.T) {
@@ -31,7 +32,120 @@ func Test_Parse_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse function ARRAY_CONTAINS()", func(t *testing.T) {
testQueryParse(
t,
`SELECT ARRAY_CONTAINS(c.a1, "value") FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "a1"},
Type: parsers.SelectItemTypeField,
},
testutils.SelectItem_Constant_String("value"),
nil,
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse function ARRAY_CONTAINS() with partial match", func(t *testing.T) {
testQueryParse(
t,
`SELECT ARRAY_CONTAINS(["a", "b"], "value", true) FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
parsers.SelectItem{
Type: parsers.SelectItemTypeArray,
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Constant_String("a"),
testutils.SelectItem_Constant_String("b"),
},
},
testutils.SelectItem_Constant_String("value"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse function ARRAY_CONTAINS_ANY()", func(t *testing.T) {
testQueryParse(
t,
`SELECT ARRAY_CONTAINS_ANY(["a", "b"], "value", true) FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
parsers.SelectItem{
Type: parsers.SelectItemTypeArray,
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Constant_String("a"),
testutils.SelectItem_Constant_String("b"),
},
},
testutils.SelectItem_Constant_String("value"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse function ARRAY_CONTAINS_ALL()", func(t *testing.T) {
testQueryParse(
t,
`SELECT ARRAY_CONTAINS_ALL(["a", "b"], "value", true) FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
parsers.SelectItem{
Type: parsers.SelectItemTypeArray,
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Constant_String("a"),
testutils.SelectItem_Constant_String("b"),
},
},
testutils.SelectItem_Constant_String("value"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -55,7 +169,7 @@ func Test_Parse_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -75,25 +189,13 @@ func Test_Parse_ArrayFunctions(t *testing.T) {
Path: []string{"c", "array"}, Path: []string{"c", "array"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(0),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(2),
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 0,
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 2,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -121,7 +223,7 @@ func Test_Parse_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -149,7 +251,7 @@ func Test_Parse_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })

View File

@@ -0,0 +1,58 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Parse_Join(t *testing.T) {
t.Run("Should parse simple JOIN", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id, c["pk"] FROM c JOIN cc IN c["tags"]`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
)
})
t.Run("Should parse JOIN VALUE", func(t *testing.T) {
testQueryParse(
t,
`SELECT VALUE cc FROM c JOIN cc IN c["tags"]`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"cc"}, IsTopLevel: true},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
)
})
}

View File

@@ -0,0 +1,651 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_MathFunctions(t *testing.T) {
t.Run("Should parse function ABS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ABS(c.value) FROM c`,
parsers.FunctionCallMathAbs,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ACOS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ACOS(c.value) FROM c`,
parsers.FunctionCallMathAcos,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ASIN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ASIN(c.value) FROM c`,
parsers.FunctionCallMathAsin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ATAN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ATAN(c.value) FROM c`,
parsers.FunctionCallMathAtan,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function CEILING(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT CEILING(c.value) FROM c`,
parsers.FunctionCallMathCeiling,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function COS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT COS(c.value) FROM c`,
parsers.FunctionCallMathCos,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function COT(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT COT(c.value) FROM c`,
parsers.FunctionCallMathCot,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function DEGREES(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT DEGREES(c.value) FROM c`,
parsers.FunctionCallMathDegrees,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function EXP(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT EXP(c.value) FROM c`,
parsers.FunctionCallMathExp,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function FLOOR(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT FLOOR(c.value) FROM c`,
parsers.FunctionCallMathFloor,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitNot(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitNot(c.value) FROM c`,
parsers.FunctionCallMathIntBitNot,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG10(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG10(c.value) FROM c`,
parsers.FunctionCallMathLog10,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function RADIANS(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT RADIANS(c.value) FROM c`,
parsers.FunctionCallMathRadians,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ROUND(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ROUND(c.value) FROM c`,
parsers.FunctionCallMathRound,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SIGN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SIGN(c.value) FROM c`,
parsers.FunctionCallMathSign,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SIN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SIN(c.value) FROM c`,
parsers.FunctionCallMathSin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SQRT(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SQRT(c.value) FROM c`,
parsers.FunctionCallMathSqrt,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function SQUARE(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT SQUARE(c.value) FROM c`,
parsers.FunctionCallMathSquare,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function TAN(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT TAN(c.value) FROM c`,
parsers.FunctionCallMathTan,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function TRUNC(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT TRUNC(c.value) FROM c`,
parsers.FunctionCallMathTrunc,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function ATN2(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT ATN2(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathAtn2,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntAdd(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntAdd(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntAdd,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitAnd(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitAnd(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitAnd,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitLeftShift(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitLeftShift(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitLeftShift,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitOr(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitOr(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitOr,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitRightShift(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitRightShift(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitRightShift,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntBitXor(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntBitXor(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntBitXor,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntDiv(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntDiv(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntDiv,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntMod(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntMod(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntMod,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntMul(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntMul(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntMul,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function IntSub(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT IntSub(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathIntSub,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function POWER(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT POWER(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathPower,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG(c.value) FROM c`,
parsers.FunctionCallMathLog,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function LOG(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT LOG(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathLog,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function NumberBin(ex)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT NumberBin(c.value) FROM c`,
parsers.FunctionCallMathNumberBin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function NumberBin(ex1, ex2)", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT NumberBin(c.value, c.secondValue) FROM c`,
parsers.FunctionCallMathNumberBin,
[]interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
parsers.SelectItem{
Path: []string{"c", "secondValue"},
Type: parsers.SelectItemTypeField,
},
},
"c",
)
})
t.Run("Should parse function PI()", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT PI() FROM c`,
parsers.FunctionCallMathPi,
[]interface{}{},
"c",
)
})
t.Run("Should parse function RAND()", func(t *testing.T) {
testMathFunctionParse(
t,
`SELECT RAND() FROM c`,
parsers.FunctionCallMathRand,
[]interface{}{},
"c",
)
})
}
func testMathFunctionParse(
t *testing.T,
query string,
expectedFunctionType parsers.FunctionCallType,
expectedArguments []interface{},
expectedTable string,
) {
testQueryParse(
t,
query,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: expectedFunctionType,
Arguments: expectedArguments,
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path(expectedTable)},
},
)
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
"github.com/pikami/cosmium/parsers/nosql" "github.com/pikami/cosmium/parsers/nosql"
testutils "github.com/pikami/cosmium/test_utils"
) )
// For Parser Debugging // For Parser Debugging
@@ -48,7 +49,7 @@ func Test_Parse(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
OrderExpressions: []parsers.OrderExpression{ OrderExpressions: []parsers.OrderExpression{
{ {
SelectItem: parsers.SelectItem{Path: []string{"c", "id"}}, SelectItem: parsers.SelectItem{Path: []string{"c", "id"}},
@@ -72,7 +73,7 @@ func Test_Parse(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
@@ -92,7 +93,7 @@ func Test_Parse(t *testing.T) {
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -102,24 +103,32 @@ func Test_Parse(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_String("456"),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "456",
},
},
}, },
}, },
}, },
}, },
) )
}) })
t.Run("Should parse IN selector", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM c IN c.tags`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
},
Table: parsers.Table{
Value: "c",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
},
)
})
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,24 @@ package nosql
import "github.com/pikami/cosmium/parsers" import "github.com/pikami/cosmium/parsers"
func makeSelectStmt( func makeSelectStmt(
columns, table, columns, fromClause, joinItems,
whereClause interface{}, distinctClause interface{}, whereClause interface{}, distinctClause interface{},
count interface{}, groupByClause interface{}, orderList interface{}, count interface{}, groupByClause interface{}, orderList interface{},
offsetClause interface{}, offsetClause interface{},
) (parsers.SelectStmt, error) { ) (parsers.SelectStmt, error) {
selectStmt := parsers.SelectStmt{ selectStmt := parsers.SelectStmt{
SelectItems: columns.([]parsers.SelectItem), SelectItems: columns.([]parsers.SelectItem),
Table: table.(parsers.Table), }
if fromTable, ok := fromClause.(parsers.Table); ok {
selectStmt.Table = fromTable
}
if joinItemsArray, ok := joinItems.([]interface{}); ok && len(joinItemsArray) > 0 {
selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray))
for i, joinItem := range joinItemsArray {
selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem)
}
} }
switch v := whereClause.(type) { switch v := whereClause.(type) {
@@ -48,6 +58,21 @@ func makeSelectStmt(
return selectStmt, nil return selectStmt, nil
} }
func makeJoin(table interface{}, column interface{}) (parsers.JoinItem, error) {
joinItem := parsers.JoinItem{}
if selectItem, isSelectItem := column.(parsers.SelectItem); isSelectItem {
joinItem.SelectItem = selectItem
joinItem.Table.Value = selectItem.Alias
}
if tableTyped, isTable := table.(parsers.Table); isTable {
joinItem.Table = tableTyped
}
return joinItem, nil
}
func makeSelectItem(name interface{}, path interface{}, selectItemType parsers.SelectItemType) (parsers.SelectItem, error) { func makeSelectItem(name interface{}, path interface{}, selectItemType parsers.SelectItemType) (parsers.SelectItem, error) {
ps := path.([]interface{}) ps := path.([]interface{})
@@ -161,13 +186,15 @@ Input <- selectStmt:SelectStmt {
SelectStmt <- Select ws SelectStmt <- Select ws
distinctClause:DistinctClause? ws distinctClause:DistinctClause? ws
topClause:TopClause? ws columns:Selection ws topClause:TopClause? ws
From ws table:TableName ws columns:Selection ws
fromClause:FromClause? ws
joinClauses:(ws join:JoinClause { return join, nil })* ws
whereClause:(ws Where ws condition:Condition { return condition, nil })? whereClause:(ws Where ws condition:Condition { return condition, nil })?
groupByClause:(ws GroupBy ws columns:ColumnList { return columns, nil })? groupByClause:(ws GroupBy ws columns:ColumnList { return columns, nil })?
orderByClause:OrderByClause? orderByClause:(ws order:OrderByClause { return order, nil })?
offsetClause:OffsetClause? { offsetClause:(ws offset:OffsetClause { return offset, nil })? {
return makeSelectStmt(columns, table, whereClause, return makeSelectStmt(columns, fromClause, joinClauses, whereClause,
distinctClause, topClause, groupByClause, orderByClause, offsetClause) distinctClause, topClause, groupByClause, orderByClause, offsetClause)
} }
@@ -177,7 +204,60 @@ TopClause <- Top ws count:Integer {
return count, nil return count, nil
} }
OffsetClause <- "OFFSET"i ws offset:IntegerLiteral ws "LIMIT"i ws limit:IntegerLiteral { FromClause <- From ws table:TableName selectItem:(ws In ws column:SelectItem { return column, nil }) {
tableTyped := table.(parsers.Table)
if selectItem != nil {
tableTyped.SelectItem = selectItem.(parsers.SelectItem)
tableTyped.IsInSelect = true
}
return tableTyped, nil
} / From ws column:SelectItem {
tableSelectItem := column.(parsers.SelectItem)
table := parsers.Table{
Value: tableSelectItem.Alias,
SelectItem: tableSelectItem,
}
return table, nil
} / From ws subQuery:SubQuerySelectItem {
subQueryTyped := subQuery.(parsers.SelectItem)
table := parsers.Table{
Value: subQueryTyped.Alias,
SelectItem: subQueryTyped,
}
return table, nil
}
SubQuery <- exists:(exists:Exists ws { return exists, nil })? "(" ws selectStmt:SelectStmt ws ")" {
if selectStatement, isGoodValue := selectStmt.(parsers.SelectStmt); isGoodValue {
selectStatement.Exists = exists != nil
return selectStatement, nil
}
return selectStmt, nil
}
SubQuerySelectItem <- subQuery:SubQuery asClause:(ws alias:AsClause { return alias, nil })? {
selectItem := parsers.SelectItem{
Type: parsers.SelectItemTypeSubQuery,
Value: subQuery,
}
if tableName, isString := asClause.(string); isString {
selectItem.Alias = tableName
}
return selectItem, nil
}
JoinClause <- Join ws table:TableName ws In ws column:SelectItem {
return makeJoin(table, column)
} / Join ws subQuery:SubQuerySelectItem {
return makeJoin(nil, subQuery)
}
OffsetClause <- Offset ws offset:IntegerLiteral ws "LIMIT"i ws limit:IntegerLiteral {
return []interface{}{offset.(parsers.Constant).Value, limit.(parsers.Constant).Value}, nil return []interface{}{offset.(parsers.Constant).Value, limit.(parsers.Constant).Value}, nil
} }
@@ -221,7 +301,7 @@ SelectProperty <- name:Identifier path:(DotFieldAccess / ArrayFieldAccess)* {
return makeSelectItem(name, path, parsers.SelectItemTypeField) return makeSelectItem(name, path, parsers.SelectItemTypeField)
} }
SelectItem <- selectItem:(Literal / FunctionCall / SelectArray / SelectObject / SelectProperty) asClause:AsClause? { SelectItem <- selectItem:(SubQuerySelectItem / Literal / FunctionCall / SelectArray / SelectObject / SelectProperty) asClause:AsClause? {
var itemResult parsers.SelectItem var itemResult parsers.SelectItem
switch typedValue := selectItem.(type) { switch typedValue := selectItem.(type) {
case parsers.SelectItem: case parsers.SelectItem:
@@ -245,15 +325,19 @@ SelectItem <- selectItem:(Literal / FunctionCall / SelectArray / SelectObject /
return itemResult, nil return itemResult, nil
} }
AsClause <- ws As ws alias:Identifier { return alias, nil } AsClause <- (ws As)? ws !ExcludedKeywords alias:Identifier {
return alias, nil
}
ExcludedKeywords <- Select / Top / As / From / In / Join / Exists / Where / And / Or / GroupBy / OrderBy / Offset
DotFieldAccess <- "." id:Identifier { DotFieldAccess <- "." id:Identifier {
return id, nil return id, nil
} }
ArrayFieldAccess <- "[\"" id:Identifier "\"]" { ArrayFieldAccess <- "[\"" id:Identifier "\"]" { return id, nil }
return id, nil / "[" id:Integer "]" { return strconv.Itoa(id.(int)), nil }
} / "[" id:ParameterConstant "]" { return id.(parsers.Constant).Value.(string), nil }
Identifier <- [a-zA-Z_][a-zA-Z0-9_]* { Identifier <- [a-zA-Z_][a-zA-Z0-9_]* {
return string(c.text), nil return string(c.text), nil
@@ -301,16 +385,24 @@ As <- "AS"i
From <- "FROM"i From <- "FROM"i
In <- "IN"i
Join <- "JOIN"i
Exists <- "EXISTS"i
Where <- "WHERE"i Where <- "WHERE"i
And <- "AND"i And <- "AND"i
Or <- "OR"i Or <- "OR"i wss
GroupBy <- "GROUP"i ws "BY"i GroupBy <- "GROUP"i ws "BY"i
OrderBy <- "ORDER"i ws "BY"i OrderBy <- "ORDER"i ws "BY"i
Offset <- "OFFSET"i
ComparisonOperator <- ("=" / "!=" / "<" / "<=" / ">" / ">=") { ComparisonOperator <- ("=" / "!=" / "<" / "<=" / ">" / ">=") {
return string(c.text), nil return string(c.text), nil
} }
@@ -344,6 +436,7 @@ FunctionCall <- StringFunctions
/ ArrayFunctions / ArrayFunctions
/ InFunction / InFunction
/ AggregateFunctions / AggregateFunctions
/ MathFunctions
StringFunctions <- StringEqualsExpression StringFunctions <- StringEqualsExpression
/ ToStringExpression / ToStringExpression
@@ -380,11 +473,51 @@ AggregateFunctions <- AvgAggregateExpression
/ SumAggregateExpression / SumAggregateExpression
ArrayFunctions <- ArrayConcatExpression ArrayFunctions <- ArrayConcatExpression
/ ArrayContainsExpression
/ ArrayContainsAnyExpression
/ ArrayContainsAllExpression
/ ArrayLengthExpression / ArrayLengthExpression
/ ArraySliceExpression / ArraySliceExpression
/ SetIntersectExpression / SetIntersectExpression
/ SetUnionExpression / SetUnionExpression
MathFunctions <- MathAbsExpression
/ MathAcosExpression
/ MathAsinExpression
/ MathAtanExpression
/ MathCeilingExpression
/ MathCosExpression
/ MathCotExpression
/ MathDegreesExpression
/ MathExpExpression
/ MathFloorExpression
/ MathIntBitNotExpression
/ MathLog10Expression
/ MathRadiansExpression
/ MathRoundExpression
/ MathSignExpression
/ MathSinExpression
/ MathSqrtExpression
/ MathSquareExpression
/ MathTanExpression
/ MathTruncExpression
/ MathAtn2Expression
/ MathIntAddExpression
/ MathIntBitAndExpression
/ MathIntBitLeftShiftExpression
/ MathIntBitOrExpression
/ MathIntBitRightShiftExpression
/ MathIntBitXorExpression
/ MathIntDivExpression
/ MathIntModExpression
/ MathIntMulExpression
/ MathIntSubExpression
/ MathPowerExpression
/ MathLogExpression
/ MathNumberBinExpression
/ MathPiExpression
/ MathRandExpression
UpperExpression <- "UPPER"i ws "(" ex:SelectItem ")" { UpperExpression <- "UPPER"i ws "(" ex:SelectItem ")" {
return createFunctionCall(parsers.FunctionCallUpper, []interface{}{ex}) return createFunctionCall(parsers.FunctionCallUpper, []interface{}{ex})
} }
@@ -512,6 +645,18 @@ ArrayConcatExpression <- "ARRAY_CONCAT"i ws "(" ws arrays:SelectItem others:(ws
return createFunctionCall(parsers.FunctionCallArrayConcat, append([]interface{}{arrays}, others.([]interface{})...)) return createFunctionCall(parsers.FunctionCallArrayConcat, append([]interface{}{arrays}, others.([]interface{})...))
} }
ArrayContainsExpression <- "ARRAY_CONTAINS"i ws "(" ws array:SelectItem ws "," ws item:SelectItem partialMatch:(ws "," ws ex:SelectItem { return ex, nil })? ws ")" {
return createFunctionCall(parsers.FunctionCallArrayContains, []interface{}{array, item, partialMatch})
}
ArrayContainsAnyExpression <- "ARRAY_CONTAINS_ANY"i ws "(" ws array:SelectItem items:(ws "," ws ex:SelectItem { return ex, nil })+ ws ")" {
return createFunctionCall(parsers.FunctionCallArrayContainsAny, append([]interface{}{array}, items.([]interface{})...))
}
ArrayContainsAllExpression <- "ARRAY_CONTAINS_ALL"i ws "(" ws array:SelectItem items:(ws "," ws ex:SelectItem { return ex, nil })+ ws ")" {
return createFunctionCall(parsers.FunctionCallArrayContainsAll, append([]interface{}{array}, items.([]interface{})...))
}
ArrayLengthExpression <- "ARRAY_LENGTH"i ws "(" ws array:SelectItem ws ")" { ArrayLengthExpression <- "ARRAY_LENGTH"i ws "(" ws array:SelectItem ws ")" {
return createFunctionCall(parsers.FunctionCallArrayLength, []interface{}{array}) return createFunctionCall(parsers.FunctionCallArrayLength, []interface{}{array})
} }
@@ -528,7 +673,50 @@ SetUnionExpression <- "SetUnion"i ws "(" ws set1:SelectItem ws "," ws set2:Selec
return createFunctionCall(parsers.FunctionCallSetUnion, []interface{}{set1, set2}) return createFunctionCall(parsers.FunctionCallSetUnion, []interface{}{set1, set2})
} }
InFunction <- ex1:SelectProperty ws "IN"i ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" { MathAbsExpression <- "ABS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAbs, []interface{}{ex}) }
MathAcosExpression <- "ACOS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAcos, []interface{}{ex}) }
MathAsinExpression <- "ASIN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAsin, []interface{}{ex}) }
MathAtanExpression <- "ATAN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAtan, []interface{}{ex}) }
MathCeilingExpression <- "CEILING"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCeiling, []interface{}{ex}) }
MathCosExpression <- "COS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCos, []interface{}{ex}) }
MathCotExpression <- "COT"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathCot, []interface{}{ex}) }
MathDegreesExpression <- "DEGREES"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathDegrees, []interface{}{ex}) }
MathExpExpression <- "EXP"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathExp, []interface{}{ex}) }
MathFloorExpression <- "FLOOR"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathFloor, []interface{}{ex}) }
MathIntBitNotExpression <- "IntBitNot"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitNot, []interface{}{ex}) }
MathLog10Expression <- "LOG10"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathLog10, []interface{}{ex}) }
MathRadiansExpression <- "RADIANS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathRadians, []interface{}{ex}) }
MathRoundExpression <- "ROUND"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathRound, []interface{}{ex}) }
MathSignExpression <- "SIGN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSign, []interface{}{ex}) }
MathSinExpression <- "SIN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSin, []interface{}{ex}) }
MathSqrtExpression <- "SQRT"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSqrt, []interface{}{ex}) }
MathSquareExpression <- "SQUARE"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathSquare, []interface{}{ex}) }
MathTanExpression <- "TAN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathTan, []interface{}{ex}) }
MathTruncExpression <- "TRUNC"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathTrunc, []interface{}{ex}) }
MathAtn2Expression <- "ATN2"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAtn2, []interface{}{set1, set2}) }
MathIntAddExpression <- "IntAdd"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntAdd, []interface{}{set1, set2}) }
MathIntBitAndExpression <- "IntBitAnd"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitAnd, []interface{}{set1, set2}) }
MathIntBitLeftShiftExpression <- "IntBitLeftShift"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitLeftShift, []interface{}{set1, set2}) }
MathIntBitOrExpression <- "IntBitOr"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitOr, []interface{}{set1, set2}) }
MathIntBitRightShiftExpression <- "IntBitRightShift"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitRightShift, []interface{}{set1, set2}) }
MathIntBitXorExpression <- "IntBitXor"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntBitXor, []interface{}{set1, set2}) }
MathIntDivExpression <- "IntDiv"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntDiv, []interface{}{set1, set2}) }
MathIntModExpression <- "IntMod"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntMod, []interface{}{set1, set2}) }
MathIntMulExpression <- "IntMul"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntMul, []interface{}{set1, set2}) }
MathIntSubExpression <- "IntSub"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathIntSub, []interface{}{set1, set2}) }
MathPowerExpression <- "POWER"i ws "(" ws set1:SelectItem ws "," ws set2:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathPower, []interface{}{set1, set2}) }
MathLogExpression <- "LOG"i ws "(" ws ex1:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallMathLog, append([]interface{}{ex1}, others.([]interface{})...))
}
MathNumberBinExpression <- "NumberBin"i ws "(" ws ex1:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallMathNumberBin, append([]interface{}{ex1}, others.([]interface{})...))
}
MathPiExpression <- "PI"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathPi, []interface{}{}) }
MathRandExpression <- "RAND"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathRand, []interface{}{}) }
InFunction <- ex1:SelectProperty ws In ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallIn, append([]interface{}{ex1, ex2}, others.([]interface{})...)) return createFunctionCall(parsers.FunctionCallIn, append([]interface{}{ex1, ex2}, others.([]interface{})...))
} }
@@ -575,4 +763,6 @@ non_escape_character <- !(escape_character) char:.
ws <- [ \t\n\r]* ws <- [ \t\n\r]*
wss <- [ \t\n\r]+
EOF <- !. EOF <- !.

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Parse_Select(t *testing.T) { func Test_Parse_Select(t *testing.T) {
@@ -17,7 +18,21 @@ func Test_Parse_Select(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse SELECT with query parameters as accessor", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id, c[@param] FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "@param"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -30,7 +45,7 @@ func Test_Parse_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Distinct: true, Distinct: true,
}, },
) )
@@ -44,7 +59,7 @@ func Test_Parse_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Count: 1, Count: 1,
}, },
) )
@@ -58,7 +73,7 @@ func Test_Parse_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Count: 5, Count: 5,
Offset: 3, Offset: 3,
}, },
@@ -73,7 +88,7 @@ func Test_Parse_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}, IsTopLevel: true}, {Path: []string{"c", "id"}, IsTopLevel: true},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -86,7 +101,20 @@ func Test_Parse_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c"}, IsTopLevel: true}, {Path: []string{"c"}, IsTopLevel: true},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse SELECT c", func(t *testing.T) {
testQueryParse(
t,
`SELECT c FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c"}, IsTopLevel: false},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -106,7 +134,27 @@ func Test_Parse_Select(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse SELECT with alias", func(t *testing.T) {
testQueryParse(
t,
`SELECT
c.id AS aliasWithAs,
c.pk aliasWithoutAs
FROM root c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Alias: "aliasWithAs", Path: []string{"c", "id"}},
{Alias: "aliasWithoutAs", Path: []string{"c", "pk"}},
},
Table: parsers.Table{
Value: "c",
SelectItem: parsers.SelectItem{Alias: "c", Path: []string{"root"}},
},
}, },
) )
}) })
@@ -126,7 +174,7 @@ func Test_Parse_Select(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_StringFunctions(t *testing.T) { func Test_Execute_StringFunctions(t *testing.T) {
@@ -23,25 +24,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -61,19 +50,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
nil, nil,
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -93,13 +76,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{ parsers.SelectItem{
Path: []string{"c", "pk"}, Path: []string{"c", "pk"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
@@ -108,7 +85,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -128,25 +105,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -166,25 +131,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -204,25 +157,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -242,25 +183,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("2"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(1),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "2",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 1,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -284,7 +213,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -308,7 +237,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -332,7 +261,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -352,18 +281,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(5),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 5,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -387,7 +310,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -411,7 +334,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -431,25 +354,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("old"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_String("new"),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "old",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "new",
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -469,18 +380,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(3),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 3,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -504,7 +409,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -524,18 +429,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(3),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 3,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -559,7 +458,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -579,25 +478,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(1),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(5),
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 1,
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 5,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -621,7 +508,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })

View File

@@ -0,0 +1,122 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Parse_SubQuery(t *testing.T) {
t.Run("Should parse FROM subquery", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM (SELECT VALUE cc["info"] FROM cc) AS c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
},
Table: parsers.Table{
Value: "c",
SelectItem: parsers.SelectItem{
Alias: "c",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("cc")},
SelectItems: []parsers.SelectItem{
{Path: []string{"cc", "info"}, IsTopLevel: true},
},
},
},
},
},
)
})
t.Run("Should parse JOIN subquery", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id, cc.name FROM c JOIN (SELECT tag.name FROM tag IN c.tags) AS cc`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"cc", "name"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Alias: "cc",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("tag", "name"),
},
Table: parsers.Table{
Value: "tag",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
},
},
},
},
},
)
})
t.Run("Should parse JOIN EXISTS subquery", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id
FROM c
JOIN (
SELECT VALUE EXISTS(SELECT tag.name FROM tag IN c.tags)
) AS hasTags
WHERE hasTags`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
},
Table: parsers.Table{
SelectItem: testutils.SelectItem_Path("c"),
},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{Value: "hasTags"},
SelectItem: parsers.SelectItem{
Alias: "hasTags",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
IsTopLevel: true,
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("tag", "name"),
},
Table: parsers.Table{
Value: "tag",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
Exists: true,
},
},
},
},
},
},
},
Filters: parsers.SelectItem{
Path: []string{"hasTags"},
},
},
)
})
}

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_TypeCheckingFunctions(t *testing.T) { func Test_Execute_TypeCheckingFunctions(t *testing.T) {
@@ -27,7 +28,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -63,7 +64,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -99,7 +100,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -135,7 +136,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -171,7 +172,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -207,7 +208,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -243,7 +244,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -279,7 +280,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -315,7 +316,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -351,7 +352,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{

View File

@@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Parse_Were(t *testing.T) { func Test_Parse_Were(t *testing.T) {
@@ -18,14 +19,11 @@ func Test_Parse_Were(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.ComparisonExpression{ Filters: parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "isCool"}}, Left: parsers.SelectItem{Path: []string{"c", "isCool"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Bool(true),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeBoolean, Value: true},
},
}, },
}, },
) )
@@ -44,25 +42,19 @@ func Test_Parse_Were(t *testing.T) {
{Path: []string{"c", "_rid"}}, {Path: []string{"c", "_rid"}},
{Path: []string{"c", "_ts"}}, {Path: []string{"c", "_ts"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{ Filters: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeOr, Operation: parsers.LogicalExpressionTypeOr,
Expressions: []interface{}{ Expressions: []interface{}{
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("12345"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "12345"},
},
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "pk"}}, Left: parsers.SelectItem{Path: []string{"c", "pk"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Int(123),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeInteger, Value: 123},
},
}, },
}, },
}, },
@@ -80,17 +72,14 @@ func Test_Parse_Were(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{ Filters: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd, Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{ Expressions: []interface{}{
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "isCool"}}, Left: parsers.SelectItem{Path: []string{"c", "isCool"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Bool(true),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeBoolean, Value: true},
},
}, },
parsers.LogicalExpression{ parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeOr, Operation: parsers.LogicalExpressionTypeOr,
@@ -98,18 +87,12 @@ func Test_Parse_Were(t *testing.T) {
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "123"},
},
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("456"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "456"},
},
}, },
}, },
}, },
@@ -131,47 +114,32 @@ func Test_Parse_Were(t *testing.T) {
AND c.param=@param_id1`, AND c.param=@param_id1`,
parsers.SelectStmt{ parsers.SelectStmt{
SelectItems: []parsers.SelectItem{{Path: []string{"c", "id"}, Alias: ""}}, SelectItems: []parsers.SelectItem{{Path: []string{"c", "id"}, Alias: ""}},
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{ Filters: parsers.LogicalExpression{
Expressions: []interface{}{ Expressions: []interface{}{
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Left: parsers.SelectItem{Path: []string{"c", "boolean"}}, Left: parsers.SelectItem{Path: []string{"c", "boolean"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Bool(true),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeBoolean, Value: true},
},
Operation: "=", Operation: "=",
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Left: parsers.SelectItem{Path: []string{"c", "integer"}}, Left: parsers.SelectItem{Path: []string{"c", "integer"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Int(1),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeInteger, Value: 1},
},
Operation: "=", Operation: "=",
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Left: parsers.SelectItem{Path: []string{"c", "float"}}, Left: parsers.SelectItem{Path: []string{"c", "float"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Float(6.9),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeFloat, Value: 6.9},
},
Operation: "=", Operation: "=",
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Left: parsers.SelectItem{Path: []string{"c", "string"}}, Left: parsers.SelectItem{Path: []string{"c", "string"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("hello"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "hello"},
},
Operation: "=", Operation: "=",
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Left: parsers.SelectItem{Path: []string{"c", "param"}}, Left: parsers.SelectItem{Path: []string{"c", "param"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Parameter("@param_id1"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeParameterConstant, Value: "@param_id1"},
},
Operation: "=", Operation: "=",
}, },
}, },

View File

@@ -6,21 +6,19 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
) )
func (c memoryExecutorContext) aggregate_Avg(arguments []interface{}, row RowType) interface{} { func (r rowContext) aggregate_Avg(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
sum := 0.0 sum := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { for _, item := range r.grouppedRows {
for _, item := range array { value := item.resolveSelectItem(selectExpression)
value := c.getFieldValue(selectExpression, item) if numericValue, ok := value.(float64); ok {
if numericValue, ok := value.(float64); ok { sum += numericValue
sum += numericValue count++
count++ } else if numericValue, ok := value.(int); ok {
} else if numericValue, ok := value.(int); ok { sum += float64(numericValue)
sum += float64(numericValue) count++
count++
}
} }
} }
@@ -31,41 +29,37 @@ func (c memoryExecutorContext) aggregate_Avg(arguments []interface{}, row RowTyp
} }
} }
func (c memoryExecutorContext) aggregate_Count(arguments []interface{}, row RowType) interface{} { func (r rowContext) aggregate_Count(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { for _, item := range r.grouppedRows {
for _, item := range array { value := item.resolveSelectItem(selectExpression)
value := c.getFieldValue(selectExpression, item) if value != nil {
if value != nil { count++
count++
}
} }
} }
return count return count
} }
func (c memoryExecutorContext) aggregate_Max(arguments []interface{}, row RowType) interface{} { func (r rowContext) aggregate_Max(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
max := 0.0 max := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { for _, item := range r.grouppedRows {
for _, item := range array { value := item.resolveSelectItem(selectExpression)
value := c.getFieldValue(selectExpression, item) if numericValue, ok := value.(float64); ok {
if numericValue, ok := value.(float64); ok { if numericValue > max {
if numericValue > max { max = numericValue
max = numericValue
}
count++
} else if numericValue, ok := value.(int); ok {
if float64(numericValue) > max {
max = float64(numericValue)
}
count++
} }
count++
} else if numericValue, ok := value.(int); ok {
if float64(numericValue) > max {
max = float64(numericValue)
}
count++
} }
} }
@@ -76,25 +70,23 @@ func (c memoryExecutorContext) aggregate_Max(arguments []interface{}, row RowTyp
} }
} }
func (c memoryExecutorContext) aggregate_Min(arguments []interface{}, row RowType) interface{} { func (r rowContext) aggregate_Min(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
min := math.MaxFloat64 min := math.MaxFloat64
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { for _, item := range r.grouppedRows {
for _, item := range array { value := item.resolveSelectItem(selectExpression)
value := c.getFieldValue(selectExpression, item) if numericValue, ok := value.(float64); ok {
if numericValue, ok := value.(float64); ok { if numericValue < min {
if numericValue < min { min = numericValue
min = numericValue
}
count++
} else if numericValue, ok := value.(int); ok {
if float64(numericValue) < min {
min = float64(numericValue)
}
count++
} }
count++
} else if numericValue, ok := value.(int); ok {
if float64(numericValue) < min {
min = float64(numericValue)
}
count++
} }
} }
@@ -105,21 +97,19 @@ func (c memoryExecutorContext) aggregate_Min(arguments []interface{}, row RowTyp
} }
} }
func (c memoryExecutorContext) aggregate_Sum(arguments []interface{}, row RowType) interface{} { func (r rowContext) aggregate_Sum(arguments []interface{}) interface{} {
selectExpression := arguments[0].(parsers.SelectItem) selectExpression := arguments[0].(parsers.SelectItem)
sum := 0.0 sum := 0.0
count := 0 count := 0
if array, isArray := row.([]RowType); isArray { for _, item := range r.grouppedRows {
for _, item := range array { value := item.resolveSelectItem(selectExpression)
value := c.getFieldValue(selectExpression, item) if numericValue, ok := value.(float64); ok {
if numericValue, ok := value.(float64); ok { sum += numericValue
sum += numericValue count++
count++ } else if numericValue, ok := value.(int); ok {
} else if numericValue, ok := value.(int); ok { sum += float64(numericValue)
sum += float64(numericValue) count++
count++
}
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_AggregateFunctions(t *testing.T) { func Test_Execute_AggregateFunctions(t *testing.T) {
@@ -38,7 +39,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "key"}}, {Path: []string{"c", "key"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -67,7 +68,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -99,7 +100,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "key"}}, {Path: []string{"c", "key"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -132,7 +133,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "key"}}, {Path: []string{"c", "key"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -165,7 +166,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "key"}}, {Path: []string{"c", "key"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -198,7 +199,7 @@ func Test_Execute_AggregateFunctions(t *testing.T) {
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "key"}}, {Path: []string{"c", "key"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{

View File

@@ -7,17 +7,97 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
) )
func (c memoryExecutorContext) array_Concat(arguments []interface{}, row RowType) []interface{} { func (r rowContext) array_Concat(arguments []interface{}) []interface{} {
var result []interface{} var result []interface{}
for _, arg := range arguments { for _, arg := range arguments {
array := c.parseArray(arg, row) array := r.parseArray(arg)
result = append(result, array...) result = append(result, array...)
} }
return result return result
} }
func (c memoryExecutorContext) array_Length(arguments []interface{}, row RowType) int { func (r rowContext) array_Contains(arguments []interface{}) bool {
array := c.parseArray(arguments[0], row) array := r.parseArray(arguments[0])
if array == nil {
return false
}
exprToSearch := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
partialSearch := false
if len(arguments) > 2 {
boolExpr := r.resolveSelectItem(arguments[2].(parsers.SelectItem))
if boolValue, ok := boolExpr.(bool); ok {
partialSearch = boolValue
} else {
logger.ErrorLn("array_Contains - got parameters of wrong type")
return false
}
}
for _, item := range array {
if partialSearch {
if r.partialMatch(item, exprToSearch) {
return true
}
} else {
if reflect.DeepEqual(item, exprToSearch) {
return true
}
}
}
return false
}
func (r rowContext) array_Contains_Any(arguments []interface{}) bool {
array := r.parseArray(arguments[0])
if array == nil {
return false
}
valueSelectItems := arguments[1:]
for _, valueSelectItem := range valueSelectItems {
value := r.resolveSelectItem(valueSelectItem.(parsers.SelectItem))
for _, item := range array {
if reflect.DeepEqual(item, value) {
return true
}
}
}
return false
}
func (r rowContext) array_Contains_All(arguments []interface{}) bool {
array := r.parseArray(arguments[0])
if array == nil {
return false
}
valueSelectItems := arguments[1:]
for _, valueSelectItem := range valueSelectItems {
value := r.resolveSelectItem(valueSelectItem.(parsers.SelectItem))
found := false
for _, item := range array {
if reflect.DeepEqual(item, value) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func (r rowContext) array_Length(arguments []interface{}) int {
array := r.parseArray(arguments[0])
if array == nil { if array == nil {
return 0 return 0
} }
@@ -25,24 +105,24 @@ func (c memoryExecutorContext) array_Length(arguments []interface{}, row RowType
return len(array) return len(array)
} }
func (c memoryExecutorContext) array_Slice(arguments []interface{}, row RowType) []interface{} { func (r rowContext) array_Slice(arguments []interface{}) []interface{} {
var ok bool var ok bool
var start int var start int
var length int var length int
array := c.parseArray(arguments[0], row) array := r.parseArray(arguments[0])
startEx := c.getFieldValue(arguments[1].(parsers.SelectItem), row) startEx := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
if arguments[2] != nil { if arguments[2] != nil {
lengthEx := c.getFieldValue(arguments[2].(parsers.SelectItem), row) lengthEx := r.resolveSelectItem(arguments[2].(parsers.SelectItem))
if length, ok = lengthEx.(int); !ok { if length, ok = lengthEx.(int); !ok {
logger.Error("array_Slice - got length parameters of wrong type") logger.ErrorLn("array_Slice - got length parameters of wrong type")
return []interface{}{} return []interface{}{}
} }
} }
if start, ok = startEx.(int); !ok { if start, ok = startEx.(int); !ok {
logger.Error("array_Slice - got start parameters of wrong type") logger.ErrorLn("array_Slice - got start parameters of wrong type")
return []interface{}{} return []interface{}{}
} }
@@ -65,9 +145,9 @@ func (c memoryExecutorContext) array_Slice(arguments []interface{}, row RowType)
return array[start:end] return array[start:end]
} }
func (c memoryExecutorContext) set_Intersect(arguments []interface{}, row RowType) []interface{} { func (r rowContext) set_Intersect(arguments []interface{}) []interface{} {
set1 := c.parseArray(arguments[0], row) set1 := r.parseArray(arguments[0])
set2 := c.parseArray(arguments[1], row) set2 := r.parseArray(arguments[1])
intersection := make(map[interface{}]struct{}) intersection := make(map[interface{}]struct{})
if set1 == nil || set2 == nil { if set1 == nil || set2 == nil {
@@ -88,9 +168,9 @@ func (c memoryExecutorContext) set_Intersect(arguments []interface{}, row RowTyp
return result return result
} }
func (c memoryExecutorContext) set_Union(arguments []interface{}, row RowType) []interface{} { func (r rowContext) set_Union(arguments []interface{}) []interface{} {
set1 := c.parseArray(arguments[0], row) set1 := r.parseArray(arguments[0])
set2 := c.parseArray(arguments[1], row) set2 := r.parseArray(arguments[1])
var result []interface{} var result []interface{}
union := make(map[interface{}]struct{}) union := make(map[interface{}]struct{})
@@ -111,13 +191,13 @@ func (c memoryExecutorContext) set_Union(arguments []interface{}, row RowType) [
return result return result
} }
func (c memoryExecutorContext) parseArray(argument interface{}, row RowType) []interface{} { func (r rowContext) parseArray(argument interface{}) []interface{} {
exItem := argument.(parsers.SelectItem) exItem := argument.(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
arrValue := reflect.ValueOf(ex) arrValue := reflect.ValueOf(ex)
if arrValue.Kind() != reflect.Slice { if arrValue.Kind() != reflect.Slice {
logger.Error("parseArray got parameters of wrong type") logger.ErrorLn("parseArray got parameters of wrong type")
return nil return nil
} }
@@ -129,3 +209,21 @@ func (c memoryExecutorContext) parseArray(argument interface{}, row RowType) []i
return result return result
} }
func (r rowContext) partialMatch(item interface{}, exprToSearch interface{}) bool {
itemValue := reflect.ValueOf(item)
exprValue := reflect.ValueOf(exprToSearch)
if itemValue.Kind() != reflect.Map || exprValue.Kind() != reflect.Map {
logger.ErrorLn("partialMatch got parameters of wrong type")
return false
}
for _, key := range exprValue.MapKeys() {
if !reflect.DeepEqual(itemValue.MapIndex(key).Interface(), exprValue.MapIndex(key).Interface()) {
return false
}
}
return true
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_ArrayFunctions(t *testing.T) { func Test_Execute_ArrayFunctions(t *testing.T) {
@@ -41,7 +42,7 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -52,6 +53,300 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
) )
}) })
t.Run("Should execute function ARRAY_CONTAINS()", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
Parameters: map[string]interface{}{
"@categories": []interface{}{"coats", "jackets", "sweatshirts"},
"@objectArray": []interface{}{map[string]interface{}{"category": "shirts", "color": "blue", "nestedObject": map[string]interface{}{"size": "M"}}},
"@fullMatchObject": map[string]interface{}{"category": "shirts", "color": "blue", "nestedObject": map[string]interface{}{"size": "M"}},
"@partialMatchObject": map[string]interface{}{"category": "shirts"},
"@missingPartialMatchObject": map[string]interface{}{"category": "shorts", "color": "blue"},
"@nestedPartialMatchObject": map[string]interface{}{"nestedObject": map[string]interface{}{"size": "M"}},
},
SelectItems: []parsers.SelectItem{
{
Alias: "ContainsItem",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@categories"),
testutils.SelectItem_Constant_String("coats"),
},
},
},
{
Alias: "MissingItem",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@categories"),
testutils.SelectItem_Constant_String("hoodies"),
},
},
},
{
Alias: "ContainsFullMatchObject",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@objectArray"),
testutils.SelectItem_Constant_Parameter("@fullMatchObject"),
},
},
},
{
Alias: "MissingFullMatchObject",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@objectArray"),
testutils.SelectItem_Constant_Parameter("@partialMatchObject"),
},
},
},
{
Alias: "ContainsPartialMatchObject",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@objectArray"),
testutils.SelectItem_Constant_Parameter("@partialMatchObject"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
{
Alias: "MissingPartialMatchObject",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@objectArray"),
testutils.SelectItem_Constant_Parameter("@missingPartialMatchObject"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
{
Alias: "ContainsNestedPartialMatchObject",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContains,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@objectArray"),
testutils.SelectItem_Constant_Parameter("@nestedPartialMatchObject"),
testutils.SelectItem_Constant_Bool(true),
},
},
},
},
},
[]memoryexecutor.RowType{map[string]interface{}{"id": "123"}},
[]memoryexecutor.RowType{
map[string]interface{}{
"ContainsItem": true,
"MissingItem": false,
"ContainsFullMatchObject": true,
"MissingFullMatchObject": false,
"ContainsPartialMatchObject": true,
"MissingPartialMatchObject": false,
"ContainsNestedPartialMatchObject": true,
},
},
)
})
t.Run("Should execute function ARRAY_CONTAINS_ANY()", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
Parameters: map[string]interface{}{
"@mixedArray": []interface{}{1, true, "3", []int{1, 2, 3}},
"@numbers": []interface{}{1, 2, 3, 4},
"@emptyArray": []interface{}{},
"@arr123": []interface{}{1, 2, 3},
},
SelectItems: []parsers.SelectItem{
{
Alias: "matchesEntireArray",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@mixedArray"),
testutils.SelectItem_Constant_Int(1),
testutils.SelectItem_Constant_Bool(true),
testutils.SelectItem_Constant_String("3"),
testutils.SelectItem_Constant_Parameter("@arr123"),
},
},
},
{
Alias: "matchesSomeValues",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(2),
testutils.SelectItem_Constant_Int(3),
testutils.SelectItem_Constant_Int(4),
testutils.SelectItem_Constant_Int(5),
},
},
},
{
Alias: "matchSingleValue",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(1),
},
},
},
{
Alias: "noMatches",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(5),
testutils.SelectItem_Constant_Int(6),
testutils.SelectItem_Constant_Int(7),
testutils.SelectItem_Constant_Int(8),
},
},
},
{
Alias: "emptyArray",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAny,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@emptyArray"),
testutils.SelectItem_Constant_Int(1),
testutils.SelectItem_Constant_Int(2),
testutils.SelectItem_Constant_Int(3),
},
},
},
},
},
[]memoryexecutor.RowType{map[string]interface{}{"id": "123"}},
[]memoryexecutor.RowType{
map[string]interface{}{
"matchesEntireArray": true,
"matchesSomeValues": true,
"matchSingleValue": true,
"noMatches": false,
"emptyArray": false,
},
},
)
})
t.Run("Should execute function ARRAY_CONTAINS_ALL()", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
Parameters: map[string]interface{}{
"@mixedArray": []interface{}{1, true, "3", []interface{}{1, 2, 3}},
"@numbers": []interface{}{1, 2, 3, 4},
"@emptyArray": []interface{}{},
"@arr123": []interface{}{1, 2, 3},
},
SelectItems: []parsers.SelectItem{
{
Alias: "matchesEntireArray",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@mixedArray"),
testutils.SelectItem_Constant_Int(1),
testutils.SelectItem_Constant_Bool(true),
testutils.SelectItem_Constant_String("3"),
testutils.SelectItem_Constant_Parameter("@arr123"),
},
},
},
{
Alias: "matchesSomeValues",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(2),
testutils.SelectItem_Constant_Int(3),
testutils.SelectItem_Constant_Int(4),
testutils.SelectItem_Constant_Int(5),
},
},
},
{
Alias: "matchSingleValue",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(1),
},
},
},
{
Alias: "noMatches",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@numbers"),
testutils.SelectItem_Constant_Int(5),
testutils.SelectItem_Constant_Int(6),
testutils.SelectItem_Constant_Int(7),
testutils.SelectItem_Constant_Int(8),
},
},
},
{
Alias: "emptyArray",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallArrayContainsAll,
Arguments: []interface{}{
testutils.SelectItem_Constant_Parameter("@emptyArray"),
testutils.SelectItem_Constant_Int(1),
testutils.SelectItem_Constant_Int(2),
testutils.SelectItem_Constant_Int(3),
},
},
},
},
},
[]memoryexecutor.RowType{map[string]interface{}{"id": "123"}},
[]memoryexecutor.RowType{
map[string]interface{}{
"matchesEntireArray": true,
"matchesSomeValues": false,
"matchSingleValue": true,
"noMatches": false,
"emptyArray": false,
},
},
)
})
t.Run("Should execute function ARRAY_LENGTH()", func(t *testing.T) { t.Run("Should execute function ARRAY_LENGTH()", func(t *testing.T) {
testQueryExecute( testQueryExecute(
t, t,
@@ -75,7 +370,7 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -105,25 +400,13 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
Path: []string{"c", "arr2"}, Path: []string{"c", "arr2"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(1),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(2),
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 1,
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 2,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -161,7 +444,7 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -199,7 +482,7 @@ func Test_Execute_ArrayFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{

View File

@@ -0,0 +1,87 @@
package memoryexecutor_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_Joins(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{
"id": 1,
"tags": []map[string]interface{}{
{"name": "a"},
{"name": "b"},
},
},
map[string]interface{}{
"id": 2,
"tags": []map[string]interface{}{
{"name": "b"},
{"name": "c"},
},
},
}
t.Run("Should execute JOIN on 'tags'", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"cc", "name"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": 1, "name": "a"},
map[string]interface{}{"id": 1, "name": "b"},
map[string]interface{}{"id": 2, "name": "b"},
map[string]interface{}{"id": 2, "name": "c"},
},
)
})
t.Run("Should execute JOIN VALUE on 'tags'", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"cc"}, IsTopLevel: true},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Path: []string{"c", "tags"},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"name": "a"},
map[string]interface{}{"name": "b"},
map[string]interface{}{"name": "b"},
map[string]interface{}{"name": "c"},
},
)
})
}

View File

@@ -0,0 +1,615 @@
package memoryexecutor
import (
"math"
"math/rand"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/parsers"
)
func (r rowContext) math_Abs(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
return math.Abs(val)
case int:
if val < 0 {
return -val
}
return val
default:
logger.DebugLn("math_Abs - got parameters of wrong type")
return 0
}
}
func (r rowContext) math_Acos(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Acos - got parameters of wrong type")
return nil
}
if val < -1 || val > 1 {
logger.DebugLn("math_Acos - value out of domain for acos")
return nil
}
return math.Acos(val) * 180 / math.Pi
}
func (r rowContext) math_Asin(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Asin - got parameters of wrong type")
return nil
}
if val < -1 || val > 1 {
logger.DebugLn("math_Asin - value out of domain for acos")
return nil
}
return math.Asin(val) * 180 / math.Pi
}
func (r rowContext) math_Atan(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Atan - got parameters of wrong type")
return nil
}
return math.Atan(val) * 180 / math.Pi
}
func (r rowContext) math_Ceiling(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
return math.Ceil(val)
case int:
return val
default:
logger.DebugLn("math_Ceiling - got parameters of wrong type")
return 0
}
}
func (r rowContext) math_Cos(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Cos - got parameters of wrong type")
return nil
}
return math.Cos(val)
}
func (r rowContext) math_Cot(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Cot - got parameters of wrong type")
return nil
}
if val == 0 {
logger.DebugLn("math_Cot - cotangent undefined for zero")
return nil
}
return 1 / math.Tan(val)
}
func (r rowContext) math_Degrees(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Degrees - got parameters of wrong type")
return nil
}
return val * (180 / math.Pi)
}
func (r rowContext) math_Exp(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Exp - got parameters of wrong type")
return nil
}
return math.Exp(val)
}
func (r rowContext) math_Floor(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
return math.Floor(val)
case int:
return val
default:
logger.DebugLn("math_Floor - got parameters of wrong type")
return 0
}
}
func (r rowContext) math_IntBitNot(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case int:
return ^val
default:
logger.DebugLn("math_IntBitNot - got parameters of wrong type")
return nil
}
}
func (r rowContext) math_Log10(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Log10 - got parameters of wrong type")
return nil
}
if val <= 0 {
logger.DebugLn("math_Log10 - value must be greater than 0")
return nil
}
return math.Log10(val)
}
func (r rowContext) math_Radians(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Radians - got parameters of wrong type")
return nil
}
return val * (math.Pi / 180.0)
}
func (r rowContext) math_Round(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
return math.Round(val)
case int:
return val
default:
logger.DebugLn("math_Round - got parameters of wrong type")
return nil
}
}
func (r rowContext) math_Sign(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
if val > 0 {
return 1
} else if val < 0 {
return -1
} else {
return 0
}
case int:
if val > 0 {
return 1
} else if val < 0 {
return -1
} else {
return 0
}
default:
logger.DebugLn("math_Sign - got parameters of wrong type")
return nil
}
}
func (r rowContext) math_Sin(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Sin - got parameters of wrong type")
return nil
}
return math.Sin(val)
}
func (r rowContext) math_Sqrt(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Sqrt - got parameters of wrong type")
return nil
}
return math.Sqrt(val)
}
func (r rowContext) math_Square(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Square - got parameters of wrong type")
return nil
}
return math.Pow(val, 2)
}
func (r rowContext) math_Tan(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
val, valIsNumber := numToFloat64(ex)
if !valIsNumber {
logger.DebugLn("math_Tan - got parameters of wrong type")
return nil
}
return math.Tan(val)
}
func (r rowContext) math_Trunc(arguments []interface{}) interface{} {
exItem := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem)
switch val := ex.(type) {
case float64:
return math.Trunc(val)
case int:
return float64(val)
default:
logger.DebugLn("math_Trunc - got parameters of wrong type")
return nil
}
}
func (r rowContext) math_Atn2(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
y, yIsNumber := numToFloat64(ex1)
x, xIsNumber := numToFloat64(ex2)
if !yIsNumber || !xIsNumber {
logger.DebugLn("math_Atn2 - got parameters of wrong type")
return nil
}
return math.Atan2(y, x)
}
func (r rowContext) math_IntAdd(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
ex1Number, ex1IsNumber := numToInt(ex1)
ex2Number, ex2IsNumber := numToInt(ex2)
if !ex1IsNumber || !ex2IsNumber {
logger.DebugLn("math_IntAdd - got parameters of wrong type")
return nil
}
return ex1Number + ex2Number
}
func (r rowContext) math_IntBitAnd(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
ex1Int, ex1IsInt := numToInt(ex1)
ex2Int, ex2IsInt := numToInt(ex2)
if !ex1IsInt || !ex2IsInt {
logger.DebugLn("math_IntBitAnd - got parameters of wrong type")
return nil
}
return ex1Int & ex2Int
}
func (r rowContext) math_IntBitLeftShift(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := numToInt(ex1)
num2, num2IsInt := numToInt(ex2)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntBitLeftShift - got parameters of wrong type")
return nil
}
return num1 << uint(num2)
}
func (r rowContext) math_IntBitOr(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntBitOr - got parameters of wrong type")
return nil
}
return num1 | num2
}
func (r rowContext) math_IntBitRightShift(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := numToInt(ex1)
num2, num2IsInt := numToInt(ex2)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntBitRightShift - got parameters of wrong type")
return nil
}
return num1 >> uint(num2)
}
func (r rowContext) math_IntBitXor(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntBitXor - got parameters of wrong type")
return nil
}
return num1 ^ num2
}
func (r rowContext) math_IntDiv(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt || num2 == 0 {
logger.DebugLn("math_IntDiv - got parameters of wrong type or divide by zero")
return nil
}
return num1 / num2
}
func (r rowContext) math_IntMul(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntMul - got parameters of wrong type")
return nil
}
return num1 * num2
}
func (r rowContext) math_IntSub(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt {
logger.DebugLn("math_IntSub - got parameters of wrong type")
return nil
}
return num1 - num2
}
func (r rowContext) math_IntMod(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
num1, num1IsInt := ex1.(int)
num2, num2IsInt := ex2.(int)
if !num1IsInt || !num2IsInt || num2 == 0 {
logger.DebugLn("math_IntMod - got parameters of wrong type or divide by zero")
return nil
}
return num1 % num2
}
func (r rowContext) math_Power(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
exItem2 := arguments[1].(parsers.SelectItem)
ex1 := r.resolveSelectItem(exItem1)
ex2 := r.resolveSelectItem(exItem2)
base, baseIsNumber := numToFloat64(ex1)
exponent, exponentIsNumber := numToFloat64(ex2)
if !baseIsNumber || !exponentIsNumber {
logger.DebugLn("math_Power - got parameters of wrong type")
return nil
}
return math.Pow(base, exponent)
}
func (r rowContext) math_Log(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem1)
var base float64 = math.E
if len(arguments) > 1 {
exItem2 := arguments[1].(parsers.SelectItem)
baseValueObject := r.resolveSelectItem(exItem2)
baseValue, baseValueIsNumber := numToFloat64(baseValueObject)
if !baseValueIsNumber {
logger.DebugLn("math_Log - base parameter must be a numeric value")
return nil
}
if baseValue > 0 && baseValue != 1 {
base = baseValue
} else {
logger.DebugLn("math_Log - base must be greater than 0 and not equal to 1")
return nil
}
}
num, numIsNumber := numToFloat64(ex)
if !numIsNumber || num <= 0 {
logger.DebugLn("math_Log - parameter must be a positive numeric value")
return nil
}
return math.Log(num) / math.Log(base)
}
func (r rowContext) math_NumberBin(arguments []interface{}) interface{} {
exItem1 := arguments[0].(parsers.SelectItem)
ex := r.resolveSelectItem(exItem1)
binSize := 1.0
if len(arguments) > 1 {
exItem2 := arguments[1].(parsers.SelectItem)
binSizeValueObject := r.resolveSelectItem(exItem2)
binSizeValue, binSizeValueIsNumber := numToFloat64(binSizeValueObject)
if !binSizeValueIsNumber {
logger.DebugLn("math_NumberBin - base parameter must be a numeric value")
return nil
}
if binSizeValue != 0 {
binSize = binSizeValue
} else {
logger.DebugLn("math_NumberBin - base must not be equal to 0")
return nil
}
}
num, numIsNumber := numToFloat64(ex)
if !numIsNumber {
logger.DebugLn("math_NumberBin - parameter must be a numeric value")
return nil
}
return math.Floor(num/binSize) * binSize
}
func (r rowContext) math_Pi() interface{} {
return math.Pi
}
func (r rowContext) math_Rand() interface{} {
return rand.Float64()
}
func numToInt(ex interface{}) (int, bool) {
switch val := ex.(type) {
case float64:
return int(val), true
case int:
return val, true
default:
return 0, false
}
}
func numToFloat64(num interface{}) (float64, bool) {
switch val := num.(type) {
case float64:
return val, true
case int:
return float64(val), true
default:
return 0, false
}
}

View File

@@ -0,0 +1,270 @@
package memoryexecutor_test
import (
"math"
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_MathFunctions(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": 1, "value": 0.0},
map[string]interface{}{"id": 2, "value": 1.0},
map[string]interface{}{"id": 3, "value": -1.0},
map[string]interface{}{"id": 4, "value": 0.5},
map[string]interface{}{"id": 5, "value": -0.5},
map[string]interface{}{"id": 6, "value": 0.707},
map[string]interface{}{"id": 7, "value": -0.707},
map[string]interface{}{"id": 8, "value": 0.866},
map[string]interface{}{"id": 9, "value": -0.866},
}
mockDataInts := []memoryexecutor.RowType{
map[string]interface{}{"id": 1, "value": -1},
map[string]interface{}{"id": 2, "value": 0},
map[string]interface{}{"id": 3, "value": 1},
map[string]interface{}{"id": 4, "value": 5},
}
t.Run("Should execute function ABS(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAbs,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": 0.0},
map[string]interface{}{"value": 1.0, "result": 1.0},
map[string]interface{}{"value": -1.0, "result": 1.0},
map[string]interface{}{"value": 0.5, "result": 0.5},
map[string]interface{}{"value": -0.5, "result": 0.5},
map[string]interface{}{"value": 0.707, "result": 0.707},
map[string]interface{}{"value": -0.707, "result": 0.707},
map[string]interface{}{"value": 0.866, "result": 0.866},
map[string]interface{}{"value": -0.866, "result": 0.866},
},
)
})
t.Run("Should execute function ACOS(cosine)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAcos,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Acos(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Acos(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Acos(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Acos(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Acos(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Acos(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Acos(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Acos(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Acos(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function ASIN(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAsin,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Asin(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Asin(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Asin(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Asin(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Asin(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Asin(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Asin(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Asin(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Asin(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function ATAN(tangent)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathAtan,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Atan(0.0) * 180 / math.Pi},
map[string]interface{}{"value": 1.0, "result": math.Atan(1.0) * 180 / math.Pi},
map[string]interface{}{"value": -1.0, "result": math.Atan(-1.0) * 180 / math.Pi},
map[string]interface{}{"value": 0.5, "result": math.Atan(0.5) * 180 / math.Pi},
map[string]interface{}{"value": -0.5, "result": math.Atan(-0.5) * 180 / math.Pi},
map[string]interface{}{"value": 0.707, "result": math.Atan(0.707) * 180 / math.Pi},
map[string]interface{}{"value": -0.707, "result": math.Atan(-0.707) * 180 / math.Pi},
map[string]interface{}{"value": 0.866, "result": math.Atan(0.866) * 180 / math.Pi},
map[string]interface{}{"value": -0.866, "result": math.Atan(-0.866) * 180 / math.Pi},
},
)
})
t.Run("Should execute function COS(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathCos,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Cos(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Cos(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Cos(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Cos(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Cos(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Cos(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Cos(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Cos(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Cos(-0.866)},
},
)
})
t.Run("Should execute function COT(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathCot,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": nil},
map[string]interface{}{"value": 1.0, "result": 1 / math.Tan(1.0)},
map[string]interface{}{"value": -1.0, "result": 1 / math.Tan(-1.0)},
map[string]interface{}{"value": 0.5, "result": 1 / math.Tan(0.5)},
map[string]interface{}{"value": -0.5, "result": 1 / math.Tan(-0.5)},
map[string]interface{}{"value": 0.707, "result": 1 / math.Tan(0.707)},
map[string]interface{}{"value": -0.707, "result": 1 / math.Tan(-0.707)},
map[string]interface{}{"value": 0.866, "result": 1 / math.Tan(0.866)},
map[string]interface{}{"value": -0.866, "result": 1 / math.Tan(-0.866)},
},
)
})
t.Run("Should execute function Degrees(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathDegrees,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": 0.0 * (180 / math.Pi)},
map[string]interface{}{"value": 1.0, "result": 1.0 * (180 / math.Pi)},
map[string]interface{}{"value": -1.0, "result": -1.0 * (180 / math.Pi)},
map[string]interface{}{"value": 0.5, "result": 0.5 * (180 / math.Pi)},
map[string]interface{}{"value": -0.5, "result": -0.5 * (180 / math.Pi)},
map[string]interface{}{"value": 0.707, "result": 0.707 * (180 / math.Pi)},
map[string]interface{}{"value": -0.707, "result": -0.707 * (180 / math.Pi)},
map[string]interface{}{"value": 0.866, "result": 0.866 * (180 / math.Pi)},
map[string]interface{}{"value": -0.866, "result": -0.866 * (180 / math.Pi)},
},
)
})
t.Run("Should execute function EXP(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathExp,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Exp(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Exp(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Exp(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Exp(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Exp(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Exp(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Exp(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Exp(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Exp(-0.866)},
},
)
})
t.Run("Should execute function FLOOR(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathFloor,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": math.Floor(0.0)},
map[string]interface{}{"value": 1.0, "result": math.Floor(1.0)},
map[string]interface{}{"value": -1.0, "result": math.Floor(-1.0)},
map[string]interface{}{"value": 0.5, "result": math.Floor(0.5)},
map[string]interface{}{"value": -0.5, "result": math.Floor(-0.5)},
map[string]interface{}{"value": 0.707, "result": math.Floor(0.707)},
map[string]interface{}{"value": -0.707, "result": math.Floor(-0.707)},
map[string]interface{}{"value": 0.866, "result": math.Floor(0.866)},
map[string]interface{}{"value": -0.866, "result": math.Floor(-0.866)},
},
)
})
t.Run("Should execute function IntBitNot(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathIntBitNot,
mockDataInts,
[]memoryexecutor.RowType{
map[string]interface{}{"value": -1, "result": ^-1},
map[string]interface{}{"value": 0, "result": ^0},
map[string]interface{}{"value": 1, "result": ^1},
map[string]interface{}{"value": 5, "result": ^5},
},
)
})
t.Run("Should execute function LOG10(value)", func(t *testing.T) {
testMathFunctionExecute(
t,
parsers.FunctionCallMathLog10,
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"value": 0.0, "result": nil},
map[string]interface{}{"value": 1.0, "result": math.Log10(1.0)},
map[string]interface{}{"value": -1.0, "result": nil},
map[string]interface{}{"value": 0.5, "result": math.Log10(0.5)},
map[string]interface{}{"value": -0.5, "result": nil},
map[string]interface{}{"value": 0.707, "result": math.Log10(0.707)},
map[string]interface{}{"value": -0.707, "result": nil},
map[string]interface{}{"value": 0.866, "result": math.Log10(0.866)},
map[string]interface{}{"value": -0.866, "result": nil},
},
)
})
}
func testMathFunctionExecute(
t *testing.T,
functionCallType parsers.FunctionCallType,
data []memoryexecutor.RowType,
expectedData []memoryexecutor.RowType,
) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
{
Alias: "result",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: functionCallType,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "value"},
Type: parsers.SelectItemTypeField,
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
data,
expectedData,
)
}

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"sort" "sort"
"strconv"
"strings" "strings"
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
@@ -12,326 +13,243 @@ import (
) )
type RowType interface{} type RowType interface{}
type ExpressionType interface{} type rowContext struct {
tables map[string]RowType
type memoryExecutorContext struct { parameters map[string]interface{}
parameters map[string]interface{} grouppedRows []rowContext
} }
func Execute(query parsers.SelectStmt, data []RowType) []RowType { func ExecuteQuery(query parsers.SelectStmt, documents []RowType) []RowType {
ctx := memoryExecutorContext{ currentDocuments := make([]rowContext, 0)
parameters: query.Parameters, for _, doc := range documents {
currentDocuments = append(currentDocuments, resolveFrom(query, doc)...)
} }
result := make([]RowType, 0) // Handle JOINS
nextDocuments := make([]rowContext, 0)
for _, currentDocument := range currentDocuments {
rowContexts := currentDocument.handleJoin(query)
nextDocuments = append(nextDocuments, rowContexts...)
}
currentDocuments = nextDocuments
// Apply Filter // Apply filters
for _, row := range data { nextDocuments = make([]rowContext, 0)
if ctx.evaluateFilters(query.Filters, row) { for _, currentDocument := range currentDocuments {
result = append(result, row) if currentDocument.applyFilters(query.Filters) {
nextDocuments = append(nextDocuments, currentDocument)
} }
} }
currentDocuments = nextDocuments
// Apply order // Apply order
if query.OrderExpressions != nil && len(query.OrderExpressions) > 0 { if len(query.OrderExpressions) > 0 {
ctx.orderBy(query.OrderExpressions, result) applyOrder(currentDocuments, query.OrderExpressions)
} }
// Apply group // Apply group by
isGroupSelect := query.GroupBy != nil && len(query.GroupBy) > 0 if len(query.GroupBy) > 0 {
if isGroupSelect { currentDocuments = applyGroupBy(currentDocuments, query.GroupBy)
result = ctx.groupBy(query, result)
} }
// Apply select // Apply select
if !isGroupSelect { projectedDocuments := applyProjection(currentDocuments, query.SelectItems, query.GroupBy)
selectedData := make([]RowType, 0)
if hasAggregateFunctions(query.SelectItems) {
// When can have aggregate functions without GROUP BY clause,
// we should aggregate all rows in that case
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, result))
} else {
for _, row := range result {
selectedData = append(selectedData, ctx.selectRow(query.SelectItems, row))
}
}
result = selectedData
}
// Apply distinct // Apply distinct
if query.Distinct { if query.Distinct {
result = deduplicate(result) projectedDocuments = deduplicate(projectedDocuments)
} }
// Apply result limit // Apply result limit
if query.Count > 0 { if query.Count > 0 && len(projectedDocuments) > query.Count {
count := func() int { projectedDocuments = projectedDocuments[:query.Count]
if len(result) < query.Count {
return len(result)
}
return query.Count
}()
result = result[:count]
} }
return result return projectedDocuments
} }
func (c memoryExecutorContext) selectRow(selectItems []parsers.SelectItem, row RowType) interface{} { func resolveFrom(query parsers.SelectStmt, doc RowType) []rowContext {
// When the first value is top level, select it instead initialRow, gotParentContext := doc.(rowContext)
if len(selectItems) > 0 && selectItems[0].IsTopLevel { if !gotParentContext {
return c.getFieldValue(selectItems[0], row) var initialTableName string
if query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
initialTableName = query.Table.SelectItem.Value.(parsers.SelectStmt).Table.Value
}
if initialTableName == "" {
initialTableName = query.Table.Value
}
if initialTableName == "" {
initialTableName = resolveDestinationColumnName(query.Table.SelectItem, 0, query.Parameters)
}
initialRow = rowContext{
parameters: query.Parameters,
tables: map[string]RowType{
initialTableName: doc,
"$root": doc,
},
}
} }
// Construct a new row based on the selected columns if query.Table.SelectItem.Path != nil || query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
newRow := make(map[string]interface{}) destinationTableName := query.Table.SelectItem.Alias
for index, column := range selectItems { if destinationTableName == "" {
destinationName := column.Alias destinationTableName = query.Table.Value
if destinationName == "" { }
if len(column.Path) > 0 { if destinationTableName == "" {
destinationName = column.Path[len(column.Path)-1] destinationTableName = resolveDestinationColumnName(query.Table.SelectItem, 0, initialRow.parameters)
} else { }
destinationName = fmt.Sprintf("$%d", index+1)
if query.Table.IsInSelect || query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := initialRow.parseArray(query.Table.SelectItem)
rowContexts := make([]rowContext, len(selectValue))
for i, newRowData := range selectValue {
rowContexts[i].parameters = initialRow.parameters
rowContexts[i].tables = copyMap(initialRow.tables)
rowContexts[i].tables[destinationTableName] = newRowData
}
return rowContexts
}
if len(query.Table.SelectItem.Path) > 0 {
sourceTableName := query.Table.SelectItem.Path[0]
sourceTableData := initialRow.tables[sourceTableName]
if sourceTableData == nil {
// When source table is not found, assume it's root document
initialRow.tables[sourceTableName] = initialRow.tables["$root"]
} }
} }
newRow[destinationName] = c.getFieldValue(column, row) newRowData := initialRow.resolveSelectItem(query.Table.SelectItem)
initialRow.tables[destinationTableName] = newRowData
return []rowContext{initialRow}
} }
return newRow return []rowContext{initialRow}
} }
func (c memoryExecutorContext) evaluateFilters(expr ExpressionType, row RowType) bool { func (r rowContext) handleJoin(query parsers.SelectStmt) []rowContext {
if expr == nil { currentDocuments := []rowContext{r}
for _, joinItem := range query.JoinItems {
nextDocuments := make([]rowContext, 0)
for _, currentDocument := range currentDocuments {
joinedItems := currentDocument.resolveJoinItemSelect(joinItem.SelectItem)
for _, joinedItem := range joinedItems {
tablesCopy := copyMap(currentDocument.tables)
tablesCopy[joinItem.Table.Value] = joinedItem
nextDocuments = append(nextDocuments, rowContext{
parameters: currentDocument.parameters,
tables: tablesCopy,
})
}
}
currentDocuments = nextDocuments
}
return currentDocuments
}
func (r rowContext) resolveJoinItemSelect(selectItem parsers.SelectItem) []RowType {
if selectItem.Path != nil || selectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := r.parseArray(selectItem)
documents := make([]RowType, len(selectValue))
for i, newRowData := range selectValue {
documents[i] = newRowData
}
return documents
}
return []RowType{}
}
func (r rowContext) applyFilters(filters interface{}) bool {
if filters == nil {
return true return true
} }
switch typedValue := expr.(type) { switch typedFilters := filters.(type) {
case parsers.ComparisonExpression: case parsers.ComparisonExpression:
leftValue := c.getExpressionParameterValue(typedValue.Left, row) return r.filters_ComparisonExpression(typedFilters)
rightValue := c.getExpressionParameterValue(typedValue.Right, row)
cmp := compareValues(leftValue, rightValue)
switch typedValue.Operation {
case "=":
return cmp == 0
case "!=":
return cmp != 0
case "<":
return cmp < 0
case ">":
return cmp > 0
case "<=":
return cmp <= 0
case ">=":
return cmp >= 0
}
case parsers.LogicalExpression: case parsers.LogicalExpression:
var result bool return r.filters_LogicalExpression(typedFilters)
for i, expression := range typedValue.Expressions {
expressionResult := c.evaluateFilters(expression, row)
if i == 0 {
result = expressionResult
}
switch typedValue.Operation {
case parsers.LogicalExpressionTypeAnd:
result = result && expressionResult
if !result {
return false
}
case parsers.LogicalExpressionTypeOr:
result = result || expressionResult
if result {
return true
}
}
}
return result
case parsers.Constant: case parsers.Constant:
if value, ok := typedValue.Value.(bool); ok { if value, ok := typedFilters.Value.(bool); ok {
return value return value
} }
return false return false
case parsers.SelectItem: case parsers.SelectItem:
resolvedValue := c.getFieldValue(typedValue, row) resolvedValue := r.resolveSelectItem(typedFilters)
if value, ok := resolvedValue.(bool); ok { if value, ok := resolvedValue.(bool); ok {
return value return value
} }
} }
return false return false
} }
func (c memoryExecutorContext) getFieldValue(field parsers.SelectItem, row RowType) interface{} { func (r rowContext) filters_ComparisonExpression(expression parsers.ComparisonExpression) bool {
if field.Type == parsers.SelectItemTypeArray { leftExpression, leftExpressionOk := expression.Left.(parsers.SelectItem)
arrayValue := make([]interface{}, 0) rightExpression, rightExpressionOk := expression.Right.(parsers.SelectItem)
for _, selectItem := range field.SelectItems {
arrayValue = append(arrayValue, c.getFieldValue(selectItem, row)) if !leftExpressionOk || !rightExpressionOk {
} logger.ErrorLn("ComparisonExpression has incorrect Left or Right type")
return arrayValue return false
} }
if field.Type == parsers.SelectItemTypeObject { leftValue := r.resolveSelectItem(leftExpression)
objectValue := make(map[string]interface{}) rightValue := r.resolveSelectItem(rightExpression)
for _, selectItem := range field.SelectItems {
objectValue[selectItem.Alias] = c.getFieldValue(selectItem, row) cmp := compareValues(leftValue, rightValue)
} switch expression.Operation {
return objectValue case "=":
return cmp == 0
case "!=":
return cmp != 0
case "<":
return cmp < 0
case ">":
return cmp > 0
case "<=":
return cmp <= 0
case ">=":
return cmp >= 0
} }
if field.Type == parsers.SelectItemTypeConstant { return false
var typedValue parsers.Constant }
var ok bool
if typedValue, ok = field.Value.(parsers.Constant); !ok { func (r rowContext) filters_LogicalExpression(expression parsers.LogicalExpression) bool {
// TODO: Handle error var result bool
logger.Error("parsers.Constant has incorrect Value type") for i, subExpression := range expression.Expressions {
expressionResult := r.applyFilters(subExpression)
if i == 0 {
result = expressionResult
} }
if typedValue.Type == parsers.ConstantTypeParameterConstant && switch expression.Operation {
c.parameters != nil { case parsers.LogicalExpressionTypeAnd:
if key, ok := typedValue.Value.(string); ok { result = result && expressionResult
return c.parameters[key] if !result {
return false
} }
} case parsers.LogicalExpressionTypeOr:
result = result || expressionResult
return typedValue.Value if result {
} return true
rowValue := row
if array, isArray := row.([]RowType); isArray {
rowValue = array[0]
}
if field.Type == parsers.SelectItemTypeFunctionCall {
var typedValue parsers.FunctionCall
var ok bool
if typedValue, ok = field.Value.(parsers.FunctionCall); !ok {
// TODO: Handle error
logger.Error("parsers.Constant has incorrect Value type")
}
switch typedValue.Type {
case parsers.FunctionCallStringEquals:
return c.strings_StringEquals(typedValue.Arguments, rowValue)
case parsers.FunctionCallContains:
return c.strings_Contains(typedValue.Arguments, rowValue)
case parsers.FunctionCallEndsWith:
return c.strings_EndsWith(typedValue.Arguments, rowValue)
case parsers.FunctionCallStartsWith:
return c.strings_StartsWith(typedValue.Arguments, rowValue)
case parsers.FunctionCallConcat:
return c.strings_Concat(typedValue.Arguments, rowValue)
case parsers.FunctionCallIndexOf:
return c.strings_IndexOf(typedValue.Arguments, rowValue)
case parsers.FunctionCallToString:
return c.strings_ToString(typedValue.Arguments, rowValue)
case parsers.FunctionCallUpper:
return c.strings_Upper(typedValue.Arguments, rowValue)
case parsers.FunctionCallLower:
return c.strings_Lower(typedValue.Arguments, rowValue)
case parsers.FunctionCallLeft:
return c.strings_Left(typedValue.Arguments, rowValue)
case parsers.FunctionCallLength:
return c.strings_Length(typedValue.Arguments, rowValue)
case parsers.FunctionCallLTrim:
return c.strings_LTrim(typedValue.Arguments, rowValue)
case parsers.FunctionCallReplace:
return c.strings_Replace(typedValue.Arguments, rowValue)
case parsers.FunctionCallReplicate:
return c.strings_Replicate(typedValue.Arguments, rowValue)
case parsers.FunctionCallReverse:
return c.strings_Reverse(typedValue.Arguments, rowValue)
case parsers.FunctionCallRight:
return c.strings_Right(typedValue.Arguments, rowValue)
case parsers.FunctionCallRTrim:
return c.strings_RTrim(typedValue.Arguments, rowValue)
case parsers.FunctionCallSubstring:
return c.strings_Substring(typedValue.Arguments, rowValue)
case parsers.FunctionCallTrim:
return c.strings_Trim(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsDefined:
return c.typeChecking_IsDefined(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsArray:
return c.typeChecking_IsArray(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsBool:
return c.typeChecking_IsBool(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsFiniteNumber:
return c.typeChecking_IsFiniteNumber(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsInteger:
return c.typeChecking_IsInteger(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsNull:
return c.typeChecking_IsNull(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsNumber:
return c.typeChecking_IsNumber(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsObject:
return c.typeChecking_IsObject(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsPrimitive:
return c.typeChecking_IsPrimitive(typedValue.Arguments, rowValue)
case parsers.FunctionCallIsString:
return c.typeChecking_IsString(typedValue.Arguments, rowValue)
case parsers.FunctionCallArrayConcat:
return c.array_Concat(typedValue.Arguments, rowValue)
case parsers.FunctionCallArrayLength:
return c.array_Length(typedValue.Arguments, rowValue)
case parsers.FunctionCallArraySlice:
return c.array_Slice(typedValue.Arguments, rowValue)
case parsers.FunctionCallSetIntersect:
return c.set_Intersect(typedValue.Arguments, rowValue)
case parsers.FunctionCallSetUnion:
return c.set_Union(typedValue.Arguments, rowValue)
case parsers.FunctionCallAggregateAvg:
return c.aggregate_Avg(typedValue.Arguments, row)
case parsers.FunctionCallAggregateCount:
return c.aggregate_Count(typedValue.Arguments, row)
case parsers.FunctionCallAggregateMax:
return c.aggregate_Max(typedValue.Arguments, row)
case parsers.FunctionCallAggregateMin:
return c.aggregate_Min(typedValue.Arguments, row)
case parsers.FunctionCallAggregateSum:
return c.aggregate_Sum(typedValue.Arguments, row)
case parsers.FunctionCallIn:
return c.misc_In(typedValue.Arguments, rowValue)
}
}
value := rowValue
if len(field.Path) > 1 {
for _, pathSegment := range field.Path[1:] {
if nestedValue, ok := value.(map[string]interface{}); ok {
value = nestedValue[pathSegment]
} else {
return nil
} }
} }
} }
return value return result
} }
func (c memoryExecutorContext) getExpressionParameterValue( func applyOrder(documents []rowContext, orderExpressions []parsers.OrderExpression) {
parameter interface{},
row RowType,
) interface{} {
switch typedParameter := parameter.(type) {
case parsers.SelectItem:
return c.getFieldValue(typedParameter, row)
}
logger.Error("getExpressionParameterValue - got incorrect parameter type")
return nil
}
func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data []RowType) {
less := func(i, j int) bool { less := func(i, j int) bool {
for _, order := range orderBy { for _, order := range orderExpressions {
val1 := c.getFieldValue(order.SelectItem, data[i]) val1 := documents[i].resolveSelectItem(order.SelectItem)
val2 := c.getFieldValue(order.SelectItem, data[j]) val2 := documents[j].resolveSelectItem(order.SelectItem)
cmp := compareValues(val1, val2) cmp := compareValues(val1, val2)
if cmp != 0 { if cmp != 0 {
@@ -344,48 +262,398 @@ func (c memoryExecutorContext) orderBy(orderBy []parsers.OrderExpression, data [
return i < j return i < j
} }
sort.SliceStable(data, less) sort.SliceStable(documents, less)
} }
func (c memoryExecutorContext) groupBy(selectStmt parsers.SelectStmt, data []RowType) []RowType { func applyGroupBy(documents []rowContext, groupBy []parsers.SelectItem) []rowContext {
groupedRows := make(map[string][]RowType) groupedRows := make(map[string][]rowContext)
groupedKeys := make([]string, 0) groupedKeys := make([]string, 0)
// Group rows by group by columns for _, row := range documents {
for _, row := range data { key := row.generateGroupByKey(groupBy)
key := c.generateGroupKey(selectStmt.GroupBy, row)
if _, ok := groupedRows[key]; !ok { if _, ok := groupedRows[key]; !ok {
groupedKeys = append(groupedKeys, key) groupedKeys = append(groupedKeys, key)
} }
groupedRows[key] = append(groupedRows[key], row) groupedRows[key] = append(groupedRows[key], row)
} }
// Aggregate each group grouppedRows := make([]rowContext, 0)
aggregatedRows := make([]RowType, 0)
for _, key := range groupedKeys { for _, key := range groupedKeys {
groupRows := groupedRows[key] grouppedRowContext := rowContext{
aggregatedRow := c.aggregateGroup(selectStmt, groupRows) tables: groupedRows[key][0].tables,
aggregatedRows = append(aggregatedRows, aggregatedRow) parameters: groupedRows[key][0].parameters,
grouppedRows: groupedRows[key],
}
grouppedRows = append(grouppedRows, grouppedRowContext)
} }
return aggregatedRows return grouppedRows
} }
func (c memoryExecutorContext) generateGroupKey(groupByFields []parsers.SelectItem, row RowType) string { func (r rowContext) generateGroupByKey(groupBy []parsers.SelectItem) string {
var keyBuilder strings.Builder var keyBuilder strings.Builder
for _, column := range groupByFields { for _, selectItem := range groupBy {
fieldValue := c.getFieldValue(column, row) value := r.resolveSelectItem(selectItem)
keyBuilder.WriteString(fmt.Sprintf("%v", fieldValue)) keyBuilder.WriteString(fmt.Sprintf("%v", value))
keyBuilder.WriteString(":") keyBuilder.WriteString(":")
} }
return keyBuilder.String() return keyBuilder.String()
} }
func (c memoryExecutorContext) aggregateGroup(selectStmt parsers.SelectStmt, groupRows []RowType) RowType { func applyProjection(documents []rowContext, selectItems []parsers.SelectItem, groupBy []parsers.SelectItem) []RowType {
aggregatedRow := c.selectRow(selectStmt.SelectItems, groupRows) if len(documents) == 0 {
return []RowType{}
}
return aggregatedRow if hasAggregateFunctions(selectItems) && len(groupBy) == 0 {
// When can have aggregate functions without GROUP BY clause,
// we should aggregate all rows in that case
rowContext := rowContext{
tables: documents[0].tables,
parameters: documents[0].parameters,
grouppedRows: documents,
}
return []RowType{rowContext.applyProjection(selectItems)}
}
projectedDocuments := make([]RowType, len(documents))
for index, row := range documents {
projectedDocuments[index] = row.applyProjection(selectItems)
}
return projectedDocuments
}
func (r rowContext) applyProjection(selectItems []parsers.SelectItem) RowType {
// When the first value is top level, select it instead
if len(selectItems) > 0 && selectItems[0].IsTopLevel {
return r.resolveSelectItem(selectItems[0])
}
// Construct a new row based on the selected columns
row := make(map[string]interface{})
for index, selectItem := range selectItems {
destinationName := resolveDestinationColumnName(selectItem, index, r.parameters)
row[destinationName] = r.resolveSelectItem(selectItem)
}
return row
}
func resolveDestinationColumnName(selectItem parsers.SelectItem, itemIndex int, queryParameters map[string]interface{}) string {
if selectItem.Alias != "" {
return selectItem.Alias
}
destinationName := fmt.Sprintf("$%d", itemIndex+1)
if len(selectItem.Path) > 0 {
destinationName = selectItem.Path[len(selectItem.Path)-1]
}
if destinationName[0] == '@' {
destinationName = queryParameters[destinationName].(string)
}
return destinationName
}
func (r rowContext) resolveSelectItem(selectItem parsers.SelectItem) interface{} {
if selectItem.Type == parsers.SelectItemTypeArray {
return r.selectItem_SelectItemTypeArray(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeObject {
return r.selectItem_SelectItemTypeObject(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeConstant {
return r.selectItem_SelectItemTypeConstant(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeSubQuery {
return r.selectItem_SelectItemTypeSubQuery(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedFunctionCall, ok := selectItem.Value.(parsers.FunctionCall); ok {
return r.selectItem_SelectItemTypeFunctionCall(typedFunctionCall)
}
logger.ErrorLn("parsers.SelectItem has incorrect Value type (expected parsers.FunctionCall)")
return nil
}
return r.selectItem_SelectItemTypeField(selectItem)
}
func (r rowContext) selectItem_SelectItemTypeArray(selectItem parsers.SelectItem) interface{} {
arrayValue := make([]interface{}, 0)
for _, subSelectItem := range selectItem.SelectItems {
arrayValue = append(arrayValue, r.resolveSelectItem(subSelectItem))
}
return arrayValue
}
func (r rowContext) selectItem_SelectItemTypeObject(selectItem parsers.SelectItem) interface{} {
objectValue := make(map[string]interface{})
for _, subSelectItem := range selectItem.SelectItems {
objectValue[subSelectItem.Alias] = r.resolveSelectItem(subSelectItem)
}
return objectValue
}
func (r rowContext) selectItem_SelectItemTypeConstant(selectItem parsers.SelectItem) interface{} {
var typedValue parsers.Constant
var ok bool
if typedValue, ok = selectItem.Value.(parsers.Constant); !ok {
// TODO: Handle error
logger.ErrorLn("parsers.Constant has incorrect Value type")
}
if typedValue.Type == parsers.ConstantTypeParameterConstant &&
r.parameters != nil {
if key, ok := typedValue.Value.(string); ok {
return r.parameters[key]
}
}
return typedValue.Value
}
func (r rowContext) selectItem_SelectItemTypeSubQuery(selectItem parsers.SelectItem) interface{} {
subQuery := selectItem.Value.(parsers.SelectStmt)
subQueryResult := ExecuteQuery(
subQuery,
[]RowType{r},
)
if subQuery.Exists {
return len(subQueryResult) > 0
}
return subQueryResult
}
func (r rowContext) selectItem_SelectItemTypeFunctionCall(functionCall parsers.FunctionCall) interface{} {
switch functionCall.Type {
case parsers.FunctionCallStringEquals:
return r.strings_StringEquals(functionCall.Arguments)
case parsers.FunctionCallContains:
return r.strings_Contains(functionCall.Arguments)
case parsers.FunctionCallEndsWith:
return r.strings_EndsWith(functionCall.Arguments)
case parsers.FunctionCallStartsWith:
return r.strings_StartsWith(functionCall.Arguments)
case parsers.FunctionCallConcat:
return r.strings_Concat(functionCall.Arguments)
case parsers.FunctionCallIndexOf:
return r.strings_IndexOf(functionCall.Arguments)
case parsers.FunctionCallToString:
return r.strings_ToString(functionCall.Arguments)
case parsers.FunctionCallUpper:
return r.strings_Upper(functionCall.Arguments)
case parsers.FunctionCallLower:
return r.strings_Lower(functionCall.Arguments)
case parsers.FunctionCallLeft:
return r.strings_Left(functionCall.Arguments)
case parsers.FunctionCallLength:
return r.strings_Length(functionCall.Arguments)
case parsers.FunctionCallLTrim:
return r.strings_LTrim(functionCall.Arguments)
case parsers.FunctionCallReplace:
return r.strings_Replace(functionCall.Arguments)
case parsers.FunctionCallReplicate:
return r.strings_Replicate(functionCall.Arguments)
case parsers.FunctionCallReverse:
return r.strings_Reverse(functionCall.Arguments)
case parsers.FunctionCallRight:
return r.strings_Right(functionCall.Arguments)
case parsers.FunctionCallRTrim:
return r.strings_RTrim(functionCall.Arguments)
case parsers.FunctionCallSubstring:
return r.strings_Substring(functionCall.Arguments)
case parsers.FunctionCallTrim:
return r.strings_Trim(functionCall.Arguments)
case parsers.FunctionCallIsDefined:
return r.typeChecking_IsDefined(functionCall.Arguments)
case parsers.FunctionCallIsArray:
return r.typeChecking_IsArray(functionCall.Arguments)
case parsers.FunctionCallIsBool:
return r.typeChecking_IsBool(functionCall.Arguments)
case parsers.FunctionCallIsFiniteNumber:
return r.typeChecking_IsFiniteNumber(functionCall.Arguments)
case parsers.FunctionCallIsInteger:
return r.typeChecking_IsInteger(functionCall.Arguments)
case parsers.FunctionCallIsNull:
return r.typeChecking_IsNull(functionCall.Arguments)
case parsers.FunctionCallIsNumber:
return r.typeChecking_IsNumber(functionCall.Arguments)
case parsers.FunctionCallIsObject:
return r.typeChecking_IsObject(functionCall.Arguments)
case parsers.FunctionCallIsPrimitive:
return r.typeChecking_IsPrimitive(functionCall.Arguments)
case parsers.FunctionCallIsString:
return r.typeChecking_IsString(functionCall.Arguments)
case parsers.FunctionCallArrayConcat:
return r.array_Concat(functionCall.Arguments)
case parsers.FunctionCallArrayContains:
return r.array_Contains(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAny:
return r.array_Contains_Any(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAll:
return r.array_Contains_All(functionCall.Arguments)
case parsers.FunctionCallArrayLength:
return r.array_Length(functionCall.Arguments)
case parsers.FunctionCallArraySlice:
return r.array_Slice(functionCall.Arguments)
case parsers.FunctionCallSetIntersect:
return r.set_Intersect(functionCall.Arguments)
case parsers.FunctionCallSetUnion:
return r.set_Union(functionCall.Arguments)
case parsers.FunctionCallMathAbs:
return r.math_Abs(functionCall.Arguments)
case parsers.FunctionCallMathAcos:
return r.math_Acos(functionCall.Arguments)
case parsers.FunctionCallMathAsin:
return r.math_Asin(functionCall.Arguments)
case parsers.FunctionCallMathAtan:
return r.math_Atan(functionCall.Arguments)
case parsers.FunctionCallMathCeiling:
return r.math_Ceiling(functionCall.Arguments)
case parsers.FunctionCallMathCos:
return r.math_Cos(functionCall.Arguments)
case parsers.FunctionCallMathCot:
return r.math_Cot(functionCall.Arguments)
case parsers.FunctionCallMathDegrees:
return r.math_Degrees(functionCall.Arguments)
case parsers.FunctionCallMathExp:
return r.math_Exp(functionCall.Arguments)
case parsers.FunctionCallMathFloor:
return r.math_Floor(functionCall.Arguments)
case parsers.FunctionCallMathIntBitNot:
return r.math_IntBitNot(functionCall.Arguments)
case parsers.FunctionCallMathLog10:
return r.math_Log10(functionCall.Arguments)
case parsers.FunctionCallMathRadians:
return r.math_Radians(functionCall.Arguments)
case parsers.FunctionCallMathRound:
return r.math_Round(functionCall.Arguments)
case parsers.FunctionCallMathSign:
return r.math_Sign(functionCall.Arguments)
case parsers.FunctionCallMathSin:
return r.math_Sin(functionCall.Arguments)
case parsers.FunctionCallMathSqrt:
return r.math_Sqrt(functionCall.Arguments)
case parsers.FunctionCallMathSquare:
return r.math_Square(functionCall.Arguments)
case parsers.FunctionCallMathTan:
return r.math_Tan(functionCall.Arguments)
case parsers.FunctionCallMathTrunc:
return r.math_Trunc(functionCall.Arguments)
case parsers.FunctionCallMathAtn2:
return r.math_Atn2(functionCall.Arguments)
case parsers.FunctionCallMathIntAdd:
return r.math_IntAdd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitAnd:
return r.math_IntBitAnd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitLeftShift:
return r.math_IntBitLeftShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitOr:
return r.math_IntBitOr(functionCall.Arguments)
case parsers.FunctionCallMathIntBitRightShift:
return r.math_IntBitRightShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitXor:
return r.math_IntBitXor(functionCall.Arguments)
case parsers.FunctionCallMathIntDiv:
return r.math_IntDiv(functionCall.Arguments)
case parsers.FunctionCallMathIntMod:
return r.math_IntMod(functionCall.Arguments)
case parsers.FunctionCallMathIntMul:
return r.math_IntMul(functionCall.Arguments)
case parsers.FunctionCallMathIntSub:
return r.math_IntSub(functionCall.Arguments)
case parsers.FunctionCallMathPower:
return r.math_Power(functionCall.Arguments)
case parsers.FunctionCallMathLog:
return r.math_Log(functionCall.Arguments)
case parsers.FunctionCallMathNumberBin:
return r.math_NumberBin(functionCall.Arguments)
case parsers.FunctionCallMathPi:
return r.math_Pi()
case parsers.FunctionCallMathRand:
return r.math_Rand()
case parsers.FunctionCallAggregateAvg:
return r.aggregate_Avg(functionCall.Arguments)
case parsers.FunctionCallAggregateCount:
return r.aggregate_Count(functionCall.Arguments)
case parsers.FunctionCallAggregateMax:
return r.aggregate_Max(functionCall.Arguments)
case parsers.FunctionCallAggregateMin:
return r.aggregate_Min(functionCall.Arguments)
case parsers.FunctionCallAggregateSum:
return r.aggregate_Sum(functionCall.Arguments)
case parsers.FunctionCallIn:
return r.misc_In(functionCall.Arguments)
}
logger.Errorf("Unknown function call type: %v", functionCall.Type)
return nil
}
func (r rowContext) selectItem_SelectItemTypeField(selectItem parsers.SelectItem) interface{} {
value := r.tables[selectItem.Path[0]]
if len(selectItem.Path) > 1 {
for _, pathSegment := range selectItem.Path[1:] {
if pathSegment[0] == '@' {
pathSegment = r.parameters[pathSegment].(string)
}
switch nestedValue := value.(type) {
case map[string]interface{}:
value = nestedValue[pathSegment]
case map[string]RowType:
value = nestedValue[pathSegment]
case []int, []string, []interface{}:
slice := reflect.ValueOf(nestedValue)
if arrayIndex, err := strconv.Atoi(pathSegment); err == nil && slice.Len() > arrayIndex {
value = slice.Index(arrayIndex).Interface()
} else {
return nil
}
default:
return nil
}
}
}
return value
}
func hasAggregateFunctions(selectItems []parsers.SelectItem) bool {
if selectItems == nil {
return false
}
for _, selectItem := range selectItems {
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedValue, ok := selectItem.Value.(parsers.FunctionCall); ok && slices.Contains[[]parsers.FunctionCallType](parsers.AggregateFunctions, typedValue.Type) {
return true
}
}
if hasAggregateFunctions(selectItem.SelectItems) {
return true
}
}
return false
} }
func compareValues(val1, val2 interface{}) int { func compareValues(val1, val2 interface{}) int {
@@ -431,8 +699,9 @@ func compareValues(val1, val2 interface{}) int {
} }
} }
func deduplicate(slice []RowType) []RowType { func deduplicate[T RowType | interface{}](slice []T) []T {
var result []RowType var result []T
result = make([]T, 0)
for i := 0; i < len(slice); i++ { for i := 0; i < len(slice); i++ {
unique := true unique := true
@@ -451,22 +720,12 @@ func deduplicate(slice []RowType) []RowType {
return result return result
} }
func hasAggregateFunctions(selectItems []parsers.SelectItem) bool { func copyMap[T RowType | []RowType](originalMap map[string]T) map[string]T {
if selectItems == nil { targetMap := make(map[string]T)
return false
for k, v := range originalMap {
targetMap[k] = v
} }
for _, selectItem := range selectItems { return targetMap
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedValue, ok := selectItem.Value.(parsers.FunctionCall); ok && slices.Contains[[]parsers.FunctionCallType](parsers.AggregateFunctions, typedValue.Type) {
return true
}
}
if hasAggregateFunctions(selectItem.SelectItems) {
return true
}
}
return false
} }

View File

@@ -4,11 +4,11 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
) )
func (c memoryExecutorContext) misc_In(arguments []interface{}, row RowType) bool { func (r rowContext) misc_In(arguments []interface{}) bool {
value := c.getFieldValue(arguments[0].(parsers.SelectItem), row) value := r.resolveSelectItem(arguments[0].(parsers.SelectItem))
for i := 1; i < len(arguments); i++ { for i := 1; i < len(arguments); i++ {
compareValue := c.getFieldValue(arguments[i].(parsers.SelectItem), row) compareValue := r.resolveSelectItem(arguments[i].(parsers.SelectItem))
if compareValues(value, compareValue) == 0 { if compareValues(value, compareValue) == 0 {
return true return true
} }

View File

@@ -6,6 +6,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func testQueryExecute( func testQueryExecute(
@@ -14,7 +15,7 @@ func testQueryExecute(
data []memoryexecutor.RowType, data []memoryexecutor.RowType,
expectedData []memoryexecutor.RowType, expectedData []memoryexecutor.RowType,
) { ) {
result := memoryexecutor.Execute(query, data) result := memoryexecutor.ExecuteQuery(query, data)
if !reflect.DeepEqual(result, expectedData) { if !reflect.DeepEqual(result, expectedData) {
t.Errorf("execution result does not match expected data.\nExpected: %+v\nGot: %+v", expectedData, result) t.Errorf("execution result does not match expected data.\nExpected: %+v\nGot: %+v", expectedData, result)
@@ -25,8 +26,20 @@ func Test_Execute(t *testing.T) {
mockData := []memoryexecutor.RowType{ mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": "12345", "pk": 123, "_self": "self1", "_rid": "rid1", "_ts": 123456, "isCool": false}, map[string]interface{}{"id": "12345", "pk": 123, "_self": "self1", "_rid": "rid1", "_ts": 123456, "isCool": false},
map[string]interface{}{"id": "67890", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true}, map[string]interface{}{"id": "67890", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true},
map[string]interface{}{"id": "456", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true}, map[string]interface{}{
map[string]interface{}{"id": "123", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true}, "id": "456", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true,
"tags": []map[string]interface{}{
{"name": "tag-a"},
{"name": "tag-b"},
},
},
map[string]interface{}{
"id": "123", "pk": 456, "_self": "self2", "_rid": "rid2", "_ts": 789012, "isCool": true,
"tags": []map[string]interface{}{
{"name": "tag-b"},
{"name": "tag-c"},
},
},
} }
t.Run("Should execute SELECT with ORDER BY", func(t *testing.T) { t.Run("Should execute SELECT with ORDER BY", func(t *testing.T) {
@@ -37,7 +50,7 @@ func Test_Execute(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
OrderExpressions: []parsers.OrderExpression{ OrderExpressions: []parsers.OrderExpression{
{ {
SelectItem: parsers.SelectItem{Path: []string{"c", "pk"}}, SelectItem: parsers.SelectItem{Path: []string{"c", "pk"}},
@@ -66,7 +79,7 @@ func Test_Execute(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
GroupBy: []parsers.SelectItem{ GroupBy: []parsers.SelectItem{
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
@@ -89,7 +102,7 @@ func Test_Execute(t *testing.T) {
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{ Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall, Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{ Value: parsers.FunctionCall{
@@ -99,20 +112,8 @@ func Test_Execute(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_String("456"),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "123",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "456",
},
},
}, },
}, },
}, },
@@ -124,4 +125,30 @@ func Test_Execute(t *testing.T) {
}, },
) )
}) })
t.Run("Should execute IN selector", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "name"},
Type: parsers.SelectItemTypeField,
},
},
Table: parsers.Table{
Value: "c",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"name": "tag-a"},
map[string]interface{}{"name": "tag-b"},
map[string]interface{}{"name": "tag-b"},
map[string]interface{}{"name": "tag-c"},
},
)
})
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_Select(t *testing.T) { func Test_Execute_Select(t *testing.T) {
@@ -23,7 +24,30 @@ func Test_Execute_Select(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "12345", "pk": 123},
map[string]interface{}{"id": "67890", "pk": 456},
map[string]interface{}{"id": "456", "pk": 456},
map[string]interface{}{"id": "123", "pk": 456},
},
)
})
t.Run("Should execute SELECT with query parameters as accessor", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"c", "@param"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Parameters: map[string]interface{}{
"@param": "pk",
},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -42,7 +66,7 @@ func Test_Execute_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Distinct: true, Distinct: true,
}, },
mockData, mockData,
@@ -61,7 +85,7 @@ func Test_Execute_Select(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Count: 1, Count: 1,
}, },
mockData, mockData,
@@ -79,7 +103,7 @@ func Test_Execute_Select(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "pk"}}, {Path: []string{"c", "pk"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Count: 2, Count: 2,
Offset: 1, Offset: 1,
OrderExpressions: []parsers.OrderExpression{ OrderExpressions: []parsers.OrderExpression{
@@ -104,7 +128,7 @@ func Test_Execute_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}, IsTopLevel: true}, {Path: []string{"c", "id"}, IsTopLevel: true},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -123,7 +147,7 @@ func Test_Execute_Select(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c"}, IsTopLevel: true}, {Path: []string{"c"}, IsTopLevel: true},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
mockData, mockData,
@@ -144,7 +168,7 @@ func Test_Execute_Select(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -170,7 +194,7 @@ func Test_Execute_Select(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{

View File

@@ -8,10 +8,10 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
) )
func (c memoryExecutorContext) strings_StringEquals(arguments []interface{}, row RowType) bool { func (r rowContext) strings_StringEquals(arguments []interface{}) bool {
str1 := c.parseString(arguments[0], row) str1 := r.parseString(arguments[0])
str2 := c.parseString(arguments[1], row) str2 := r.parseString(arguments[1])
ignoreCase := c.getBoolFlag(arguments, row) ignoreCase := r.getBoolFlag(arguments)
if ignoreCase { if ignoreCase {
return strings.EqualFold(str1, str2) return strings.EqualFold(str1, str2)
@@ -20,10 +20,10 @@ func (c memoryExecutorContext) strings_StringEquals(arguments []interface{}, row
return str1 == str2 return str1 == str2
} }
func (c memoryExecutorContext) strings_Contains(arguments []interface{}, row RowType) bool { func (r rowContext) strings_Contains(arguments []interface{}) bool {
str1 := c.parseString(arguments[0], row) str1 := r.parseString(arguments[0])
str2 := c.parseString(arguments[1], row) str2 := r.parseString(arguments[1])
ignoreCase := c.getBoolFlag(arguments, row) ignoreCase := r.getBoolFlag(arguments)
if ignoreCase { if ignoreCase {
str1 = strings.ToLower(str1) str1 = strings.ToLower(str1)
@@ -33,10 +33,10 @@ func (c memoryExecutorContext) strings_Contains(arguments []interface{}, row Row
return strings.Contains(str1, str2) return strings.Contains(str1, str2)
} }
func (c memoryExecutorContext) strings_EndsWith(arguments []interface{}, row RowType) bool { func (r rowContext) strings_EndsWith(arguments []interface{}) bool {
str1 := c.parseString(arguments[0], row) str1 := r.parseString(arguments[0])
str2 := c.parseString(arguments[1], row) str2 := r.parseString(arguments[1])
ignoreCase := c.getBoolFlag(arguments, row) ignoreCase := r.getBoolFlag(arguments)
if ignoreCase { if ignoreCase {
str1 = strings.ToLower(str1) str1 = strings.ToLower(str1)
@@ -46,10 +46,10 @@ func (c memoryExecutorContext) strings_EndsWith(arguments []interface{}, row Row
return strings.HasSuffix(str1, str2) return strings.HasSuffix(str1, str2)
} }
func (c memoryExecutorContext) strings_StartsWith(arguments []interface{}, row RowType) bool { func (r rowContext) strings_StartsWith(arguments []interface{}) bool {
str1 := c.parseString(arguments[0], row) str1 := r.parseString(arguments[0])
str2 := c.parseString(arguments[1], row) str2 := r.parseString(arguments[1])
ignoreCase := c.getBoolFlag(arguments, row) ignoreCase := r.getBoolFlag(arguments)
if ignoreCase { if ignoreCase {
str1 = strings.ToLower(str1) str1 = strings.ToLower(str1)
@@ -59,12 +59,12 @@ func (c memoryExecutorContext) strings_StartsWith(arguments []interface{}, row R
return strings.HasPrefix(str1, str2) return strings.HasPrefix(str1, str2)
} }
func (c memoryExecutorContext) strings_Concat(arguments []interface{}, row RowType) string { func (r rowContext) strings_Concat(arguments []interface{}) string {
result := "" result := ""
for _, arg := range arguments { for _, arg := range arguments {
if selectItem, ok := arg.(parsers.SelectItem); ok { if selectItem, ok := arg.(parsers.SelectItem); ok {
value := c.getFieldValue(selectItem, row) value := r.resolveSelectItem(selectItem)
result += convertToString(value) result += convertToString(value)
} }
} }
@@ -72,13 +72,13 @@ func (c memoryExecutorContext) strings_Concat(arguments []interface{}, row RowTy
return result return result
} }
func (c memoryExecutorContext) strings_IndexOf(arguments []interface{}, row RowType) int { func (r rowContext) strings_IndexOf(arguments []interface{}) int {
str1 := c.parseString(arguments[0], row) str1 := r.parseString(arguments[0])
str2 := c.parseString(arguments[1], row) str2 := r.parseString(arguments[1])
start := 0 start := 0
if len(arguments) > 2 && arguments[2] != nil { if len(arguments) > 2 && arguments[2] != nil {
if startPos, ok := c.getFieldValue(arguments[2].(parsers.SelectItem), row).(int); ok { if startPos, ok := r.resolveSelectItem(arguments[2].(parsers.SelectItem)).(int); ok {
start = startPos start = startPos
} }
} }
@@ -97,29 +97,29 @@ func (c memoryExecutorContext) strings_IndexOf(arguments []interface{}, row RowT
} }
} }
func (c memoryExecutorContext) strings_ToString(arguments []interface{}, row RowType) string { func (r rowContext) strings_ToString(arguments []interface{}) string {
value := c.getFieldValue(arguments[0].(parsers.SelectItem), row) value := r.resolveSelectItem(arguments[0].(parsers.SelectItem))
return convertToString(value) return convertToString(value)
} }
func (c memoryExecutorContext) strings_Upper(arguments []interface{}, row RowType) string { func (r rowContext) strings_Upper(arguments []interface{}) string {
value := c.getFieldValue(arguments[0].(parsers.SelectItem), row) value := r.resolveSelectItem(arguments[0].(parsers.SelectItem))
return strings.ToUpper(convertToString(value)) return strings.ToUpper(convertToString(value))
} }
func (c memoryExecutorContext) strings_Lower(arguments []interface{}, row RowType) string { func (r rowContext) strings_Lower(arguments []interface{}) string {
value := c.getFieldValue(arguments[0].(parsers.SelectItem), row) value := r.resolveSelectItem(arguments[0].(parsers.SelectItem))
return strings.ToLower(convertToString(value)) return strings.ToLower(convertToString(value))
} }
func (c memoryExecutorContext) strings_Left(arguments []interface{}, row RowType) string { func (r rowContext) strings_Left(arguments []interface{}) string {
var ok bool var ok bool
var length int var length int
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
lengthEx := c.getFieldValue(arguments[1].(parsers.SelectItem), row) lengthEx := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
if length, ok = lengthEx.(int); !ok { if length, ok = lengthEx.(int); !ok {
logger.Error("strings_Left - got parameters of wrong type") logger.ErrorLn("strings_Left - got parameters of wrong type")
return "" return ""
} }
@@ -134,31 +134,31 @@ func (c memoryExecutorContext) strings_Left(arguments []interface{}, row RowType
return str[:length] return str[:length]
} }
func (c memoryExecutorContext) strings_Length(arguments []interface{}, row RowType) int { func (r rowContext) strings_Length(arguments []interface{}) int {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
return len(str) return len(str)
} }
func (c memoryExecutorContext) strings_LTrim(arguments []interface{}, row RowType) string { func (r rowContext) strings_LTrim(arguments []interface{}) string {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
return strings.TrimLeft(str, " ") return strings.TrimLeft(str, " ")
} }
func (c memoryExecutorContext) strings_Replace(arguments []interface{}, row RowType) string { func (r rowContext) strings_Replace(arguments []interface{}) string {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
oldStr := c.parseString(arguments[1], row) oldStr := r.parseString(arguments[1])
newStr := c.parseString(arguments[2], row) newStr := r.parseString(arguments[2])
return strings.Replace(str, oldStr, newStr, -1) return strings.Replace(str, oldStr, newStr, -1)
} }
func (c memoryExecutorContext) strings_Replicate(arguments []interface{}, row RowType) string { func (r rowContext) strings_Replicate(arguments []interface{}) string {
var ok bool var ok bool
var times int var times int
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
timesEx := c.getFieldValue(arguments[1].(parsers.SelectItem), row) timesEx := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
if times, ok = timesEx.(int); !ok { if times, ok = timesEx.(int); !ok {
logger.Error("strings_Replicate - got parameters of wrong type") logger.ErrorLn("strings_Replicate - got parameters of wrong type")
return "" return ""
} }
@@ -173,8 +173,8 @@ func (c memoryExecutorContext) strings_Replicate(arguments []interface{}, row Ro
return strings.Repeat(str, times) return strings.Repeat(str, times)
} }
func (c memoryExecutorContext) strings_Reverse(arguments []interface{}, row RowType) string { func (r rowContext) strings_Reverse(arguments []interface{}) string {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
runes := []rune(str) runes := []rune(str)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
@@ -184,14 +184,14 @@ func (c memoryExecutorContext) strings_Reverse(arguments []interface{}, row RowT
return string(runes) return string(runes)
} }
func (c memoryExecutorContext) strings_Right(arguments []interface{}, row RowType) string { func (r rowContext) strings_Right(arguments []interface{}) string {
var ok bool var ok bool
var length int var length int
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
lengthEx := c.getFieldValue(arguments[1].(parsers.SelectItem), row) lengthEx := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
if length, ok = lengthEx.(int); !ok { if length, ok = lengthEx.(int); !ok {
logger.Error("strings_Right - got parameters of wrong type") logger.ErrorLn("strings_Right - got parameters of wrong type")
return "" return ""
} }
@@ -206,25 +206,25 @@ func (c memoryExecutorContext) strings_Right(arguments []interface{}, row RowTyp
return str[len(str)-length:] return str[len(str)-length:]
} }
func (c memoryExecutorContext) strings_RTrim(arguments []interface{}, row RowType) string { func (r rowContext) strings_RTrim(arguments []interface{}) string {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
return strings.TrimRight(str, " ") return strings.TrimRight(str, " ")
} }
func (c memoryExecutorContext) strings_Substring(arguments []interface{}, row RowType) string { func (r rowContext) strings_Substring(arguments []interface{}) string {
var ok bool var ok bool
var startPos int var startPos int
var length int var length int
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
startPosEx := c.getFieldValue(arguments[1].(parsers.SelectItem), row) startPosEx := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
lengthEx := c.getFieldValue(arguments[2].(parsers.SelectItem), row) lengthEx := r.resolveSelectItem(arguments[2].(parsers.SelectItem))
if startPos, ok = startPosEx.(int); !ok { if startPos, ok = startPosEx.(int); !ok {
logger.Error("strings_Substring - got start parameters of wrong type") logger.ErrorLn("strings_Substring - got start parameters of wrong type")
return "" return ""
} }
if length, ok = lengthEx.(int); !ok { if length, ok = lengthEx.(int); !ok {
logger.Error("strings_Substring - got length parameters of wrong type") logger.ErrorLn("strings_Substring - got length parameters of wrong type")
return "" return ""
} }
@@ -240,16 +240,16 @@ func (c memoryExecutorContext) strings_Substring(arguments []interface{}, row Ro
return str[startPos:endPos] return str[startPos:endPos]
} }
func (c memoryExecutorContext) strings_Trim(arguments []interface{}, row RowType) string { func (r rowContext) strings_Trim(arguments []interface{}) string {
str := c.parseString(arguments[0], row) str := r.parseString(arguments[0])
return strings.TrimSpace(str) return strings.TrimSpace(str)
} }
func (c memoryExecutorContext) getBoolFlag(arguments []interface{}, row RowType) bool { func (r rowContext) getBoolFlag(arguments []interface{}) bool {
ignoreCase := false ignoreCase := false
if len(arguments) > 2 && arguments[2] != nil { if len(arguments) > 2 && arguments[2] != nil {
ignoreCaseItem := arguments[2].(parsers.SelectItem) ignoreCaseItem := arguments[2].(parsers.SelectItem)
if value, ok := c.getFieldValue(ignoreCaseItem, row).(bool); ok { if value, ok := r.resolveSelectItem(ignoreCaseItem).(bool); ok {
ignoreCase = value ignoreCase = value
} }
} }
@@ -257,14 +257,14 @@ func (c memoryExecutorContext) getBoolFlag(arguments []interface{}, row RowType)
return ignoreCase return ignoreCase
} }
func (c memoryExecutorContext) parseString(argument interface{}, row RowType) string { func (r rowContext) parseString(argument interface{}) string {
exItem := argument.(parsers.SelectItem) exItem := argument.(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
if str1, ok := ex.(string); ok { if str1, ok := ex.(string); ok {
return str1 return str1
} }
logger.Error("StringEquals got parameters of wrong type") logger.ErrorLn("StringEquals got parameters of wrong type")
return "" return ""
} }

View File

@@ -5,6 +5,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_StringFunctions(t *testing.T) { func Test_Execute_StringFunctions(t *testing.T) {
@@ -33,25 +34,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "pk"}, Path: []string{"c", "pk"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("aaa"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "aaa",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -81,19 +70,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "pk"}, Path: []string{"c", "pk"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("aaa"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "aaa",
},
},
nil, nil,
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -119,20 +102,8 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String(" "),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(123),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: " ",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: 123,
},
},
parsers.SelectItem{ parsers.SelectItem{
Path: []string{"c", "pk"}, Path: []string{"c", "pk"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
@@ -141,7 +112,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -171,25 +142,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("2"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "2",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -219,25 +178,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("3"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "3",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -267,25 +214,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "id"}, Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("1"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Bool(true),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "1",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: true,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -315,25 +250,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("o"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(4),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "o",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 4,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -367,7 +290,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -397,18 +320,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(3),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 3,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -442,7 +359,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -476,7 +393,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -506,25 +423,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_String("world"),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_String("universe"),
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "world",
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: "universe",
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -554,18 +459,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(3),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 3,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -599,7 +498,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -629,18 +528,12 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(3),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 3,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -674,7 +567,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -704,25 +597,13 @@ func Test_Execute_StringFunctions(t *testing.T) {
Path: []string{"c", "str"}, Path: []string{"c", "str"},
Type: parsers.SelectItemTypeField, Type: parsers.SelectItemTypeField,
}, },
parsers.SelectItem{ testutils.SelectItem_Constant_Int(2),
Type: parsers.SelectItemTypeConstant, testutils.SelectItem_Constant_Int(4),
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 2,
},
},
parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: 4,
},
},
}, },
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -756,7 +637,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{

View File

@@ -0,0 +1,152 @@
package memoryexecutor_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_SubQuery(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": "123", "info": map[string]interface{}{"name": "row-1"}},
map[string]interface{}{
"id": "456",
"info": map[string]interface{}{"name": "row-2"},
"tags": []map[string]interface{}{
{"name": "tag-a"},
{"name": "tag-b"},
},
},
map[string]interface{}{
"id": "789",
"info": map[string]interface{}{"name": "row-3"},
"tags": []map[string]interface{}{
{"name": "tag-b"},
{"name": "tag-c"},
},
},
}
t.Run("Should execute FROM subquery", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "name"}},
},
Table: parsers.Table{
Value: "c",
SelectItem: parsers.SelectItem{
Alias: "c",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("cc")},
SelectItems: []parsers.SelectItem{
{Path: []string{"cc", "info"}, IsTopLevel: true},
},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"name": "row-1"},
map[string]interface{}{"name": "row-2"},
map[string]interface{}{"name": "row-3"},
},
)
})
t.Run("Should execute JOIN subquery", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}},
{Path: []string{"cc", "name"}},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{
Value: "cc",
},
SelectItem: parsers.SelectItem{
Alias: "cc",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("tag", "name"),
},
Table: parsers.Table{
Value: "tag",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
},
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "456", "name": "tag-a"},
map[string]interface{}{"id": "456", "name": "tag-b"},
map[string]interface{}{"id": "789", "name": "tag-b"},
map[string]interface{}{"id": "789", "name": "tag-c"},
},
)
})
t.Run("Should execute JOIN EXISTS subquery", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
},
Table: parsers.Table{
SelectItem: testutils.SelectItem_Path("c"),
},
JoinItems: []parsers.JoinItem{
{
Table: parsers.Table{Value: "hasTags"},
SelectItem: parsers.SelectItem{
Alias: "hasTags",
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
IsTopLevel: true,
Type: parsers.SelectItemTypeSubQuery,
Value: parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("tag", "name"),
},
Table: parsers.Table{
Value: "tag",
SelectItem: testutils.SelectItem_Path("c", "tags"),
IsInSelect: true,
},
Exists: true,
},
},
},
},
},
},
},
Filters: parsers.SelectItem{
Path: []string{"hasTags"},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "456"},
map[string]interface{}{"id": "789"},
},
)
})
}

View File

@@ -6,32 +6,32 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
) )
func (c memoryExecutorContext) typeChecking_IsDefined(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsDefined(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
return ex != nil return ex != nil
} }
func (c memoryExecutorContext) typeChecking_IsArray(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsArray(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isArray := ex.([]interface{}) _, isArray := ex.([]interface{})
return isArray return isArray
} }
func (c memoryExecutorContext) typeChecking_IsBool(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsBool(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isBool := ex.(bool) _, isBool := ex.(bool)
return isBool return isBool
} }
func (c memoryExecutorContext) typeChecking_IsFiniteNumber(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsFiniteNumber(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
switch num := ex.(type) { switch num := ex.(type) {
case int: case int:
@@ -43,41 +43,41 @@ func (c memoryExecutorContext) typeChecking_IsFiniteNumber(arguments []interface
} }
} }
func (c memoryExecutorContext) typeChecking_IsInteger(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsInteger(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isInt := ex.(int) _, isInt := ex.(int)
return isInt return isInt
} }
func (c memoryExecutorContext) typeChecking_IsNull(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsNull(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
return ex == nil return ex == nil
} }
func (c memoryExecutorContext) typeChecking_IsNumber(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsNumber(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isFloat := ex.(float64) _, isFloat := ex.(float64)
_, isInt := ex.(int) _, isInt := ex.(int)
return isFloat || isInt return isFloat || isInt
} }
func (c memoryExecutorContext) typeChecking_IsObject(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsObject(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isObject := ex.(map[string]interface{}) _, isObject := ex.(map[string]interface{})
return isObject return isObject
} }
func (c memoryExecutorContext) typeChecking_IsPrimitive(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsPrimitive(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
switch ex.(type) { switch ex.(type) {
case bool, string, float64, int, nil: case bool, string, float64, int, nil:
@@ -87,9 +87,9 @@ func (c memoryExecutorContext) typeChecking_IsPrimitive(arguments []interface{},
} }
} }
func (c memoryExecutorContext) typeChecking_IsString(arguments []interface{}, row RowType) bool { func (r rowContext) typeChecking_IsString(arguments []interface{}) bool {
exItem := arguments[0].(parsers.SelectItem) exItem := arguments[0].(parsers.SelectItem)
ex := c.getFieldValue(exItem, row) ex := r.resolveSelectItem(exItem)
_, isStr := ex.(string) _, isStr := ex.(string)
return isStr return isStr

View File

@@ -6,6 +6,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_TypeCheckingFunctions(t *testing.T) { func Test_Execute_TypeCheckingFunctions(t *testing.T) {
@@ -40,7 +41,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -76,7 +77,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -112,7 +113,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -148,7 +149,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -184,7 +185,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -220,7 +221,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -256,7 +257,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -292,7 +293,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -328,7 +329,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{
@@ -364,7 +365,7 @@ func Test_Execute_TypeCheckingFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
mockData, mockData,
[]memoryexecutor.RowType{ []memoryexecutor.RowType{

View File

@@ -5,6 +5,7 @@ import (
"github.com/pikami/cosmium/parsers" "github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor" memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
) )
func Test_Execute_Where(t *testing.T) { func Test_Execute_Where(t *testing.T) {
@@ -22,14 +23,11 @@ func Test_Execute_Where(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.ComparisonExpression{ Filters: parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "isCool"}}, Left: parsers.SelectItem{Path: []string{"c", "isCool"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Bool(true),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeBoolean, Value: true},
},
}, },
}, },
mockData, mockData,
@@ -48,14 +46,11 @@ func Test_Execute_Where(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.ComparisonExpression{ Filters: parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Parameter("@param_id"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeParameterConstant, Value: "@param_id"},
},
}, },
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"@param_id": "456", "@param_id": "456",
@@ -76,25 +71,19 @@ func Test_Execute_Where(t *testing.T) {
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
{Path: []string{"c", "_self"}, Alias: "self"}, {Path: []string{"c", "_self"}, Alias: "self"},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{ Filters: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd, Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{ Expressions: []interface{}{
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("67890"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "67890"},
},
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "pk"}}, Left: parsers.SelectItem{Path: []string{"c", "pk"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Int(456),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeInteger, Value: 456},
},
}, },
}, },
}, },
@@ -113,17 +102,14 @@ func Test_Execute_Where(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}}, {Path: []string{"c", "id"}},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.LogicalExpression{ Filters: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd, Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{ Expressions: []interface{}{
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "isCool"}}, Left: parsers.SelectItem{Path: []string{"c", "isCool"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_Bool(true),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeBoolean, Value: true},
},
}, },
parsers.LogicalExpression{ parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeOr, Operation: parsers.LogicalExpressionTypeOr,
@@ -131,18 +117,12 @@ func Test_Execute_Where(t *testing.T) {
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("123"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "123"},
},
}, },
parsers.ComparisonExpression{ parsers.ComparisonExpression{
Operation: "=", Operation: "=",
Left: parsers.SelectItem{Path: []string{"c", "id"}}, Left: parsers.SelectItem{Path: []string{"c", "id"}},
Right: parsers.SelectItem{ Right: testutils.SelectItem_Constant_String("456"),
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{Type: parsers.ConstantTypeString, Value: "456"},
},
}, },
}, },
}, },

View File

@@ -0,0 +1,95 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateCollection
func CreateCollection(serverName *C.char, databaseId *C.char, collectionJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionStr := C.GoString(collectionJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var collection repositorymodels.Collection
err := json.Unmarshal([]byte(collectionStr), &collection)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateCollection(databaseIdStr, collection)
return repositoryStatusToResponseCode(code)
}
//export GetCollection
func GetCollection(serverName *C.char, databaseId *C.char, collectionId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
collection, code := serverInstance.repository.GetCollection(databaseIdStr, collectionIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
collectionJson, err := json.Marshal(collection)
if err != nil {
return C.CString("")
}
return C.CString(string(collectionJson))
}
//export GetAllCollections
func GetAllCollections(serverName *C.char, databaseId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
collections, code := serverInstance.repository.GetAllCollections(databaseIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
collectionsJson, err := json.Marshal(collections)
if err != nil {
return C.CString("")
}
return C.CString(string(collectionsJson))
}
//export DeleteCollection
func DeleteCollection(serverName *C.char, databaseId *C.char, collectionId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteCollection(databaseIdStr, collectionIdStr)
return repositoryStatusToResponseCode(code)
}

View File

@@ -0,0 +1,92 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateDatabase
func CreateDatabase(serverName *C.char, databaseJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseStr := C.GoString(databaseJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var database repositorymodels.Database
err := json.Unmarshal([]byte(databaseStr), &database)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateDatabase(database)
return repositoryStatusToResponseCode(code)
}
//export GetDatabase
func GetDatabase(serverName *C.char, databaseId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
database, code := serverInstance.repository.GetDatabase(databaseIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
databaseJson, err := json.Marshal(database)
if err != nil {
return C.CString("")
}
return C.CString(string(databaseJson))
}
//export GetAllDatabases
func GetAllDatabases(serverName *C.char) *C.char {
serverNameStr := C.GoString(serverName)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
databases, code := serverInstance.repository.GetAllDatabases()
if code != repositorymodels.StatusOk {
return C.CString("")
}
databasesJson, err := json.Marshal(databases)
if err != nil {
return C.CString("")
}
return C.CString(string(databasesJson))
}
//export DeleteDatabase
func DeleteDatabase(serverName *C.char, databaseId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteDatabase(databaseIdStr)
return repositoryStatusToResponseCode(code)
}

128
sharedlibrary/documents.go Normal file
View File

@@ -0,0 +1,128 @@
package main
import "C"
import (
"encoding/json"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
//export CreateDocument
func CreateDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentStr := C.GoString(documentJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var document repositorymodels.Document
err := json.Unmarshal([]byte(documentStr), &document)
if err != nil {
return ResponseFailedToParseRequest
}
_, code := serverInstance.repository.CreateDocument(databaseIdStr, collectionIdStr, document)
return repositoryStatusToResponseCode(code)
}
//export GetDocument
func GetDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
document, code := serverInstance.repository.GetDocument(databaseIdStr, collectionIdStr, documentIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
documentJson, err := json.Marshal(document)
if err != nil {
return C.CString("")
}
return C.CString(string(documentJson))
}
//export GetAllDocuments
func GetAllDocuments(serverName *C.char, databaseId *C.char, collectionId *C.char) *C.char {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return C.CString("")
}
documents, code := serverInstance.repository.GetAllDocuments(databaseIdStr, collectionIdStr)
if code != repositorymodels.StatusOk {
return C.CString("")
}
documentsJson, err := json.Marshal(documents)
if err != nil {
return C.CString("")
}
return C.CString(string(documentsJson))
}
//export UpdateDocument
func UpdateDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char, documentJson *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
documentStr := C.GoString(documentJson)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
var document repositorymodels.Document
err := json.Unmarshal([]byte(documentStr), &document)
if err != nil {
return ResponseFailedToParseRequest
}
code := serverInstance.repository.DeleteDocument(databaseIdStr, collectionIdStr, documentIdStr)
if code != repositorymodels.StatusOk {
return repositoryStatusToResponseCode(code)
}
_, code = serverInstance.repository.CreateDocument(databaseIdStr, collectionIdStr, document)
return repositoryStatusToResponseCode(code)
}
//export DeleteDocument
func DeleteDocument(serverName *C.char, databaseId *C.char, collectionId *C.char, documentId *C.char) int {
serverNameStr := C.GoString(serverName)
databaseIdStr := C.GoString(databaseId)
collectionIdStr := C.GoString(collectionId)
documentIdStr := C.GoString(documentId)
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = getInstance(serverNameStr); !ok {
return ResponseServerInstanceNotFound
}
code := serverInstance.repository.DeleteDocument(databaseIdStr, collectionIdStr, documentIdStr)
return repositoryStatusToResponseCode(code)
}

87
sharedlibrary/shared.go Normal file
View File

@@ -0,0 +1,87 @@
package main
import (
"sync"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/internal/repositories"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
type ServerInstance struct {
server *api.ApiServer
repository *repositories.DataRepository
}
var serverInstances map[string]*ServerInstance
var mutex sync.Mutex
const (
ResponseSuccess = 0
ResponseUnknown = 100
ResponseFailedToParseConfiguration = 101
ResponseFailedToLoadState = 102
ResponseFailedToParseRequest = 103
ResponseServerInstanceAlreadyExists = 104
ResponseServerInstanceNotFound = 105
ResponseFailedToStartServer = 106
ResponseRepositoryNotFound = 200
ResponseRepositoryConflict = 201
ResponseRepositoryBadRequest = 202
)
func getInstance(serverName string) (*ServerInstance, bool) {
mutex.Lock()
defer mutex.Unlock()
if serverInstances == nil {
serverInstances = make(map[string]*ServerInstance)
}
var ok bool
var serverInstance *ServerInstance
if serverInstance, ok = serverInstances[serverName]; !ok {
return nil, false
}
return serverInstance, true
}
func addInstance(serverName string, serverInstance *ServerInstance) {
mutex.Lock()
defer mutex.Unlock()
if serverInstances == nil {
serverInstances = make(map[string]*ServerInstance)
}
serverInstances[serverName] = serverInstance
}
func removeInstance(serverName string) {
mutex.Lock()
defer mutex.Unlock()
if serverInstances == nil {
return
}
delete(serverInstances, serverName)
}
func repositoryStatusToResponseCode(status repositorymodels.RepositoryStatus) int {
switch status {
case repositorymodels.StatusOk:
return ResponseSuccess
case repositorymodels.StatusNotFound:
return ResponseRepositoryNotFound
case repositorymodels.Conflict:
return ResponseRepositoryConflict
case repositorymodels.BadRequest:
return ResponseRepositoryBadRequest
default:
return ResponseUnknown
}
}

View File

@@ -0,0 +1,102 @@
package main
/*
#include <stdlib.h>
*/
import "C"
import (
"encoding/json"
"unsafe"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
)
//export CreateServerInstance
func CreateServerInstance(serverName *C.char, configurationJSON *C.char) int {
configStr := C.GoString(configurationJSON)
serverNameStr := C.GoString(serverName)
if _, ok := getInstance(serverNameStr); ok {
return ResponseServerInstanceAlreadyExists
}
var configuration config.ServerConfig
err := json.Unmarshal([]byte(configStr), &configuration)
if err != nil {
return ResponseFailedToParseConfiguration
}
configuration.PopulateCalculatedFields()
configuration.ApplyDefaultsToEmptyFields()
repository := repositories.NewDataRepository(repositories.RepositoryOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
PersistDataFilePath: configuration.PersistDataFilePath,
})
server := api.NewApiServer(repository, configuration)
err = server.Start()
if err != nil {
return ResponseFailedToStartServer
}
addInstance(serverNameStr, &ServerInstance{
server: server,
repository: repository,
})
return ResponseSuccess
}
//export StopServerInstance
func StopServerInstance(serverName *C.char) int {
serverNameStr := C.GoString(serverName)
if serverInstance, ok := getInstance(serverNameStr); ok {
serverInstance.server.Stop()
removeInstance(serverNameStr)
return ResponseSuccess
}
return ResponseServerInstanceNotFound
}
//export GetServerInstanceState
func GetServerInstanceState(serverName *C.char) *C.char {
serverNameStr := C.GoString(serverName)
if serverInstance, ok := getInstance(serverNameStr); ok {
stateJSON, err := serverInstance.repository.GetState()
if err != nil {
return nil
}
return C.CString(stateJSON)
}
return nil
}
//export LoadServerInstanceState
func LoadServerInstanceState(serverName *C.char, stateJSON *C.char) int {
serverNameStr := C.GoString(serverName)
stateJSONStr := C.GoString(stateJSON)
if serverInstance, ok := getInstance(serverNameStr); ok {
err := serverInstance.repository.LoadStateJSON(stateJSONStr)
if err != nil {
return ResponseFailedToLoadState
}
return ResponseSuccess
}
return ResponseServerInstanceNotFound
}
//export FreeMemory
func FreeMemory(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}
func main() {}

View File

@@ -0,0 +1,46 @@
#include "shared.h"
int test_CreateServerInstance();
int test_StopServerInstance();
int test_ServerInstanceStateMethods();
int test_Databases();
int main(int argc, char *argv[])
{
if (argc < 2)
{
fprintf(stderr, "Usage: %s <path_to_shared_library>\n", argv[0]);
return EXIT_FAILURE;
}
const char *libPath = argv[1];
handle = dlopen(libPath, RTLD_LAZY);
if (!handle)
{
fprintf(stderr, "Failed to load shared library: %s\n", dlerror());
return EXIT_FAILURE;
}
printf("Running tests for library: %s\n", libPath);
int results[] = {
test_CreateServerInstance(),
test_Databases(),
test_ServerInstanceStateMethods(),
test_StopServerInstance(),
};
int numTests = sizeof(results) / sizeof(results[0]);
int numPassed = 0;
for (int i = 0; i < numTests; i++)
{
if (results[i])
{
numPassed++;
}
}
printf("Tests passed: %d/%d\n", numPassed, numTests);
dlclose(handle);
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,36 @@
#include "shared.h"
void *handle = NULL;
void *load_function(const char *func_name)
{
void *func = dlsym(handle, func_name);
if (!func)
{
fprintf(stderr, "Failed to load function %s: %s\n", func_name, dlerror());
}
return func;
}
char *compact_json(const char *json)
{
size_t len = strlen(json);
char *compact = (char *)malloc(len + 1);
if (!compact)
{
fprintf(stderr, "Failed to allocate memory for compacted JSON\n");
return NULL;
}
char *dest = compact;
for (const char *src = json; *src != '\0'; ++src)
{
if (!isspace((unsigned char)*src)) // Skip spaces, newlines, tabs, etc.
{
*dest++ = *src;
}
}
*dest = '\0'; // Null-terminate the string
return compact;
}

View File

@@ -0,0 +1,15 @@
#ifndef SHARED_H
#define SHARED_H
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include <ctype.h>
extern void *handle;
void *load_function(const char *func_name);
char *compact_json(const char *json);
#endif

View File

@@ -0,0 +1,29 @@
#include "shared.h"
int test_CreateServerInstance()
{
typedef int (*CreateServerInstanceFn)(char *, char *);
CreateServerInstanceFn CreateServerInstance = (CreateServerInstanceFn)load_function("CreateServerInstance");
if (!CreateServerInstance)
{
fprintf(stderr, "Failed to find CreateServerInstance function\n");
return 0;
}
char *serverName = "TestServer";
char *configJSON = "{\"host\":\"localhost\",\"port\":8080}";
int result = CreateServerInstance(serverName, configJSON);
if (result == 0)
{
printf("CreateServerInstance: SUCCESS\n");
}
else
{
printf("CreateServerInstance: FAILED (result = %d)\n", result);
return 0;
}
return 1;
}

View File

@@ -0,0 +1,47 @@
#include "shared.h"
int test_Databases()
{
typedef int (*CreateDatabaseFn)(char *, char *);
CreateDatabaseFn CreateDatabase = (CreateDatabaseFn)load_function("CreateDatabase");
if (!CreateDatabase)
{
fprintf(stderr, "Failed to find CreateDatabase function\n");
return 0;
}
char *serverName = "TestServer";
char *configJSON = "{\"id\":\"test-db\"}";
int result = CreateDatabase(serverName, configJSON);
if (result == 0)
{
printf("CreateDatabase: SUCCESS\n");
}
else
{
printf("CreateDatabase: FAILED (result = %d)\n", result);
return 0;
}
typedef char *(*GetDatabaseFn)(char *, char *);
GetDatabaseFn GetDatabase = (GetDatabaseFn)load_function("GetDatabase");
if (!GetDatabase)
{
fprintf(stderr, "Failed to find GetDatabase function\n");
return 0;
}
char *database = GetDatabase(serverName, "test-db");
if (database)
{
printf("GetDatabase: SUCCESS (database = %s)\n", database);
}
else
{
printf("GetDatabase: FAILED\n");
return 0;
}
return 1;
}

View File

@@ -0,0 +1,68 @@
#include "shared.h"
int test_ServerInstanceStateMethods()
{
typedef int (*LoadServerInstanceStateFn)(char *, char *);
LoadServerInstanceStateFn LoadServerInstanceState = (LoadServerInstanceStateFn)load_function("LoadServerInstanceState");
if (!LoadServerInstanceState)
{
fprintf(stderr, "Failed to find LoadServerInstanceState function\n");
return 0;
}
char *serverName = "TestServer";
char *stateJSON = "{\"databases\":{\"test-db\":{\"id\":\"test-db\"}}}";
int result = LoadServerInstanceState(serverName, stateJSON);
if (result == 0)
{
printf("LoadServerInstanceState: SUCCESS\n");
}
else
{
printf("LoadServerInstanceState: FAILED (result = %d)\n", result);
return 0;
}
typedef char *(*GetServerInstanceStateFn)(char *);
GetServerInstanceStateFn GetServerInstanceState = (GetServerInstanceStateFn)load_function("GetServerInstanceState");
if (!GetServerInstanceState)
{
fprintf(stderr, "Failed to find GetServerInstanceState function\n");
return 0;
}
char *state = GetServerInstanceState(serverName);
if (state)
{
printf("GetServerInstanceState: SUCCESS (state = %s)\n", state);
}
else
{
printf("GetServerInstanceState: FAILED\n");
return 0;
}
const char *expected_state = "{\"databases\":{\"test-db\":{\"id\":\"test-db\",\"_ts\":0,\"_rid\":\"\",\"_etag\":\"\",\"_self\":\"\"}},\"collections\":{\"test-db\":{}},\"documents\":{\"test-db\":{}},\"triggers\":{\"test-db\":{}},\"sprocs\":{\"test-db\":{}},\"udfs\":{\"test-db\":{}}}";
char *compact_state = compact_json(state);
if (!compact_state)
{
free(state);
return 0;
}
if (strcmp(compact_state, expected_state) == 0)
{
printf("GetServerInstanceState: State matches expected value.\n");
}
else
{
printf("GetServerInstanceState: State does not match expected value.\n");
printf("Expected: %s\n", expected_state);
printf("Actual: %s\n", compact_state);
return 0;
}
free(state);
free(compact_state);
return 1;
}

View File

@@ -0,0 +1,27 @@
#include "shared.h"
int test_StopServerInstance()
{
typedef int (*StopServerInstanceFn)(char *);
StopServerInstanceFn StopServerInstance = (StopServerInstanceFn)load_function("StopServerInstance");
if (!StopServerInstance)
{
fprintf(stderr, "Failed to find StopServerInstance function\n");
return 0;
}
char *serverName = "TestServer";
int result = StopServerInstance(serverName);
if (result == 0)
{
printf("StopServerInstance: SUCCESS\n");
}
else
{
printf("StopServerInstance: FAILED (result = %d)\n", result);
return 0;
}
return 1;
}

View File

@@ -0,0 +1,59 @@
package testutils
import "github.com/pikami/cosmium/parsers"
func SelectItem_Constant_String(value string) parsers.SelectItem {
return parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeString,
Value: value,
},
}
}
func SelectItem_Constant_Int(value int) parsers.SelectItem {
return parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeInteger,
Value: value,
},
}
}
func SelectItem_Constant_Float(value float64) parsers.SelectItem {
return parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeFloat,
Value: value,
},
}
}
func SelectItem_Constant_Bool(value bool) parsers.SelectItem {
return parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeBoolean,
Value: value,
},
}
}
func SelectItem_Constant_Parameter(name string) parsers.SelectItem {
return parsers.SelectItem{
Type: parsers.SelectItemTypeConstant,
Value: parsers.Constant{
Type: parsers.ConstantTypeParameterConstant,
Value: name,
},
}
}
func SelectItem_Path(path ...string) parsers.SelectItem {
return parsers.SelectItem{
Path: path,
}
}