Compare commits

..

83 Commits

Author SHA1 Message Date
Pijus Kamandulis 90de21c7b0 1.26.x (#17)
* Upgrade to go 1.26.3

* Update dependencies

* Update actions
2026-06-11 23:27:46 +03:00
Pijus Kamandulis 7c9c8ec9dc Update packages 2026-06-11 22:58:05 +03:00
Pijus Kamandulis be761badae Initial RNTBD server implementation 2026-06-11 22:43:05 +03:00
Pijus Kamandulis 36fd7f48cc Add document ETag optimistic concurrency (#16)
* Add ETag optimistic concurrency for document replace

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>

* Expose precondition error code header

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>

* Stop Badger GC before closing datastore

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-06-05 23:26:25 +03:00
Pijus Kamandulis 05e8cd2842 Implement REGEXMATCH function (#15)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-30 21:31:45 +03:00
Pijus Kamandulis c3726a6633 Fix ARRAY_CONTAINS panic when optional partial-match argument is omitted (#14)
* Fix ARRAY_CONTAINS panic when partial match arg is omitted

The NoSQL parser always emits a third (nil) argument for the optional
partial-match flag of ARRAY_CONTAINS. The executor checked only
len(arguments) > 2 before type-asserting arguments[2] to
parsers.SelectItem, which panicked on the nil value whenever the query
omitted the partial-match argument (e.g. ARRAY_CONTAINS(c.arr, 2)).

Guard the type assertion with a nil check and add an API test covering
ARRAY_CONTAINS with and without the optional partial-match argument.

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>

* Remove comments from ARRAY_CONTAINS API test

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-05-30 17:43:05 +03:00
Pijus Kamandulis d76cc88175 Fix 'NOT (bool)' statements 2026-04-04 15:04:42 +03:00
Pijus Kamandulis 3daba9d0eb Update dependencies 2026-01-29 21:47:35 +02:00
Pijus Kamandulis d3d238fa98 Implement continuation tokens 2026-01-29 21:45:46 +02:00
Pijus Kamandulis cae6fda95c Added cross-platform shared library tests 2025-11-27 21:26:54 +02:00
Pijus Kamandulis 46c446c273 Update dependencies 2025-11-27 00:28:37 +02:00
Pijus Kamandulis d64bdeb385 Handle 'NOT IN' statement 2025-10-27 21:33:49 +02:00
Pijus Kamandulis 11f3a1ad01 Fix database and collection deletion 2025-10-12 23:26:44 +03:00
Pijus Kamandulis 4d67212f1b Update dependencies 2025-10-08 23:21:30 +03:00
Pijus Kamandulis 03cd04e996 Downgrade Go to 1.24.7 due to windows cross-compile issue 2025-10-08 23:14:44 +03:00
Pijus Kamandulis a3bea16a26 Updated GO version to 1.25.1 2025-09-16 22:35:44 +03:00
Pijus Kamandulis 67b6c86e14 Update dependencies 2025-09-16 22:28:22 +03:00
Pijus Kamandulis 4872ec72fd Added support for initial data loading when using badger db 2025-09-16 20:21:27 +03:00
Pijus Kamandulis 89b914310c Fix badger prefix scans 2025-09-16 19:27:52 +03:00
Pijus Kamandulis c988741f8e Fix query creation via explorer; Extract header names to constants 2025-09-16 19:13:45 +03:00
Pijus Kamandulis 51e3311ba4 Update dependencies; Fix authentication for UDF, SPROC and TRIGGER endpoints 2025-08-20 00:12:02 +03:00
zecka fb1c080034 docs(readme): fix docker example to create valid save.json if missing (#12)
* docs(readme): fix docker example to create valid save.json if missing

Prevent runtime errors by initializing save.json with '{}' if the file does not exist before running the container.

Fixes #11

* Update README.md

---------

Co-authored-by: Pijus Kamandulis <pikami@users.noreply.github.com>
2025-07-06 11:27:09 +03:00
Pijus Kamandulis fba9b3df5f Run badger garbage collector periodically 2025-05-30 00:25:17 +03:00
Pijus Kamandulis b743e23ff9 Added support for arithmetics inside queries 2025-05-30 00:15:55 +03:00
Pijus Kamandulis 11851297f5 Fix formatting for grammar file 2025-05-20 22:43:00 +03:00
Pijus Kamandulis 560ea5296d Add support for expressions in SELECT clause 2025-05-20 22:40:00 +03:00
Pijus Kamandulis e20a6ca7cd Extract constants instead of duplicating literals 2025-05-14 20:01:46 +03:00
Pijus Kamandulis 7e0c10479b Implement IIF function; Fix empty object select 2025-05-14 18:48:30 +03:00
Pijus Kamandulis 30195fae96 Update dependencies 2025-05-14 08:25:53 +03:00
Pijus Kamandulis 598f2837af Fix issues with persist flag; Use custom logger for badger 2025-04-03 23:48:20 +03:00
Pijus Kamandulis 28e3c0c3d8 Rename 'MapDS' to 'JsonDS'; Added some docs 2025-03-14 22:40:12 +02:00
Pijus Kamandulis 97eea30c97 Use msgpack instead of gob; Added data persistance for badger data store 2025-03-13 23:59:07 +02:00
Pijus Kamandulis 5fe60d831a Pinned 3rd party Github Actions 2025-03-12 23:48:42 +02:00
Pijus Kamandulis d309d99906 Update dependancies 2025-03-12 23:24:08 +02:00
Pijus Kamandulis b2516eda9f Stability improvements 2025-03-12 22:00:30 +02:00
Pijus Kamandulis 813b9faeaa Added support for Badger as an alternative storage backend 2025-03-12 21:06:10 +02:00
Pijus Kamandulis e526b2269e Refactored query engine utilizing iterators 2025-03-11 17:36:28 +02:00
Pijus Kamandulis 221f029a1d DataStore is interface now. Liskov would be proud. 2025-03-09 18:34:07 +02:00
Pijus Kamandulis bd4fe5abec Update azcosmos package 2025-02-25 20:43:23 +02:00
Pijus Kamandulis f062e03f0c Update packages 2025-02-25 19:56:02 +02:00
Pijus Kamandulis 058b3271b7 OrderBy should bring NULL values to front 2025-02-25 19:47:29 +02:00
Pijus Kamandulis 1711c8fb5c Implement NOT logical operator 2025-02-25 19:33:32 +02:00
Pijus Kamandulis 851b3ca3a8 Fix IN clause with function calls 2025-02-20 18:45:20 +02:00
Pijus Kamandulis d27c633e1d Better handling when passing null to string functions 2025-02-18 20:11:11 +02:00
Pijus Kamandulis 3987df89c0 Upgrade to golang 1.24.0 2025-02-18 19:16:21 +02:00
Pijus Kamandulis 6e3f4169a1 Fix 'ComparisonOperator' parsing 2025-02-18 19:12:08 +02:00
Pijus Kamandulis 14c5400d23 Keep old explorer images tagged with version 2025-02-09 22:42:51 +02:00
Pijus Kamandulis 1cf5ae92f4 Shared library stability improvements 2025-02-09 11:45:10 +02:00
Pijus Kamandulis 5d99b653cc Generate more realistic resource ids 2025-02-09 00:36:35 +02:00
Pijus Kamandulis 787cdb33cf Fix OFFSET clause 2025-02-08 15:28:06 +02:00
Pijus Kamandulis 5caa829ac1 Implement 'Transactional batch operations' 2025-02-04 20:35:15 +02:00
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
147 changed files with 15310 additions and 4750 deletions
@@ -0,0 +1,89 @@
name: Cross-Compile Shared Libraries
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: shared-libraries
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@de82f877ff4552f03b66c146f608233849e9c3dc # v4.0.0
with:
xgo_version: latest
go_version: 1.26.3
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@v7
with:
name: shared-libraries
path: dist/*
test:
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
lib_ext: amd64.so
- os: windows-latest
lib_ext: amd64.dll
- os: macos-latest
lib_ext: arm64.dylib
steps:
- uses: actions/checkout@v6
- name: Download shared libraries
uses: actions/download-artifact@v8
with:
name: shared-libraries
path: libs
- name: Install MinGW (GCC) on Windows
if: runner.os == 'Windows'
run: choco install mingw --no-progress
- name: Build test loader (Windows)
if: runner.os == 'Windows'
run: |
mkdir build
gcc -Wall -o build/test_loader.exe sharedlibrary/tests/*.c
- name: Build test loader (Unix)
if: runner.os != 'Windows'
run: |
mkdir build
gcc -Wall -ldl -o build/test_loader sharedlibrary/tests/*.c
- name: Run test (Unix)
if: runner.os != 'Windows'
run: |
LIB=$(ls libs/*${{ matrix.lib_ext }} | head -n 1)
echo "Testing library: $LIB"
chmod +x build/test_loader
./build/test_loader "$LIB"
- name: Run test (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$lib = Get-ChildItem "libs/*${{ matrix.lib_ext }}" | Select-Object -First 1
Write-Host "Testing library: $($lib.FullName)"
.\build\test_loader.exe $lib.FullName
+20 -4
View File
@@ -3,7 +3,7 @@ name: goreleaser
on: on:
push: push:
tags: tags:
- '*' - "*"
permissions: permissions:
contents: write contents: write
@@ -14,21 +14,37 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
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.26.3
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@de82f877ff4552f03b66c146f608233849e9c3dc # v4.0.0
with:
xgo_version: latest
go_version: 1.26.3
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@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5
with: with:
distribution: goreleaser distribution: goreleaser
version: ${{ env.GITHUB_REF_NAME }} version: ${{ env.GITHUB_REF_NAME }}
+1
View File
@@ -1,4 +1,5 @@
dist/ dist/
sharedlibrary_dist/
ignored/ ignored/
explorer_www/ explorer_www/
main main
+103 -9
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,85 @@ 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"
- name_template: 'ghcr.io/pikami/{{ .ProjectName }}:{{ .Version }}-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'
+1 -1
View File
@@ -1,4 +1,4 @@
FROM scratch FROM alpine:latest
WORKDIR /app WORKDIR /app
COPY cosmium /app/cosmium COPY cosmium /app/cosmium
+9
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"]
+46 -5
View File
@@ -4,28 +4,69 @@ 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.26.3
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-darwin-arm64:
@echo "Building shared library for macOS ARM..."
@GOOS=darwin GOARCH=arm64 $(GOBUILD) $(SHARED_LIB_OPT) -o $(DIST_DIR)/$(BINARY_NAME)-darwin-arm64.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
+27 -4
View File
@@ -26,9 +26,11 @@ 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
- **Linux on ARM**: cosmium-linux-arm64
- **macOS**: cosmium-darwin-amd64 - **macOS**: cosmium-darwin-amd64
- **macOS on Apple Silicon**: cosmium-darwin-arm64 - **macOS on Apple Silicon**: cosmium-darwin-arm64
- **Windows**: cosmium-windows-amd64.exe - **Windows**: cosmium-windows-amd64.exe
- **Windows on ARM**: cosmium-windows-arm64.exe
### Running Cosmium ### Running Cosmium
@@ -48,18 +50,26 @@ AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJI
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 \ # Ensure save.json exists so Docker volume mounts correctly
[ -f save.json ] || echo '{}' > save.json && docker run --rm \
-e COSMIUM_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
@@ -76,7 +86,8 @@ To disable SSL and run Cosmium on HTTP instead, you can use the `-DisableTls` fl
- **-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)
- **-DataStore**: Allows selecting [storage backend](#data-storage-backends) (default "json")
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.
@@ -88,7 +99,19 @@ All mentioned arguments can also be set using environment variables:
- **COSMIUM_INITIALDATA** for `-InitialData` - **COSMIUM_INITIALDATA** for `-InitialData`
- **COSMIUM_PERSIST** for `-Persist` - **COSMIUM_PERSIST** for `-Persist`
- **COSMIUM_PORT** for `-Port` - **COSMIUM_PORT** for `-Port`
- **COSMIUM_DEBUG** for `-Debug` - **COSMIUM_LOGLEVEL** for `-LogLevel`
### Data Storage Backends
Cosmium supports multiple storage backends for saving, loading, and managing data at runtime.
| Backend | Storage Location | Write Behavior | Memory Usage |
|----------|--------------------------|--------------------------|----------------------|
| `json` (default) | JSON file on disk 📄 | On application exit ⏳ | 🛑 More than Badger |
| `badger` | BadgerDB database on disk ⚡ | Immediately on write 🚀 | ✅ Less than JSON |
The `badger` backend is generally recommended as it uses less memory and writes data to disk immediately. However, if you need to load initial data from a JSON file, use the `json` backend.
# License # License
+24
View File
@@ -0,0 +1,24 @@
package apimodels
const (
BatchOperationTypeCreate = "Create"
BatchOperationTypeDelete = "Delete"
BatchOperationTypeReplace = "Replace"
BatchOperationTypeUpsert = "Upsert"
BatchOperationTypeRead = "Read"
BatchOperationTypePatch = "Patch"
)
type BatchOperation struct {
OperationType string `json:"operationType"`
Id string `json:"id"`
ResourceBody map[string]interface{} `json:"resourceBody"`
}
type BatchOperationResult struct {
StatusCode int `json:"statusCode"`
RequestCharge float64 `json:"requestCharge"`
ResourceBody map[string]interface{} `json:"resourceBody"`
Etag string `json:"etag"`
Message string `json:"message"`
}
+39
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/datastore"
)
type ApiServer struct {
stopServer chan interface{}
onServerShutdown chan interface{}
isActive bool
router *gin.Engine
config *config.ServerConfig
}
func NewApiServer(dataStore datastore.DataStore, config *config.ServerConfig) *ApiServer {
stopChan := make(chan interface{})
onServerShutdownChan := make(chan interface{})
apiServer := &ApiServer{
stopServer: stopChan,
onServerShutdown: onServerShutdownChan,
config: config,
}
apiServer.CreateRouter(dataStore)
return apiServer
}
func (s *ApiServer) GetRouter() *gin.Engine {
return s.router
}
func (s *ApiServer) Stop() {
s.stopServer <- true
<-s.onServerShutdown
}
+82 -18
View File
@@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/pikami/cosmium/internal/logger"
) )
const ( const (
@@ -13,11 +15,15 @@ const (
ExplorerBaseUrlLocation = "/_explorer" ExplorerBaseUrlLocation = "/_explorer"
) )
var Config = ServerConfig{} const (
DataStoreJson = "json"
DataStoreBadger = "badger"
)
func ParseFlags() { func ParseFlags() ServerConfig {
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")
rntbdPort := flag.Int("RntbdPort", 10000, "RNTBD listen port")
explorerPath := flag.String("ExplorerDir", "", "Path to cosmos-explorer files") explorerPath := flag.String("ExplorerDir", "", "Path to cosmos-explorer files")
tlsCertificatePath := flag.String("Cert", "", "Hostname") tlsCertificatePath := flag.String("Cert", "", "Hostname")
tlsCertificateKey := flag.String("CertKey", "", "Hostname") tlsCertificateKey := flag.String("CertKey", "", "Hostname")
@@ -26,27 +32,85 @@ 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()))
dataStore := NewEnumValue("json", []string{DataStoreJson, DataStoreBadger})
flag.Var(dataStore, "DataStore", fmt.Sprintf("Sets the data store %s", dataStore.AllowedValuesList()))
enableRntbd := flag.Bool("ExperimentalEnableRntbd", false, "EXPERIMENTAL: Enable RNTBD (CosmosDB Direct Connection Mode)")
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.RntbdPort = *rntbdPort
Config.TLS_CertificateKey = *tlsCertificateKey config.ExplorerPath = *explorerPath
Config.InitialDataFilePath = *initialDataPath config.TLS_CertificatePath = *tlsCertificatePath
Config.PersistDataFilePath = *persistDataPath config.TLS_CertificateKey = *tlsCertificateKey
Config.DisableAuth = *disableAuthentication config.InitialDataFilePath = *initialDataPath
Config.DisableTls = *disableTls config.PersistDataFilePath = *persistDataPath
Config.Debug = *debug config.DisableAuth = *disableAuthentication
config.DisableTls = *disableTls
config.AccountKey = *accountKey
config.LogLevel = logLevel.value
config.DataStore = dataStore.value
config.EnableRntbd = *enableRntbd
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 }
Config.ExplorerBaseUrlLocation = ExplorerBaseUrlLocation
func (c *ServerConfig) PopulateCalculatedFields() {
c.DatabaseAccount = c.Host
c.DatabaseDomain = c.Host
c.DatabaseEndpoint = fmt.Sprintf("https://%s:%d/", c.Host, c.Port)
c.RntbdEndpoint = fmt.Sprintf("rntbd://%s:%d/", c.Host, c.RntbdPort)
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)
}
fileInfo, err := os.Stat(c.PersistDataFilePath)
if c.PersistDataFilePath != "" && !os.IsNotExist(err) {
if err != nil {
logger.ErrorLn("Failed to get file info for persist path:", err)
os.Exit(1)
}
if c.DataStore == DataStoreJson && fileInfo.IsDir() {
logger.ErrorLn("--Persist cannot be a directory when using json data store")
os.Exit(1)
}
if c.DataStore == DataStoreBadger && !fileInfo.IsDir() {
logger.ErrorLn("--Persist must be a directory when using Badger data store")
os.Exit(1)
}
}
}
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
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, ", "))
}
+20 -15
View File
@@ -1,20 +1,25 @@
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 RntbdEndpoint string `json:"rntbdEndpoint"`
AccountKey string `json:"accountKey"`
ExplorerPath string ExplorerPath string `json:"explorerPath"`
Port int Port int `json:"port"`
Host string RntbdPort int `json:"rntbdPort"`
TLS_CertificatePath string Host string `json:"host"`
TLS_CertificateKey string TLS_CertificatePath string `json:"tlsCertificatePath"`
InitialDataFilePath string TLS_CertificateKey string `json:"tlsCertificateKey"`
PersistDataFilePath string InitialDataFilePath string `json:"initialDataFilePath"`
DisableAuth bool PersistDataFilePath string `json:"persistDataFilePath"`
DisableTls bool DisableAuth bool `json:"disableAuth"`
Debug bool DisableTls bool `json:"disableTls"`
ExplorerBaseUrlLocation string LogLevel string `json:"logLevel"`
ExplorerBaseUrlLocation string `json:"explorerBaseUrlLocation"`
EnableRntbd bool `json:"enableRntbd"`
DataStore string `json:"dataStore"`
} }
+29 -28
View File
@@ -5,18 +5,19 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
) )
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.dataStore.GetAllCollections(databaseId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
database, _ := repositories.GetDatabase(databaseId) database, _ := h.dataStore.GetDatabase(databaseId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(collections))) c.Header(headers.ItemCount, 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,
@@ -25,48 +26,48 @@ func GetAllCollections(c *gin.Context) {
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.GetCollection(databaseId, id)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, collection) c.IndentedJSON(http.StatusOK, collection)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.DeleteCollection(databaseId, id)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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 datastore.Collection
if err := c.BindJSON(&newCollection); err != nil { if err := c.BindJSON(&newCollection); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
@@ -74,20 +75,20 @@ func CreateCollection(c *gin.Context) {
} }
if newCollection.ID == "" { if newCollection.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"}) c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return return
} }
createdCollection, status := repositories.CreateCollection(databaseId, newCollection) createdCollection, status := h.dataStore.CreateCollection(databaseId, newCollection)
if status == repositorymodels.Conflict { if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return return
} }
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdCollection) c.IndentedJSON(http.StatusCreated, createdCollection)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+8 -3
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()) dataStoreState, err := h.dataStore.DumpToJson()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Data(http.StatusOK, "application/json", []byte(dataStoreState))
} }
+28 -27
View File
@@ -5,14 +5,15 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
) )
func GetAllDatabases(c *gin.Context) { func (h *Handlers) GetAllDatabases(c *gin.Context) {
databases, status := repositories.GetAllDatabases() databases, status := h.dataStore.GetAllDatabases()
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(databases))) c.Header(headers.ItemCount, fmt.Sprintf("%d", len(databases)))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "", "_rid": "",
"Databases": databases, "Databases": databases,
@@ -21,45 +22,45 @@ func GetAllDatabases(c *gin.Context) {
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.GetDatabase(id)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, database) c.IndentedJSON(http.StatusOK, database)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.DeleteDatabase(id)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
func CreateDatabase(c *gin.Context) { func (h *Handlers) CreateDatabase(c *gin.Context) {
var newDatabase repositorymodels.Database var newDatabase datastore.Database
if err := c.BindJSON(&newDatabase); err != nil { if err := c.BindJSON(&newDatabase); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
@@ -67,20 +68,20 @@ func CreateDatabase(c *gin.Context) {
} }
if newDatabase.ID == "" { if newDatabase.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"}) c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return return
} }
createdDatabase, status := repositories.CreateDatabase(newDatabase) createdDatabase, status := h.dataStore.CreateDatabase(newDatabase)
if status == repositorymodels.Conflict { if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return return
} }
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDatabase) c.IndentedJSON(http.StatusCreated, createdDatabase)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+283 -79
View File
@@ -6,23 +6,29 @@ import (
"net/http" "net/http"
"strconv" "strconv"
jsonpatch "github.com/evanphx/json-patch/v5" jsonpatch "github.com/cosmiumdev/json-patch/v5"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
apimodels "github.com/pikami/cosmium/api/api_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants" "github.com/pikami/cosmium/internal/constants"
continuationtoken "github.com/pikami/cosmium/internal/continuation_token"
"github.com/pikami/cosmium/internal/converters"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/parsers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/parsers/nosql"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
) )
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.dataStore.GetAllDocuments(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
collection, _ := repositories.GetCollection(databaseId, collectionId) collection, _ := h.dataStore.GetCollection(databaseId, collectionId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(documents))) c.Header(headers.ItemCount, 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,
@@ -31,49 +37,52 @@ func GetAllDocuments(c *gin.Context) {
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
if etag, ok := document["_etag"].(string); ok {
c.Header(headers.ETag, etag)
}
c.IndentedJSON(http.StatusOK, document) c.IndentedJSON(http.StatusOK, document)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
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.dataStore.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Status(http.StatusNoContent) c.Status(http.StatusNoContent)
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
// TODO: Maybe move "replace" logic to repository // TODO: Maybe move "replace" logic to data store
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")
@@ -84,34 +93,52 @@ func ReplaceDocument(c *gin.Context) {
return return
} }
status := repositories.DeleteDocument(databaseId, collectionId, documentId) existingDocument, status := h.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
if status != datastore.StatusOk {
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
return return
} }
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody) if ifMatch := c.GetHeader(headers.IfMatch); ifMatch != "" {
if status == repositorymodels.Conflict { if existingDocument["_etag"] != ifMatch {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.Header(headers.ErrorCode, "PreconditionFailed")
c.JSON(http.StatusPreconditionFailed, constants.PreconditionFailedResponse)
return
}
}
status = h.dataStore.DeleteDocument(databaseId, collectionId, documentId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
if status == repositorymodels.StatusOk { createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, requestBody)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument) c.IndentedJSON(http.StatusCreated, createdDocument)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
func PatchDocument(c *gin.Context) { func (h *Handlers) PatchDocument(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.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
@@ -136,7 +163,7 @@ func PatchDocument(c *gin.Context) {
currentDocumentBytes, err := json.Marshal(document) currentDocumentBytes, err := json.Marshal(document)
if err != nil { if err != nil {
logger.Error("Failed to marshal existing document:", err) logger.ErrorLn("Failed to marshal existing document:", err)
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to marshal existing document"}) c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to marshal existing document"})
return return
} }
@@ -150,7 +177,7 @@ func PatchDocument(c *gin.Context) {
var modifiedDocument map[string]interface{} var modifiedDocument map[string]interface{}
err = json.Unmarshal(modifiedDocumentBytes, &modifiedDocument) err = json.Unmarshal(modifiedDocumentBytes, &modifiedDocument)
if err != nil { if err != nil {
logger.Error("Failed to unmarshal modified document:", err) logger.ErrorLn("Failed to unmarshal modified document:", err)
c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to unmarshal modified document"}) c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to unmarshal modified document"})
return return
} }
@@ -160,87 +187,80 @@ func PatchDocument(c *gin.Context) {
return return
} }
status = repositories.DeleteDocument(databaseId, collectionId, documentId) status = h.dataStore.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, modifiedDocument) createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, modifiedDocument)
if status == repositorymodels.Conflict { if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return return
} }
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument) c.IndentedJSON(http.StatusCreated, createdDocument)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
func DocumentsPost(c *gin.Context) { func (h *Handlers) DocumentsPost(c *gin.Context) {
databaseId := c.Param("databaseId") databaseId := c.Param("databaseId")
collectionId := c.Param("collId") collectionId := c.Param("collId")
// Handle batch requests
isBatchRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsBatchRequest))
if isBatchRequest {
h.handleBatchRequest(c)
return
}
var requestBody map[string]interface{} var requestBody map[string]interface{}
if err := c.BindJSON(&requestBody); err != nil { if err := c.BindJSON(&requestBody); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return return
} }
query := requestBody["query"] // Handle query plan requests
if query != nil { isQueryPlanRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsQueryPlanRequest))
if c.GetHeader("x-ms-cosmos-is-query-plan-request") != "" { if isQueryPlanRequest {
c.IndentedJSON(http.StatusOK, constants.QueryPlanResponse) c.IndentedJSON(http.StatusOK, constants.QueryPlanResponse)
return return
} }
var queryParameters map[string]interface{} // Handle query requests
if paramsArray, ok := requestBody["parameters"].([]interface{}); ok { isQueryRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsQuery))
queryParameters = parametersToMap(paramsArray) isQueryRequestAltHeader, _ := strconv.ParseBool(c.GetHeader(headers.Query))
} if isQueryRequest || isQueryRequestAltHeader {
h.handleDocumentQuery(c, requestBody)
docs, status := repositories.ExecuteQueryDocuments(databaseId, collectionId, query.(string), queryParameters)
if status != repositorymodels.StatusOk {
// TODO: Currently we return everything if the query fails
GetAllDocuments(c)
return
}
collection, _ := repositories.GetCollection(databaseId, collectionId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(docs)))
c.IndentedJSON(http.StatusOK, gin.H{
"_rid": collection.ResourceID,
"Documents": docs,
"_count": len(docs),
})
return return
} }
if requestBody["id"] == "" { if requestBody["id"] == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"}) c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return return
} }
isUpsert, _ := strconv.ParseBool(c.GetHeader("x-ms-documentdb-is-upsert")) isUpsert, _ := strconv.ParseBool(c.GetHeader(headers.IsUpsert))
if isUpsert { if isUpsert {
repositories.DeleteDocument(databaseId, collectionId, requestBody["id"].(string)) h.dataStore.DeleteDocument(databaseId, collectionId, requestBody["id"].(string))
} }
createdDocument, status := repositories.CreateDocument(databaseId, collectionId, requestBody) createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict { if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"}) c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return return
} }
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument) c.IndentedJSON(http.StatusCreated, createdDocument)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
func parametersToMap(pairs []interface{}) map[string]interface{} { func parametersToMap(pairs []interface{}) map[string]interface{} {
@@ -254,3 +274,187 @@ func parametersToMap(pairs []interface{}) map[string]interface{} {
return result return result
} }
func (h *Handlers) handleDocumentQuery(c *gin.Context, requestBody map[string]interface{}) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var queryParameters map[string]interface{}
if paramsArray, ok := requestBody["parameters"].([]interface{}); ok {
queryParameters = parametersToMap(paramsArray)
}
collection, collectionStatus := h.dataStore.GetCollection(databaseId, collectionId)
if collectionStatus == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
if collectionStatus != datastore.StatusOk {
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
return
}
continuationToken := continuationtoken.GenerateDefault(collection.ResourceID)
continuationTokenHeader := c.GetHeader(headers.ContinuationToken)
if continuationTokenHeader != "" {
continuationToken = continuationtoken.FromString(continuationTokenHeader)
}
pageMaxItemCount, maxItemCountError := strconv.Atoi(c.GetHeader(headers.MaxItemCount))
if maxItemCountError != nil {
pageMaxItemCount = 1000
}
queryText := requestBody["query"].(string)
executeQueryResult, status := h.executeQueryDocuments(
databaseId, collectionId, queryText, queryParameters, pageMaxItemCount, continuationToken.Token.TotalResults)
if status != datastore.StatusOk {
// TODO: Currently we return everything if the query fails
logger.Infof("Query failed: %s", queryText)
h.GetAllDocuments(c)
return
}
resultCount := len(executeQueryResult.Rows)
if executeQueryResult.HasMorePages {
nextContinuationToken := continuationtoken.Generate(
collection.ResourceID, continuationToken.Token.PageIndex+1, continuationToken.Token.TotalResults+resultCount)
c.Header(headers.ContinuationToken, nextContinuationToken.ToString())
}
c.Header(headers.ItemCount, fmt.Sprintf("%d", resultCount))
c.IndentedJSON(http.StatusOK, gin.H{
"_rid": collection.ResourceID,
"Documents": executeQueryResult.Rows,
"_count": resultCount,
})
}
func (h *Handlers) handleBatchRequest(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
batchOperations := make([]apimodels.BatchOperation, 0)
if err := c.BindJSON(&batchOperations); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
return
}
batchOperationResults := make([]apimodels.BatchOperationResult, len(batchOperations))
for idx, operation := range batchOperations {
switch operation.OperationType {
case apimodels.BatchOperationTypeCreate:
createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := dataStoreStatusToResponseCode(status)
if status == datastore.StatusOk {
responseCode = http.StatusCreated
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: responseCode,
ResourceBody: createdDocument,
}
case apimodels.BatchOperationTypeDelete:
status := h.dataStore.DeleteDocument(databaseId, collectionId, operation.Id)
responseCode := dataStoreStatusToResponseCode(status)
if status == datastore.StatusOk {
responseCode = http.StatusNoContent
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: responseCode,
}
case apimodels.BatchOperationTypeReplace:
deleteStatus := h.dataStore.DeleteDocument(databaseId, collectionId, operation.Id)
if deleteStatus == datastore.StatusNotFound {
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: http.StatusNotFound,
}
continue
}
createdDocument, createStatus := h.dataStore.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := dataStoreStatusToResponseCode(createStatus)
if createStatus == datastore.StatusOk {
responseCode = http.StatusCreated
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: responseCode,
ResourceBody: createdDocument,
}
case apimodels.BatchOperationTypeUpsert:
documentId := operation.ResourceBody["id"].(string)
h.dataStore.DeleteDocument(databaseId, collectionId, documentId)
createdDocument, createStatus := h.dataStore.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := dataStoreStatusToResponseCode(createStatus)
if createStatus == datastore.StatusOk {
responseCode = http.StatusCreated
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: responseCode,
ResourceBody: createdDocument,
}
case apimodels.BatchOperationTypeRead:
document, status := h.dataStore.GetDocument(databaseId, collectionId, operation.Id)
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: dataStoreStatusToResponseCode(status),
ResourceBody: document,
}
case apimodels.BatchOperationTypePatch:
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: http.StatusNotImplemented,
Message: "Patch operation is not implemented",
}
default:
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: http.StatusBadRequest,
Message: "Unknown operation type",
}
}
}
c.JSON(http.StatusOK, batchOperationResults)
}
func dataStoreStatusToResponseCode(status datastore.DataStoreStatus) int {
switch status {
case datastore.StatusOk:
return http.StatusOK
case datastore.StatusNotFound:
return http.StatusNotFound
case datastore.Conflict:
return http.StatusConflict
case datastore.BadRequest:
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
func (h *Handlers) executeQueryDocuments(
databaseId string,
collectionId string,
query string,
queryParameters map[string]interface{},
pageMaxItemCount int,
pageCursor int,
) (memoryexecutor.ExecuteQueryResult, datastore.DataStoreStatus) {
parsedQuery, err := nosql.Parse("", []byte(query))
if err != nil {
logger.Errorf("Failed to parse query: %s\nerr: %v", query, err)
return memoryexecutor.ExecuteQueryResult{}, datastore.BadRequest
}
allDocumentsIterator, status := h.dataStore.GetDocumentIterator(databaseId, collectionId)
if status != datastore.StatusOk {
return memoryexecutor.ExecuteQueryResult{}, status
}
defer allDocumentsIterator.Close()
rowsIterator := converters.NewDocumentToRowTypeIterator(allDocumentsIterator)
if typedQuery, ok := parsedQuery.(parsers.SelectStmt); ok {
typedQuery.Parameters = queryParameters
return memoryexecutor.ExecuteQuery(typedQuery, rowsIterator, pageCursor, pageMaxItemCount), datastore.StatusOk
}
return memoryexecutor.ExecuteQueryResult{}, datastore.BadRequest
}
+5 -6
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(config.Config.ExplorerBaseUrlLocation) 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
View File
@@ -0,0 +1,18 @@
package handlers
import (
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/datastore"
)
type Handlers struct {
dataStore datastore.DataStore
config *config.ServerConfig
}
func NewHandlers(dataStore datastore.DataStore, config *config.ServerConfig) *Handlers {
return &Handlers{
dataStore: dataStore,
config: config,
}
}
+20 -7
View File
@@ -6,15 +6,16 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/authentication" "github.com/pikami/cosmium/internal/authentication"
"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, config.Config.ExplorerBaseUrlLocation) || strings.HasPrefix(requestUrl, config.ExplorerBaseUrlLocation) ||
strings.HasPrefix(requestUrl, "/cosmium") { strings.HasPrefix(requestUrl, "/cosmium") {
return return
} }
@@ -22,10 +23,10 @@ func Authentication() gin.HandlerFunc {
resourceType := urlToResourceType(requestUrl) resourceType := urlToResourceType(requestUrl)
resourceId := requestToResourceId(c) resourceId := requestToResourceId(c)
authHeader := c.Request.Header.Get("authorization") authHeader := c.Request.Header.Get(headers.Authorization)
date := c.Request.Header.Get("x-ms-date") date := c.Request.Header.Get(headers.XDate)
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)
@@ -60,6 +61,9 @@ func requestToResourceId(c *gin.Context) string {
databaseId, _ := c.Params.Get("databaseId") databaseId, _ := c.Params.Get("databaseId")
collId, _ := c.Params.Get("collId") collId, _ := c.Params.Get("collId")
docId, _ := c.Params.Get("docId") docId, _ := c.Params.Get("docId")
triggerId, _ := c.Params.Get("triggerId")
sprocId, _ := c.Params.Get("sprocId")
udfId, _ := c.Params.Get("udfId")
resourceType := urlToResourceType(c.Request.URL.String()) resourceType := urlToResourceType(c.Request.URL.String())
var resourceId string var resourceId string
@@ -72,8 +76,17 @@ func requestToResourceId(c *gin.Context) string {
if docId != "" { if docId != "" {
resourceId += "/docs/" + docId resourceId += "/docs/" + docId
} }
if triggerId != "" {
resourceId += "/triggers/" + triggerId
}
if sprocId != "" {
resourceId += "/sprocs/" + sprocId
}
if udfId != "" {
resourceId += "/udfs/" + udfId
}
isFeed := c.Request.Header.Get("A-Im") == "Incremental Feed" isFeed := c.Request.Header.Get(headers.AIM) == "Incremental Feed"
if resourceType == "pkranges" && isFeed { if resourceType == "pkranges" && isFeed {
resourceId = collId resourceId = collId
} }
+1 -1
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
@@ -7,10 +7,10 @@ import (
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
) )
func StripTrailingSlashes(r *gin.Engine) gin.HandlerFunc { func StripTrailingSlashes(r *gin.Engine, config *config.ServerConfig) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
path := c.Request.URL.Path path := c.Request.URL.Path
if len(path) > 1 && path[len(path)-1] == '/' && !strings.Contains(path, config.Config.ExplorerBaseUrlLocation) { if len(path) > 1 && path[len(path)-1] == '/' && !strings.Contains(path, config.ExplorerBaseUrlLocation) {
c.Request.URL.Path = path[:len(path)-1] c.Request.URL.Path = path[:len(path)-1]
r.HandleContext(c) r.HandleContext(c)
c.Abort() c.Abort()
+2 -1
View File
@@ -4,10 +4,11 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/headers"
) )
func GetOffers(c *gin.Context) { func GetOffers(c *gin.Context) {
c.Header("x-ms-item-count", "0") c.Header(headers.ItemCount, "0")
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "", "_rid": "",
"_count": 0, "_count": 0,
+19 -16
View File
@@ -5,45 +5,48 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
) )
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")
if c.Request.Header.Get("if-none-match") != "" { if c.Request.Header.Get(headers.IfNoneMatch) != "" {
c.AbortWithStatus(http.StatusNotModified) c.AbortWithStatus(http.StatusNotModified)
return return
} }
partitionKeyRanges, status := repositories.GetPartitionKeyRanges(databaseId, collectionId) partitionKeyRanges, status := h.dataStore.GetPartitionKeyRanges(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Header("etag", "\"420\"") c.Header(headers.ETag, "\"420\"")
c.Header("lsn", "420") c.Header(headers.LSN, "420")
c.Header("x-ms-cosmos-llsn", "420") c.Header(headers.CosmosLsn, "420")
c.Header("x-ms-global-committed-lsn", "420") c.Header(headers.GlobalCommittedLsn, "420")
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(partitionKeyRanges))) c.Header(headers.ItemCount, fmt.Sprintf("%d", len(partitionKeyRanges)))
collectionRid := collectionId collectionRid := collectionId
collection, _ := repositories.GetCollection(databaseId, collectionId) collection, _ := h.dataStore.GetCollection(databaseId, collectionId)
if collection.ResourceID != "" { if collection.ResourceID != "" {
collectionRid = collection.ResourceID collectionRid = collection.ResourceID
} }
rid := resourceid.NewCombined(collectionRid, resourceid.New(resourceid.ResourceTypePartitionKeyRange))
c.IndentedJSON(http.StatusOK, gin.H{ c.IndentedJSON(http.StatusOK, gin.H{
"_rid": collectionRid, "_rid": rid,
"_count": len(partitionKeyRanges), "_count": len(partitionKeyRanges),
"PartitionKeyRanges": partitionKeyRanges, "PartitionKeyRanges": partitionKeyRanges,
}) })
return return
} }
if status == repositorymodels.StatusNotFound { if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"}) c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return return
} }
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"}) c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+52 -7
View File
@@ -3,32 +3,34 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"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,
"continuousBackupEnabled": false,
"enableNRegionSynchronousCommit": false,
"userReplicationPolicy": map[string]interface{}{ "userReplicationPolicy": map[string]interface{}{
"asyncReplication": false, "asyncReplication": false,
"minReplicaSetSize": 1, "minReplicaSetSize": 1,
@@ -40,3 +42,46 @@ func GetServerInfo(c *gin.Context) {
"queryEngineConfiguration": "{\"allowNewKeywords\":true,\"maxJoinsPerSqlQuery\":10,\"maxQueryRequestTimeoutFraction\":0.9,\"maxSqlQueryInputLength\":524288,\"maxUdfRefPerSqlQuery\":10,\"queryMaxInMemorySortDocumentCount\":-1000,\"spatialMaxGeometryPointCount\":256,\"sqlAllowNonFiniteNumbers\":false,\"sqlDisableOptimizationFlags\":0,\"enableSpatialIndexing\":true,\"maxInExpressionItemsCount\":2147483647,\"maxLogicalAndPerSqlQuery\":2147483647,\"maxLogicalOrPerSqlQuery\":2147483647,\"maxSpatialQueryCells\":2147483647,\"sqlAllowAggregateFunctions\":true,\"sqlAllowGroupByClause\":true,\"sqlAllowLike\":true,\"sqlAllowSubQuery\":true,\"sqlAllowScalarSubQuery\":true,\"sqlAllowTop\":true}", "queryEngineConfiguration": "{\"allowNewKeywords\":true,\"maxJoinsPerSqlQuery\":10,\"maxQueryRequestTimeoutFraction\":0.9,\"maxSqlQueryInputLength\":524288,\"maxUdfRefPerSqlQuery\":10,\"queryMaxInMemorySortDocumentCount\":-1000,\"spatialMaxGeometryPointCount\":256,\"sqlAllowNonFiniteNumbers\":false,\"sqlDisableOptimizationFlags\":0,\"enableSpatialIndexing\":true,\"maxInExpressionItemsCount\":2147483647,\"maxLogicalAndPerSqlQuery\":2147483647,\"maxLogicalOrPerSqlQuery\":2147483647,\"maxSpatialQueryCells\":2147483647,\"sqlAllowAggregateFunctions\":true,\"sqlAllowGroupByClause\":true,\"sqlAllowLike\":true,\"sqlAllowSubQuery\":true,\"sqlAllowScalarSubQuery\":true,\"sqlAllowTop\":true}",
}) })
} }
type Address struct {
IsPrimary bool `json:"isPrimary"`
PhyscialUri string `json:"physcialUri"`
IsAuxiliary bool `json:"isAuxiliary"`
PartitionTargetReplicaSetSize int `json:"partitionTargetReplicaSetSize"`
Protocol string `json:"protocol"`
PartitionKeyRangeId string `json:"partitionKeyRangeId"`
PartitionIndex string `json:"partitionIndex"`
}
func (h *Handlers) GetAddresses(c *gin.Context) {
addresses := []Address{}
if h.config.EnableRntbd {
addresses = append(addresses, Address{
IsPrimary: true,
PhyscialUri: h.config.RntbdEndpoint,
IsAuxiliary: false,
PartitionTargetReplicaSetSize: 1,
Protocol: "rntbd",
PartitionKeyRangeId: "0",
PartitionIndex: "0@0",
})
}
if !strings.Contains(c.Request.RequestURI, "protocol%20eq%20rntbd") {
addresses = append(addresses, Address{
IsPrimary: true,
PhyscialUri: h.config.DatabaseEndpoint,
IsAuxiliary: false,
PartitionTargetReplicaSetSize: 1,
Protocol: "https",
PartitionKeyRangeId: "0",
PartitionIndex: "0@0",
})
}
c.IndentedJSON(http.StatusOK, gin.H{
"Addresss": addresses,
"_count": len(addresses),
})
}
+102 -7
View File
@@ -5,21 +5,116 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
) )
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.dataStore.GetAllStoredProcedures(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(sps))) c.Header(headers.ItemCount, 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, constants.UnknownErrorResponse)
}
func (h *Handlers) GetStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
sp, status := h.dataStore.GetStoredProcedure(databaseId, collectionId, spId)
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, sp)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
status := h.dataStore.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
spId := c.Param("spId")
var sp datastore.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.dataStore.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdSP, status := h.dataStore.CreateStoredProcedure(databaseId, collectionId, sp)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var sp datastore.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdSP, status := h.dataStore.CreateStoredProcedure(databaseId, collectionId, sp)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+102 -7
View File
@@ -5,21 +5,116 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
) )
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.dataStore.GetAllTriggers(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(triggers))) c.Header(headers.ItemCount, 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, constants.UnknownErrorResponse)
}
func (h *Handlers) GetTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
trigger, status := h.dataStore.GetTrigger(databaseId, collectionId, triggerId)
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, trigger)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
status := h.dataStore.DeleteTrigger(databaseId, collectionId, triggerId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
var trigger datastore.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.dataStore.DeleteTrigger(databaseId, collectionId, triggerId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdTrigger, status := h.dataStore.CreateTrigger(databaseId, collectionId, trigger)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var trigger datastore.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdTrigger, status := h.dataStore.CreateTrigger(databaseId, collectionId, trigger)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+102 -7
View File
@@ -5,21 +5,116 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pikami/cosmium/internal/repositories" "github.com/pikami/cosmium/api/headers"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
) )
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.dataStore.GetAllUserDefinedFunctions(databaseId, collectionId)
if status == repositorymodels.StatusOk { if status == datastore.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(udfs))) c.Header(headers.ItemCount, 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, constants.UnknownErrorResponse)
}
func (h *Handlers) GetUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
udf, status := h.dataStore.GetUserDefinedFunction(databaseId, collectionId, udfId)
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, udf)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
status := h.dataStore.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfId := c.Param("udfId")
var udf datastore.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.dataStore.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdUdf, status := h.dataStore.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var udf datastore.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdUdf, status := h.dataStore.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
} }
+30
View File
@@ -0,0 +1,30 @@
package headers
const (
AIM = "A-Im"
Authorization = "authorization"
CosmosLsn = "x-ms-cosmos-llsn"
ErrorCode = "x-ms-error-code"
ETag = "etag"
GlobalCommittedLsn = "x-ms-global-committed-lsn"
IfMatch = "if-match"
IfNoneMatch = "if-none-match"
IsBatchRequest = "x-ms-cosmos-is-batch-request"
IsQueryPlanRequest = "x-ms-cosmos-is-query-plan-request"
IsUpsert = "x-ms-documentdb-is-upsert"
ItemCount = "x-ms-item-count"
LSN = "lsn"
XDate = "x-ms-date"
MaxItemCount = "x-ms-max-item-count"
ContinuationToken = "x-ms-continuation"
// Kinda retarded, but what can I do ¯\_(ツ)_/¯
IsQuery = "x-ms-documentdb-isquery" // Sent from python sdk and web explorer
Query = "x-ms-documentdb-query" // Sent from Go sdk
// I kinda don't use these, but I've seen them in the wild xd
SupportedCapabilities = "x-ms-cosmos-sdk-supportedcapabilities"
ClientRetryAttemptCount = "x-ms-client-retry-attempt-count"
RemainingTimeInMsOnClient = "x-ms-remaining-time-in-ms-on-client"
ConsistencyLevel = "x-ms-consistency-level"
)
+105 -57
View File
@@ -1,98 +1,146 @@
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/datastore"
"github.com/pikami/cosmium/internal/logger" "github.com/pikami/cosmium/internal/logger"
tlsprovider "github.com/pikami/cosmium/internal/tls_provider" tlsprovider "github.com/pikami/cosmium/internal/tls_provider"
) )
func CreateRouter() *gin.Engine { var ginMux sync.Mutex
func (s *ApiServer) CreateRouter(dataStore datastore.DataStore) {
routeHandlers := handlers.NewHandlers(dataStore, 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) { router := gin.Default(func(e *gin.Engine) {
e.RedirectTrailingSlash = false e.RedirectTrailingSlash = false
e.RemoveExtraSlash = true
}) })
if config.Config.Debug { if s.config.LogLevel == "debug" {
router.Use(middleware.RequestLogger()) router.Use(middleware.RequestLogger())
} }
router.Use(middleware.StripTrailingSlashes(router)) router.Use(middleware.StripTrailingSlashes(router, s.config))
router.Use(middleware.Authentication()) 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.PATCH("/dbs/:databaseId/colls/:collId/docs/:docId", handlers.PatchDocument) router.PATCH("/dbs/:databaseId/colls/:collId/docs/:docId", routeHandlers.PatchDocument)
router.DELETE("/dbs/:databaseId/colls/:collId/docs/:docId", handlers.DeleteDocument) 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("//addresses", routeHandlers.GetAddresses)
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()
} }
+36 -32
View File
@@ -2,27 +2,24 @@ package tests_test
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"testing" "testing"
"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.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey), formatConnectionString(ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
) )
assert.Nil(t, err) assert.Nil(t, err)
@@ -35,28 +32,10 @@ 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.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString( client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"), formatConnectionString(ts.URL, "AAAA"),
&azcosmos.ClientOptions{}, &azcosmos.ClientOptions{},
) )
assert.Nil(t, err) assert.Nil(t, err)
@@ -66,12 +45,7 @@ func Test_Authentication(t *testing.T) {
azcosmos.DatabaseProperties{ID: testDatabaseName}, azcosmos.DatabaseProperties{ID: testDatabaseName},
&azcosmos.CreateDatabaseOptions{}) &azcosmos.CreateDatabaseOptions{})
var respErr *azcore.ResponseError assert.Contains(t, err.Error(), "401 Unauthorized")
if errors.As(err, &respErr) {
assert.Equal(t, respErr.StatusCode, http.StatusUnauthorized)
} else {
panic(err)
}
}) })
t.Run("Should allow unauthorized requests to /_explorer", func(t *testing.T) { t.Run("Should allow unauthorized requests to /_explorer", func(t *testing.T) {
@@ -85,3 +59,33 @@ 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.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
formatConnectionString(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)
})
}
func formatConnectionString(endpoint, key string) string {
return fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", endpoint, key)
}
+43 -22
View File
@@ -3,33 +3,29 @@ package tests_test
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"testing" "testing"
"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/internal/datastore"
"github.com/pikami/cosmium/internal/repositories"
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() presets := []testPreset{PresetJsonStore, PresetBadgerStore}
defer ts.Close()
client, err := azcosmos.NewClientFromConnectionString( setUp := func(ts *TestServer, client *azcosmos.Client) *azcosmos.DatabaseClient {
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.Config.AccountKey), ts.DataStore.CreateDatabase(datastore.Database{ID: testDatabaseName})
&azcosmos.ClientOptions{}, databaseClient, err := client.NewDatabase(testDatabaseName)
) assert.Nil(t, err)
assert.Nil(t, err)
repositories.CreateDatabase(repositorymodels.Database{ID: testDatabaseName}) return databaseClient
databaseClient, err := client.NewDatabase(testDatabaseName) }
assert.Nil(t, err)
runTestsWithPresets(t, "Collection Create", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
databaseClient := setUp(ts, client)
t.Run("Collection Create", func(t *testing.T) {
t.Run("Should create collection", func(t *testing.T) { t.Run("Should create collection", func(t *testing.T) {
createResponse, err := databaseClient.CreateContainer(context.TODO(), azcosmos.ContainerProperties{ createResponse, err := databaseClient.CreateContainer(context.TODO(), azcosmos.ContainerProperties{
ID: testCollectionName, ID: testCollectionName,
@@ -40,7 +36,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.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -58,9 +54,11 @@ func Test_Collections(t *testing.T) {
}) })
}) })
t.Run("Collection Read", func(t *testing.T) { runTestsWithPresets(t, "Collection Read", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
databaseClient := setUp(ts, client)
t.Run("Should read collection", func(t *testing.T) { t.Run("Should read collection", func(t *testing.T) {
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -74,7 +72,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.DataStore.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName) collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -91,9 +89,11 @@ func Test_Collections(t *testing.T) {
}) })
}) })
t.Run("Collection Delete", func(t *testing.T) { runTestsWithPresets(t, "Collection Delete", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
databaseClient := setUp(ts, client)
t.Run("Should delete collection", func(t *testing.T) { t.Run("Should delete collection", func(t *testing.T) {
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName, ID: testCollectionName,
}) })
@@ -106,7 +106,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.DataStore.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName) collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -121,5 +121,26 @@ func Test_Collections(t *testing.T) {
panic(err) panic(err)
} }
}) })
t.Run("Should delete collection with exactly matching name", func(t *testing.T) {
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName + "extra",
})
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName,
})
collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err)
readResponse, err := collectionResponse.Delete(context.TODO(), &azcosmos.DeleteContainerOptions{})
assert.Nil(t, err)
assert.Equal(t, readResponse.RawResponse.StatusCode, http.StatusNoContent)
collections, status := ts.DataStore.GetAllCollections(testDatabaseName)
assert.Equal(t, status, datastore.StatusOk)
assert.Len(t, collections, 1)
assert.Equal(t, collections[0].ID, testCollectionName+"extra")
})
}) })
} }
+95 -5
View File
@@ -1,18 +1,64 @@
package tests_test package tests_test
import ( import (
"fmt"
"net/http/httptest" "net/http/httptest"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"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/datastore"
badgerdatastore "github.com/pikami/cosmium/internal/datastore/badger_datastore"
jsondatastore "github.com/pikami/cosmium/internal/datastore/json_datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/stretchr/testify/assert"
) )
func runTestServer() *httptest.Server { type TestServer struct {
config.Config.AccountKey = config.DefaultAccountKey Server *httptest.Server
config.Config.ExplorerPath = "/tmp/nothing" DataStore datastore.DataStore
config.Config.ExplorerBaseUrlLocation = config.ExplorerBaseUrlLocation URL string
}
return httptest.NewServer(api.CreateRouter()) func getDefaultTestServerConfig() *config.ServerConfig {
return &config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
DataStore: "json",
}
}
func runTestServerCustomConfig(configuration *config.ServerConfig) *TestServer {
var dataStore datastore.DataStore
switch configuration.DataStore {
case config.DataStoreBadger:
dataStore = badgerdatastore.NewBadgerDataStore(badgerdatastore.BadgerDataStoreOptions{})
default:
dataStore = jsondatastore.NewJsonDataStore(jsondatastore.JsonDataStoreOptions{})
}
api := api.NewApiServer(dataStore, configuration)
server := httptest.NewServer(api.GetRouter())
configuration.DatabaseEndpoint = server.URL
return &TestServer{
Server: server,
DataStore: dataStore,
URL: server.URL,
}
}
func runTestServer() *TestServer {
config := getDefaultTestServerConfig()
config.LogLevel = "debug"
logger.SetLogLevel(logger.LogLevelDebug)
return runTestServerCustomConfig(config)
} }
const ( const (
@@ -20,3 +66,47 @@ const (
testDatabaseName = "test-db" testDatabaseName = "test-db"
testCollectionName = "test-coll" testCollectionName = "test-coll"
) )
type testFunc func(t *testing.T, ts *TestServer, cosmosClient *azcosmos.Client)
type testPreset string
const (
PresetJsonStore testPreset = "JsonDS"
PresetBadgerStore testPreset = "BadgerDS"
)
func runTestsWithPreset(t *testing.T, name string, testPreset testPreset, f testFunc) {
serverConfig := getDefaultTestServerConfig()
serverConfig.LogLevel = "debug"
logger.SetLogLevel(logger.LogLevelDebug)
switch testPreset {
case PresetBadgerStore:
serverConfig.DataStore = config.DataStoreBadger
case PresetJsonStore:
serverConfig.DataStore = config.DataStoreJson
}
ts := runTestServerCustomConfig(serverConfig)
defer ts.Server.Close()
defer ts.DataStore.Close()
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
testName := fmt.Sprintf("%s_%s", testPreset, name)
t.Run(testName, func(t *testing.T) {
f(t, ts, client)
})
}
func runTestsWithPresets(t *testing.T, name string, testPresets []testPreset, f testFunc) {
for _, testPreset := range testPresets {
runTestsWithPreset(t, name, testPreset, f)
}
}
+32 -21
View File
@@ -3,31 +3,21 @@ package tests_test
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/http" "net/http"
"testing" "testing"
"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/internal/datastore"
"github.com/pikami/cosmium/internal/repositories"
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() presets := []testPreset{PresetJsonStore, PresetBadgerStore}
defer ts.Close()
client, err := azcosmos.NewClientFromConnectionString( runTestsWithPresets(t, "Database Create", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.Config.AccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
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.DataStore.DeleteDatabase(testDatabaseName)
createResponse, err := client.CreateDatabase(context.TODO(), azcosmos.DatabaseProperties{ createResponse, err := client.CreateDatabase(context.TODO(), azcosmos.DatabaseProperties{
ID: testDatabaseName, ID: testDatabaseName,
@@ -38,7 +28,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.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -56,9 +46,9 @@ func Test_Databases(t *testing.T) {
}) })
}) })
t.Run("Database Read", func(t *testing.T) { runTestsWithPresets(t, "Database Read", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
t.Run("Should read database", func(t *testing.T) { t.Run("Should read database", func(t *testing.T) {
repositories.CreateDatabase(repositorymodels.Database{ ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -72,7 +62,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.DataStore.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName) databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -89,9 +79,9 @@ func Test_Databases(t *testing.T) {
}) })
}) })
t.Run("Database Delete", func(t *testing.T) { runTestsWithPresets(t, "Database Delete", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
t.Run("Should delete database", func(t *testing.T) { t.Run("Should delete database", func(t *testing.T) {
repositories.CreateDatabase(repositorymodels.Database{ ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName, ID: testDatabaseName,
}) })
@@ -104,7 +94,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.DataStore.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName) databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err) assert.Nil(t, err)
@@ -119,5 +109,26 @@ func Test_Databases(t *testing.T) {
panic(err) panic(err)
} }
}) })
t.Run("Should delete database with exactly matching name", func(t *testing.T) {
ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName + "extra",
})
ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName,
})
databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err)
readResponse, err := databaseResponse.Delete(context.TODO(), &azcosmos.DeleteDatabaseOptions{})
assert.Nil(t, err)
assert.Equal(t, readResponse.RawResponse.StatusCode, http.StatusNoContent)
dbs, status := ts.DataStore.GetAllDatabases()
assert.Equal(t, status, datastore.StatusOk)
assert.Len(t, dbs, 1)
assert.Equal(t, dbs[0].ID, testDatabaseName+"extra")
})
}) })
} }
@@ -0,0 +1,47 @@
package tests_test
import (
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
)
func Test_Documents_ArrayContains(t *testing.T) {
presets := []testPreset{PresetJsonStore, PresetBadgerStore}
runTestsWithPresets(t, "Test_Documents_ArrayContains", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
collectionClient := documents_InitializeDb(t, ts)
t.Run("Should execute ARRAY_CONTAINS() without partial match argument", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT VALUE ARRAY_CONTAINS(["apple", "banana", "cherry"], "banana") FROM c ORDER BY c.id`,
nil,
[]interface{}{true, true},
)
})
t.Run("Should execute ARRAY_CONTAINS() returning false for missing item", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT VALUE ARRAY_CONTAINS(["apple", "banana", "cherry"], "grape") FROM c ORDER BY c.id`,
nil,
[]interface{}{false, false},
)
})
t.Run("Should execute ARRAY_CONTAINS() with object full match", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT VALUE ARRAY_CONTAINS([{"name": "apple", "color": "red"}], {"name": "apple"}) FROM c ORDER BY c.id`,
nil,
[]interface{}{false, false},
)
})
t.Run("Should execute ARRAY_CONTAINS() with object partial match", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT VALUE ARRAY_CONTAINS([{"name": "apple", "color": "red"}], {"name": "apple"}, true) FROM c ORDER BY c.id`,
nil,
[]interface{}{true, true},
)
})
})
}
@@ -0,0 +1,73 @@
package tests_test
import (
"fmt"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/datastore"
"github.com/stretchr/testify/assert"
)
func documents_InitializeSingleDocumentDb(t *testing.T, ts *TestServer) *azcosmos.ContainerClient {
ts.DataStore.CreateDatabase(datastore.Database{ID: testDatabaseName})
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName,
PartitionKey: struct {
Paths []string "json:\"paths\""
Kind string "json:\"kind\""
Version int "json:\"Version\""
}{
Paths: []string{"/pk"},
},
})
ts.DataStore.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "regexmatch-test", "pk": "regexmatch-test"})
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
collectionClient, err := client.NewContainer(testDatabaseName, testCollectionName)
assert.Nil(t, err)
return collectionClient
}
func Test_Documents_RegexMatch(t *testing.T) {
presets := []testPreset{PresetJsonStore, PresetBadgerStore}
runTestsWithPresets(t, "Test_Documents_RegexMatch", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
collectionClient := documents_InitializeSingleDocumentDb(t, ts)
t.Run("Should execute REGEXMATCH()", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`SELECT VALUE {
noModifiers: REGEXMATCH("abcd", "ABC"),
caseInsensitive: REGEXMATCH("abcd", "ABC", "i"),
wildcardCharacter: REGEXMATCH("abcd", "ab.", ""),
ignoreWhiteSpace: REGEXMATCH("abcd", "ab c", "x"),
caseInsensitiveAndIgnoreWhiteSpace: REGEXMATCH("abcd", "aB c", "ix"),
containNumberBetweenZeroAndNine: REGEXMATCH("03a", "[0-9]"),
containPrefix: REGEXMATCH("salt3824908", "salt{1}"),
containsFiveLetterWordStartingWithS: REGEXMATCH("shame", "s....", "i")
}`,
nil,
[]interface{}{
map[string]interface{}{
"noModifiers": false,
"caseInsensitive": true,
"wildcardCharacter": true,
"ignoreWhiteSpace": true,
"caseInsensitiveAndIgnoreWhiteSpace": true,
"containNumberBetweenZeroAndNine": true,
"containPrefix": true,
"containsFiveLetterWordStartingWithS": true,
},
},
)
})
})
}
+548 -226
View File
@@ -5,16 +5,18 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/http/httptest"
"reflect" "reflect"
"sync"
"testing" "testing"
"time"
"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" continuationtoken "github.com/pikami/cosmium/internal/continuation_token"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/datastore"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -53,9 +55,9 @@ func testCosmosQuery(t *testing.T,
} }
} }
func documents_InitializeDb(t *testing.T) (*httptest.Server, *azcosmos.ContainerClient) { func documents_InitializeDb(t *testing.T, ts *TestServer) *azcosmos.ContainerClient {
repositories.CreateDatabase(repositorymodels.Database{ID: testDatabaseName}) ts.DataStore.CreateDatabase(datastore.Database{ID: testDatabaseName})
repositories.CreateCollection(testDatabaseName, repositorymodels.Collection{ ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName, ID: testCollectionName,
PartitionKey: struct { PartitionKey: struct {
Paths []string "json:\"paths\"" Paths []string "json:\"paths\""
@@ -65,13 +67,11 @@ func documents_InitializeDb(t *testing.T) (*httptest.Server, *azcosmos.Container
Paths: []string{"/pk"}, Paths: []string{"/pk"},
}, },
}) })
repositories.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "12345", "pk": "123", "isCool": false, "arr": []int{1, 2, 3}}) ts.DataStore.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, "arr": []int{6, 7, 8}}) ts.DataStore.CreateDocument(testDatabaseName, testCollectionName, map[string]interface{}{"id": "67890", "pk": "456", "isCool": true, "arr": []int{6, 7, 8}})
ts := runTestServer()
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)
@@ -79,233 +79,555 @@ func documents_InitializeDb(t *testing.T) (*httptest.Server, *azcosmos.Container
collectionClient, err := client.NewContainer(testDatabaseName, testCollectionName) collectionClient, err := client.NewContainer(testDatabaseName, testCollectionName)
assert.Nil(t, err) assert.Nil(t, err)
return ts, collectionClient return collectionClient
} }
func Test_Documents(t *testing.T) { func Test_Documents(t *testing.T) {
ts, collectionClient := documents_InitializeDb(t) presets := []testPreset{PresetJsonStore, PresetBadgerStore}
defer ts.Close()
t.Run("Should query document", func(t *testing.T) { runTestsWithPresets(t, "Test_Documents", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
testCosmosQuery(t, collectionClient, collectionClient := documents_InitializeDb(t, ts)
"SELECT c.id, c[\"pk\"] FROM c ORDER BY c.id",
nil, t.Run("Should query document", func(t *testing.T) {
[]interface{}{ testCosmosQuery(t, collectionClient,
map[string]interface{}{"id": "12345", "pk": "123"}, "SELECT c.id, c[\"pk\"] FROM c ORDER BY c.id",
map[string]interface{}{"id": "67890", "pk": "456"}, nil,
}, []interface{}{
) map[string]interface{}{"id": "12345", "pk": "123"},
map[string]interface{}{"id": "67890", "pk": "456"},
},
)
})
t.Run("Should query VALUE array", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
"SELECT VALUE [c.id, c[\"pk\"]] FROM c ORDER BY c.id",
nil,
[]interface{}{
[]interface{}{"12345", "123"},
[]interface{}{"67890", "456"},
},
)
})
t.Run("Should query VALUE object", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
"SELECT VALUE { id: c.id, _pk: c.pk } FROM c ORDER BY c.id",
nil,
[]interface{}{
map[string]interface{}{"id": "12345", "_pk": "123"},
map[string]interface{}{"id": "67890", "_pk": "456"},
},
)
})
t.Run("Should query document with single WHERE condition", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c.isCool=true
ORDER BY c.id`,
nil,
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should query document with query parameters", func(t *testing.T) {
testCosmosQuery(t, collectionClient,
`select c.id
FROM c
WHERE c.id=@param_id
ORDER BY c.id`,
[]azcosmos.QueryParameter{
{Name: "@param_id", Value: "67890"},
},
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
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)
}
}
})
}) })
t.Run("Should query VALUE array", func(t *testing.T) { runTestsWithPresets(t, "Test_Documents_Patch", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
testCosmosQuery(t, collectionClient, collectionClient := documents_InitializeDb(t, ts)
"SELECT VALUE [c.id, c[\"pk\"]] FROM c ORDER BY c.id",
nil, t.Run("Should PATCH document", func(t *testing.T) {
[]interface{}{ context := context.TODO()
[]interface{}{"12345", "123"}, expectedData := map[string]interface{}{"id": "67890", "pk": "666", "newField": "newValue", "incr": 15., "setted": "isSet"}
[]interface{}{"67890", "456"},
}, 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)
})
}) })
t.Run("Should query VALUE object", func(t *testing.T) { runTestsWithPresets(t, "Test_Documents_ETag_OptimisticConcurrency", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
testCosmosQuery(t, collectionClient, collectionClient := documents_InitializeDb(t, ts)
"SELECT VALUE { id: c.id, _pk: c.pk } FROM c ORDER BY c.id",
nil, t.Run("Should fail replace with incorrect etag", func(t *testing.T) {
[]interface{}{ context := context.TODO()
map[string]interface{}{"id": "12345", "_pk": "123"},
map[string]interface{}{"id": "67890", "_pk": "456"}, item := map[string]interface{}{"id": "12345", "pk": "123", "isCool": true}
}, bytes, err := json.Marshal(item)
) assert.Nil(t, err)
wrongETag := azcore.ETag("\"incorrect-etag\"")
_, err = collectionClient.ReplaceItem(
context,
azcosmos.PartitionKey{},
"12345",
bytes,
&azcosmos.ItemOptions{IfMatchEtag: &wrongETag},
)
assert.NotNil(t, err)
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
assert.Equal(t, http.StatusPreconditionFailed, respErr.StatusCode)
assert.Equal(t, "PreconditionFailed", respErr.RawResponse.Header.Get("x-ms-error-code"))
responseBody, readErr := io.ReadAll(respErr.RawResponse.Body)
assert.Nil(t, readErr)
assert.JSONEq(t,
`{"code":"PreconditionFailed","message":"Operation cannot be performed because one of the specified precondition is not met."}`,
string(responseBody),
)
} else {
panic(err)
}
document, status := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, "12345")
assert.Equal(t, datastore.StatusOk, status)
assert.Equal(t, false, document["isCool"])
})
t.Run("Should replace with correct etag", func(t *testing.T) {
context := context.TODO()
readResponse, err := collectionClient.ReadItem(context, azcosmos.PartitionKey{}, "12345", nil)
assert.Nil(t, err)
assert.NotEmpty(t, readResponse.ETag)
var item map[string]interface{}
err = json.Unmarshal(readResponse.Value, &item)
assert.Nil(t, err)
assert.Equal(t, string(readResponse.ETag), item["_etag"])
item["pk"] = "999"
item["isCool"] = true
bytes, err := json.Marshal(item)
assert.Nil(t, err)
etag := readResponse.ETag
_, err = collectionClient.ReplaceItem(
context,
azcosmos.PartitionKey{},
"12345",
bytes,
&azcosmos.ItemOptions{IfMatchEtag: &etag},
)
assert.Nil(t, err)
document, status := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, "12345")
assert.Equal(t, datastore.StatusOk, status)
assert.Equal(t, "999", document["pk"])
assert.Equal(t, true, document["isCool"])
})
}) })
t.Run("Should query document with single WHERE condition", func(t *testing.T) { runTestsWithPresets(t, "Test_Documents_TransactionalBatch", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
testCosmosQuery(t, collectionClient, collectionClient := documents_InitializeDb(t, ts)
`select c.id
FROM c t.Run("Should execute CREATE transactional batch", func(t *testing.T) {
WHERE c.isCool=true context := context.TODO()
ORDER BY c.id`, batch := collectionClient.NewTransactionalBatch(azcosmos.NewPartitionKeyString("pk"))
nil,
[]interface{}{ newItem := map[string]interface{}{
map[string]interface{}{"id": "67890"}, "id": "678901",
}, }
) bytes, err := json.Marshal(newItem)
assert.Nil(t, err)
batch.CreateItem(bytes, nil)
response, err := collectionClient.ExecuteTransactionalBatch(context, batch, &azcosmos.TransactionalBatchOptions{})
assert.Nil(t, err)
assert.True(t, response.Success)
assert.Equal(t, 1, len(response.OperationResults))
operationResponse := response.OperationResults[0]
assert.NotNil(t, operationResponse)
assert.NotNil(t, operationResponse.ResourceBody)
assert.Equal(t, int32(http.StatusCreated), operationResponse.StatusCode)
var itemResponseBody map[string]interface{}
json.Unmarshal(operationResponse.ResourceBody, &itemResponseBody)
assert.Equal(t, newItem["id"], itemResponseBody["id"])
createdDoc, _ := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, newItem["id"].(string))
assert.Equal(t, newItem["id"], createdDoc["id"])
})
t.Run("Should execute DELETE transactional batch", func(t *testing.T) {
context := context.TODO()
batch := collectionClient.NewTransactionalBatch(azcosmos.NewPartitionKeyString("pk"))
batch.DeleteItem("12345", nil)
response, err := collectionClient.ExecuteTransactionalBatch(context, batch, &azcosmos.TransactionalBatchOptions{})
assert.Nil(t, err)
assert.True(t, response.Success)
assert.Equal(t, 1, len(response.OperationResults))
operationResponse := response.OperationResults[0]
assert.NotNil(t, operationResponse)
assert.Equal(t, int32(http.StatusNoContent), operationResponse.StatusCode)
_, status := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, "12345")
assert.Equal(t, datastore.StatusNotFound, status)
})
t.Run("Should execute REPLACE transactional batch", func(t *testing.T) {
context := context.TODO()
batch := collectionClient.NewTransactionalBatch(azcosmos.NewPartitionKeyString("pk"))
newItem := map[string]interface{}{
"id": "67890",
"pk": "666",
}
bytes, err := json.Marshal(newItem)
assert.Nil(t, err)
batch.ReplaceItem("67890", bytes, nil)
response, err := collectionClient.ExecuteTransactionalBatch(context, batch, &azcosmos.TransactionalBatchOptions{})
assert.Nil(t, err)
assert.True(t, response.Success)
assert.Equal(t, 1, len(response.OperationResults))
operationResponse := response.OperationResults[0]
assert.NotNil(t, operationResponse)
assert.NotNil(t, operationResponse.ResourceBody)
assert.Equal(t, int32(http.StatusCreated), operationResponse.StatusCode)
var itemResponseBody map[string]interface{}
json.Unmarshal(operationResponse.ResourceBody, &itemResponseBody)
assert.Equal(t, newItem["id"], itemResponseBody["id"])
assert.Equal(t, newItem["pk"], itemResponseBody["pk"])
updatedDoc, _ := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, newItem["id"].(string))
assert.Equal(t, newItem["id"], updatedDoc["id"])
assert.Equal(t, newItem["pk"], updatedDoc["pk"])
})
t.Run("Should execute UPSERT transactional batch", func(t *testing.T) {
context := context.TODO()
batch := collectionClient.NewTransactionalBatch(azcosmos.NewPartitionKeyString("pk"))
newItem := map[string]interface{}{
"id": "678901",
"pk": "666",
}
bytes, err := json.Marshal(newItem)
assert.Nil(t, err)
batch.UpsertItem(bytes, nil)
response, err := collectionClient.ExecuteTransactionalBatch(context, batch, &azcosmos.TransactionalBatchOptions{})
assert.Nil(t, err)
assert.True(t, response.Success)
assert.Equal(t, 1, len(response.OperationResults))
operationResponse := response.OperationResults[0]
assert.NotNil(t, operationResponse)
assert.NotNil(t, operationResponse.ResourceBody)
assert.Equal(t, int32(http.StatusCreated), operationResponse.StatusCode)
var itemResponseBody map[string]interface{}
json.Unmarshal(operationResponse.ResourceBody, &itemResponseBody)
assert.Equal(t, newItem["id"], itemResponseBody["id"])
assert.Equal(t, newItem["pk"], itemResponseBody["pk"])
updatedDoc, _ := ts.DataStore.GetDocument(testDatabaseName, testCollectionName, newItem["id"].(string))
assert.Equal(t, newItem["id"], updatedDoc["id"])
assert.Equal(t, newItem["pk"], updatedDoc["pk"])
})
t.Run("Should execute READ transactional batch", func(t *testing.T) {
context := context.TODO()
batch := collectionClient.NewTransactionalBatch(azcosmos.NewPartitionKeyString("pk"))
batch.ReadItem("67890", nil)
response, err := collectionClient.ExecuteTransactionalBatch(context, batch, &azcosmos.TransactionalBatchOptions{})
assert.Nil(t, err)
assert.True(t, response.Success)
assert.Equal(t, 1, len(response.OperationResults))
operationResponse := response.OperationResults[0]
assert.NotNil(t, operationResponse)
assert.NotNil(t, operationResponse.ResourceBody)
assert.Equal(t, int32(http.StatusOK), operationResponse.StatusCode)
var itemResponseBody map[string]interface{}
json.Unmarshal(operationResponse.ResourceBody, &itemResponseBody)
assert.Equal(t, "67890", itemResponseBody["id"])
})
}) })
t.Run("Should query document with query parameters", func(t *testing.T) { runTestsWithPresets(t, "Test_Documents_With_Continuation_Token", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
testCosmosQuery(t, collectionClient, collectionClient := documents_InitializeDb(t, ts)
`select c.id
FROM c
WHERE c.id=@param_id
ORDER BY c.id`,
[]azcosmos.QueryParameter{
{Name: "@param_id", Value: "67890"},
},
[]interface{}{
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should query array accessor", func(t *testing.T) { t.Run("Should query document with continuation token", func(t *testing.T) {
testCosmosQuery(t, collectionClient, context := context.TODO()
`SELECT c.id, pager := collectionClient.NewQueryItemsPager(
c["arr"][0] AS arr0, "SELECT c.id, c[\"pk\"] FROM c ORDER BY c.id",
c["arr"][1] AS arr1, azcosmos.PartitionKey{},
c["arr"][2] AS arr2, &azcosmos.QueryOptions{
c["arr"][3] AS arr3 PageSizeHint: 1,
FROM c ORDER BY c.id`, })
nil,
[]interface{}{ assert.True(t, pager.More())
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}, firstResponse, err := pager.NextPage(context)
}, assert.Nil(t, err)
) assert.Equal(t, 1, len(firstResponse.Items))
var firstItem map[string]interface{}
err = json.Unmarshal(firstResponse.Items[0], &firstItem)
assert.Nil(t, err)
assert.Equal(t, "12345", firstItem["id"])
assert.Equal(t, "123", firstItem["pk"])
firstContinuationToken := continuationtoken.FromString(*firstResponse.ContinuationToken)
assert.Equal(t, 1, firstContinuationToken.Token.PageIndex)
assert.Equal(t, 1, firstContinuationToken.Token.TotalResults)
assert.True(t, pager.More())
secondResponse, err := pager.NextPage(context)
assert.Nil(t, err)
assert.Equal(t, 1, len(secondResponse.Items))
var secondItem map[string]interface{}
err = json.Unmarshal(secondResponse.Items[0], &secondItem)
assert.Nil(t, err)
assert.Equal(t, "67890", secondItem["id"])
assert.Equal(t, "456", secondItem["pk"])
assert.Nil(t, secondResponse.ContinuationToken)
assert.False(t, pager.More())
})
}) })
} }
func Test_Documents_Patch(t *testing.T) {
ts, collectionClient := documents_InitializeDb(t)
defer ts.Close()
t.Run("Should PATCH document", func(t *testing.T) {
context := context.TODO()
expectedData := map[string]interface{}{"id": "67890", "pk": "456", "newField": "newValue"}
patch := azcosmos.PatchOperations{}
patch.AppendAdd("/newField", "newValue")
patch.AppendRemove("/isCool")
itemResponse, err := collectionClient.PatchItem(
context,
azcosmos.PartitionKey{},
"67890",
patch,
&azcosmos.ItemOptions{
EnableContentResponseOnWrite: false,
},
)
assert.Nil(t, err)
var itemResponseBody map[string]string
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"])
})
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)
})
}
+7 -5
View File
@@ -8,25 +8,27 @@ import (
"time" "time"
"github.com/pikami/cosmium/api/config" "github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/authentication" "github.com/pikami/cosmium/internal/authentication"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// Request document with trailing slash like python cosmosdb client does. // Request document with trailing slash like python cosmosdb client does.
func Test_Documents_Read_Trailing_Slash(t *testing.T) { func Test_Documents_Read_Trailing_Slash(t *testing.T) {
ts, _ := documents_InitializeDb(t) ts := runTestServer()
defer ts.Close() documents_InitializeDb(t, ts)
defer ts.Server.Close()
t.Run("Read doc with client that appends slash to path", func(t *testing.T) { t.Run("Read doc with client that appends slash to path", func(t *testing.T) {
resourceIdTemplate := "dbs/%s/colls/%s/docs/%s" resourceIdTemplate := "dbs/%s/colls/%s/docs/%s"
path := fmt.Sprintf(resourceIdTemplate, testDatabaseName, testCollectionName, "12345") path := fmt.Sprintf(resourceIdTemplate, testDatabaseName, testCollectionName, "12345")
testUrl := ts.URL + "/" + path + "/" testUrl := ts.URL + "/" + path + "/"
date := time.Now().Format(time.RFC1123) date := time.Now().Format(time.RFC1123)
signature := authentication.GenerateSignature("GET", "docs", path, date, config.Config.AccountKey) signature := authentication.GenerateSignature("GET", "docs", path, date, config.DefaultAccountKey)
httpClient := &http.Client{} httpClient := &http.Client{}
req, _ := http.NewRequest("GET", testUrl, nil) req, _ := http.NewRequest("GET", testUrl, nil)
req.Header.Add("x-ms-date", date) req.Header.Add(headers.XDate, date)
req.Header.Add("authorization", "sig="+url.QueryEscape(signature)) req.Header.Add(headers.Authorization, "sig="+url.QueryEscape(signature))
res, err := httpClient.Do(req) res, err := httpClient.Do(req)
assert.Nil(t, err) assert.Nil(t, err)
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"github.com/pikami/cosmium/internal/rntbd"
)
func main() {
input := flag.String("input", "", "Input hex string")
isResponse := flag.Bool("response", false, "Is response")
flag.Parse()
data, err := hex.DecodeString(*input)
if err != nil {
fmt.Printf("Error decoding hex string: %v\n", err)
return
}
frame, err := rntbd.ParseFrame(data, *isResponse)
if err != nil {
fmt.Printf("Error parsing frame: %v\n", err)
return
}
fmt.Printf("Activity ID: %s\n", hex.EncodeToString(frame.ActivityId))
fmt.Printf("Resource Type: %s\n", frame.ResourceType.String())
fmt.Printf("Operation Type: %s\n", frame.OperationType.String())
if len(frame.RequestHeaders) > 0 {
fmt.Printf("=== Request Headers ===\n")
for header, value := range frame.RequestHeaders {
fmt.Printf("%s: %v\n", header.String(), value)
}
}
if len(frame.ResponseHeaders) > 0 {
fmt.Printf("=== Response Headers ===\n")
for header, value := range frame.ResponseHeaders {
fmt.Printf("%s: %v\n", header.String(), value)
}
}
if len(frame.ContextHeaders) > 0 {
fmt.Printf("=== Context Headers ===\n")
for header, value := range frame.ContextHeaders {
fmt.Printf("%s: %v\n", header.String(), value)
}
}
if len(frame.Payload) > 0 {
var jsonObj any
err := json.Unmarshal(frame.Payload, &jsonObj)
if err != nil {
fmt.Printf("Payload: %s\n", hex.EncodeToString(frame.Payload))
} else {
fmt.Printf("Payload: %+v\n", jsonObj)
}
}
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"os"
"os/signal"
"syscall"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/datastore"
badgerdatastore "github.com/pikami/cosmium/internal/datastore/badger_datastore"
jsondatastore "github.com/pikami/cosmium/internal/datastore/json_datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/rntbd"
)
func main() {
configuration := config.ParseFlags()
var dataStore datastore.DataStore
switch configuration.DataStore {
case config.DataStoreBadger:
dataStore = badgerdatastore.NewBadgerDataStore(badgerdatastore.BadgerDataStoreOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
PersistDataFilePath: configuration.PersistDataFilePath,
})
logger.InfoLn("Using Badger data store")
default:
dataStore = jsondatastore.NewJsonDataStore(jsondatastore.JsonDataStoreOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
PersistDataFilePath: configuration.PersistDataFilePath,
})
logger.InfoLn("Using in-memory data store")
}
server := api.NewApiServer(dataStore, &configuration)
err := server.Start()
if err != nil {
panic(err)
}
if configuration.EnableRntbd {
rntbdServer := rntbd.NewRntbdServer(configuration.RntbdPort, server)
err = rntbdServer.Start()
if err != nil {
panic(err)
}
defer rntbdServer.Stop()
}
waitForExit(server, dataStore)
}
func waitForExit(server *api.ApiServer, dataStore datastore.DataStore) {
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()
dataStore.Close()
}
+28 -13
View File
@@ -18,8 +18,8 @@ Cosmium strives to support the core features of Cosmos DB, including:
| 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 |
@@ -62,22 +62,24 @@ Cosmium strives to support the core features of Cosmos DB, including:
### Array Functions ### Array Functions
| Function | Implemented | | Function | Implemented |
| -------------- | ----------- | | ------------------ | ----------- |
| ARRAY_CONCAT | Yes | | ARRAY_CONCAT | Yes |
| ARRAY_CONTAINS | No | | ARRAY_CONTAINS | Yes |
| ARRAY_LENGTH | Yes | | ARRAY_CONTAINS_ANY | Yes |
| ARRAY_SLICE | Yes | | ARRAY_CONTAINS_ALL | Yes |
| CHOOSE | No | | ARRAY_LENGTH | Yes |
| ObjectToArray | No | | ARRAY_SLICE | Yes |
| SetIntersect | Yes | | CHOOSE | No |
| SetUnion | Yes | | ObjectToArray | No |
| SetIntersect | Yes |
| SetUnion | Yes |
### Conditional Functions ### Conditional Functions
| Function | Implemented | | Function | Implemented |
| -------- | ----------- | | -------- | ----------- |
| IIF | No | | IIF | Yes |
### Date and time Functions ### Date and time Functions
@@ -202,6 +204,19 @@ Cosmium strives to support the core features of Cosmos DB, including:
| IS_PRIMITIVE | Yes | | IS_PRIMITIVE | Yes |
| IS_STRING | Yes | | IS_STRING | Yes |
### Transactional batch operations
Note: There's actually no transaction here. Think of this as a 'bulk operation' that can partially succeed.
| Operation | Implemented |
| --------- | ----------- |
| Create | Yes |
| Delete | Yes |
| Replace | Yes |
| Upsert | Yes |
| Read | Yes |
| Patch | 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:
+44 -27
View File
@@ -1,47 +1,64 @@
module github.com/pikami/cosmium module github.com/pikami/cosmium
go 1.21.6 go 1.26.3
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.2
github.com/evanphx/json-patch/v5 v5.9.0 github.com/cosmiumdev/json-patch/v5 v5.9.11
github.com/gin-gonic/gin v1.10.0 github.com/dgraph-io/badger/v4 v4.9.2
github.com/gin-gonic/gin v1.12.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/exp v0.0.0-20260603202125-055de637280b
) )
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.12.0 // indirect
github.com/bytedance/sonic v1.11.8 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic v1.15.2 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.7 // 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.4 // indirect github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // 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.21.0 // indirect github.com/go-playground/validator/v10 v10.30.3 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/flatbuffers v25.12.19+incompatible // 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.7 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.22 // 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.2.2 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // 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/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.60.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.12 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
golang.org/x/arch v0.8.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.23.0 // indirect go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
golang.org/x/net v0.25.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
golang.org/x/sys v0.20.0 // indirect go.opentelemetry.io/otel v1.44.0 // indirect
golang.org/x/text v0.15.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect
golang.org/x/arch v0.28.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+112 -72
View File
@@ -1,113 +1,153 @@
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.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
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.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.0.0 h1:Yoicul8bnVdQrhDMTHxdEckRGX01XvwXDHUT9zYZ3k0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
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.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
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 v1.4.2 h1:zqxnp53f5Jn5PFU5Av4mvyWEbZ7whg72AoOCEzlXFKc=
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 v1.4.2/go.mod h1:Krtog/7tz27z75TwM5cIS8bxEH4dcBUezcq+kGVeZEo=
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.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
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.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
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.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/bytedance/sonic v1.11.8 h1:Zw/j1KfiS+OYTi9lyB3bb0CFxPJVkM17k1wyDG32LRA= github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/sonic v1.11.8/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
github.com/cosmiumdev/json-patch/v5 v5.9.11 h1:WD2Wqaz/vO987z2FFdqgkj15HgYZ/Y5TpqE3I4T/iOQ=
github.com/cosmiumdev/json-patch/v5 v5.9.11/go.mod h1:YPZmckmv4ZY+oxKIOjgq3sIudHVB6VEMcicCS9LtVLM=
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/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/dgraph-io/badger/v4 v4.9.2 h1:Wb5qw8gElqwV1a8msHTeQKova9b1V10heFKMIiPd80E=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/dgraph-io/badger/v4 v4.9.2/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/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/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
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.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/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.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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg= go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+9
View File
@@ -30,3 +30,12 @@ var QueryPlanResponse = gin.H{
}, },
}, },
} }
var UnknownErrorResponse = gin.H{"message": "Unknown error"}
var NotFoundResponse = gin.H{"message": "NotFound"}
var ConflictResponse = gin.H{"message": "Conflict"}
var BadRequestResponse = gin.H{"message": "BadRequest"}
var PreconditionFailedResponse = gin.H{
"code": "PreconditionFailed",
"message": "Operation cannot be performed because one of the specified precondition is not met.",
}
@@ -0,0 +1,145 @@
package continuationtoken
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/pikami/cosmium/internal/logger"
)
type ContinuationTokenExternal struct {
Token string `json:"token"`
Range struct {
Min string `json:"min"`
Max string `json:"max"`
} `json:"range"`
}
type ContinuationToken struct {
Token struct {
ResourceId string // RID
PageIndex int // RT
TotalResults int // TRC
ISV int // ISV
IEO int // IEO
QCF int // QCF
LR int // LR
}
Range struct {
Min string
Max string
}
}
func Generate(resourceid string, pageIndex int, totalResults int) ContinuationToken {
ct := ContinuationToken{}
ct.Token.ResourceId = resourceid
ct.Token.PageIndex = pageIndex
ct.Token.TotalResults = totalResults
ct.Token.ISV = 2
ct.Token.IEO = 65567
ct.Token.QCF = 8
ct.Token.LR = 1
ct.Range.Min = ""
ct.Range.Max = "FF"
return ct
}
func GenerateDefault(resourceid string) ContinuationToken {
return Generate(resourceid, 0, 0)
}
func (ct *ContinuationToken) ToString() string {
token := fmt.Sprintf(
"-RID:~%s#RT:%d#TRC:%d#ISV:%d#IEO:%d#QCF:%d#LR:%d",
ct.Token.ResourceId,
ct.Token.PageIndex,
ct.Token.TotalResults,
ct.Token.ISV,
ct.Token.IEO,
ct.Token.QCF,
ct.Token.LR,
)
ect := ContinuationTokenExternal{}
ect.Token = token
ect.Range.Min = ct.Range.Min
ect.Range.Max = ct.Range.Max
json, err := json.Marshal(ect)
if err != nil {
logger.Error(err, "failed to marshal continuation token")
return ""
}
return string(json)
}
func FromString(token string) ContinuationToken {
ect := ContinuationTokenExternal{}
err := json.Unmarshal([]byte(token), &ect)
if err != nil {
logger.Error(err, "failed to unmarshal continuation token")
return ContinuationToken{}
}
ct, err := parseContinuationToken(ect.Token, ect.Range.Min, ect.Range.Max)
if err != nil {
logger.Error(err, "failed to parse continuation token")
return ContinuationToken{}
}
return *ct
}
func parseContinuationToken(token string, minRange string, maxRange string) (*ContinuationToken, error) {
const prefix = "-RID:~"
if !strings.HasPrefix(token, prefix) {
return nil, fmt.Errorf("invalid token prefix")
}
parts := strings.Split(token[len(prefix):], "#")
if len(parts) != 7 {
return nil, fmt.Errorf("invalid token format: expected 7 fields, got %d", len(parts))
}
ct := &ContinuationToken{}
ct.Token.ResourceId = parts[0]
parseIntField := func(part, key string) (int, error) {
if !strings.HasPrefix(part, key+":") {
return 0, fmt.Errorf("expected %s field", key)
}
return strconv.Atoi(strings.TrimPrefix(part, key+":"))
}
var err error
if ct.Token.PageIndex, err = parseIntField(parts[1], "RT"); err != nil {
return nil, err
}
if ct.Token.TotalResults, err = parseIntField(parts[2], "TRC"); err != nil {
return nil, err
}
if ct.Token.ISV, err = parseIntField(parts[3], "ISV"); err != nil {
return nil, err
}
if ct.Token.IEO, err = parseIntField(parts[4], "IEO"); err != nil {
return nil, err
}
if ct.Token.QCF, err = parseIntField(parts[5], "QCF"); err != nil {
return nil, err
}
if ct.Token.LR, err = parseIntField(parts[6], "LR"); err != nil {
return nil, err
}
ct.Range.Min = minRange
ct.Range.Max = maxRange
return ct, nil
}
@@ -0,0 +1,35 @@
package continuationtoken
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Generate(t *testing.T) {
token := Generate("test-resource-id", 1, 100)
assert.Equal(t, "test-resource-id", token.Token.ResourceId)
assert.Equal(t, 1, token.Token.PageIndex)
assert.Equal(t, 100, token.Token.TotalResults)
}
func Test_FromString(t *testing.T) {
token := FromString("{\"token\":\"-RID:~test-resource-id#RT:1#TRC:100#ISV:2#IEO:65567#QCF:8#LR:1\",\"range\":{\"min\":\"\",\"max\":\"FF\"}}")
assert.Equal(t, "test-resource-id", token.Token.ResourceId)
assert.Equal(t, 1, token.Token.PageIndex)
assert.Equal(t, 100, token.Token.TotalResults)
}
func Test_ToString(t *testing.T) {
token := Generate("test-resource-id", 1, 100)
assert.Equal(t, "{\"token\":\"-RID:~test-resource-id#RT:1#TRC:100#ISV:2#IEO:65567#QCF:8#LR:1\",\"range\":{\"min\":\"\",\"max\":\"FF\"}}", token.ToString())
}
func Test_GenerateDefault(t *testing.T) {
token := GenerateDefault("test-resource-id")
assert.Equal(t, "test-resource-id", token.Token.ResourceId)
assert.Equal(t, 0, token.Token.PageIndex)
assert.Equal(t, 0, token.Token.TotalResults)
}
@@ -0,0 +1,20 @@
package converters
import (
"github.com/pikami/cosmium/internal/datastore"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
)
type DocumentToRowTypeIterator struct {
documents datastore.DocumentIterator
}
func NewDocumentToRowTypeIterator(documents datastore.DocumentIterator) *DocumentToRowTypeIterator {
return &DocumentToRowTypeIterator{
documents: documents,
}
}
func (di *DocumentToRowTypeIterator) Next() (memoryexecutor.RowType, datastore.DataStoreStatus) {
return di.documents.Next()
}
@@ -0,0 +1,136 @@
package badgerdatastore
import (
"encoding/json"
"log"
"os"
"time"
"github.com/dgraph-io/badger/v4"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
)
type BadgerDataStore struct {
db *badger.DB
gcTicker *time.Ticker
gcDone chan struct{}
gcStopped chan struct{}
}
type BadgerDataStoreOptions struct {
InitialDataFilePath string
PersistDataFilePath string
}
func NewBadgerDataStore(options BadgerDataStoreOptions) *BadgerDataStore {
badgerOpts := badger.DefaultOptions(options.PersistDataFilePath)
badgerOpts = badgerOpts.WithLogger(newBadgerLogger())
if options.PersistDataFilePath == "" {
badgerOpts = badgerOpts.WithInMemory(true)
}
db, err := badger.Open(badgerOpts)
if err != nil {
panic(err)
}
gcTicker := time.NewTicker(5 * time.Minute)
ds := &BadgerDataStore{
db: db,
gcTicker: gcTicker,
gcDone: make(chan struct{}),
gcStopped: make(chan struct{}),
}
ds.initializeDataStore(options.InitialDataFilePath)
go ds.runGarbageCollector()
return ds
}
func (r *BadgerDataStore) Close() {
if r.gcTicker != nil {
r.gcTicker.Stop()
close(r.gcDone)
<-r.gcStopped
}
r.db.Close()
r.db = nil
}
func (r *BadgerDataStore) DumpToJson() (string, error) {
logger.ErrorLn("Badger datastore does not support state export currently.")
return "{}", nil
}
func (r *BadgerDataStore) runGarbageCollector() {
defer close(r.gcStopped)
for {
select {
case <-r.gcTicker.C:
for {
err := r.db.RunValueLogGC(0.7)
if err != nil {
break
}
}
case <-r.gcDone:
return
}
}
}
func (r *BadgerDataStore) initializeDataStore(initialDataFilePath string) {
if initialDataFilePath == "" {
return
}
stat, err := os.Stat(initialDataFilePath)
if err != nil {
panic(err)
}
if stat.IsDir() {
logger.ErrorLn("Argument '-Persist' must be a path to file, not a directory.")
os.Exit(1)
}
jsonData, err := os.ReadFile(initialDataFilePath)
if err != nil {
log.Fatalf("Error reading state JSON file: %v", err)
return
}
var state datastore.InitialDataModel
if err := json.Unmarshal([]byte(jsonData), &state); err != nil {
log.Fatalf("Error parsing state JSON file: %v", err)
return
}
for dbName, dbModel := range state.Databases {
r.CreateDatabase(dbModel)
for colName, colModel := range state.Collections[dbName] {
r.CreateCollection(dbName, colModel)
for _, docModel := range state.Documents[dbName][colName] {
r.CreateDocument(dbName, colName, docModel)
}
for _, triggerModel := range state.Triggers[dbName][colName] {
r.CreateTrigger(dbName, colName, triggerModel)
}
for _, spModel := range state.StoredProcedures[dbName][colName] {
r.CreateStoredProcedure(dbName, colName, spModel)
}
for _, udfModel := range state.UserDefinedFunctions[dbName][colName] {
r.CreateUserDefinedFunction(dbName, colName, udfModel)
}
}
}
}
@@ -0,0 +1,28 @@
package badgerdatastore
import (
"github.com/dgraph-io/badger/v4"
"github.com/pikami/cosmium/internal/logger"
)
type badgerLogger struct{}
func newBadgerLogger() badger.Logger {
return &badgerLogger{}
}
func (l *badgerLogger) Errorf(format string, v ...interface{}) {
logger.Errorf(format, v...)
}
func (l *badgerLogger) Warningf(format string, v ...interface{}) {
logger.Infof(format, v...)
}
func (l *badgerLogger) Infof(format string, v ...interface{}) {
logger.Infof(format, v...)
}
func (l *badgerLogger) Debugf(format string, v ...interface{}) {
logger.Debugf(format, v...)
}
@@ -0,0 +1,105 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
structhidrators "github.com/pikami/cosmium/internal/struct_hidrators"
)
func (r *BadgerDataStore) GetAllCollections(databaseId string) ([]datastore.Collection, datastore.DataStoreStatus) {
exists, err := keyExists(r.db.NewTransaction(false), generateDatabaseKey(databaseId))
if err != nil {
logger.ErrorLn("Error while checking if database exists:", err)
return nil, datastore.Unknown
}
if !exists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeCollection, databaseId, "", "") + "/"
colls, status := listByPrefix[datastore.Collection](r.db, prefix)
if status == datastore.StatusOk {
return colls, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetCollection(databaseId string, collectionId string) (datastore.Collection, datastore.DataStoreStatus) {
collectionKey := generateCollectionKey(databaseId, collectionId)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var collection datastore.Collection
status := getKey(txn, collectionKey, &collection)
return collection, status
}
func (r *BadgerDataStore) DeleteCollection(databaseId string, collectionId string) datastore.DataStoreStatus {
collectionKey := generateCollectionKey(databaseId, collectionId)
txn := r.db.NewTransaction(true)
defer txn.Discard()
prefixes := []string{
generateKey(resourceid.ResourceTypeDocument, databaseId, collectionId, "") + "/",
generateKey(resourceid.ResourceTypeTrigger, databaseId, collectionId, "") + "/",
generateKey(resourceid.ResourceTypeStoredProcedure, databaseId, collectionId, "") + "/",
generateKey(resourceid.ResourceTypeUserDefinedFunction, databaseId, collectionId, "") + "/",
}
for _, prefix := range prefixes {
if err := deleteKeysByPrefix(txn, prefix); err != nil {
return datastore.Unknown
}
}
deleteKey(txn, collectionKey)
err := txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateCollection(databaseId string, newCollection datastore.Collection) (datastore.Collection, datastore.DataStoreStatus) {
collectionKey := generateCollectionKey(databaseId, newCollection.ID)
txn := r.db.NewTransaction(true)
defer txn.Discard()
collectionExists, err := keyExists(txn, collectionKey)
if err != nil || collectionExists {
return datastore.Collection{}, datastore.Conflict
}
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
return datastore.Collection{}, status
}
newCollection = structhidrators.Hidrate(newCollection).(datastore.Collection)
newCollection.TimeStamp = time.Now().Unix()
newCollection.ResourceID = resourceid.NewCombined(database.ResourceID, resourceid.New(resourceid.ResourceTypeCollection))
newCollection.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newCollection.Self = fmt.Sprintf("dbs/%s/colls/%s/", database.ResourceID, newCollection.ResourceID)
status = insertKey(txn, collectionKey, newCollection)
if status != datastore.StatusOk {
return datastore.Collection{}, status
}
return newCollection, datastore.StatusOk
}
@@ -0,0 +1,81 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
)
func (r *BadgerDataStore) GetAllDatabases() ([]datastore.Database, datastore.DataStoreStatus) {
dbs, status := listByPrefix[datastore.Database](r.db, DatabaseKeyPrefix)
if status == datastore.StatusOk {
return dbs, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetDatabase(id string) (datastore.Database, datastore.DataStoreStatus) {
databaseKey := generateDatabaseKey(id)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var database datastore.Database
status := getKey(txn, databaseKey, &database)
return database, status
}
func (r *BadgerDataStore) DeleteDatabase(id string) datastore.DataStoreStatus {
databaseKey := generateDatabaseKey(id)
txn := r.db.NewTransaction(true)
defer txn.Discard()
prefixes := []string{
generateKey(resourceid.ResourceTypeCollection, id, "", "") + "/",
generateKey(resourceid.ResourceTypeDocument, id, "", "") + "/",
generateKey(resourceid.ResourceTypeTrigger, id, "", "") + "/",
generateKey(resourceid.ResourceTypeStoredProcedure, id, "", "") + "/",
generateKey(resourceid.ResourceTypeUserDefinedFunction, id, "", "") + "/",
}
for _, prefix := range prefixes {
if err := deleteKeysByPrefix(txn, prefix); err != nil {
return datastore.Unknown
}
}
deleteKey(txn, databaseKey)
err := txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateDatabase(newDatabase datastore.Database) (datastore.Database, datastore.DataStoreStatus) {
databaseKey := generateDatabaseKey(newDatabase.ID)
txn := r.db.NewTransaction(true)
defer txn.Discard()
newDatabase.TimeStamp = time.Now().Unix()
newDatabase.ResourceID = resourceid.New(resourceid.ResourceTypeDatabase)
newDatabase.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID)
status := insertKey(txn, databaseKey, newDatabase)
if status != datastore.StatusOk {
return datastore.Database{}, status
}
return newDatabase, datastore.StatusOk
}
@@ -0,0 +1,223 @@
package badgerdatastore
import (
"github.com/dgraph-io/badger/v4"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
"github.com/vmihailenco/msgpack/v5"
)
const (
DatabaseKeyPrefix = "DB:"
CollectionKeyPrefix = "COL:"
DocumentKeyPrefix = "DOC:"
TriggerKeyPrefix = "TRG:"
StoredProcedureKeyPrefix = "SP:"
UserDefinedFunctionKeyPrefix = "UDF:"
)
func generateKey(
resourceType resourceid.ResourceType,
databaseId string,
collectionId string,
resourceId string,
) string {
result := ""
switch resourceType {
case resourceid.ResourceTypeDatabase:
result += DatabaseKeyPrefix
case resourceid.ResourceTypeCollection:
result += CollectionKeyPrefix
case resourceid.ResourceTypeDocument:
result += DocumentKeyPrefix
case resourceid.ResourceTypeTrigger:
result += TriggerKeyPrefix
case resourceid.ResourceTypeStoredProcedure:
result += StoredProcedureKeyPrefix
case resourceid.ResourceTypeUserDefinedFunction:
result += UserDefinedFunctionKeyPrefix
}
if databaseId != "" {
result += databaseId
}
if collectionId != "" {
result += "/colls/" + collectionId
}
if resourceId != "" {
result += "/" + resourceId
}
return result
}
func generateDatabaseKey(databaseId string) string {
return generateKey(resourceid.ResourceTypeDatabase, databaseId, "", "")
}
func generateCollectionKey(databaseId string, collectionId string) string {
return generateKey(resourceid.ResourceTypeCollection, databaseId, collectionId, "")
}
func generateDocumentKey(databaseId string, collectionId string, documentId string) string {
return generateKey(resourceid.ResourceTypeDocument, databaseId, collectionId, documentId)
}
func generateTriggerKey(databaseId string, collectionId string, triggerId string) string {
return generateKey(resourceid.ResourceTypeTrigger, databaseId, collectionId, triggerId)
}
func generateStoredProcedureKey(databaseId string, collectionId string, storedProcedureId string) string {
return generateKey(resourceid.ResourceTypeStoredProcedure, databaseId, collectionId, storedProcedureId)
}
func generateUserDefinedFunctionKey(databaseId string, collectionId string, udfId string) string {
return generateKey(resourceid.ResourceTypeUserDefinedFunction, databaseId, collectionId, udfId)
}
func insertKey(txn *badger.Txn, key string, value interface{}) datastore.DataStoreStatus {
_, err := txn.Get([]byte(key))
if err == nil {
return datastore.Conflict
}
if err != badger.ErrKeyNotFound {
logger.ErrorLn("Error while checking if key exists:", err)
return datastore.Unknown
}
buf, err := msgpack.Marshal(value)
if err != nil {
logger.ErrorLn("Error while encoding value:", err)
return datastore.Unknown
}
err = txn.Set([]byte(key), buf)
if err != nil {
logger.ErrorLn("Error while setting key:", err)
return datastore.Unknown
}
err = txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func getKey(txn *badger.Txn, key string, value interface{}) datastore.DataStoreStatus {
item, err := txn.Get([]byte(key))
if err != nil {
if err == badger.ErrKeyNotFound {
return datastore.StatusNotFound
}
logger.ErrorLn("Error while getting key:", err)
return datastore.Unknown
}
val, err := item.ValueCopy(nil)
if err != nil {
logger.ErrorLn("Error while copying value:", err)
return datastore.Unknown
}
if value == nil {
logger.ErrorLn("getKey called with nil value")
return datastore.Unknown
}
err = msgpack.Unmarshal(val, &value)
if err != nil {
logger.ErrorLn("Error while decoding value:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func keyExists(txn *badger.Txn, key string) (bool, error) {
_, err := txn.Get([]byte(key))
if err == nil {
return true, nil
}
if err == badger.ErrKeyNotFound {
return false, nil
}
return false, err
}
func listByPrefix[T any](db *badger.DB, prefix string) ([]T, datastore.DataStoreStatus) {
results := make([]T, 0)
err := db.View(func(txn *badger.Txn) error {
opts := badger.DefaultIteratorOptions
opts.Prefix = []byte(prefix)
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
var entry T
status := getKey(txn, string(item.Key()), &entry)
if status != datastore.StatusOk {
logger.ErrorLn("Failed to retrieve entry:", string(item.Key()))
continue
}
results = append(results, entry)
}
return nil
})
if err != nil {
logger.ErrorLn("Error while listing entries:", err)
return nil, datastore.Unknown
}
return results, datastore.StatusOk
}
func deleteKeysByPrefix(txn *badger.Txn, prefix string) error {
opts := badger.DefaultIteratorOptions
opts.Prefix = []byte(prefix)
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
key := it.Item().KeyCopy(nil)
if err := txn.Delete(key); err != nil {
logger.ErrorLn("Failed to delete key:", string(key), "Error:", err)
return err
}
}
return nil
}
func deleteKey(txn *badger.Txn, key string) error {
_, err := txn.Get([]byte(key))
if err == badger.ErrKeyNotFound {
return nil
}
if err != nil {
logger.ErrorLn("Error while checking if key exists:", err)
return err
}
err = txn.Delete([]byte(key))
if err != nil {
logger.ErrorLn("Error while deleting key:", err)
return err
}
return nil
}
@@ -0,0 +1,58 @@
package badgerdatastore
import (
"github.com/dgraph-io/badger/v4"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/vmihailenco/msgpack/v5"
)
type BadgerDocumentIterator struct {
txn *badger.Txn
it *badger.Iterator
prefix string
}
func NewBadgerDocumentIterator(txn *badger.Txn, prefix string) *BadgerDocumentIterator {
opts := badger.DefaultIteratorOptions
opts.Prefix = []byte(prefix)
it := txn.NewIterator(opts)
it.Rewind()
return &BadgerDocumentIterator{
txn: txn,
it: it,
prefix: prefix,
}
}
func (i *BadgerDocumentIterator) Next() (datastore.Document, datastore.DataStoreStatus) {
if !i.it.Valid() {
i.it.Close()
return datastore.Document{}, datastore.IterEOF
}
item := i.it.Item()
val, err := item.ValueCopy(nil)
if err != nil {
logger.ErrorLn("Error while copying value:", err)
return datastore.Document{}, datastore.Unknown
}
current := &datastore.Document{}
err = msgpack.Unmarshal(val, &current)
if err != nil {
logger.ErrorLn("Error while decoding value:", err)
return datastore.Document{}, datastore.Unknown
}
i.it.Next()
return *current, datastore.StatusOk
}
func (i *BadgerDocumentIterator) Close() {
i.it.Close()
i.txn.Discard()
}
@@ -0,0 +1,129 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
)
func (r *BadgerDataStore) GetAllDocuments(databaseId string, collectionId string) ([]datastore.Document, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(false)
defer txn.Discard()
dbExists, err := keyExists(txn, generateDatabaseKey(databaseId))
if err != nil || !dbExists {
return nil, datastore.StatusNotFound
}
collExists, err := keyExists(txn, generateCollectionKey(databaseId, collectionId))
if err != nil || !collExists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeDocument, databaseId, collectionId, "") + "/"
docs, status := listByPrefix[datastore.Document](r.db, prefix)
if status == datastore.StatusOk {
return docs, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetDocumentIterator(databaseId string, collectionId string) (datastore.DocumentIterator, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(false)
dbExists, err := keyExists(txn, generateDatabaseKey(databaseId))
if err != nil || !dbExists {
return nil, datastore.StatusNotFound
}
collExists, err := keyExists(txn, generateCollectionKey(databaseId, collectionId))
if err != nil || !collExists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeDocument, databaseId, collectionId, "") + "/"
iter := NewBadgerDocumentIterator(txn, prefix)
return iter, datastore.StatusOk
}
func (r *BadgerDataStore) GetDocument(databaseId string, collectionId string, documentId string) (datastore.Document, datastore.DataStoreStatus) {
documentKey := generateDocumentKey(databaseId, collectionId, documentId)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var document datastore.Document
status := getKey(txn, documentKey, &document)
return document, status
}
func (r *BadgerDataStore) DeleteDocument(databaseId string, collectionId string, documentId string) datastore.DataStoreStatus {
documentKey := generateDocumentKey(databaseId, collectionId, documentId)
txn := r.db.NewTransaction(true)
defer txn.Discard()
exists, err := keyExists(txn, documentKey)
if err != nil {
return datastore.Unknown
}
if !exists {
return datastore.StatusNotFound
}
err = txn.Delete([]byte(documentKey))
if err != nil {
logger.ErrorLn("Error while deleting document:", err)
return datastore.Unknown
}
err = txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (datastore.Document, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(true)
defer txn.Discard()
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
return datastore.Document{}, status
}
var collection datastore.Collection
status = getKey(txn, generateCollectionKey(databaseId, collectionId), &collection)
if status != datastore.StatusOk {
return datastore.Document{}, status
}
var ok bool
var documentId string
if documentId, ok = document["id"].(string); !ok || documentId == "" {
documentId = fmt.Sprint(uuid.New())
document["id"] = documentId
}
document["_ts"] = time.Now().Unix()
document["_rid"] = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeDocument))
document["_etag"] = fmt.Sprintf("\"%s\"", uuid.New())
document["_self"] = fmt.Sprintf("dbs/%s/colls/%s/docs/%s/", database.ResourceID, collection.ResourceID, document["_rid"])
status = insertKey(txn, generateDocumentKey(databaseId, collectionId, documentId), document)
if status != datastore.StatusOk {
return datastore.Document{}, status
}
return document, datastore.StatusOk
}
@@ -0,0 +1,53 @@
package badgerdatastore
import (
"fmt"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
)
// I have no idea what this is tbh
func (r *BadgerDataStore) GetPartitionKeyRanges(databaseId string, collectionId string) ([]datastore.PartitionKeyRange, datastore.DataStoreStatus) {
databaseRid := databaseId
collectionRid := collectionId
var timestamp int64 = 0
txn := r.db.NewTransaction(false)
defer txn.Discard()
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
databaseRid = database.ResourceID
}
var collection datastore.Collection
status = getKey(txn, generateCollectionKey(databaseId, collectionId), &collection)
if status != datastore.StatusOk {
collectionRid = collection.ResourceID
timestamp = collection.TimeStamp
}
pkrResourceId := resourceid.NewCombined(collectionRid, resourceid.New(resourceid.ResourceTypePartitionKeyRange))
pkrSelf := fmt.Sprintf("dbs/%s/colls/%s/pkranges/%s/", databaseRid, collectionRid, pkrResourceId)
etag := fmt.Sprintf("\"%s\"", uuid.New())
return []datastore.PartitionKeyRange{
{
ResourceID: pkrResourceId,
ID: "0",
Etag: etag,
MinInclusive: "",
MaxExclusive: "FF",
RidPrefix: 0,
Self: pkrSelf,
ThroughputFraction: 1,
Status: "online",
Parents: []interface{}{},
TimeStamp: timestamp,
Lsn: 17,
},
}, datastore.StatusOk
}
@@ -0,0 +1,108 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
)
func (r *BadgerDataStore) GetAllStoredProcedures(databaseId string, collectionId string) ([]datastore.StoredProcedure, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(false)
defer txn.Discard()
dbExists, err := keyExists(txn, generateDatabaseKey(databaseId))
if err != nil || !dbExists {
return nil, datastore.StatusNotFound
}
collExists, err := keyExists(txn, generateCollectionKey(databaseId, collectionId))
if err != nil || !collExists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeStoredProcedure, databaseId, collectionId, "") + "/"
storedProcedures, status := listByPrefix[datastore.StoredProcedure](r.db, prefix)
if status == datastore.StatusOk {
return storedProcedures, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetStoredProcedure(databaseId string, collectionId string, storedProcedureId string) (datastore.StoredProcedure, datastore.DataStoreStatus) {
storedProcedureKey := generateStoredProcedureKey(databaseId, collectionId, storedProcedureId)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var storedProcedure datastore.StoredProcedure
status := getKey(txn, storedProcedureKey, &storedProcedure)
return storedProcedure, status
}
func (r *BadgerDataStore) DeleteStoredProcedure(databaseId string, collectionId string, storedProcedureId string) datastore.DataStoreStatus {
storedProcedureKey := generateStoredProcedureKey(databaseId, collectionId, storedProcedureId)
txn := r.db.NewTransaction(true)
defer txn.Discard()
exists, err := keyExists(txn, storedProcedureKey)
if err != nil {
return datastore.Unknown
}
if !exists {
return datastore.StatusNotFound
}
err = txn.Delete([]byte(storedProcedureKey))
if err != nil {
logger.ErrorLn("Error while deleting stored procedure:", err)
return datastore.Unknown
}
err = txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateStoredProcedure(databaseId string, collectionId string, storedProcedure datastore.StoredProcedure) (datastore.StoredProcedure, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(true)
defer txn.Discard()
if storedProcedure.ID == "" {
return datastore.StoredProcedure{}, datastore.BadRequest
}
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
return datastore.StoredProcedure{}, status
}
var collection datastore.Collection
status = getKey(txn, generateCollectionKey(databaseId, collectionId), &collection)
if status != datastore.StatusOk {
return datastore.StoredProcedure{}, status
}
storedProcedure.TimeStamp = time.Now().Unix()
storedProcedure.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeStoredProcedure))
storedProcedure.ETag = fmt.Sprintf("\"%s\"", uuid.New())
storedProcedure.Self = fmt.Sprintf("dbs/%s/colls/%s/sprocs/%s/", database.ResourceID, collection.ResourceID, storedProcedure.ResourceID)
status = insertKey(txn, generateStoredProcedureKey(databaseId, collectionId, storedProcedure.ID), storedProcedure)
if status != datastore.StatusOk {
return datastore.StoredProcedure{}, status
}
return storedProcedure, datastore.StatusOk
}
@@ -0,0 +1,108 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
)
func (r *BadgerDataStore) GetAllTriggers(databaseId string, collectionId string) ([]datastore.Trigger, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(false)
defer txn.Discard()
dbExists, err := keyExists(txn, generateDatabaseKey(databaseId))
if err != nil || !dbExists {
return nil, datastore.StatusNotFound
}
collExists, err := keyExists(txn, generateCollectionKey(databaseId, collectionId))
if err != nil || !collExists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeTrigger, databaseId, collectionId, "") + "/"
triggers, status := listByPrefix[datastore.Trigger](r.db, prefix)
if status == datastore.StatusOk {
return triggers, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetTrigger(databaseId string, collectionId string, triggerId string) (datastore.Trigger, datastore.DataStoreStatus) {
triggerKey := generateTriggerKey(databaseId, collectionId, triggerId)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var trigger datastore.Trigger
status := getKey(txn, triggerKey, &trigger)
return trigger, status
}
func (r *BadgerDataStore) DeleteTrigger(databaseId string, collectionId string, triggerId string) datastore.DataStoreStatus {
triggerKey := generateTriggerKey(databaseId, collectionId, triggerId)
txn := r.db.NewTransaction(true)
defer txn.Discard()
exists, err := keyExists(txn, triggerKey)
if err != nil {
return datastore.Unknown
}
if !exists {
return datastore.StatusNotFound
}
err = txn.Delete([]byte(triggerKey))
if err != nil {
logger.ErrorLn("Error while deleting trigger:", err)
return datastore.Unknown
}
err = txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateTrigger(databaseId string, collectionId string, trigger datastore.Trigger) (datastore.Trigger, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(true)
defer txn.Discard()
if trigger.ID == "" {
return datastore.Trigger{}, datastore.BadRequest
}
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
return datastore.Trigger{}, status
}
var collection datastore.Collection
status = getKey(txn, generateCollectionKey(databaseId, collectionId), &collection)
if status != datastore.StatusOk {
return datastore.Trigger{}, status
}
trigger.TimeStamp = time.Now().Unix()
trigger.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeTrigger))
trigger.ETag = fmt.Sprintf("\"%s\"", uuid.New())
trigger.Self = fmt.Sprintf("dbs/%s/colls/%s/triggers/%s/", database.ResourceID, collection.ResourceID, trigger.ResourceID)
status = insertKey(txn, generateTriggerKey(databaseId, collectionId, trigger.ID), trigger)
if status != datastore.StatusOk {
return datastore.Trigger{}, status
}
return trigger, datastore.StatusOk
}
@@ -0,0 +1,108 @@
package badgerdatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/resourceid"
)
func (r *BadgerDataStore) GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]datastore.UserDefinedFunction, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(false)
defer txn.Discard()
dbExists, err := keyExists(txn, generateDatabaseKey(databaseId))
if err != nil || !dbExists {
return nil, datastore.StatusNotFound
}
collExists, err := keyExists(txn, generateCollectionKey(databaseId, collectionId))
if err != nil || !collExists {
return nil, datastore.StatusNotFound
}
prefix := generateKey(resourceid.ResourceTypeUserDefinedFunction, databaseId, collectionId, "") + "/"
udfs, status := listByPrefix[datastore.UserDefinedFunction](r.db, prefix)
if status == datastore.StatusOk {
return udfs, datastore.StatusOk
}
return nil, status
}
func (r *BadgerDataStore) GetUserDefinedFunction(databaseId string, collectionId string, udfId string) (datastore.UserDefinedFunction, datastore.DataStoreStatus) {
udfKey := generateUserDefinedFunctionKey(databaseId, collectionId, udfId)
txn := r.db.NewTransaction(false)
defer txn.Discard()
var udf datastore.UserDefinedFunction
status := getKey(txn, udfKey, &udf)
return udf, status
}
func (r *BadgerDataStore) DeleteUserDefinedFunction(databaseId string, collectionId string, udfId string) datastore.DataStoreStatus {
udfKey := generateUserDefinedFunctionKey(databaseId, collectionId, udfId)
txn := r.db.NewTransaction(true)
defer txn.Discard()
exists, err := keyExists(txn, udfKey)
if err != nil {
return datastore.Unknown
}
if !exists {
return datastore.StatusNotFound
}
err = txn.Delete([]byte(udfKey))
if err != nil {
logger.ErrorLn("Error while deleting user defined function:", err)
return datastore.Unknown
}
err = txn.Commit()
if err != nil {
logger.ErrorLn("Error while committing transaction:", err)
return datastore.Unknown
}
return datastore.StatusOk
}
func (r *BadgerDataStore) CreateUserDefinedFunction(databaseId string, collectionId string, udf datastore.UserDefinedFunction) (datastore.UserDefinedFunction, datastore.DataStoreStatus) {
txn := r.db.NewTransaction(true)
defer txn.Discard()
if udf.ID == "" {
return datastore.UserDefinedFunction{}, datastore.BadRequest
}
var database datastore.Database
status := getKey(txn, generateDatabaseKey(databaseId), &database)
if status != datastore.StatusOk {
return datastore.UserDefinedFunction{}, status
}
var collection datastore.Collection
status = getKey(txn, generateCollectionKey(databaseId, collectionId), &collection)
if status != datastore.StatusOk {
return datastore.UserDefinedFunction{}, status
}
udf.TimeStamp = time.Now().Unix()
udf.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeUserDefinedFunction))
udf.ETag = fmt.Sprintf("\"%s\"", uuid.New())
udf.Self = fmt.Sprintf("dbs/%s/colls/%s/udfs/%s/", database.ResourceID, collection.ResourceID, udf.ResourceID)
status = insertKey(txn, generateUserDefinedFunctionKey(databaseId, collectionId, udf.ID), udf)
if status != datastore.StatusOk {
return datastore.UserDefinedFunction{}, status
}
return udf, datastore.StatusOk
}
+44
View File
@@ -0,0 +1,44 @@
package datastore
type DataStore interface {
GetAllDatabases() ([]Database, DataStoreStatus)
GetDatabase(databaseId string) (Database, DataStoreStatus)
DeleteDatabase(databaseId string) DataStoreStatus
CreateDatabase(newDatabase Database) (Database, DataStoreStatus)
GetAllCollections(databaseId string) ([]Collection, DataStoreStatus)
GetCollection(databaseId string, collectionId string) (Collection, DataStoreStatus)
DeleteCollection(databaseId string, collectionId string) DataStoreStatus
CreateCollection(databaseId string, newCollection Collection) (Collection, DataStoreStatus)
GetAllDocuments(databaseId string, collectionId string) ([]Document, DataStoreStatus)
GetDocumentIterator(databaseId string, collectionId string) (DocumentIterator, DataStoreStatus)
GetDocument(databaseId string, collectionId string, documentId string) (Document, DataStoreStatus)
DeleteDocument(databaseId string, collectionId string, documentId string) DataStoreStatus
CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (Document, DataStoreStatus)
GetAllTriggers(databaseId string, collectionId string) ([]Trigger, DataStoreStatus)
GetTrigger(databaseId string, collectionId string, triggerId string) (Trigger, DataStoreStatus)
DeleteTrigger(databaseId string, collectionId string, triggerId string) DataStoreStatus
CreateTrigger(databaseId string, collectionId string, trigger Trigger) (Trigger, DataStoreStatus)
GetAllStoredProcedures(databaseId string, collectionId string) ([]StoredProcedure, DataStoreStatus)
GetStoredProcedure(databaseId string, collectionId string, storedProcedureId string) (StoredProcedure, DataStoreStatus)
DeleteStoredProcedure(databaseId string, collectionId string, storedProcedureId string) DataStoreStatus
CreateStoredProcedure(databaseId string, collectionId string, storedProcedure StoredProcedure) (StoredProcedure, DataStoreStatus)
GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]UserDefinedFunction, DataStoreStatus)
GetUserDefinedFunction(databaseId string, collectionId string, udfId string) (UserDefinedFunction, DataStoreStatus)
DeleteUserDefinedFunction(databaseId string, collectionId string, udfId string) DataStoreStatus
CreateUserDefinedFunction(databaseId string, collectionId string, udf UserDefinedFunction) (UserDefinedFunction, DataStoreStatus)
GetPartitionKeyRanges(databaseId string, collectionId string) ([]PartitionKeyRange, DataStoreStatus)
Close()
DumpToJson() (string, error)
}
type DocumentIterator interface {
Next() (Document, DataStoreStatus)
Close()
}
+21
View File
@@ -0,0 +1,21 @@
package datastore
type InitialDataModel struct {
// Map databaseId -> Database
Databases map[string]Database `json:"databases"`
// Map databaseId -> collectionId -> Collection
Collections map[string]map[string]Collection `json:"collections"`
// Map databaseId -> collectionId -> documentId -> 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"`
}
@@ -0,0 +1,21 @@
package jsondatastore
import "github.com/pikami/cosmium/internal/datastore"
type ArrayDocumentIterator struct {
documents []datastore.Document
index int
}
func (i *ArrayDocumentIterator) Next() (datastore.Document, datastore.DataStoreStatus) {
i.index++
if i.index >= len(i.documents) {
return datastore.Document{}, datastore.StatusNotFound
}
return i.documents[i.index], datastore.StatusOk
}
func (i *ArrayDocumentIterator) Close() {
i.documents = []datastore.Document{}
}
@@ -0,0 +1,89 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
structhidrators "github.com/pikami/cosmium/internal/struct_hidrators"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllCollections(databaseId string) ([]datastore.Collection, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return make([]datastore.Collection, 0), datastore.StatusNotFound
}
return maps.Values(r.storeState.Collections[databaseId]), datastore.StatusOk
}
func (r *JsonDataStore) GetCollection(databaseId string, collectionId string) (datastore.Collection, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.Collection{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.Collection{}, datastore.StatusNotFound
}
return r.storeState.Collections[databaseId][collectionId], datastore.StatusOk
}
func (r *JsonDataStore) DeleteCollection(databaseId string, collectionId string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.Collections[databaseId], collectionId)
delete(r.storeState.Documents[databaseId], collectionId)
delete(r.storeState.Triggers[databaseId], collectionId)
delete(r.storeState.StoredProcedures[databaseId], collectionId)
delete(r.storeState.UserDefinedFunctions[databaseId], collectionId)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateCollection(databaseId string, newCollection datastore.Collection) (datastore.Collection, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database datastore.Database
if database, ok = r.storeState.Databases[databaseId]; !ok {
return datastore.Collection{}, datastore.StatusNotFound
}
if _, ok = r.storeState.Collections[databaseId][newCollection.ID]; ok {
return datastore.Collection{}, datastore.Conflict
}
newCollection = structhidrators.Hidrate(newCollection).(datastore.Collection)
newCollection.TimeStamp = time.Now().Unix()
newCollection.ResourceID = resourceid.NewCombined(database.ResourceID, resourceid.New(resourceid.ResourceTypeCollection))
newCollection.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newCollection.Self = fmt.Sprintf("dbs/%s/colls/%s/", database.ResourceID, newCollection.ResourceID)
r.storeState.Collections[databaseId][newCollection.ID] = newCollection
r.storeState.Documents[databaseId][newCollection.ID] = make(map[string]datastore.Document)
r.storeState.Triggers[databaseId][newCollection.ID] = make(map[string]datastore.Trigger)
r.storeState.StoredProcedures[databaseId][newCollection.ID] = make(map[string]datastore.StoredProcedure)
r.storeState.UserDefinedFunctions[databaseId][newCollection.ID] = make(map[string]datastore.UserDefinedFunction)
return newCollection, datastore.StatusOk
}
@@ -0,0 +1,70 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllDatabases() ([]datastore.Database, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.Databases), datastore.StatusOk
}
func (r *JsonDataStore) GetDatabase(id string) (datastore.Database, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if database, ok := r.storeState.Databases[id]; ok {
return database, datastore.StatusOk
}
return datastore.Database{}, datastore.StatusNotFound
}
func (r *JsonDataStore) DeleteDatabase(id string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[id]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.Databases, id)
delete(r.storeState.Collections, id)
delete(r.storeState.Documents, id)
delete(r.storeState.Triggers, id)
delete(r.storeState.StoredProcedures, id)
delete(r.storeState.UserDefinedFunctions, id)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateDatabase(newDatabase datastore.Database) (datastore.Database, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[newDatabase.ID]; ok {
return datastore.Database{}, datastore.Conflict
}
newDatabase.TimeStamp = time.Now().Unix()
newDatabase.ResourceID = resourceid.New(resourceid.ResourceTypeDatabase)
newDatabase.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID)
r.storeState.Databases[newDatabase.ID] = newDatabase
r.storeState.Collections[newDatabase.ID] = make(map[string]datastore.Collection)
r.storeState.Documents[newDatabase.ID] = make(map[string]map[string]datastore.Document)
r.storeState.Triggers[newDatabase.ID] = make(map[string]map[string]datastore.Trigger)
r.storeState.StoredProcedures[newDatabase.ID] = make(map[string]map[string]datastore.StoredProcedure)
r.storeState.UserDefinedFunctions[newDatabase.ID] = make(map[string]map[string]datastore.UserDefinedFunction)
return newDatabase, datastore.StatusOk
}
@@ -0,0 +1,113 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllDocuments(databaseId string, collectionId string) ([]datastore.Document, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return make([]datastore.Document, 0), datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return make([]datastore.Document, 0), datastore.StatusNotFound
}
return maps.Values(r.storeState.Documents[databaseId][collectionId]), datastore.StatusOk
}
func (r *JsonDataStore) GetDocument(databaseId string, collectionId string, documentId string) (datastore.Document, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.Document{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.Document{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return datastore.Document{}, datastore.StatusNotFound
}
return r.storeState.Documents[databaseId][collectionId][documentId], datastore.StatusOk
}
func (r *JsonDataStore) DeleteDocument(databaseId string, collectionId string, documentId string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.Documents[databaseId][collectionId], documentId)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (datastore.Document, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var documentId string
var database datastore.Database
var collection datastore.Collection
if documentId, ok = document["id"].(string); !ok || documentId == "" {
documentId = fmt.Sprint(uuid.New())
document["id"] = documentId
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return datastore.Document{}, datastore.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.Document{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; ok {
return datastore.Document{}, datastore.Conflict
}
document["_ts"] = time.Now().Unix()
document["_rid"] = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeDocument))
document["_etag"] = fmt.Sprintf("\"%s\"", uuid.New())
document["_self"] = fmt.Sprintf("dbs/%s/colls/%s/docs/%s/", database.ResourceID, collection.ResourceID, document["_rid"])
r.storeState.Documents[databaseId][collectionId][documentId] = document
return document, datastore.StatusOk
}
func (r *JsonDataStore) GetDocumentIterator(databaseId string, collectionId string) (datastore.DocumentIterator, datastore.DataStoreStatus) {
documents, status := r.GetAllDocuments(databaseId, collectionId)
if status != datastore.StatusOk {
return nil, status
}
return &ArrayDocumentIterator{
documents: documents,
index: -1,
}, datastore.StatusOk
}
@@ -0,0 +1,34 @@
package jsondatastore
import "github.com/pikami/cosmium/internal/datastore"
type JsonDataStore struct {
storeState State
initialDataFilePath string
persistDataFilePath string
}
type JsonDataStoreOptions struct {
InitialDataFilePath string
PersistDataFilePath string
}
func NewJsonDataStore(options JsonDataStoreOptions) *JsonDataStore {
dataStore := &JsonDataStore{
storeState: State{
Databases: make(map[string]datastore.Database),
Collections: make(map[string]map[string]datastore.Collection),
Documents: make(map[string]map[string]map[string]datastore.Document),
Triggers: make(map[string]map[string]map[string]datastore.Trigger),
StoredProcedures: make(map[string]map[string]map[string]datastore.StoredProcedure),
UserDefinedFunctions: make(map[string]map[string]map[string]datastore.UserDefinedFunction),
},
initialDataFilePath: options.InitialDataFilePath,
persistDataFilePath: options.PersistDataFilePath,
}
dataStore.InitializeDataStore()
return dataStore
}
@@ -1,33 +1,36 @@
package repositories package jsondatastore
import ( import (
"fmt" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid" "github.com/pikami/cosmium/internal/resourceid"
) )
// 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 *JsonDataStore) GetPartitionKeyRanges(databaseId string, collectionId string) ([]datastore.PartitionKeyRange, datastore.DataStoreStatus) {
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
} }
pkrResourceId := resourceid.NewCombined(databaseRid, collectionRid, resourceid.New()) pkrResourceId := resourceid.NewCombined(collectionRid, resourceid.New(resourceid.ResourceTypePartitionKeyRange))
pkrSelf := fmt.Sprintf("dbs/%s/colls/%s/pkranges/%s/", databaseRid, collectionRid, pkrResourceId) pkrSelf := fmt.Sprintf("dbs/%s/colls/%s/pkranges/%s/", databaseRid, collectionRid, pkrResourceId)
etag := fmt.Sprintf("\"%s\"", uuid.New()) etag := fmt.Sprintf("\"%s\"", uuid.New())
return []repositorymodels.PartitionKeyRange{ return []datastore.PartitionKeyRange{
{ {
ResourceID: pkrResourceId, ResourceID: pkrResourceId,
ID: "0", ID: "0",
@@ -42,5 +45,5 @@ func GetPartitionKeyRanges(databaseId string, collectionId string) ([]repository
TimeStamp: timestamp, TimeStamp: timestamp,
Lsn: 17, Lsn: 17,
}, },
}, repositorymodels.StatusOk }, datastore.StatusOk
} }
+236
View File
@@ -0,0 +1,236 @@
package jsondatastore
import (
"encoding/json"
"log"
"os"
"reflect"
"sync"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
)
type State struct {
sync.RWMutex
// Map databaseId -> Database
Databases map[string]datastore.Database `json:"databases"`
// Map databaseId -> collectionId -> Collection
Collections map[string]map[string]datastore.Collection `json:"collections"`
// Map databaseId -> collectionId -> documentId -> Documents
Documents map[string]map[string]map[string]datastore.Document `json:"documents"`
// Map databaseId -> collectionId -> triggerId -> Trigger
Triggers map[string]map[string]map[string]datastore.Trigger `json:"triggers"`
// Map databaseId -> collectionId -> spId -> StoredProcedure
StoredProcedures map[string]map[string]map[string]datastore.StoredProcedure `json:"sprocs"`
// Map databaseId -> collectionId -> udfId -> UserDefinedFunction
UserDefinedFunctions map[string]map[string]map[string]datastore.UserDefinedFunction `json:"udfs"`
}
func (r *JsonDataStore) InitializeDataStore() {
if r.initialDataFilePath != "" {
r.LoadStateFS(r.initialDataFilePath)
return
}
if r.persistDataFilePath != "" {
stat, err := os.Stat(r.persistDataFilePath)
if err != nil {
return
}
if stat.IsDir() {
logger.ErrorLn("Argument '-Persist' must be a path to file, not a directory.")
os.Exit(1)
}
r.LoadStateFS(r.persistDataFilePath)
return
}
}
func (r *JsonDataStore) LoadStateFS(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("Error reading state JSON file: %v", err)
return
}
err = r.LoadStateJSON(string(data))
if err != nil {
log.Fatalf("Error unmarshalling state JSON: %v", err)
}
}
func (r *JsonDataStore) LoadStateJSON(jsonData string) error {
r.storeState.Lock()
defer r.storeState.Unlock()
var state 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 *JsonDataStore) SaveStateFS(filePath string) {
r.storeState.RLock()
defer r.storeState.RUnlock()
data, err := json.MarshalIndent(r.storeState, "", "\t")
if err != nil {
logger.Errorf("Failed to save state: %v\n", err)
return
}
os.WriteFile(filePath, data, os.ModePerm)
logger.InfoLn("Saved 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))
}
func (r *JsonDataStore) DumpToJson() (string, error) {
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 (r *JsonDataStore) Close() {
if r.persistDataFilePath != "" {
r.SaveStateFS(r.persistDataFilePath)
}
}
func getLength(v interface{}) int {
switch v.(type) {
case datastore.Database,
datastore.Collection,
datastore.Document,
datastore.Trigger,
datastore.StoredProcedure,
datastore.UserDefinedFunction:
return 1
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Map {
return -1
}
count := 0
for _, key := range rv.MapKeys() {
if rv.MapIndex(key).Kind() == reflect.Map {
count += getLength(rv.MapIndex(key).Interface())
} else {
count++
}
}
return count
}
func (r *JsonDataStore) ensureStoreStateNoNullReferences() {
if r.storeState.Databases == nil {
r.storeState.Databases = make(map[string]datastore.Database)
}
if r.storeState.Collections == nil {
r.storeState.Collections = make(map[string]map[string]datastore.Collection)
}
if r.storeState.Documents == nil {
r.storeState.Documents = make(map[string]map[string]map[string]datastore.Document)
}
if r.storeState.Triggers == nil {
r.storeState.Triggers = make(map[string]map[string]map[string]datastore.Trigger)
}
if r.storeState.StoredProcedures == nil {
r.storeState.StoredProcedures = make(map[string]map[string]map[string]datastore.StoredProcedure)
}
if r.storeState.UserDefinedFunctions == nil {
r.storeState.UserDefinedFunctions = make(map[string]map[string]map[string]datastore.UserDefinedFunction)
}
for database := range r.storeState.Databases {
if r.storeState.Collections[database] == nil {
r.storeState.Collections[database] = make(map[string]datastore.Collection)
}
if r.storeState.Documents[database] == nil {
r.storeState.Documents[database] = make(map[string]map[string]datastore.Document)
}
if r.storeState.Triggers[database] == nil {
r.storeState.Triggers[database] = make(map[string]map[string]datastore.Trigger)
}
if r.storeState.StoredProcedures[database] == nil {
r.storeState.StoredProcedures[database] = make(map[string]map[string]datastore.StoredProcedure)
}
if r.storeState.UserDefinedFunctions[database] == nil {
r.storeState.UserDefinedFunctions[database] = make(map[string]map[string]datastore.UserDefinedFunction)
}
for collection := range r.storeState.Collections[database] {
if r.storeState.Documents[database][collection] == nil {
r.storeState.Documents[database][collection] = make(map[string]datastore.Document)
}
for document := range r.storeState.Documents[database][collection] {
if r.storeState.Documents[database][collection][document] == nil {
delete(r.storeState.Documents[database][collection], document)
}
}
if r.storeState.Triggers[database][collection] == nil {
r.storeState.Triggers[database][collection] = make(map[string]datastore.Trigger)
}
if r.storeState.StoredProcedures[database][collection] == nil {
r.storeState.StoredProcedures[database][collection] = make(map[string]datastore.StoredProcedure)
}
if r.storeState.UserDefinedFunctions[database][collection] == nil {
r.storeState.UserDefinedFunctions[database][collection] = make(map[string]datastore.UserDefinedFunction)
}
}
}
}
@@ -0,0 +1,91 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllStoredProcedures(databaseId string, collectionId string) ([]datastore.StoredProcedure, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.StoredProcedures[databaseId][collectionId]), datastore.StatusOk
}
func (r *JsonDataStore) GetStoredProcedure(databaseId string, collectionId string, spId string) (datastore.StoredProcedure, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StoredProcedure{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StoredProcedure{}, datastore.StatusNotFound
}
if sp, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; ok {
return sp, datastore.StatusOk
}
return datastore.StoredProcedure{}, datastore.StatusNotFound
}
func (r *JsonDataStore) DeleteStoredProcedure(databaseId string, collectionId string, spId string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.StoredProcedures[databaseId][collectionId], spId)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateStoredProcedure(databaseId string, collectionId string, sp datastore.StoredProcedure) (datastore.StoredProcedure, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database datastore.Database
var collection datastore.Collection
if sp.ID == "" {
return datastore.StoredProcedure{}, datastore.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return datastore.StoredProcedure{}, datastore.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StoredProcedure{}, datastore.StatusNotFound
}
if _, ok = r.storeState.StoredProcedures[databaseId][collectionId][sp.ID]; ok {
return datastore.StoredProcedure{}, datastore.Conflict
}
sp.TimeStamp = time.Now().Unix()
sp.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeStoredProcedure))
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, datastore.StatusOk
}
@@ -0,0 +1,91 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllTriggers(databaseId string, collectionId string) ([]datastore.Trigger, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.Triggers[databaseId][collectionId]), datastore.StatusOk
}
func (r *JsonDataStore) GetTrigger(databaseId string, collectionId string, triggerId string) (datastore.Trigger, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.Trigger{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.Trigger{}, datastore.StatusNotFound
}
if trigger, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; ok {
return trigger, datastore.StatusOk
}
return datastore.Trigger{}, datastore.StatusNotFound
}
func (r *JsonDataStore) DeleteTrigger(databaseId string, collectionId string, triggerId string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.Triggers[databaseId][collectionId], triggerId)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateTrigger(databaseId string, collectionId string, trigger datastore.Trigger) (datastore.Trigger, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database datastore.Database
var collection datastore.Collection
if trigger.ID == "" {
return datastore.Trigger{}, datastore.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return datastore.Trigger{}, datastore.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.Trigger{}, datastore.StatusNotFound
}
if _, ok = r.storeState.Triggers[databaseId][collectionId][trigger.ID]; ok {
return datastore.Trigger{}, datastore.Conflict
}
trigger.TimeStamp = time.Now().Unix()
trigger.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeTrigger))
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, datastore.StatusOk
}
@@ -0,0 +1,91 @@
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *JsonDataStore) GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]datastore.UserDefinedFunction, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.UserDefinedFunctions[databaseId][collectionId]), datastore.StatusOk
}
func (r *JsonDataStore) GetUserDefinedFunction(databaseId string, collectionId string, udfId string) (datastore.UserDefinedFunction, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.UserDefinedFunction{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.UserDefinedFunction{}, datastore.StatusNotFound
}
if udf, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; ok {
return udf, datastore.StatusOk
}
return datastore.UserDefinedFunction{}, datastore.StatusNotFound
}
func (r *JsonDataStore) DeleteUserDefinedFunction(databaseId string, collectionId string, udfId string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; !ok {
return datastore.StatusNotFound
}
delete(r.storeState.UserDefinedFunctions[databaseId][collectionId], udfId)
return datastore.StatusOk
}
func (r *JsonDataStore) CreateUserDefinedFunction(databaseId string, collectionId string, udf datastore.UserDefinedFunction) (datastore.UserDefinedFunction, datastore.DataStoreStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database datastore.Database
var collection datastore.Collection
if udf.ID == "" {
return datastore.UserDefinedFunction{}, datastore.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return datastore.UserDefinedFunction{}, datastore.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return datastore.UserDefinedFunction{}, datastore.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udf.ID]; ok {
return datastore.UserDefinedFunction{}, datastore.Conflict
}
udf.TimeStamp = time.Now().Unix()
udf.ResourceID = resourceid.NewCombined(collection.ResourceID, resourceid.New(resourceid.ResourceTypeUserDefinedFunction))
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, datastore.StatusOk
}
@@ -1,4 +1,4 @@
package repositorymodels package datastore
type Database struct { type Database struct {
ID string `json:"id"` ID string `json:"id"`
@@ -8,13 +8,31 @@ type Database struct {
Self string `json:"_self"` Self string `json:"_self"`
} }
type RepositoryStatus int type DataStoreStatus int
const ( const (
StatusOk = 1 StatusOk DataStoreStatus = 1
StatusNotFound = 2 StatusNotFound DataStoreStatus = 2
Conflict = 3 Conflict DataStoreStatus = 3
BadRequest = 4 BadRequest DataStoreStatus = 4
IterEOF DataStoreStatus = 5
Unknown DataStoreStatus = 6
)
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 {
@@ -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{}
@@ -99,14 +117,3 @@ type PartitionKeyRange struct {
TimeStamp int64 `json:"_ts"` TimeStamp int64 `json:"_ts"`
Lsn int `json:"lsn"` Lsn int `json:"lsn"`
} }
type State struct {
// Map databaseId -> Database
Databases map[string]Database `json:"databases"`
// Map databaseId -> collectionId -> Collection
Collections map[string]map[string]Collection `json:"collections"`
// Map databaseId -> collectionId -> documentId -> Documents
Documents map[string]map[string]map[string]Document `json:"documents"`
}
+112 -12
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 ""
} }
-70
View File
@@ -1,70 +0,0 @@
package repositories
import (
"fmt"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
structhidrators "github.com/pikami/cosmium/internal/struct_hidrators"
"golang.org/x/exp/maps"
)
func GetAllCollections(databaseId string) ([]repositorymodels.Collection, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok {
return make([]repositorymodels.Collection, 0), repositorymodels.StatusNotFound
}
return maps.Values(storeState.Collections[databaseId]), repositorymodels.StatusOk
}
func GetCollection(databaseId string, collectionId string) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound
}
if _, ok := storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound
}
return storeState.Collections[databaseId][collectionId], repositorymodels.StatusOk
}
func DeleteCollection(databaseId string, collectionId string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
delete(storeState.Collections[databaseId], collectionId)
return repositorymodels.StatusOk
}
func CreateCollection(databaseId string, newCollection repositorymodels.Collection) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
var ok bool
var database repositorymodels.Database
if database, ok = storeState.Databases[databaseId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound
}
if _, ok = storeState.Collections[databaseId][newCollection.ID]; ok {
return repositorymodels.Collection{}, repositorymodels.Conflict
}
newCollection = structhidrators.Hidrate(newCollection).(repositorymodels.Collection)
newCollection.TimeStamp = time.Now().Unix()
newCollection.ResourceID = resourceid.NewCombined(database.ResourceID, resourceid.New())
newCollection.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newCollection.Self = fmt.Sprintf("dbs/%s/colls/%s/", database.ResourceID, newCollection.ResourceID)
storeState.Collections[databaseId][newCollection.ID] = newCollection
storeState.Documents[databaseId][newCollection.ID] = make(map[string]repositorymodels.Document)
return newCollection, repositorymodels.StatusOk
}
-50
View File
@@ -1,50 +0,0 @@
package repositories
import (
"fmt"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func GetAllDatabases() ([]repositorymodels.Database, repositorymodels.RepositoryStatus) {
return maps.Values(storeState.Databases), repositorymodels.StatusOk
}
func GetDatabase(id string) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
if database, ok := storeState.Databases[id]; ok {
return database, repositorymodels.StatusOk
}
return repositorymodels.Database{}, repositorymodels.StatusNotFound
}
func DeleteDatabase(id string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[id]; !ok {
return repositorymodels.StatusNotFound
}
delete(storeState.Databases, id)
return repositorymodels.StatusOk
}
func CreateDatabase(newDatabase repositorymodels.Database) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[newDatabase.ID]; ok {
return repositorymodels.Database{}, repositorymodels.Conflict
}
newDatabase.TimeStamp = time.Now().Unix()
newDatabase.ResourceID = resourceid.New()
newDatabase.ETag = fmt.Sprintf("\"%s\"", uuid.New())
newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID)
storeState.Databases[newDatabase.ID] = newDatabase
storeState.Collections[newDatabase.ID] = make(map[string]repositorymodels.Collection)
storeState.Documents[newDatabase.ID] = make(map[string]map[string]repositorymodels.Document)
return newDatabase, repositorymodels.StatusOk
}
-118
View File
@@ -1,118 +0,0 @@
package repositories
import (
"fmt"
"log"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/resourceid"
"github.com/pikami/cosmium/parsers"
"github.com/pikami/cosmium/parsers/nosql"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
"golang.org/x/exp/maps"
)
func GetAllDocuments(databaseId string, collectionId string) ([]repositorymodels.Document, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
}
if _, ok := storeState.Collections[databaseId][collectionId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
}
return maps.Values(storeState.Documents[databaseId][collectionId]), repositorymodels.StatusOk
}
func GetDocument(databaseId string, collectionId string, documentId string) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
if _, ok := storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
return storeState.Documents[databaseId][collectionId][documentId], repositorymodels.StatusOk
}
func DeleteDocument(databaseId string, collectionId string, documentId string) repositorymodels.RepositoryStatus {
if _, ok := storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.StatusNotFound
}
delete(storeState.Documents[databaseId][collectionId], documentId)
return repositorymodels.StatusOk
}
func CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
var ok bool
var documentId string
var database repositorymodels.Database
var collection repositorymodels.Collection
if documentId, ok = document["id"].(string); !ok || documentId == "" {
documentId = fmt.Sprint(uuid.New())
document["id"] = documentId
}
if database, ok = storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if collection, ok = storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := storeState.Documents[databaseId][collectionId][documentId]; ok {
return repositorymodels.Document{}, repositorymodels.Conflict
}
document["_ts"] = time.Now().Unix()
document["_rid"] = resourceid.NewCombined(database.ResourceID, collection.ResourceID, resourceid.New())
document["_etag"] = fmt.Sprintf("\"%s\"", uuid.New())
document["_self"] = fmt.Sprintf("dbs/%s/colls/%s/docs/%s/", database.ResourceID, collection.ResourceID, document["_rid"])
storeState.Documents[databaseId][collectionId][documentId] = document
return document, repositorymodels.StatusOk
}
func ExecuteQueryDocuments(databaseId string, collectionId string, query string, queryParameters map[string]interface{}) ([]memoryexecutor.RowType, repositorymodels.RepositoryStatus) {
parsedQuery, err := nosql.Parse("", []byte(query))
if err != nil {
log.Printf("Failed to parse query: %s\nerr: %v", query, err)
return nil, repositorymodels.BadRequest
}
collectionDocuments, status := GetAllDocuments(databaseId, collectionId)
if status != repositorymodels.StatusOk {
return nil, status
}
covDocs := make([]memoryexecutor.RowType, 0)
for _, doc := range collectionDocuments {
covDocs = append(covDocs, map[string]interface{}(doc))
}
if typedQuery, ok := parsedQuery.(parsers.SelectStmt); ok {
typedQuery.Parameters = queryParameters
return memoryexecutor.Execute(typedQuery, covDocs), repositorymodels.StatusOk
}
return nil, repositorymodels.BadRequest
}
-146
View File
@@ -1,146 +0,0 @@
package repositories
import (
"encoding/json"
"log"
"os"
"reflect"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/logger"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
var storedProcedures = []repositorymodels.StoredProcedure{}
var triggers = []repositorymodels.Trigger{}
var userDefinedFunctions = []repositorymodels.UserDefinedFunction{}
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
}
if config.Config.PersistDataFilePath != "" {
stat, err := os.Stat(config.Config.PersistDataFilePath)
if err != nil {
return
}
if stat.IsDir() {
logger.Error("Argument '-Persist' must be a path to file, not a directory.")
os.Exit(1)
}
LoadStateFS(config.Config.PersistDataFilePath)
return
}
}
func LoadStateFS(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("Error reading state JSON file: %v", err)
return
}
var state repositorymodels.State
if err := json.Unmarshal(data, &state); err != nil {
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) {
data, err := json.MarshalIndent(storeState, "", "\t")
if err != nil {
logger.Errorf("Failed to save state: %v\n", err)
return
}
os.WriteFile(filePath, data, os.ModePerm)
logger.Info("Saved state:")
logger.Infof("Databases: %d\n", getLength(storeState.Databases))
logger.Infof("Collections: %d\n", getLength(storeState.Collections))
logger.Infof("Documents: %d\n", getLength(storeState.Documents))
}
func GetState() repositorymodels.State {
return storeState
}
func getLength(v interface{}) int {
switch v.(type) {
case repositorymodels.Database,
repositorymodels.Collection,
repositorymodels.Document:
return 1
}
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Map {
return -1
}
count := 0
for _, key := range rv.MapKeys() {
if rv.MapIndex(key).Kind() == reflect.Map {
count += getLength(rv.MapIndex(key).Interface())
} else {
count++
}
}
return count
}
func ensureStoreStateNoNullReferences() {
if storeState.Databases == nil {
storeState.Databases = make(map[string]repositorymodels.Database)
}
if storeState.Collections == nil {
storeState.Collections = make(map[string]map[string]repositorymodels.Collection)
}
if storeState.Documents == nil {
storeState.Documents = make(map[string]map[string]map[string]repositorymodels.Document)
}
for database := range storeState.Databases {
if storeState.Collections[database] == nil {
storeState.Collections[database] = make(map[string]repositorymodels.Collection)
}
if storeState.Documents[database] == nil {
storeState.Documents[database] = make(map[string]map[string]repositorymodels.Document)
}
for collection := range storeState.Collections[database] {
if storeState.Documents[database][collection] == nil {
storeState.Documents[database][collection] = make(map[string]repositorymodels.Document)
}
for document := range storeState.Documents[database][collection] {
if storeState.Documents[database][collection][document] == nil {
delete(storeState.Documents[database][collection], document)
}
}
}
}
}
@@ -1,7 +0,0 @@
package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models"
func GetAllStoredProcedures(databaseId string, collectionId string) ([]repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
return storedProcedures, repositorymodels.StatusOk
}
-7
View File
@@ -1,7 +0,0 @@
package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models"
func GetAllTriggers(databaseId string, collectionId string) ([]repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
return triggers, repositorymodels.StatusOk
}
@@ -1,7 +0,0 @@
package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models"
func GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
return userDefinedFunctions, repositorymodels.StatusOk
}
+64 -10
View File
@@ -3,32 +3,76 @@ package resourceid
import ( import (
"encoding/base64" "encoding/base64"
"math/rand" "math/rand"
"strings"
"github.com/google/uuid" "github.com/google/uuid"
) )
func New() string { type ResourceType int
id := uuid.New().ID()
idBytes := uintToBytes(id)
// first byte should be bigger than 0x80 for collection ids const (
// clients classify this id as "user" otherwise ResourceTypeDatabase ResourceType = iota
if (idBytes[0] & 0x80) <= 0 { ResourceTypeCollection
idBytes[0] = byte(rand.Intn(0x80) + 0x80) ResourceTypeDocument
ResourceTypeStoredProcedure
ResourceTypeTrigger
ResourceTypeUserDefinedFunction
ResourceTypeConflict
ResourceTypePartitionKeyRange
ResourceTypeSchema
)
func New(resourceType ResourceType) string {
var idBytes []byte
switch resourceType {
case ResourceTypeDatabase:
idBytes = randomBytes(4)
case ResourceTypeCollection:
idBytes = randomBytes(4)
// 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)
}
case ResourceTypeDocument:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) // Upper 4 bits = 0
case ResourceTypeStoredProcedure:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) | 0x08 // Upper 4 bits = 0x08
case ResourceTypeTrigger:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) | 0x07 // Upper 4 bits = 0x07
case ResourceTypeUserDefinedFunction:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) | 0x06 // Upper 4 bits = 0x06
case ResourceTypeConflict:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) | 0x04 // Upper 4 bits = 0x04
case ResourceTypePartitionKeyRange:
// we don't do partitions yet, so just use a fixed id
idBytes = []byte{0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x50}
case ResourceTypeSchema:
idBytes = randomBytes(8)
idBytes[7] = byte(rand.Intn(0x10)) | 0x09 // Upper 4 bits = 0x09
default:
idBytes = randomBytes(4)
} }
return base64.StdEncoding.EncodeToString(idBytes) encoded := base64.StdEncoding.EncodeToString(idBytes)
return strings.ReplaceAll(encoded, "/", "-")
} }
func NewCombined(ids ...string) string { func NewCombined(ids ...string) string {
combinedIdBytes := make([]byte, 0) combinedIdBytes := make([]byte, 0)
for _, id := range ids { for _, id := range ids {
idBytes, _ := base64.StdEncoding.DecodeString(id) idBytes, _ := base64.StdEncoding.DecodeString(strings.ReplaceAll(id, "-", "/"))
combinedIdBytes = append(combinedIdBytes, idBytes...) combinedIdBytes = append(combinedIdBytes, idBytes...)
} }
return base64.StdEncoding.EncodeToString(combinedIdBytes) encoded := base64.StdEncoding.EncodeToString(combinedIdBytes)
return strings.ReplaceAll(encoded, "/", "-")
} }
func uintToBytes(id uint32) []byte { func uintToBytes(id uint32) []byte {
@@ -39,3 +83,13 @@ func uintToBytes(id uint32) []byte {
return buf return buf
} }
func randomBytes(count int) []byte {
buf := make([]byte, count)
for i := 0; i < count; i += 4 {
id := uuid.New().ID()
idBytes := uintToBytes(id)
copy(buf[i:], idBytes)
}
return buf
}
+138
View File
@@ -0,0 +1,138 @@
package rntbd
import (
"bytes"
"encoding/binary"
)
type RntbdResponseFrame struct {
StatusCode uint16
ResourceType RntbdResourceType
ActivityId []byte
ResponseHeaders []RntbdResponseHeader
Payload []byte
}
type RntbdResponseHeader struct {
HeaderId uint16
TokenType RntbdTokenType
TokenValue any
}
type RntbdResponseFrameBuilder struct {
frame RntbdResponseFrame
}
func (b *RntbdResponseFrameBuilder) AddHeader(headerId uint16, tokenType RntbdTokenType, tokenValue any) {
b.frame.ResponseHeaders = append(b.frame.ResponseHeaders, RntbdResponseHeader{
HeaderId: headerId,
TokenType: tokenType,
TokenValue: tokenValue,
})
}
func (b *RntbdResponseFrameBuilder) AddPayload(payload []byte) {
b.frame.Payload = payload
}
func (b *RntbdResponseFrameBuilder) SetStatusCode(statusCode uint16) {
b.frame.StatusCode = statusCode
}
func (b *RntbdResponseFrameBuilder) SetResourceType(resourceType RntbdResourceType) {
b.frame.ResourceType = resourceType
}
func (b *RntbdResponseFrameBuilder) SetActivityId(activityId []byte) {
b.frame.ActivityId = activityId
}
func (b *RntbdResponseFrameBuilder) Build() *RntbdResponseFrame {
return &b.frame
}
func (f *RntbdResponseFrame) ToBytes() []byte {
var buffer bytes.Buffer
binary.Write(&buffer, binary.LittleEndian, f.StatusCode)
binary.Write(&buffer, binary.LittleEndian, uint16(f.ResourceType))
binary.Write(&buffer, binary.LittleEndian, f.ActivityId)
for _, header := range f.ResponseHeaders {
binary.Write(&buffer, binary.LittleEndian, header.HeaderId)
binary.Write(&buffer, binary.LittleEndian, uint8(header.TokenType))
switch header.TokenType {
case RntbdTokenTypeByte:
buffer.Write(header.TokenValue.([]byte))
case RntbdTokenTypeUShort:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(uint16))
case RntbdTokenTypeULong:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(uint32))
case RntbdTokenTypeLong:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(int32))
case RntbdTokenTypeULongLong:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(uint64))
case RntbdTokenTypeLongLong:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(int64))
case RntbdTokenTypeGuid:
buffer.Write(header.TokenValue.([]byte))
case RntbdTokenTypeSmallString:
binary.Write(&buffer, binary.LittleEndian, uint8(len(header.TokenValue.(string))))
buffer.WriteString(header.TokenValue.(string))
case RntbdTokenTypeString:
binary.Write(&buffer, binary.LittleEndian, uint16(len(header.TokenValue.(string))))
buffer.WriteString(header.TokenValue.(string))
case RntbdTokenTypeULongString:
binary.Write(&buffer, binary.LittleEndian, uint32(len(header.TokenValue.(string))))
buffer.WriteString(header.TokenValue.(string))
case RntbdTokenTypeSmallBytes:
binary.Write(&buffer, binary.LittleEndian, uint8(len(header.TokenValue.([]byte))))
buffer.Write(header.TokenValue.([]byte))
case RntbdTokenTypeBytes:
binary.Write(&buffer, binary.LittleEndian, uint16(len(header.TokenValue.([]byte))))
buffer.Write(header.TokenValue.([]byte))
case RntbdTokenTypeULongBytes:
binary.Write(&buffer, binary.LittleEndian, uint32(len(header.TokenValue.([]byte))))
buffer.Write(header.TokenValue.([]byte))
case RntbdTokenTypeFloat:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(float32))
case RntbdTokenTypeDouble:
binary.Write(&buffer, binary.LittleEndian, header.TokenValue.(float64))
case RntbdTokenTypeInvalid:
panic("invalid token type")
default:
panic("invalid token type")
}
}
payloadSize := uint32(0)
if len(f.Payload) > 0 {
payloadSize = uint32(len(f.Payload)) + 4
}
frameSize := uint32(buffer.Len()) + 4
result := make([]byte, frameSize+payloadSize)
binary.LittleEndian.PutUint32(result, frameSize)
copy(result[4:], buffer.Bytes())
if len(f.Payload) > 0 {
binary.LittleEndian.PutUint32(result[frameSize:], payloadSize-4)
copy(result[frameSize+4:], f.Payload)
}
return result
}
func buildContextFrame(requestFrame *RntbdFrame) []byte {
builder := RntbdResponseFrameBuilder{}
builder.SetStatusCode(200)
builder.SetResourceType(RntbdResourceTypeConnection)
builder.SetActivityId(requestFrame.ActivityId)
builder.AddHeader(uint16(RntbdContextHeaderServerAgent), RntbdTokenTypeSmallString, "DocumentDB Server")
builder.AddHeader(uint16(RntbdContextHeaderServerVersion), RntbdTokenTypeSmallString, " version=2.14.0.0")
builder.AddHeader(uint16(RntbdContextHeaderIdleTimeoutInSeconds), RntbdTokenTypeULong, uint32(120))
builder.AddHeader(uint16(RntbdContextHeaderUnauthenticatedTimeoutInSeconds), RntbdTokenTypeULong, uint32(25))
return builder.Build().ToBytes()
}
+746
View File
@@ -0,0 +1,746 @@
package rntbd
import (
"fmt"
"github.com/pikami/cosmium/api/headers"
)
type RntbdOperationType uint16
const (
RntbdOperationTypeConnection RntbdOperationType = 0x0000
RntbdOperationTypeCreate RntbdOperationType = 0x0001
RntbdOperationTypeUpdate RntbdOperationType = 0x0002
RntbdOperationTypeRead RntbdOperationType = 0x0003
RntbdOperationTypeReadFeed RntbdOperationType = 0x0004
RntbdOperationTypeDelete RntbdOperationType = 0x0005
RntbdOperationTypeReplace RntbdOperationType = 0x0006
RntbdOperationTypeExecuteJavaScript RntbdOperationType = 0x0008
RntbdOperationTypeSQLQuery RntbdOperationType = 0x0009
RntbdOperationTypePause RntbdOperationType = 0x000A
RntbdOperationTypeResume RntbdOperationType = 0x000B
RntbdOperationTypeStop RntbdOperationType = 0x000C
RntbdOperationTypeRecycle RntbdOperationType = 0x000D
RntbdOperationTypeCrash RntbdOperationType = 0x000E
RntbdOperationTypeQuery RntbdOperationType = 0x000F
RntbdOperationTypeForceConfigRefresh RntbdOperationType = 0x0010
RntbdOperationTypeHead RntbdOperationType = 0x0011
RntbdOperationTypeHeadFeed RntbdOperationType = 0x0012
RntbdOperationTypeUpsert RntbdOperationType = 0x0013
RntbdOperationTypeRecreate RntbdOperationType = 0x0014
RntbdOperationTypeThrottle RntbdOperationType = 0x0015
RntbdOperationTypeGetSplitPoint RntbdOperationType = 0x0016
RntbdOperationTypePreCreateValidation RntbdOperationType = 0x0017
RntbdOperationTypeBatchApply RntbdOperationType = 0x0018
RntbdOperationTypeAbortSplit RntbdOperationType = 0x0019
RntbdOperationTypeCompleteSplit RntbdOperationType = 0x001A
RntbdOperationTypeOfferUpdateOperation RntbdOperationType = 0x001B
RntbdOperationTypeOfferPreGrowValidation RntbdOperationType = 0x001C
RntbdOperationTypeBatchReportThroughputUtilization RntbdOperationType = 0x001D
RntbdOperationTypeCompletePartitionMigration RntbdOperationType = 0x001E
RntbdOperationTypeAbortPartitionMigration RntbdOperationType = 0x001F
RntbdOperationTypePreReplaceValidation RntbdOperationType = 0x0020
RntbdOperationTypeAddComputeGatewayRequestCharges RntbdOperationType = 0x0021
RntbdOperationTypeMigratePartition RntbdOperationType = 0x0022
)
type RntbdResourceType uint16
const (
RntbdResourceTypeConnection RntbdResourceType = 0x0000
RntbdResourceTypeDatabase RntbdResourceType = 0x0001
RntbdResourceTypeCollection RntbdResourceType = 0x0002
RntbdResourceTypeDocument RntbdResourceType = 0x0003
RntbdResourceTypeAttachment RntbdResourceType = 0x0004
RntbdResourceTypeUser RntbdResourceType = 0x0005
RntbdResourceTypePermission RntbdResourceType = 0x0006
RntbdResourceTypeStoredProcedure RntbdResourceType = 0x0007
RntbdResourceTypeConflict RntbdResourceType = 0x0008
RntbdResourceTypeTrigger RntbdResourceType = 0x0009
RntbdResourceTypeUserDefinedFunction RntbdResourceType = 0x000A
RntbdResourceTypeModule RntbdResourceType = 0x000B
RntbdResourceTypeReplica RntbdResourceType = 0x000C
RntbdResourceTypeModuleCommand RntbdResourceType = 0x000D
RntbdResourceTypeRecord RntbdResourceType = 0x000E
RntbdResourceTypeOffer RntbdResourceType = 0x000F
RntbdResourceTypePartitionSetInformation RntbdResourceType = 0x0010
RntbdResourceTypeXPReplicatorAddress RntbdResourceType = 0x0011
RntbdResourceTypeMasterPartition RntbdResourceType = 0x0012
RntbdResourceTypeServerPartition RntbdResourceType = 0x0013
RntbdResourceTypeDatabaseAccount RntbdResourceType = 0x0014
RntbdResourceTypeTopology RntbdResourceType = 0x0015
RntbdResourceTypePartitionKeyRange RntbdResourceType = 0x0016
RntbdResourceTypeSchema RntbdResourceType = 0x0018
RntbdResourceTypeBatchApply RntbdResourceType = 0x0019
RntbdResourceTypeRestoreMetadata RntbdResourceType = 0x001A
RntbdResourceTypeComputeGatewayCharges RntbdResourceType = 0x001B
RntbdResourceTypeRidRange RntbdResourceType = 0x001C
RntbdResourceTypeUserDefinedType RntbdResourceType = 0x001D
)
type RntbdRequestHeader uint16
const (
RntbdRequestHeaderResourceId RntbdRequestHeader = 0x0000 // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderAuthorizationToken RntbdRequestHeader = 0x0001 // RntbdTokenType.String, required = false
RntbdRequestHeaderPayloadPresent RntbdRequestHeader = 0x0002 // RntbdTokenType.Byte, required = true
RntbdRequestHeaderDate RntbdRequestHeader = 0x0003 // RntbdTokenType.SmallString, required = false
RntbdRequestHeaderPageSize RntbdRequestHeader = 0x0004 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderSessionToken RntbdRequestHeader = 0x0005 // RntbdTokenType.String, required = false
RntbdRequestHeaderContinuationToken RntbdRequestHeader = 0x0006 // RntbdTokenType.String, required = false
RntbdRequestHeaderIndexingDirective RntbdRequestHeader = 0x0007 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderMatch RntbdRequestHeader = 0x0008 // RntbdTokenType.String, required = false
RntbdRequestHeaderPreTriggerInclude RntbdRequestHeader = 0x0009 // RntbdTokenType.String, required = false
RntbdRequestHeaderPostTriggerInclude RntbdRequestHeader = 0x000A // RntbdTokenType.String, required = false
RntbdRequestHeaderIsFanout RntbdRequestHeader = 0x000B // RntbdTokenType.Byte, required = false
RntbdRequestHeaderCollectionPartitionIndex RntbdRequestHeader = 0x000C // RntbdTokenType.ULong, required = false
RntbdRequestHeaderCollectionServiceIndex RntbdRequestHeader = 0x000D // RntbdTokenType.ULong, required = false
RntbdRequestHeaderPreTriggerExclude RntbdRequestHeader = 0x000E // RntbdTokenType.String, required = false
RntbdRequestHeaderPostTriggerExclude RntbdRequestHeader = 0x000F // RntbdTokenType.String, required = false
RntbdRequestHeaderConsistencyLevel RntbdRequestHeader = 0x0010 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderEntityId RntbdRequestHeader = 0x0011 // RntbdTokenType.String, required = false
RntbdRequestHeaderResourceSchemaName RntbdRequestHeader = 0x0012 // RntbdTokenType.SmallString, required = false
RntbdRequestHeaderReplicaPath RntbdRequestHeader = 0x0013 // RntbdTokenType.String, required = true
RntbdRequestHeaderResourceTokenExpiry RntbdRequestHeader = 0x0014 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderDatabaseName RntbdRequestHeader = 0x0015 // RntbdTokenType.String, required = false
RntbdRequestHeaderCollectionName RntbdRequestHeader = 0x0016 // RntbdTokenType.String, required = false
RntbdRequestHeaderDocumentName RntbdRequestHeader = 0x0017 // RntbdTokenType.String, required = false
RntbdRequestHeaderAttachmentName RntbdRequestHeader = 0x0018 // RntbdTokenType.String, required = false
RntbdRequestHeaderUserName RntbdRequestHeader = 0x0019 // RntbdTokenType.String, required = false
RntbdRequestHeaderPermissionName RntbdRequestHeader = 0x001A // RntbdTokenType.String, required = false
RntbdRequestHeaderStoredProcedureName RntbdRequestHeader = 0x001B // RntbdTokenType.String, required = false
RntbdRequestHeaderUserDefinedFunctionName RntbdRequestHeader = 0x001C // RntbdTokenType.String, required = false
RntbdRequestHeaderTriggerName RntbdRequestHeader = 0x001D // RntbdTokenType.String, required = false
RntbdRequestHeaderEnableScanInQuery RntbdRequestHeader = 0x001E // RntbdTokenType.Byte, required = false
RntbdRequestHeaderEmitVerboseTracesInQuery RntbdRequestHeader = 0x001F // RntbdTokenType.Byte, required = false
RntbdRequestHeaderConflictName RntbdRequestHeader = 0x0020 // RntbdTokenType.String, required = false
RntbdRequestHeaderBindReplicaDirective RntbdRequestHeader = 0x0021 // RntbdTokenType.String, required = false
RntbdRequestHeaderPrimaryMasterKey RntbdRequestHeader = 0x0022 // RntbdTokenType.String, required = false
RntbdRequestHeaderSecondaryMasterKey RntbdRequestHeader = 0x0023 // RntbdTokenType.String, required = false
RntbdRequestHeaderPrimaryReadonlyKey RntbdRequestHeader = 0x0024 // RntbdTokenType.String, required = false
RntbdRequestHeaderSecondaryReadonlyKey RntbdRequestHeader = 0x0025 // RntbdTokenType.String, required = false
RntbdRequestHeaderProfileRequest RntbdRequestHeader = 0x0026 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderEnableLowPrecisionOrderBy RntbdRequestHeader = 0x0027 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderClientVersion RntbdRequestHeader = 0x0028 // RntbdTokenType.SmallString, required = false
RntbdRequestHeaderCanCharge RntbdRequestHeader = 0x0029 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderCanThrottle RntbdRequestHeader = 0x002A // RntbdTokenType.Byte, required = false
RntbdRequestHeaderPartitionKey RntbdRequestHeader = 0x002B // RntbdTokenType.String, required = false
RntbdRequestHeaderPartitionKeyRangeId RntbdRequestHeader = 0x002C // RntbdTokenType.String, required = false
RntbdRequestHeaderNotUsed2D RntbdRequestHeader = 0x002D // RntbdTokenType.Invalid, required = false
RntbdRequestHeaderNotUsed2E RntbdRequestHeader = 0x002E // RntbdTokenType.Invalid, required = false
RntbdRequestHeaderNotUsed2F RntbdRequestHeader = 0x002F // RntbdTokenType.Invalid, required = false
RntbdRequestHeaderMigrateCollectionDirective RntbdRequestHeader = 0x0031 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderNotUsed32 RntbdRequestHeader = 0x0032 // RntbdTokenType.Invalid, required = false
RntbdRequestHeaderSupportSpatialLegacyCoordinates RntbdRequestHeader = 0x0033 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderPartitionCount RntbdRequestHeader = 0x0034 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderCollectionRid RntbdRequestHeader = 0x0035 // RntbdTokenType.String, required = false
RntbdRequestHeaderPartitionKeyRangeName RntbdRequestHeader = 0x0036 // RntbdTokenType.String, required = false
RntbdRequestHeaderSchemaName RntbdRequestHeader = 0x003A // RntbdTokenType.String, required = false
RntbdRequestHeaderFilterBySchemaRid RntbdRequestHeader = 0x003B // RntbdTokenType.String, required = false
RntbdRequestHeaderUsePolygonsSmallerThanAHemisphere RntbdRequestHeader = 0x003C // RntbdTokenType.Byte, required = false
RntbdRequestHeaderGatewaySignature RntbdRequestHeader = 0x003D // RntbdTokenType.String, required = false
RntbdRequestHeaderEnableLogging RntbdRequestHeader = 0x003E // RntbdTokenType.Byte, required = false
RntbdRequestHeaderAIM RntbdRequestHeader = 0x003F // RntbdTokenType.String, required = false
RntbdRequestHeaderPopulateQuotaInfo RntbdRequestHeader = 0x0040 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderDisableRUPerMinuteUsage RntbdRequestHeader = 0x0041 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderPopulateQueryMetrics RntbdRequestHeader = 0x0042 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderResponseContinuationTokenLimitInKb RntbdRequestHeader = 0x0043 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderPopulatePartitionStatistics RntbdRequestHeader = 0x0044 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderRemoteStorageType RntbdRequestHeader = 0x0045 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderCollectionRemoteStorageSecurityIdentifier RntbdRequestHeader = 0x0046 // RntbdTokenType.String, required = false
RntbdRequestHeaderIfModifiedSince RntbdRequestHeader = 0x0047 // RntbdTokenType.String, required = false
RntbdRequestHeaderPopulateCollectionThroughputInfo RntbdRequestHeader = 0x0048 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderRemainingTimeInMsOnClientRequest RntbdRequestHeader = 0x0049 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderClientRetryAttemptCount RntbdRequestHeader = 0x004A // RntbdTokenType.ULong, required = false
RntbdRequestHeaderTargetLsn RntbdRequestHeader = 0x004B // RntbdTokenType.LongLong, required = false
RntbdRequestHeaderTargetGlobalCommittedLsn RntbdRequestHeader = 0x004C // RntbdTokenType.LongLong, required = false
RntbdRequestHeaderTransportRequestID RntbdRequestHeader = 0x004D // RntbdTokenType.ULong, required = false
RntbdRequestHeaderRestoreMetadaFilter RntbdRequestHeader = 0x004E // RntbdTokenType.String, required = false
RntbdRequestHeaderRestoreParams RntbdRequestHeader = 0x004F // RntbdTokenType.String, required = false
RntbdRequestHeaderShareThroughput RntbdRequestHeader = 0x0050 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderPartitionResourceFilter RntbdRequestHeader = 0x0051 // RntbdTokenType.String, required = false
RntbdRequestHeaderIsReadOnlyScript RntbdRequestHeader = 0x0052 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderIsAutoScaleRequest RntbdRequestHeader = 0x0053 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderForceQueryScan RntbdRequestHeader = 0x0054 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderCanOfferReplaceComplete RntbdRequestHeader = 0x0056 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderExcludeSystemProperties RntbdRequestHeader = 0x0057 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderBinaryId RntbdRequestHeader = 0x0058 // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderTimeToLiveInSeconds RntbdRequestHeader = 0x0059 // RntbdTokenType.Long, required = false
RntbdRequestHeaderEffectivePartitionKey RntbdRequestHeader = 0x005A // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderBinaryPassthroughRequest RntbdRequestHeader = 0x005B // RntbdTokenType.Byte, required = false
RntbdRequestHeaderUserDefinedTypeName RntbdRequestHeader = 0x005C // RntbdTokenType.String, required = false
RntbdRequestHeaderEnableDynamicRidRangeAllocation RntbdRequestHeader = 0x005D // RntbdTokenType.Byte, required = false
RntbdRequestHeaderEnumerationDirection RntbdRequestHeader = 0x005E // RntbdTokenType.Byte, required = false
RntbdRequestHeaderStartId RntbdRequestHeader = 0x005F // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderEndId RntbdRequestHeader = 0x0060 // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderFanoutOperationState RntbdRequestHeader = 0x0061 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderStartEpk RntbdRequestHeader = 0x0062 // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderEndEpk RntbdRequestHeader = 0x0063 // RntbdTokenType.Bytes, required = false
RntbdRequestHeaderReadFeedKeyType RntbdRequestHeader = 0x0064 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderContentSerializationFormat RntbdRequestHeader = 0x0065 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderAllowTentativeWrites RntbdRequestHeader = 0x0066 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderIsUserRequest RntbdRequestHeader = 0x0067 // RntbdTokenType.Byte, required = false
RntbdRequestHeaderSharedOfferThroughput RntbdRequestHeader = 0x0068 // RntbdTokenType.ULong, required = false
RntbdRequestHeaderSDKSupportedCapabilities RntbdRequestHeader = 0x00A2 // RntbdTokenType.ULong, required = ?
)
type RntbdResponseHeaderType uint16
const (
RntbdResponseHeaderPayloadPresent RntbdResponseHeaderType = 0x0000 // RntbdTokenType.Byte, required = true
RntbdResponseHeaderLastStateChangeDateTime RntbdResponseHeaderType = 0x0002 // RntbdTokenType.SmallString, required = false
RntbdResponseHeaderContinuationToken RntbdResponseHeaderType = 0x0003 // RntbdTokenType.String, required = false
RntbdResponseHeaderETag RntbdResponseHeaderType = 0x0004 // RntbdTokenType.String, required = false
RntbdResponseHeaderReadsPerformed RntbdResponseHeaderType = 0x0007 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderWritesPerformed RntbdResponseHeaderType = 0x0008 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderQueriesPerformed RntbdResponseHeaderType = 0x0009 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderIndexTermsGenerated RntbdResponseHeaderType = 0x000A // RntbdTokenType.ULong, required = false
RntbdResponseHeaderScriptsExecuted RntbdResponseHeaderType = 0x000B // RntbdTokenType.ULong, required = false
RntbdResponseHeaderRetryAfterMilliseconds RntbdResponseHeaderType = 0x000C // RntbdTokenType.ULong, required = false
RntbdResponseHeaderIndexingDirective RntbdResponseHeaderType = 0x000D // RntbdTokenType.Byte, required = false
RntbdResponseHeaderStorageMaxResoureQuota RntbdResponseHeaderType = 0x000E // RntbdTokenType.String, required = false
RntbdResponseHeaderStorageResourceQuotaUsage RntbdResponseHeaderType = 0x000F // RntbdTokenType.String, required = false
RntbdResponseHeaderSchemaVersion RntbdResponseHeaderType = 0x0010 // RntbdTokenType.SmallString, required = false
RntbdResponseHeaderCollectionPartitionIndex RntbdResponseHeaderType = 0x0011 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderCollectionServiceIndex RntbdResponseHeaderType = 0x0012 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderLSN RntbdResponseHeaderType = 0x0013 // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderItemCount RntbdResponseHeaderType = 0x0014 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderRequestCharge RntbdResponseHeaderType = 0x0015 // RntbdTokenType.Double, required = false
RntbdResponseHeaderOwnerFullName RntbdResponseHeaderType = 0x0017 // RntbdTokenType.String, required = false
RntbdResponseHeaderOwnerId RntbdResponseHeaderType = 0x0018 // RntbdTokenType.String, required = false
RntbdResponseHeaderDatabaseAccountId RntbdResponseHeaderType = 0x0019 // RntbdTokenType.String, required = false
RntbdResponseHeaderQuorumAckedLSN RntbdResponseHeaderType = 0x001A // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderRequestValidationFailure RntbdResponseHeaderType = 0x001B // RntbdTokenType.Byte, required = false
RntbdResponseHeaderSubStatus RntbdResponseHeaderType = 0x001C // RntbdTokenType.ULong, required = false
RntbdResponseHeaderCollectionUpdateProgress RntbdResponseHeaderType = 0x001D // RntbdTokenType.ULong, required = false
RntbdResponseHeaderCurrentWriteQuorum RntbdResponseHeaderType = 0x001E // RntbdTokenType.ULong, required = false
RntbdResponseHeaderCurrentReplicaSetSize RntbdResponseHeaderType = 0x001F // RntbdTokenType.ULong, required = false
RntbdResponseHeaderCollectionLazyIndexProgress RntbdResponseHeaderType = 0x0020 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderPartitionKeyRangeId RntbdResponseHeaderType = 0x0021 // RntbdTokenType.String, required = false
RntbdResponseHeaderLogResults RntbdResponseHeaderType = 0x0025 // RntbdTokenType.String, required = false
RntbdResponseHeaderXPRole RntbdResponseHeaderType = 0x0026 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderIsRUPerMinuteUsed RntbdResponseHeaderType = 0x0027 // RntbdTokenType.Byte, required = false
RntbdResponseHeaderQueryMetrics RntbdResponseHeaderType = 0x0028 // RntbdTokenType.String, required = false
RntbdResponseHeaderGlobalCommittedLSN RntbdResponseHeaderType = 0x0029 // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderNumberOfReadRegions RntbdResponseHeaderType = 0x0030 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderOfferReplacePending RntbdResponseHeaderType = 0x0031 // RntbdTokenType.Byte, required = false
RntbdResponseHeaderItemLSN RntbdResponseHeaderType = 0x0032 // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderRestoreState RntbdResponseHeaderType = 0x0033 // RntbdTokenType.String, required = false
RntbdResponseHeaderCollectionSecurityIdentifier RntbdResponseHeaderType = 0x0034 // RntbdTokenType.String, required = false
RntbdResponseHeaderTransportRequestID RntbdResponseHeaderType = 0x0035 // RntbdTokenType.ULong, required = false
RntbdResponseHeaderShareThroughput RntbdResponseHeaderType = 0x0036 // RntbdTokenType.Byte, required = false
RntbdResponseHeaderDisableRntbdChannel RntbdResponseHeaderType = 0x0038 // RntbdTokenType.Byte, required = false
RntbdResponseHeaderServerDateTimeUtc RntbdResponseHeaderType = 0x0039 // RntbdTokenType.SmallString, required = false
RntbdResponseHeaderLocalLSN RntbdResponseHeaderType = 0x003A // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderQuorumAckedLocalLSN RntbdResponseHeaderType = 0x003B // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderItemLocalLSN RntbdResponseHeaderType = 0x003C // RntbdTokenType.LongLong, required = false
RntbdResponseHeaderHasTentativeWrites RntbdResponseHeaderType = 0x003D // RntbdTokenType.Byte, required = false
RntbdResponseHeaderSessionToken RntbdResponseHeaderType = 0x003E // RntbdTokenType.String, required = false
)
type RntbdContextHeader uint16
const (
RntbdContextHeaderProtocolVersion RntbdContextHeader = 0x0000 // RntbdTokenType.ULong, required = false
RntbdContextHeaderClientVersion RntbdContextHeader = 0x0001 // RntbdTokenType.SmallString, required = false
RntbdContextHeaderServerAgent RntbdContextHeader = 0x0002 // RntbdTokenType.SmallString, required = true
RntbdContextHeaderServerVersion RntbdContextHeader = 0x0003 // RntbdTokenType.SmallString, required = true
RntbdContextHeaderIdleTimeoutInSeconds RntbdContextHeader = 0x0004 // RntbdTokenType.ULong, required = false
RntbdContextHeaderUnauthenticatedTimeoutInSeconds RntbdContextHeader = 0x0005 // RntbdTokenType.ULong, required = false
)
type RntbdTokenType uint8
const (
RntbdTokenTypeByte RntbdTokenType = 0x00 // 8bit boolean
RntbdTokenTypeUShort RntbdTokenType = 0x01 // 16bit unsigned integer
RntbdTokenTypeULong RntbdTokenType = 0x02 // 32bit unsigned integer
RntbdTokenTypeLong RntbdTokenType = 0x03 // 32bit signed integer
RntbdTokenTypeULongLong RntbdTokenType = 0x04 // 64bit unsigned integer
RntbdTokenTypeLongLong RntbdTokenType = 0x05 // 64bit signed integer
RntbdTokenTypeGuid RntbdTokenType = 0x06 // 128bit GUID
RntbdTokenTypeSmallString RntbdTokenType = 0x07 // 8bit len + string
RntbdTokenTypeString RntbdTokenType = 0x08 // 16bit len + string
RntbdTokenTypeULongString RntbdTokenType = 0x09 // 32bit len + string
RntbdTokenTypeSmallBytes RntbdTokenType = 0x0A // 8bit len + bytes
RntbdTokenTypeBytes RntbdTokenType = 0x0B // 16bit len + bytes
RntbdTokenTypeULongBytes RntbdTokenType = 0x0C // 32bit len + bytes
RntbdTokenTypeFloat RntbdTokenType = 0x0D // 32bit float
RntbdTokenTypeDouble RntbdTokenType = 0x0E // 64bit double
RntbdTokenTypeInvalid RntbdTokenType = 0x0F // Invalid token type
)
func (h RntbdRequestHeader) String() string {
switch h {
case RntbdRequestHeaderResourceId:
return "RntbdRequestHeaderResourceId"
case RntbdRequestHeaderAuthorizationToken:
return headers.Authorization
case RntbdRequestHeaderPayloadPresent:
return "RntbdRequestHeaderPayloadPresent"
case RntbdRequestHeaderDate:
return headers.XDate
case RntbdRequestHeaderPageSize:
return "RntbdRequestHeaderPageSize"
case RntbdRequestHeaderSessionToken:
return "RntbdRequestHeaderSessionToken"
case RntbdRequestHeaderContinuationToken:
return "RntbdRequestHeaderContinuationToken"
case RntbdRequestHeaderIndexingDirective:
return "RntbdRequestHeaderIndexingDirective"
case RntbdRequestHeaderMatch:
return "RntbdRequestHeaderMatch"
case RntbdRequestHeaderPreTriggerInclude:
return "RntbdRequestHeaderPreTriggerInclude"
case RntbdRequestHeaderPostTriggerInclude:
return "RntbdRequestHeaderPostTriggerInclude"
case RntbdRequestHeaderIsFanout:
return "RntbdRequestHeaderIsFanout"
case RntbdRequestHeaderCollectionPartitionIndex:
return "RntbdRequestHeaderCollectionPartitionIndex"
case RntbdRequestHeaderCollectionServiceIndex:
return "RntbdRequestHeaderCollectionServiceIndex"
case RntbdRequestHeaderPreTriggerExclude:
return "RntbdRequestHeaderPreTriggerExclude"
case RntbdRequestHeaderPostTriggerExclude:
return "RntbdRequestHeaderPostTriggerExclude"
case RntbdRequestHeaderConsistencyLevel:
return headers.ConsistencyLevel
case RntbdRequestHeaderEntityId:
return "RntbdRequestHeaderEntityId"
case RntbdRequestHeaderResourceSchemaName:
return "RntbdRequestHeaderResourceSchemaName"
case RntbdRequestHeaderReplicaPath:
return "RntbdRequestHeaderReplicaPath"
case RntbdRequestHeaderResourceTokenExpiry:
return "RntbdRequestHeaderResourceTokenExpiry"
case RntbdRequestHeaderDatabaseName:
return "RntbdRequestHeaderDatabaseName"
case RntbdRequestHeaderCollectionName:
return "RntbdRequestHeaderCollectionName"
case RntbdRequestHeaderDocumentName:
return "RntbdRequestHeaderDocumentName"
case RntbdRequestHeaderAttachmentName:
return "RntbdRequestHeaderAttachmentName"
case RntbdRequestHeaderUserName:
return "RntbdRequestHeaderUserName"
case RntbdRequestHeaderPermissionName:
return "RntbdRequestHeaderPermissionName"
case RntbdRequestHeaderStoredProcedureName:
return "RntbdRequestHeaderStoredProcedureName"
case RntbdRequestHeaderUserDefinedFunctionName:
return "RntbdRequestHeaderUserDefinedFunctionName"
case RntbdRequestHeaderTriggerName:
return "RntbdRequestHeaderTriggerName"
case RntbdRequestHeaderEnableScanInQuery:
return "RntbdRequestHeaderEnableScanInQuery"
case RntbdRequestHeaderEmitVerboseTracesInQuery:
return "RntbdRequestHeaderEmitVerboseTracesInQuery"
case RntbdRequestHeaderConflictName:
return "RntbdRequestHeaderConflictName"
case RntbdRequestHeaderBindReplicaDirective:
return "RntbdRequestHeaderBindReplicaDirective"
case RntbdRequestHeaderPrimaryMasterKey:
return "RntbdRequestHeaderPrimaryMasterKey"
case RntbdRequestHeaderSecondaryMasterKey:
return "RntbdRequestHeaderSecondaryMasterKey"
case RntbdRequestHeaderPrimaryReadonlyKey:
return "RntbdRequestHeaderPrimaryReadonlyKey"
case RntbdRequestHeaderSecondaryReadonlyKey:
return "RntbdRequestHeaderSecondaryReadonlyKey"
case RntbdRequestHeaderProfileRequest:
return "RntbdRequestHeaderProfileRequest"
case RntbdRequestHeaderEnableLowPrecisionOrderBy:
return "RntbdRequestHeaderEnableLowPrecisionOrderBy"
case RntbdRequestHeaderClientVersion:
return "RntbdRequestHeaderClientVersion"
case RntbdRequestHeaderCanCharge:
return "RntbdRequestHeaderCanCharge"
case RntbdRequestHeaderCanThrottle:
return "RntbdRequestHeaderCanThrottle"
case RntbdRequestHeaderPartitionKey:
return "RntbdRequestHeaderPartitionKey"
case RntbdRequestHeaderPartitionKeyRangeId:
return "RntbdRequestHeaderPartitionKeyRangeId"
case RntbdRequestHeaderNotUsed2D:
return "RntbdRequestHeaderNotUsed2D"
case RntbdRequestHeaderNotUsed2E:
return "RntbdRequestHeaderNotUsed2E"
case RntbdRequestHeaderNotUsed2F:
return "RntbdRequestHeaderNotUsed2F"
case RntbdRequestHeaderMigrateCollectionDirective:
return "RntbdRequestHeaderMigrateCollectionDirective"
case RntbdRequestHeaderNotUsed32:
return "RntbdRequestHeaderNotUsed32"
case RntbdRequestHeaderSupportSpatialLegacyCoordinates:
return "RntbdRequestHeaderSupportSpatialLegacyCoordinates"
case RntbdRequestHeaderPartitionCount:
return "RntbdRequestHeaderPartitionCount"
case RntbdRequestHeaderCollectionRid:
return "RntbdRequestHeaderCollectionRid"
case RntbdRequestHeaderPartitionKeyRangeName:
return "RntbdRequestHeaderPartitionKeyRangeName"
case RntbdRequestHeaderSchemaName:
return "RntbdRequestHeaderSchemaName"
case RntbdRequestHeaderFilterBySchemaRid:
return "RntbdRequestHeaderFilterBySchemaRid"
case RntbdRequestHeaderUsePolygonsSmallerThanAHemisphere:
return "RntbdRequestHeaderUsePolygonsSmallerThanAHemisphere"
case RntbdRequestHeaderGatewaySignature:
return "RntbdRequestHeaderGatewaySignature"
case RntbdRequestHeaderEnableLogging:
return "RntbdRequestHeaderEnableLogging"
case RntbdRequestHeaderAIM:
return headers.AIM
case RntbdRequestHeaderPopulateQuotaInfo:
return "RntbdRequestHeaderPopulateQuotaInfo"
case RntbdRequestHeaderDisableRUPerMinuteUsage:
return "RntbdRequestHeaderDisableRUPerMinuteUsage"
case RntbdRequestHeaderPopulateQueryMetrics:
return "RntbdRequestHeaderPopulateQueryMetrics"
case RntbdRequestHeaderResponseContinuationTokenLimitInKb:
return "RntbdRequestHeaderResponseContinuationTokenLimitInKb"
case RntbdRequestHeaderPopulatePartitionStatistics:
return "RntbdRequestHeaderPopulatePartitionStatistics"
case RntbdRequestHeaderRemoteStorageType:
return "RntbdRequestHeaderRemoteStorageType"
case RntbdRequestHeaderCollectionRemoteStorageSecurityIdentifier:
return "RntbdRequestHeaderCollectionRemoteStorageSecurityIdentifier"
case RntbdRequestHeaderIfModifiedSince:
return "RntbdRequestHeaderIfModifiedSince"
case RntbdRequestHeaderPopulateCollectionThroughputInfo:
return "RntbdRequestHeaderPopulateCollectionThroughputInfo"
case RntbdRequestHeaderRemainingTimeInMsOnClientRequest:
return headers.RemainingTimeInMsOnClient
case RntbdRequestHeaderClientRetryAttemptCount:
return headers.ClientRetryAttemptCount
case RntbdRequestHeaderTargetLsn:
return "RntbdRequestHeaderTargetLsn"
case RntbdRequestHeaderTargetGlobalCommittedLsn:
return "RntbdRequestHeaderTargetGlobalCommittedLsn"
case RntbdRequestHeaderTransportRequestID:
return "RntbdRequestHeaderTransportRequestID"
case RntbdRequestHeaderRestoreMetadaFilter:
return "RntbdRequestHeaderRestoreMetadaFilter"
case RntbdRequestHeaderRestoreParams:
return "RntbdRequestHeaderRestoreParams"
case RntbdRequestHeaderShareThroughput:
return "RntbdRequestHeaderShareThroughput"
case RntbdRequestHeaderPartitionResourceFilter:
return "RntbdRequestHeaderPartitionResourceFilter"
case RntbdRequestHeaderIsReadOnlyScript:
return "RntbdRequestHeaderIsReadOnlyScript"
case RntbdRequestHeaderIsAutoScaleRequest:
return "RntbdRequestHeaderIsAutoScaleRequest"
case RntbdRequestHeaderForceQueryScan:
return "RntbdRequestHeaderForceQueryScan"
case RntbdRequestHeaderCanOfferReplaceComplete:
return "RntbdRequestHeaderCanOfferReplaceComplete"
case RntbdRequestHeaderExcludeSystemProperties:
return "RntbdRequestHeaderExcludeSystemProperties"
case RntbdRequestHeaderBinaryId:
return "RntbdRequestHeaderBinaryId"
case RntbdRequestHeaderTimeToLiveInSeconds:
return "RntbdRequestHeaderTimeToLiveInSeconds"
case RntbdRequestHeaderEffectivePartitionKey:
return "RntbdRequestHeaderEffectivePartitionKey"
case RntbdRequestHeaderBinaryPassthroughRequest:
return "RntbdRequestHeaderBinaryPassthroughRequest"
case RntbdRequestHeaderUserDefinedTypeName:
return "RntbdRequestHeaderUserDefinedTypeName"
case RntbdRequestHeaderEnableDynamicRidRangeAllocation:
return "RntbdRequestHeaderEnableDynamicRidRangeAllocation"
case RntbdRequestHeaderEnumerationDirection:
return "RntbdRequestHeaderEnumerationDirection"
case RntbdRequestHeaderStartId:
return "RntbdRequestHeaderStartId"
case RntbdRequestHeaderEndId:
return "RntbdRequestHeaderEndId"
case RntbdRequestHeaderFanoutOperationState:
return "RntbdRequestHeaderFanoutOperationState"
case RntbdRequestHeaderStartEpk:
return "RntbdRequestHeaderStartEpk"
case RntbdRequestHeaderEndEpk:
return "RntbdRequestHeaderEndEpk"
case RntbdRequestHeaderReadFeedKeyType:
return "RntbdRequestHeaderReadFeedKeyType"
case RntbdRequestHeaderContentSerializationFormat:
return "RntbdRequestHeaderContentSerializationFormat"
case RntbdRequestHeaderAllowTentativeWrites:
return "RntbdRequestHeaderAllowTentativeWrites"
case RntbdRequestHeaderIsUserRequest:
return "RntbdRequestHeaderIsUserRequest"
case RntbdRequestHeaderSharedOfferThroughput:
return "RntbdRequestHeaderSharedOfferThroughput"
case RntbdRequestHeaderSDKSupportedCapabilities:
return headers.SupportedCapabilities
}
return fmt.Sprintf("RntbdRequestHeader(%d)", h)
}
func (h RntbdContextHeader) String() string {
switch h {
case RntbdContextHeaderProtocolVersion:
return "RntbdContextHeaderProtocolVersion"
case RntbdContextHeaderClientVersion:
return "RntbdContextHeaderClientVersion"
case RntbdContextHeaderServerAgent:
return "RntbdContextHeaderServerAgent"
case RntbdContextHeaderServerVersion:
return "RntbdContextHeaderServerVersion"
case RntbdContextHeaderIdleTimeoutInSeconds:
return "RntbdContextHeaderIdleTimeoutInSeconds"
case RntbdContextHeaderUnauthenticatedTimeoutInSeconds:
return "RntbdContextHeaderUnauthenticatedTimeoutInSeconds"
}
return fmt.Sprintf("RntbdContextHeader(%d)", h)
}
func (h RntbdResponseHeaderType) String() string {
switch h {
case RntbdResponseHeaderPayloadPresent:
return "PayloadPresent"
case RntbdResponseHeaderLastStateChangeDateTime:
return "LastStateChangeDateTime"
case RntbdResponseHeaderContinuationToken:
return "ContinuationToken"
case RntbdResponseHeaderETag:
return "ETag"
case RntbdResponseHeaderReadsPerformed:
return "ReadsPerformed"
case RntbdResponseHeaderWritesPerformed:
return "WritesPerformed"
case RntbdResponseHeaderQueriesPerformed:
return "QueriesPerformed"
case RntbdResponseHeaderIndexTermsGenerated:
return "IndexTermsGenerated"
case RntbdResponseHeaderScriptsExecuted:
return "ScriptsExecuted"
case RntbdResponseHeaderRetryAfterMilliseconds:
return "RetryAfterMilliseconds"
case RntbdResponseHeaderIndexingDirective:
return "IndexingDirective"
case RntbdResponseHeaderStorageMaxResoureQuota:
return "StorageMaxResoureQuota"
case RntbdResponseHeaderStorageResourceQuotaUsage:
return "StorageResourceQuotaUsage"
case RntbdResponseHeaderSchemaVersion:
return "SchemaVersion"
case RntbdResponseHeaderCollectionPartitionIndex:
return "CollectionPartitionIndex"
case RntbdResponseHeaderCollectionServiceIndex:
return "CollectionServiceIndex"
case RntbdResponseHeaderLSN:
return "LSN"
case RntbdResponseHeaderItemCount:
return "ItemCount"
case RntbdResponseHeaderRequestCharge:
return "RequestCharge"
case RntbdResponseHeaderOwnerFullName:
return "OwnerFullName"
case RntbdResponseHeaderOwnerId:
return "OwnerId"
case RntbdResponseHeaderDatabaseAccountId:
return "DatabaseAccountId"
case RntbdResponseHeaderQuorumAckedLSN:
return "QuorumAckedLSN"
case RntbdResponseHeaderRequestValidationFailure:
return "RequestValidationFailure"
case RntbdResponseHeaderSubStatus:
return "SubStatus"
case RntbdResponseHeaderCollectionUpdateProgress:
return "CollectionUpdateProgress"
case RntbdResponseHeaderCurrentWriteQuorum:
return "CurrentWriteQuorum"
case RntbdResponseHeaderCurrentReplicaSetSize:
return "CurrentReplicaSetSize"
case RntbdResponseHeaderCollectionLazyIndexProgress:
return "CollectionLazyIndexProgress"
case RntbdResponseHeaderPartitionKeyRangeId:
return "PartitionKeyRangeId"
case RntbdResponseHeaderLogResults:
return "LogResults"
case RntbdResponseHeaderXPRole:
return "XPRole"
case RntbdResponseHeaderIsRUPerMinuteUsed:
return "IsRUPerMinuteUsed"
case RntbdResponseHeaderQueryMetrics:
return "QueryMetrics"
case RntbdResponseHeaderGlobalCommittedLSN:
return "GlobalCommittedLSN"
case RntbdResponseHeaderNumberOfReadRegions:
return "NumberOfReadRegions"
case RntbdResponseHeaderOfferReplacePending:
return "OfferReplacePending"
case RntbdResponseHeaderItemLSN:
return "ItemLSN"
case RntbdResponseHeaderRestoreState:
return "RestoreState"
case RntbdResponseHeaderCollectionSecurityIdentifier:
return "CollectionSecurityIdentifier"
case RntbdResponseHeaderTransportRequestID:
return "TransportRequestID"
case RntbdResponseHeaderShareThroughput:
return "ShareThroughput"
case RntbdResponseHeaderDisableRntbdChannel:
return "DisableRntbdChannel"
case RntbdResponseHeaderServerDateTimeUtc:
return "ServerDateTimeUtc"
case RntbdResponseHeaderLocalLSN:
return "LocalLSN"
case RntbdResponseHeaderQuorumAckedLocalLSN:
return "QuorumAckedLocalLSN"
case RntbdResponseHeaderItemLocalLSN:
return "ItemLocalLSN"
case RntbdResponseHeaderHasTentativeWrites:
return "HasTentativeWrites"
case RntbdResponseHeaderSessionToken:
return "SessionToken"
}
return fmt.Sprintf("RntbdResponseHeaderType(%d)", h)
}
func (r RntbdResourceType) String() string {
switch r {
case RntbdResourceTypeConnection:
return "Connection"
case RntbdResourceTypeDatabase:
return "Database"
case RntbdResourceTypeCollection:
return "Collection"
case RntbdResourceTypeDocument:
return "Document"
case RntbdResourceTypeAttachment:
return "Attachment"
case RntbdResourceTypeUser:
return "User"
case RntbdResourceTypePermission:
return "Permission"
case RntbdResourceTypeStoredProcedure:
return "StoredProcedure"
case RntbdResourceTypeConflict:
return "Conflict"
case RntbdResourceTypeTrigger:
return "Trigger"
case RntbdResourceTypeUserDefinedFunction:
return "UserDefinedFunction"
case RntbdResourceTypeModule:
return "Module"
case RntbdResourceTypeReplica:
return "Replica"
case RntbdResourceTypeModuleCommand:
return "ModuleCommand"
case RntbdResourceTypeRecord:
return "Record"
case RntbdResourceTypeOffer:
return "Offer"
case RntbdResourceTypePartitionSetInformation:
return "PartitionSetInformation"
case RntbdResourceTypeXPReplicatorAddress:
return "XPReplicatorAddress"
case RntbdResourceTypeMasterPartition:
return "MasterPartition"
case RntbdResourceTypeServerPartition:
return "ServerPartition"
case RntbdResourceTypeDatabaseAccount:
return "DatabaseAccount"
case RntbdResourceTypeTopology:
return "Topology"
case RntbdResourceTypePartitionKeyRange:
return "PartitionKeyRange"
case RntbdResourceTypeSchema:
return "Schema"
case RntbdResourceTypeBatchApply:
return "BatchApply"
case RntbdResourceTypeRestoreMetadata:
return "RestoreMetadata"
case RntbdResourceTypeComputeGatewayCharges:
return "ComputeGatewayCharges"
case RntbdResourceTypeRidRange:
return "RidRange"
case RntbdResourceTypeUserDefinedType:
return "UserDefinedType"
}
return fmt.Sprintf("RntbdResourceType(%d)", r)
}
func (o RntbdOperationType) String() string {
switch o {
case RntbdOperationTypeConnection:
return "Connection"
case RntbdOperationTypeCreate:
return "Create"
case RntbdOperationTypeUpdate:
return "Update"
case RntbdOperationTypeRead:
return "Read"
case RntbdOperationTypeReadFeed:
return "ReadFeed"
case RntbdOperationTypeDelete:
return "Delete"
case RntbdOperationTypeReplace:
return "Replace"
case RntbdOperationTypeExecuteJavaScript:
return "ExecuteJavaScript"
case RntbdOperationTypeSQLQuery:
return "SQLQuery"
case RntbdOperationTypePause:
return "Pause"
case RntbdOperationTypeResume:
return "Resume"
case RntbdOperationTypeStop:
return "Stop"
case RntbdOperationTypeRecycle:
return "Recycle"
case RntbdOperationTypeCrash:
return "Crash"
case RntbdOperationTypeQuery:
return "Query"
case RntbdOperationTypeForceConfigRefresh:
return "ForceConfigRefresh"
case RntbdOperationTypeHead:
return "Head"
case RntbdOperationTypeHeadFeed:
return "HeadFeed"
case RntbdOperationTypeUpsert:
return "Upsert"
case RntbdOperationTypeRecreate:
return "Recreate"
case RntbdOperationTypeThrottle:
return "Throttle"
case RntbdOperationTypeGetSplitPoint:
return "GetSplitPoint"
case RntbdOperationTypePreCreateValidation:
return "PreCreateValidation"
case RntbdOperationTypeBatchApply:
return "BatchApply"
case RntbdOperationTypeAbortSplit:
return "AbortSplit"
case RntbdOperationTypeCompleteSplit:
return "CompleteSplit"
case RntbdOperationTypeOfferUpdateOperation:
return "OfferUpdateOperation"
case RntbdOperationTypeOfferPreGrowValidation:
return "OfferPreGrowValidation"
case RntbdOperationTypeBatchReportThroughputUtilization:
return "BatchReportThroughputUtilization"
case RntbdOperationTypeCompletePartitionMigration:
return "CompletePartitionMigration"
case RntbdOperationTypeAbortPartitionMigration:
return "AbortPartitionMigration"
case RntbdOperationTypePreReplaceValidation:
return "PreReplaceValidation"
case RntbdOperationTypeAddComputeGatewayRequestCharges:
return "AddComputeGatewayRequestCharges"
case RntbdOperationTypeMigratePartition:
return "MigratePartition"
}
return fmt.Sprintf("RntbdOperationType(%d)", o)
}
+128
View File
@@ -0,0 +1,128 @@
package rntbd
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"github.com/pikami/cosmium/api/headers"
)
func (f *RntbdFrame) ToHttpRequest() *http.Request {
req := &http.Request{
Method: operationTypeToHttpMethod(f.OperationType),
URL: &url.URL{Path: frameToPath(f)},
Body: io.NopCloser(bytes.NewReader(f.Payload)),
Header: http.Header{},
}
switch f.OperationType {
case RntbdOperationTypeQuery, RntbdOperationTypeSQLQuery:
req.Header.Set(headers.Query, "true")
case RntbdOperationTypeUpsert:
req.Header.Set(headers.IsUpsert, "true")
}
if ifMatch, ok := f.RequestHeaders[RntbdRequestHeaderMatch]; ok {
if ifMatchString, ok := ifMatch.(string); ok {
req.Header.Set(headers.IfMatch, ifMatchString)
}
}
if continuationToken, ok := f.RequestHeaders[RntbdRequestHeaderContinuationToken]; ok {
if continuationTokenString, ok := continuationToken.(string); ok {
req.Header.Set(headers.ContinuationToken, continuationTokenString)
}
}
if maxItemCount, ok := f.RequestHeaders[RntbdRequestHeaderPageSize]; ok {
if maxItemCountString, ok := maxItemCount.(uint64); ok {
req.Header.Set(headers.MaxItemCount, fmt.Sprintf("%d", maxItemCountString))
}
}
return req
}
func ToRntbdResponseFrame(responseWriter *httptest.ResponseRecorder) *RntbdResponseFrameBuilder {
builder := &RntbdResponseFrameBuilder{}
builder.SetStatusCode(uint16(responseWriter.Code))
if responseWriter.Header().Get(headers.ETag) != "" {
builder.AddHeader(uint16(RntbdResponseHeaderETag), RntbdTokenTypeString, responseWriter.Header().Get(headers.ETag))
}
if responseWriter.Header().Get(headers.ContinuationToken) != "" {
builder.AddHeader(uint16(RntbdResponseHeaderContinuationToken), RntbdTokenTypeString, responseWriter.Header().Get(headers.ContinuationToken))
}
if responseWriter.Header().Get(headers.ItemCount) != "" {
itemCount, err := strconv.ParseUint(responseWriter.Header().Get(headers.ItemCount), 10, 32)
if err != nil {
panic(err)
}
builder.AddHeader(uint16(RntbdResponseHeaderItemCount), RntbdTokenTypeULong, uint32(itemCount))
}
if responseWriter.Body.Len() > 0 {
builder.AddHeader(uint16(RntbdResponseHeaderPayloadPresent), RntbdTokenTypeByte, []byte{1})
builder.AddPayload(responseWriter.Body.Bytes())
} else {
builder.AddHeader(uint16(RntbdResponseHeaderPayloadPresent), RntbdTokenTypeByte, []byte{0})
}
return builder
}
func operationTypeToHttpMethod(operationType RntbdOperationType) string {
switch operationType {
case RntbdOperationTypeRead,
RntbdOperationTypeReadFeed:
return http.MethodGet
case RntbdOperationTypeCreate,
RntbdOperationTypeUpsert,
RntbdOperationTypeQuery,
RntbdOperationTypeSQLQuery:
return http.MethodPost
case RntbdOperationTypeUpdate,
RntbdOperationTypeReplace:
return http.MethodPut
case RntbdOperationTypeDelete:
return http.MethodDelete
}
panic(fmt.Sprintf("Unknown operation type: %d", operationType))
}
func frameToPath(frame *RntbdFrame) string {
databaseName, databaseOk := frame.RequestHeaders[RntbdRequestHeaderDatabaseName]
collectionName, collectionOk := frame.RequestHeaders[RntbdRequestHeaderCollectionName]
documentName, documentOk := frame.RequestHeaders[RntbdRequestHeaderDocumentName]
urlPath := ""
if databaseOk {
urlPath += fmt.Sprintf("/dbs/%s", databaseName)
} else if frame.ResourceType == RntbdResourceTypeDatabase {
urlPath += "/dbs"
}
if collectionOk {
urlPath += fmt.Sprintf("/colls/%s", collectionName)
} else if frame.ResourceType == RntbdResourceTypeCollection {
urlPath += "/colls"
}
if documentOk {
urlPath += fmt.Sprintf("/docs/%s", documentName)
} else if frame.ResourceType == RntbdResourceTypeDocument {
urlPath += "/docs"
}
return urlPath
}
+212
View File
@@ -0,0 +1,212 @@
package rntbd
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"github.com/pikami/cosmium/internal/logger"
)
type RntbdFrame struct {
ResourceType RntbdResourceType
OperationType RntbdOperationType
ActivityId []byte
RequestHeaders map[RntbdRequestHeader]any
ResponseHeaders map[RntbdResponseHeaderType]any
ContextHeaders map[RntbdContextHeader]any
Payload []byte
}
func ReadFrame(reader *bufio.Reader) (*RntbdFrame, error) {
sizeBytes := readBytes(reader, 4)
size := binary.LittleEndian.Uint32(sizeBytes)
payload := readBytes(reader, int(size)-4)
frame, err := parseFrame_Int(payload, false)
if err != nil {
return nil, err
}
if payloadPresent, ok := frame.RequestHeaders[RntbdRequestHeaderPayloadPresent]; ok && payloadPresent.([]byte)[0] == 1 {
payloadSize := binary.LittleEndian.Uint32(readBytes(reader, 4))
payload := readBytes(reader, int(payloadSize))
frame.Payload = payload
}
if payloadPresent, ok := frame.ResponseHeaders[RntbdResponseHeaderPayloadPresent]; ok && payloadPresent.([]byte)[0] == 1 {
payloadSize := binary.LittleEndian.Uint32(readBytes(reader, 4))
payload := readBytes(reader, int(payloadSize))
frame.Payload = payload
}
return frame, nil
}
func ParseFrame(data []byte, isResponse bool) (*RntbdFrame, error) {
if len(data) < 4 {
return nil, fmt.Errorf("data too short")
}
reader := bufio.NewReader(bytes.NewReader(data))
sizeBytes := readBytes(reader, 4)
size := binary.LittleEndian.Uint32(sizeBytes)
payload := readBytes(reader, int(size)-4)
frame, err := parseFrame_Int(payload, isResponse)
if err != nil {
return nil, err
}
if payloadPresent, ok := frame.RequestHeaders[RntbdRequestHeaderPayloadPresent]; ok && payloadPresent.([]byte)[0] == 1 {
payloadSize := binary.LittleEndian.Uint32(readBytes(reader, 4))
payload := readBytes(reader, int(payloadSize))
frame.Payload = payload
}
if payloadPresent, ok := frame.ResponseHeaders[RntbdResponseHeaderPayloadPresent]; ok && payloadPresent.([]byte)[0] == 1 {
payloadSize := binary.LittleEndian.Uint32(readBytes(reader, 4))
payload := readBytes(reader, int(payloadSize))
frame.Payload = payload
}
leftOverBytes, err := io.ReadAll(reader)
if err != nil {
logger.ErrorLn("Error reading leftOverBytes:", err)
}
if len(leftOverBytes) > 0 {
logger.ErrorLn("Left over bytes:", hex.EncodeToString(leftOverBytes))
}
return frame, nil
}
func parseFrame_Int(data []byte, isResponse bool) (*RntbdFrame, error) {
payloadReader := bufio.NewReader(bytes.NewReader(data))
resourceTypeBytes := readBytes(payloadReader, 2)
resourceType := binary.LittleEndian.Uint16(resourceTypeBytes)
operationTypeBytes := readBytes(payloadReader, 2)
operationType := RntbdOperationType(binary.LittleEndian.Uint16(operationTypeBytes))
activityIdBytes := readBytes(payloadReader, 16)
requestHeaders := make(map[RntbdRequestHeader]any)
responseHeaders := make(map[RntbdResponseHeaderType]any)
contextHeaders := make(map[RntbdContextHeader]any)
for {
if _, err := payloadReader.Peek(1); err != nil {
break
}
headerIdBytes := readBytes(payloadReader, 2)
headerId := binary.LittleEndian.Uint16(headerIdBytes)
token, err := parseRntbdToken(payloadReader)
if err != nil {
return nil, err
}
if resourceType == uint16(RntbdResourceTypeConnection) {
contextHeaders[RntbdContextHeader(headerId)] = token
} else if isResponse {
responseHeaders[RntbdResponseHeaderType(headerId)] = token
} else {
requestHeaders[RntbdRequestHeader(headerId)] = token
}
}
return &RntbdFrame{
ResourceType: RntbdResourceType(resourceType),
OperationType: RntbdOperationType(operationType),
ActivityId: activityIdBytes,
RequestHeaders: requestHeaders,
ResponseHeaders: responseHeaders,
ContextHeaders: contextHeaders,
}, nil
}
func parseRntbdToken(reader *bufio.Reader) (any, error) {
tokenTypeBytes := readBytes(reader, 1)
tokenType := RntbdTokenType(tokenTypeBytes[0])
switch tokenType {
case RntbdTokenTypeByte:
token := readBytes(reader, 1)
return token, nil
case RntbdTokenTypeUShort:
token := binary.LittleEndian.Uint16(readBytes(reader, 2))
return token, nil
case RntbdTokenTypeULong:
token := binary.LittleEndian.Uint32(readBytes(reader, 4))
return token, nil
case RntbdTokenTypeLong:
token := int32(binary.LittleEndian.Uint32(readBytes(reader, 4)))
return token, nil
case RntbdTokenTypeULongLong:
token := binary.LittleEndian.Uint64(readBytes(reader, 8))
return token, nil
case RntbdTokenTypeLongLong:
token := int64(binary.LittleEndian.Uint64(readBytes(reader, 8)))
return token, nil
case RntbdTokenTypeGuid:
token := readBytes(reader, 16)
return token, nil
case RntbdTokenTypeSmallString:
lengthBytes := readBytes(reader, 1)
length := uint8(lengthBytes[0])
token := readBytes(reader, int(length))
return string(token), nil
case RntbdTokenTypeString:
length := binary.LittleEndian.Uint16(readBytes(reader, 2))
token := readBytes(reader, int(length))
return string(token), nil
case RntbdTokenTypeULongString:
length := binary.LittleEndian.Uint32(readBytes(reader, 4))
token := readBytes(reader, int(length))
return string(token), nil
case RntbdTokenTypeSmallBytes:
lengthBytes := readBytes(reader, 1)
length := uint8(lengthBytes[0])
token := readBytes(reader, int(length))
return token, nil
case RntbdTokenTypeBytes:
length := binary.LittleEndian.Uint16(readBytes(reader, 2))
token := readBytes(reader, int(length))
return token, nil
case RntbdTokenTypeULongBytes:
length := binary.LittleEndian.Uint32(readBytes(reader, 4))
token := readBytes(reader, int(length))
return token, nil
case RntbdTokenTypeFloat:
// I can't be bothered to implement this, let's just return a byte array
token := readBytes(reader, 4)
return token, nil
case RntbdTokenTypeDouble:
// I can't be bothered to implement this, let's just return a byte array
token := readBytes(reader, 8)
return token, nil
case RntbdTokenTypeInvalid:
return nil, fmt.Errorf("invalid token type")
}
return nil, fmt.Errorf("invalid token type")
}
func readBytes(reader *bufio.Reader, n int) []byte {
bytes := make([]byte, n)
_, err := io.ReadFull(reader, bytes)
if err != nil {
logger.ErrorLn("Error reading bytes:", err)
os.Exit(0)
}
return bytes
}
+120
View File
@@ -0,0 +1,120 @@
package rntbd
import (
"bufio"
"crypto/tls"
"fmt"
"net"
"net/http/httptest"
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/internal/logger"
tlsprovider "github.com/pikami/cosmium/internal/tls_provider"
)
type RntbdServer struct {
port int
listener net.Listener
apiServer *api.ApiServer
}
func NewRntbdServer(port int, apiServer *api.ApiServer) *RntbdServer {
return &RntbdServer{port: port, apiServer: apiServer}
}
func (s *RntbdServer) Start() error {
tlsConfig := tlsprovider.GetDefaultTlsConfig()
listener, err := tls.Listen("tcp", fmt.Sprintf(":%d", s.port), tlsConfig)
if err != nil {
return fmt.Errorf("failed to listen on port %d: %w", s.port, err)
}
s.listener = listener
go func() {
for {
conn, err := s.listener.Accept()
if err != nil {
logger.ErrorLn("Failed to accept connection:", err)
continue
}
go s.handleConnection(conn)
}
}()
return nil
}
func (s *RntbdServer) Stop() error {
return s.listener.Close()
}
func (s *RntbdServer) handleConnection(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
writer := bufio.NewWriter(conn)
for {
_, err := reader.Peek(4)
if err != nil {
return
}
frame, err := ReadFrame(reader)
if err != nil {
logger.ErrorLn("Failed to read frame:", err)
continue
}
if frame.ResourceType == RntbdResourceTypeConnection {
responseFrame := buildContextFrame(frame)
_, err := writer.Write(responseFrame)
writer.Flush()
if err != nil {
logger.ErrorLn("Failed to write response frame:", err)
continue
}
continue
} else if frame.ResourceType == RntbdResourceTypeDatabase ||
frame.ResourceType == RntbdResourceTypeCollection ||
frame.ResourceType == RntbdResourceTypeDocument {
responseFrameBytes := s.passToApiServer(frame)
_, err := writer.Write(responseFrameBytes)
writer.Flush()
if err != nil {
logger.ErrorLn("Failed to write response frame:", err)
continue
}
continue
} else {
logger.Errorf("Received Unhandled RNTBD request from: %s with resource type: %s\n", conn.RemoteAddr(), frame.ResourceType.String())
}
}
}
func (s *RntbdServer) passToApiServer(frame *RntbdFrame) []byte {
req := frame.ToHttpRequest()
responseWriter := httptest.NewRecorder()
s.apiServer.GetRouter().ServeHTTP(responseWriter, req)
responseFrameBuilder := ToRntbdResponseFrame(responseWriter)
responseFrameBuilder.SetActivityId(frame.ActivityId)
if transportRequestId, ok := frame.RequestHeaders[RntbdRequestHeaderTransportRequestID]; ok {
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderTransportRequestID), RntbdTokenTypeULong, transportRequestId)
}
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderItemLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderLocalLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderGlobalCommittedLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderItemLocalLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderQuorumAckedLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderQuorumAckedLocalLSN), RntbdTokenTypeLongLong, int64(420))
responseFrameBuilder.AddHeader(uint16(RntbdResponseHeaderCurrentReplicaSetSize), RntbdTokenTypeULong, uint32(1))
responseFrame := responseFrameBuilder.Build()
responseFrameBytes := responseFrame.ToBytes()
return responseFrameBytes
}
@@ -1,21 +1,19 @@
package structhidrators package structhidrators
import ( import "github.com/pikami/cosmium/internal/datastore"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
var defaultCollection repositorymodels.Collection = repositorymodels.Collection{ var defaultCollection datastore.Collection = datastore.Collection{
IndexingPolicy: repositorymodels.CollectionIndexingPolicy{ IndexingPolicy: datastore.CollectionIndexingPolicy{
IndexingMode: "consistent", IndexingMode: "consistent",
Automatic: true, Automatic: true,
IncludedPaths: []repositorymodels.CollectionIndexingPolicyPath{ IncludedPaths: []datastore.CollectionIndexingPolicyPath{
{Path: "/*"}, {Path: "/*"},
}, },
ExcludedPaths: []repositorymodels.CollectionIndexingPolicyPath{ ExcludedPaths: []datastore.CollectionIndexingPolicyPath{
{Path: "/\"_etag\"/?"}, {Path: "/\"_etag\"/?"},
}, },
}, },
PartitionKey: repositorymodels.CollectionPartitionKey{ PartitionKey: datastore.CollectionPartitionKey{
Paths: []string{"/_partitionKey"}, Paths: []string{"/_partitionKey"},
Kind: "Hash", Kind: "Hash",
Version: 2, Version: 2,
+2 -2
View File
@@ -3,11 +3,11 @@ package structhidrators
import ( import (
"reflect" "reflect"
repositorymodels "github.com/pikami/cosmium/internal/repository_models" "github.com/pikami/cosmium/internal/datastore"
) )
func Hidrate(input interface{}) interface{} { func Hidrate(input interface{}) interface{} {
if reflect.TypeOf(input) == reflect.TypeOf(repositorymodels.Collection{}) { if reflect.TypeOf(input) == reflect.TypeOf(datastore.Collection{}) {
return hidrate(input, defaultCollection) return hidrate(input, defaultCollection)
} }
return input return input
+1 -1
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
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)
}
}
+25 -6
View File
@@ -5,6 +5,7 @@ type SelectStmt struct {
Table Table Table Table
JoinItems []JoinItem JoinItems []JoinItem
Filters interface{} Filters interface{}
Exists bool
Distinct bool Distinct bool
Count int Count int
Offset int Offset int
@@ -14,7 +15,9 @@ type SelectStmt struct {
} }
type Table struct { type Table struct {
Value string Value string
SelectItem SelectItem
IsInSelect bool
} }
type JoinItem struct { type JoinItem struct {
@@ -30,6 +33,9 @@ const (
SelectItemTypeArray SelectItemTypeArray
SelectItemTypeConstant SelectItemTypeConstant
SelectItemTypeFunctionCall SelectItemTypeFunctionCall
SelectItemTypeSubQuery
SelectItemTypeExpression
SelectItemTypeBinaryExpression
) )
type SelectItem struct { type SelectItem struct {
@@ -38,6 +44,7 @@ type SelectItem struct {
SelectItems []SelectItem SelectItems []SelectItem
Type SelectItemType Type SelectItemType
Value interface{} Value interface{}
Invert bool
IsTopLevel bool IsTopLevel bool
} }
@@ -59,6 +66,12 @@ type ComparisonExpression struct {
Operation string Operation string
} }
type BinaryExpression struct {
Left interface{}
Right interface{}
Operation string
}
type ConstantType int type ConstantType int
const ( const (
@@ -94,6 +107,7 @@ const (
FunctionCallContains FunctionCallType = "Contains" FunctionCallContains FunctionCallType = "Contains"
FunctionCallEndsWith FunctionCallType = "EndsWith" FunctionCallEndsWith FunctionCallType = "EndsWith"
FunctionCallStartsWith FunctionCallType = "StartsWith" FunctionCallStartsWith FunctionCallType = "StartsWith"
FunctionCallRegexMatch FunctionCallType = "RegexMatch"
FunctionCallIndexOf FunctionCallType = "IndexOf" FunctionCallIndexOf FunctionCallType = "IndexOf"
FunctionCallToString FunctionCallType = "ToString" FunctionCallToString FunctionCallType = "ToString"
FunctionCallUpper FunctionCallType = "Upper" FunctionCallUpper FunctionCallType = "Upper"
@@ -120,11 +134,16 @@ 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"
FunctionCallIif FunctionCallType = "Iif"
FunctionCallMathAbs FunctionCallType = "MathAbs" FunctionCallMathAbs FunctionCallType = "MathAbs"
FunctionCallMathAcos FunctionCallType = "MathAcos" FunctionCallMathAcos FunctionCallType = "MathAcos"
+6 -5
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")},
}, },
) )
}) })
+366
View File
@@ -0,0 +1,366 @@
package nosql_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Parse_Arithmetics(t *testing.T) {
t.Run("Should parse multiplication before addition", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.a + c.b * c.c FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "b"),
Right: testutils.SelectItem_Path("c", "c"),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse division before subtraction", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.x - c.y / c.z FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "-",
Left: testutils.SelectItem_Path("c", "x"),
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "/",
Left: testutils.SelectItem_Path("c", "y"),
Right: testutils.SelectItem_Path("c", "z"),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle complex mixed operations", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.a + c.b * c.c - c.d / c.e FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "-",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "b"),
Right: testutils.SelectItem_Path("c", "c"),
},
},
},
},
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "/",
Left: testutils.SelectItem_Path("c", "d"),
Right: testutils.SelectItem_Path("c", "e"),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should respect parentheses overriding precedence", func(t *testing.T) {
testQueryParse(
t,
`SELECT (c.a + c.b) * c.c FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Path("c", "b"),
},
},
Right: testutils.SelectItem_Path("c", "c"),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle nested parentheses", func(t *testing.T) {
testQueryParse(
t,
`SELECT ((c.a + c.b) * c.c) - c.d FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "-",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Path("c", "b"),
},
},
Right: testutils.SelectItem_Path("c", "c"),
},
},
Right: testutils.SelectItem_Path("c", "d"),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should be left associative for same precedence operators", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.a - c.b - c.c FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "-",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "-",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Path("c", "b"),
},
},
Right: testutils.SelectItem_Path("c", "c"),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should be left associative with multiplication and division", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.a * c.b / c.c FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "/",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Path("c", "b"),
},
},
Right: testutils.SelectItem_Path("c", "c"),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle math with constants", func(t *testing.T) {
testQueryParse(
t,
`SELECT 10 + 20 * 5 FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Constant_Int(10),
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Constant_Int(20),
Right: testutils.SelectItem_Constant_Int(5),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle math with floating point numbers", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.price * 1.08 FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "price"),
Right: testutils.SelectItem_Constant_Float(1.08),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle parentheses around single value", func(t *testing.T) {
testQueryParse(
t,
`SELECT (c.value) FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "value"),
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle function calls in math expressions", func(t *testing.T) {
testQueryParse(
t,
`SELECT LENGTH(c.name) * 2 + 10 FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallLength,
Arguments: []interface{}{testutils.SelectItem_Path("c", "name")},
},
},
Right: testutils.SelectItem_Constant_Int(2),
},
},
Right: testutils.SelectItem_Constant_Int(10),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle multiple select items with math", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.a + c.b, c.x * c.y FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Path("c", "b"),
},
},
{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "x"),
Right: testutils.SelectItem_Path("c", "y"),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should handle math in WHERE clause", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM c WHERE c.price * 1.08 > 100`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.ComparisonExpression{
Operation: ">",
Left: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "price"),
Right: testutils.SelectItem_Constant_Float(1.08),
},
},
Right: testutils.SelectItem_Constant_Int(100),
},
},
)
})
}
+121 -19
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")},
}, },
) )
}) })
+3 -2
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_Join(t *testing.T) { func Test_Parse_Join(t *testing.T) {
@@ -17,7 +18,7 @@ func Test_Parse_Join(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")},
JoinItems: []parsers.JoinItem{ JoinItems: []parsers.JoinItem{
{ {
Table: parsers.Table{ Table: parsers.Table{
@@ -40,7 +41,7 @@ func Test_Parse_Join(t *testing.T) {
SelectItems: []parsers.SelectItem{ SelectItems: []parsers.SelectItem{
{Path: []string{"cc"}, IsTopLevel: true}, {Path: []string{"cc"}, IsTopLevel: true},
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
JoinItems: []parsers.JoinItem{ JoinItems: []parsers.JoinItem{
{ {
Table: parsers.Table{ Table: parsers.Table{
+2 -1
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_MathFunctions(t *testing.T) { func Test_Execute_MathFunctions(t *testing.T) {
@@ -644,7 +645,7 @@ func testMathFunctionParse(
}, },
}, },
}, },
Table: parsers.Table{Value: expectedTable}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path(expectedTable)},
}, },
) )
} }
+140 -17
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,146 @@ 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 NOT IN function", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM c WHERE c.id NOT IN ("123", "456")`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Invert: true,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallIn,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
testutils.SelectItem_Constant_String("123"),
testutils.SelectItem_Constant_String("456"),
},
},
},
},
)
})
t.Run("Should parse NOT with parentheses", func(t *testing.T) {
testQueryParse(
t,
`SELECT c.id FROM c WHERE NOT (c.id IN ("123", "456"))`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{Path: []string{"c", "id"}, Type: parsers.SelectItemTypeField},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Invert: true,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallIn,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
testutils.SelectItem_Constant_String("123"),
testutils.SelectItem_Constant_String("456"),
},
},
},
},
)
})
t.Run("Should parse IN function with function call", func(t *testing.T) {
testQueryParse(
t,
`Select c.id FROM c WHERE (ToString(c.id) IN ("123", "456"))`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallIn,
Arguments: []interface{}{
parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallToString,
Arguments: []interface{}{
testutils.SelectItem_Path("c", "id"),
},
},
},
testutils.SelectItem_Constant_String("123"),
testutils.SelectItem_Constant_String("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,
},
},
)
})
t.Run("Should parse IIF function", func(t *testing.T) {
testQueryParse(
t,
`SELECT IIF(true, c.pk, c.id) FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallIif,
Arguments: []interface{}{
testutils.SelectItem_Constant_Bool(true),
testutils.SelectItem_Path("c", "pk"),
testutils.SelectItem_Path("c", "id"),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
} }
+3815 -1968
View File
File diff suppressed because it is too large Load Diff
+324 -114
View File
@@ -4,126 +4,137 @@ package nosql
import "github.com/pikami/cosmium/parsers" import "github.com/pikami/cosmium/parsers"
func makeSelectStmt( func makeSelectStmt(
columns, table, joinItems, 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 { if joinItemsArray, ok := joinItems.([]interface{}); ok && len(joinItemsArray) > 0 {
selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray)) selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray))
for i, joinItem := range joinItemsArray { for i, joinItem := range joinItemsArray {
selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem) selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem)
} }
} }
switch v := whereClause.(type) { switch v := whereClause.(type) {
case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem: case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem:
selectStmt.Filters = v selectStmt.Filters = v
} }
if distinctClause != nil { if distinctClause != nil {
selectStmt.Distinct = true selectStmt.Distinct = true
} }
if n, ok := count.(int); ok { if n, ok := count.(int); ok {
selectStmt.Count = n selectStmt.Count = n
} }
if offsetArr, ok := offsetClause.([]interface{}); ok && len(offsetArr) == 2 { if offsetArr, ok := offsetClause.([]interface{}); ok && len(offsetArr) == 2 {
if n, ok := offsetArr[0].(int); ok { if n, ok := offsetArr[0].(int); ok {
selectStmt.Offset = n selectStmt.Offset = n
} }
if n, ok := offsetArr[1].(int); ok { if n, ok := offsetArr[1].(int); ok {
selectStmt.Count = n selectStmt.Count = n
} }
} }
if orderExpressions, ok := orderList.([]parsers.OrderExpression); ok { if orderExpressions, ok := orderList.([]parsers.OrderExpression); ok {
selectStmt.OrderExpressions = orderExpressions selectStmt.OrderExpressions = orderExpressions
} }
if groupByClause != nil { if groupByClause != nil {
selectStmt.GroupBy = groupByClause.([]parsers.SelectItem) selectStmt.GroupBy = groupByClause.([]parsers.SelectItem)
} }
return selectStmt, nil return selectStmt, nil
} }
func makeJoin(table interface{}, column interface{}) (parsers.JoinItem, error) { func makeJoin(table interface{}, column interface{}) (parsers.JoinItem, error) {
return parsers.JoinItem{ joinItem := parsers.JoinItem{}
Table: table.(parsers.Table),
SelectItem: column.(parsers.SelectItem), if selectItem, isSelectItem := column.(parsers.SelectItem); isSelectItem {
}, nil 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{})
paths := make([]string, 1) paths := make([]string, 1)
paths[0] = name.(string) paths[0] = name.(string)
for _, p := range ps { for _, p := range ps {
paths = append(paths, p.(string)) paths = append(paths, p.(string))
} }
return parsers.SelectItem{Path: paths, Type: selectItemType}, nil return parsers.SelectItem{Path: paths, Type: selectItemType}, nil
} }
func makeColumnList(column interface{}, other_columns interface{}) ([]parsers.SelectItem, error) { func makeColumnList(column interface{}, other_columns interface{}) ([]parsers.SelectItem, error) {
collsAsArray := other_columns.([]interface{}) collsAsArray := other_columns.([]interface{})
columnList := make([]parsers.SelectItem, len(collsAsArray) + 1) columnList := make([]parsers.SelectItem, len(collsAsArray) + 1)
columnList[0] = column.(parsers.SelectItem) columnList[0] = column.(parsers.SelectItem)
for i, v := range collsAsArray { for i, v := range collsAsArray {
if col, ok := v.(parsers.SelectItem); ok { if col, ok := v.(parsers.SelectItem); ok {
columnList[i+1] = col columnList[i+1] = col
} }
} }
return columnList, nil return columnList, nil
} }
func makeSelectArray(columns interface{}) (parsers.SelectItem, error) { func makeSelectArray(columns interface{}) (parsers.SelectItem, error) {
return parsers.SelectItem{ return parsers.SelectItem{
SelectItems: columns.([]parsers.SelectItem), SelectItems: columns.([]parsers.SelectItem),
Type: parsers.SelectItemTypeArray, Type: parsers.SelectItemTypeArray,
}, nil }, nil
} }
func makeSelectObject(field interface{}, other_fields interface{}) (parsers.SelectItem, error) { func makeSelectObject(field interface{}, other_fields interface{}) (parsers.SelectItem, error) {
fieldsAsArray := other_fields.([]interface{}) fieldsAsArray := other_fields.([]interface{})
fieldsList := make([]parsers.SelectItem, len(fieldsAsArray)+1) fieldsList := make([]parsers.SelectItem, len(fieldsAsArray)+1)
fieldsList[0] = field.(parsers.SelectItem) fieldsList[0] = field.(parsers.SelectItem)
for i, v := range fieldsAsArray { for i, v := range fieldsAsArray {
if col, ok := v.(parsers.SelectItem); ok { if col, ok := v.(parsers.SelectItem); ok {
fieldsList[i+1] = col fieldsList[i+1] = col
} }
} }
return parsers.SelectItem{ return parsers.SelectItem{
SelectItems: fieldsList, SelectItems: fieldsList,
Type: parsers.SelectItemTypeObject, Type: parsers.SelectItemTypeObject,
}, nil }, nil
} }
func makeOrderByClause(ex1 interface{}, others interface{}) ([]parsers.OrderExpression, error) { func makeOrderByClause(ex1 interface{}, others interface{}) ([]parsers.OrderExpression, error) {
othersArray := others.([]interface{}) othersArray := others.([]interface{})
orderList := make([]parsers.OrderExpression, len(othersArray)+1) orderList := make([]parsers.OrderExpression, len(othersArray)+1)
orderList[0] = ex1.(parsers.OrderExpression) orderList[0] = ex1.(parsers.OrderExpression)
for i, v := range othersArray { for i, v := range othersArray {
if col, ok := v.(parsers.OrderExpression); ok { if col, ok := v.(parsers.OrderExpression); ok {
orderList[i+1] = col orderList[i+1] = col
} }
} }
return orderList, nil return orderList, nil
} }
func makeOrderExpression(field interface{}, order interface{}) (parsers.OrderExpression, error) { func makeOrderExpression(field interface{}, order interface{}) (parsers.OrderExpression, error) {
@@ -133,8 +144,8 @@ func makeOrderExpression(field interface{}, order interface{}) (parsers.OrderExp
} }
if orderValue, ok := order.(parsers.OrderDirection); ok { if orderValue, ok := order.(parsers.OrderDirection); ok {
value.Direction = orderValue value.Direction = orderValue
} }
return value, nil return value, nil
} }
@@ -158,13 +169,39 @@ func joinStrings(array []interface{}) string {
func combineExpressions(ex1 interface{}, exs interface{}, operation parsers.LogicalExpressionType) (interface{}, error) { func combineExpressions(ex1 interface{}, exs interface{}, operation parsers.LogicalExpressionType) (interface{}, error) {
if exs == nil || len(exs.([]interface{})) < 1 { if exs == nil || len(exs.([]interface{})) < 1 {
return ex1, nil return ex1, nil
} }
return parsers.LogicalExpression{ return parsers.LogicalExpression{
Expressions: append([]interface{}{ex1}, exs.([]interface{})...), Expressions: append([]interface{}{ex1}, exs.([]interface{})...),
Operation: operation, Operation: operation,
}, nil }, nil
}
func makeMathExpression(left interface{}, operations interface{}) (interface{}, error) {
if operations == nil || len(operations.([]interface{})) == 0 {
return left, nil
}
result := left.(parsers.SelectItem)
ops := operations.([]interface{})
for _, op := range ops {
opData := op.([]interface{})
operation := opData[0].(string)
right := opData[1].(parsers.SelectItem)
result = parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Left: result,
Right: right,
Operation: operation,
},
}
}
return result, nil
} }
} }
@@ -177,13 +214,13 @@ SelectStmt <- Select ws
distinctClause:DistinctClause? ws distinctClause:DistinctClause? ws
topClause:TopClause? ws topClause:TopClause? ws
columns:Selection ws columns:Selection ws
From ws table:TableName ws fromClause:FromClause? ws
joinClauses:JoinClause* 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, joinClauses, whereClause, return makeSelectStmt(columns, fromClause, joinClauses, whereClause,
distinctClause, topClause, groupByClause, orderByClause, offsetClause) distinctClause, topClause, groupByClause, orderByClause, offsetClause)
} }
@@ -193,11 +230,60 @@ TopClause <- Top ws count:Integer {
return count, nil return count, nil
} }
JoinClause <- Join ws table:TableName ws "IN"i ws column:SelectItem { FromClause <- From ws table:TableName selectItem:(ws In ws column:SelectItemWithAlias { return column, nil }) {
return makeJoin(table, column) tableTyped := table.(parsers.Table)
if selectItem != nil {
tableTyped.SelectItem = selectItem.(parsers.SelectItem)
tableTyped.IsInSelect = true
}
return tableTyped, nil
} / From ws column:SelectItemWithAlias {
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
} }
OffsetClause <- "OFFSET"i ws offset:IntegerLiteral ws "LIMIT"i ws limit:IntegerLiteral { 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:SelectItemWithAlias {
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
} }
@@ -205,17 +291,40 @@ Selection <- SelectValueSpec / ColumnList / SelectAsterisk
SelectAsterisk <- "*" { SelectAsterisk <- "*" {
selectItem, _ := makeSelectItem("c", make([]interface{}, 0), parsers.SelectItemTypeField) selectItem, _ := makeSelectItem("c", make([]interface{}, 0), parsers.SelectItemTypeField)
selectItem.IsTopLevel = true selectItem.IsTopLevel = true
return makeColumnList(selectItem, make([]interface{}, 0)) return makeColumnList(selectItem, make([]interface{}, 0))
} }
ColumnList <- column:SelectItem other_columns:(ws "," ws coll:SelectItem {return coll, nil })* { ColumnList <- column:ExpressionOrSelectItem other_columns:(ws "," ws coll:ExpressionOrSelectItem {return coll, nil })* {
return makeColumnList(column, other_columns) return makeColumnList(column, other_columns)
} }
SelectValueSpec <- "VALUE"i ws column:SelectItem { ExpressionOrSelectItem <- expression:OrExpression asClause:AsClause? {
switch typedValue := expression.(type) {
case parsers.ComparisonExpression, parsers.LogicalExpression:
selectItem := parsers.SelectItem{
Type: parsers.SelectItemTypeExpression,
Value: typedValue,
}
if aliasValue, ok := asClause.(string); ok {
selectItem.Alias = aliasValue
}
return selectItem, nil
case parsers.SelectItem:
if aliasValue, ok := asClause.(string); ok {
typedValue.Alias = aliasValue
}
return typedValue, nil
default:
return typedValue, nil
}
} / item:SelectItemWithAlias { return item, nil }
SelectValueSpec <- "VALUE"i ws column:SelectItemWithAlias {
selectItem := column.(parsers.SelectItem) selectItem := column.(parsers.SelectItem)
selectItem.IsTopLevel = true selectItem.IsTopLevel = true
return makeColumnList(selectItem, make([]interface{}, 0)) return makeColumnList(selectItem, make([]interface{}, 0))
} }
@@ -229,19 +338,32 @@ SelectArray <- "[" ws columns:ColumnList ws "]" {
SelectObject <- "{" ws field:SelectObjectField ws other_fields:(ws "," ws coll:SelectObjectField {return coll, nil })* ws "}" { SelectObject <- "{" ws field:SelectObjectField ws other_fields:(ws "," ws coll:SelectObjectField {return coll, nil })* ws "}" {
return makeSelectObject(field, other_fields) return makeSelectObject(field, other_fields)
} / "{" ws "}" {
return parsers.SelectItem{
SelectItems: []parsers.SelectItem{},
Type: parsers.SelectItemTypeObject,
}, nil
} }
SelectObjectField <- name:(Identifier / "\"" key:Identifier "\"" { return key, nil }) ws ":" ws selectItem:SelectItem { SelectObjectField <- name:(Identifier / "\"" key:Identifier "\"" { return key, nil }) ws ":" ws selectItem:SelectItem {
item := selectItem.(parsers.SelectItem) item := selectItem.(parsers.SelectItem)
item.Alias = name.(string) item.Alias = name.(string)
return item, nil return item, nil
} }
SelectProperty <- name:Identifier path:(DotFieldAccess / ArrayFieldAccess)* { 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? { SelectItemWithAlias <- selectItem:SelectItem asClause:AsClause? {
item := selectItem.(parsers.SelectItem)
if aliasValue, ok := asClause.(string); ok {
item.Alias = aliasValue
}
return item, nil
}
SelectItem <- selectItem:(SubQuerySelectItem / Literal / FunctionCall / SelectArray / SelectObject / SelectProperty) {
var itemResult parsers.SelectItem var itemResult parsers.SelectItem
switch typedValue := selectItem.(type) { switch typedValue := selectItem.(type) {
case parsers.SelectItem: case parsers.SelectItem:
@@ -258,14 +380,14 @@ SelectItem <- selectItem:(Literal / FunctionCall / SelectArray / SelectObject /
} }
} }
if aliasValue, ok := asClause.(string); ok { return itemResult, nil
itemResult.Alias = aliasValue
}
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 / Not / GroupBy / OrderBy / Offset
DotFieldAccess <- "." id:Identifier { DotFieldAccess <- "." id:Identifier {
return id, nil return id, nil
@@ -273,6 +395,7 @@ DotFieldAccess <- "." id:Identifier {
ArrayFieldAccess <- "[\"" id:Identifier "\"]" { return id, nil } ArrayFieldAccess <- "[\"" id:Identifier "\"]" { return id, nil }
/ "[" id:Integer "]" { return strconv.Itoa(id.(int)), 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
@@ -290,11 +413,35 @@ AndExpression <- ex1:ComparisonExpression ex2:(ws And ws ex:ComparisonExpression
return combineExpressions(ex1, ex2, parsers.LogicalExpressionTypeAnd) return combineExpressions(ex1, ex2, parsers.LogicalExpressionTypeAnd)
} }
ComparisonExpression <- "(" ws ex:OrExpression ws ")" { return ex, nil } ComparisonExpression <- left:AddSubExpression ws op:ComparisonOperator ws right:AddSubExpression {
/ left:SelectItem ws op:ComparisonOperator ws right:SelectItem {
return parsers.ComparisonExpression{Left:left,Right:right,Operation:op.(string)}, nil return parsers.ComparisonExpression{Left:left,Right:right,Operation:op.(string)}, nil
} / ex:AddSubExpression { return ex, nil }
AddSubExpression <- left:MulDivExpression operations:(ws op:AddOrSubtractOperation ws right:MulDivExpression { return []interface{}{op, right}, nil })* {
return makeMathExpression(left, operations)
}
MulDivExpression <- left:SelectItemWithParentheses operations:(ws op:MultiplyOrDivideOperation ws right:SelectItemWithParentheses { return []interface{}{op, right}, nil })* {
return makeMathExpression(left, operations)
}
SelectItemWithParentheses <- inv:(Not ws)? "(" ws ex:OrExpression ws ")" {
if inv != nil {
if ex1, ok := ex.(parsers.SelectItem); ok {
ex1.Invert = true
return ex1, nil
}
}
return ex, nil
}
/ inv:(Not ws)? ex:SelectItem {
if inv != nil {
ex1 := ex.(parsers.SelectItem)
ex1.Invert = true
return ex1, nil
}
return ex, nil
} / ex:BooleanLiteral { return ex, nil } } / ex:BooleanLiteral { return ex, nil }
/ ex:SelectItem { return ex, nil }
OrderByClause <- OrderBy ws ex1:OrderExpression others:(ws "," ws ex:OrderExpression { return ex, nil })* { OrderByClause <- OrderBy ws ex1:OrderExpression others:(ws "," ws ex:OrderExpression { return ex, nil })* {
return makeOrderByClause(ex1, others) return makeOrderByClause(ex1, others)
@@ -306,10 +453,10 @@ OrderExpression <- field:SelectProperty ws order:OrderDirection? {
OrderDirection <- ("ASC"i / "DESC"i) { OrderDirection <- ("ASC"i / "DESC"i) {
if strings.EqualFold(string(c.text), "DESC") { if strings.EqualFold(string(c.text), "DESC") {
return parsers.OrderDirectionDesc, nil return parsers.OrderDirectionDesc, nil
} }
return parsers.OrderDirectionAsc, nil return parsers.OrderDirectionAsc, nil
} }
Select <- "SELECT"i Select <- "SELECT"i
@@ -320,22 +467,34 @@ As <- "AS"i
From <- "FROM"i From <- "FROM"i
In <- "IN"i
Join <- "JOIN"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
Not <- "NOT"i
GroupBy <- "GROUP"i ws "BY"i GroupBy <- "GROUP"i ws "BY"i
OrderBy <- "ORDER"i ws "BY"i OrderBy <- "ORDER"i ws "BY"i
ComparisonOperator <- ("=" / "!=" / "<" / "<=" / ">" / ">=") { Offset <- "OFFSET"i
ComparisonOperator <- ("<=" / ">=" / "=" / "!=" / "<" / ">") {
return string(c.text), nil return string(c.text), nil
} }
AddOrSubtractOperation <- ("+" / "-") { return string(c.text), nil }
MultiplyOrDivideOperation <- ("*" / "/") { return string(c.text), nil }
Literal <- FloatLiteral / IntegerLiteral / StringLiteral / BooleanLiteral / ParameterConstant / NullConstant Literal <- FloatLiteral / IntegerLiteral / StringLiteral / BooleanLiteral / ParameterConstant / NullConstant
ParameterConstant <- "@" Identifier { ParameterConstant <- "@" Identifier {
@@ -363,6 +522,7 @@ BooleanLiteral <- ("true"i / "false"i) {
FunctionCall <- StringFunctions FunctionCall <- StringFunctions
/ TypeCheckingFunctions / TypeCheckingFunctions
/ ArrayFunctions / ArrayFunctions
/ ConditionalFunctions
/ InFunction / InFunction
/ AggregateFunctions / AggregateFunctions
/ MathFunctions / MathFunctions
@@ -402,11 +562,16 @@ AggregateFunctions <- AvgAggregateExpression
/ SumAggregateExpression / SumAggregateExpression
ArrayFunctions <- ArrayConcatExpression ArrayFunctions <- ArrayConcatExpression
/ ArrayContainsExpression
/ ArrayContainsAnyExpression
/ ArrayContainsAllExpression
/ ArrayLengthExpression / ArrayLengthExpression
/ ArraySliceExpression / ArraySliceExpression
/ SetIntersectExpression / SetIntersectExpression
/ SetUnionExpression / SetUnionExpression
ConditionalFunctions <- IifExpression
MathFunctions <- MathAbsExpression MathFunctions <- MathAbsExpression
/ MathAcosExpression / MathAcosExpression
/ MathAsinExpression / MathAsinExpression
@@ -516,6 +681,8 @@ ThreeArgumentStringFunctionExpression <- function:ThreeArgumentStringFunction ws
functionType = parsers.FunctionCallEndsWith functionType = parsers.FunctionCallEndsWith
case "STARTSWITH": case "STARTSWITH":
functionType = parsers.FunctionCallStartsWith functionType = parsers.FunctionCallStartsWith
case "REGEXMATCH":
functionType = parsers.FunctionCallRegexMatch
case "INDEX_OF": case "INDEX_OF":
functionType = parsers.FunctionCallIndexOf functionType = parsers.FunctionCallIndexOf
} }
@@ -523,7 +690,7 @@ ThreeArgumentStringFunctionExpression <- function:ThreeArgumentStringFunction ws
return createFunctionCall(functionType, []interface{}{ex1, ex2, ignoreCase}) return createFunctionCall(functionType, []interface{}{ex1, ex2, ignoreCase})
} }
ThreeArgumentStringFunction <- ("CONTAINS"i / "ENDSWITH"i / "STARTSWITH"i / "INDEX_OF"i) { ThreeArgumentStringFunction <- ("CONTAINS"i / "ENDSWITH"i / "STARTSWITH"i / "REGEXMATCH"i / "INDEX_OF"i) {
return string(c.text), nil return string(c.text), nil
} }
@@ -571,6 +738,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})
} }
@@ -587,6 +766,10 @@ 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})
} }
IifExpression <- "IIF"i ws "(" ws condition:SelectItem ws "," ws trueValue:SelectItem ws "," ws falseValue:SelectItem ws ")" {
return createFunctionCall(parsers.FunctionCallIif, []interface{}{condition, trueValue, falseValue})
}
MathAbsExpression <- "ABS"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAbs, []interface{}{ex}) } 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}) } 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}) } MathAsinExpression <- "ASIN"i ws "(" ws ex:SelectItem ws ")" { return createFunctionCall(parsers.FunctionCallMathAsin, []interface{}{ex}) }
@@ -630,8 +813,33 @@ MathNumberBinExpression <- "NumberBin"i ws "(" ws ex1:SelectItem others:(ws ","
MathPiExpression <- "PI"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathPi, []interface{}{}) } MathPiExpression <- "PI"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathPi, []interface{}{}) }
MathRandExpression <- "RAND"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathRand, []interface{}{}) } MathRandExpression <- "RAND"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathRand, []interface{}{}) }
InFunction <- ex1:SelectProperty ws "IN"i ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" { InFunction <- ex1:SelectProperty ws notIn:("NOT"i 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{})...)) arguments := append([]interface{}{ex1, ex2}, others.([]interface{})...)
functionCall, _ := createFunctionCall(parsers.FunctionCallIn, arguments)
if notIn != nil {
return parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Value: functionCall,
Invert: true,
}, nil
}
return functionCall, nil
}
/ "(" ws ex1:SelectItem ws notIn:("NOT"i ws)? In ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" ws ")" {
arguments := append([]interface{}{ex1, ex2}, others.([]interface{})...)
functionCall, _ := createFunctionCall(parsers.FunctionCallIn, arguments)
if notIn != nil {
return parsers.SelectItem{
Type: parsers.SelectItemTypeFunctionCall,
Value: functionCall,
Invert: true,
}, nil
}
return functionCall, nil
} }
AvgAggregateExpression <- "AVG"i "(" ws ex:SelectItem ws ")" { AvgAggregateExpression <- "AVG"i "(" ws ex:SelectItem ws ")" {
@@ -677,4 +885,6 @@ non_escape_character <- !(escape_character) char:.
ws <- [ \t\n\r]* ws <- [ \t\n\r]*
wss <- [ \t\n\r]+
EOF <- !. EOF <- !.
+142 -8
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,93 @@ 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 empty object", func(t *testing.T) {
testQueryParse(
t,
`SELECT {} AS obj FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Alias: "obj",
Type: parsers.SelectItemTypeObject,
SelectItems: []parsers.SelectItem{},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse comparison expressions in SELECT", func(t *testing.T) {
testQueryParse(
t,
`SELECT c["id"] = "123", c["pk"] > 456 FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeExpression,
Value: parsers.ComparisonExpression{
Operation: "=",
Left: testutils.SelectItem_Path("c", "id"),
Right: testutils.SelectItem_Constant_String("123"),
},
},
{
Type: parsers.SelectItemTypeExpression,
Value: parsers.ComparisonExpression{
Operation: ">",
Left: testutils.SelectItem_Path("c", "pk"),
Right: testutils.SelectItem_Constant_Int(456),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
)
})
t.Run("Should parse logical expressions in SELECT", func(t *testing.T) {
testQueryParse(
t,
`SELECT c["id"] = "123" OR c["pk"] > 456, c["isCool"] AND c["hasRizz"] AS isRizzler FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeExpression,
Value: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeOr,
Expressions: []interface{}{
parsers.ComparisonExpression{
Operation: "=",
Left: testutils.SelectItem_Path("c", "id"),
Right: testutils.SelectItem_Constant_String("123"),
},
parsers.ComparisonExpression{
Operation: ">",
Left: testutils.SelectItem_Path("c", "pk"),
Right: testutils.SelectItem_Constant_Int(456),
},
},
},
},
{
Type: parsers.SelectItemTypeExpression,
Alias: "isRizzler",
Value: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{
testutils.SelectItem_Path("c", "isCool"),
testutils.SelectItem_Path("c", "hasRizz"),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
+66 -153
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,39 @@ 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")},
},
)
})
t.Run("Should parse function REGEXMATCH()", func(t *testing.T) {
testQueryParse(
t,
`SELECT REGEXMATCH(c.id, "aB c", "ix") FROM c`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallRegexMatch,
Arguments: []interface{}{
parsers.SelectItem{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
testutils.SelectItem_Constant_String("aB c"),
testutils.SelectItem_Constant_String("ix"),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -242,25 +209,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 +239,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -308,7 +263,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -332,7 +287,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -352,18 +307,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 +336,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -411,7 +360,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -431,25 +380,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 +406,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 +435,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -524,18 +455,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 +484,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })
@@ -579,25 +504,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 +534,7 @@ func Test_Execute_StringFunctions(t *testing.T) {
}, },
}, },
}, },
Table: parsers.Table{Value: "c"}, Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
}, },
) )
}) })

Some files were not shown because too many files have changed in this diff Show More