Compare commits

..

43 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
115 changed files with 10404 additions and 4165 deletions
+62 -4
View File
@@ -7,15 +7,18 @@ jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: shared-libraries
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@v3.1.0
uses: crazy-max/ghaction-xgo@de82f877ff4552f03b66c146f608233849e9c3dc # v4.0.0
with:
xgo_version: latest
go_version: 1.24.0
go_version: 1.26.3
dest: dist
pkg: sharedlibrary
prefix: cosmium
@@ -25,7 +28,62 @@ jobs:
buildvcs: true
- name: Upload artifact
uses: actions/upload-artifact@v4
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
+6 -6
View File
@@ -3,7 +3,7 @@ name: goreleaser
on:
push:
tags:
- '*'
- "*"
permissions:
contents: write
@@ -14,20 +14,20 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.24.0
go-version: 1.26.3
- name: Cross-Compile with xgo
uses: crazy-max/ghaction-xgo@v3.1.0
uses: crazy-max/ghaction-xgo@de82f877ff4552f03b66c146f608233849e9c3dc # v4.0.0
with:
xgo_version: latest
go_version: 1.24.0
go_version: 1.26.3
dest: sharedlibrary_dist
pkg: sharedlibrary
prefix: cosmium
@@ -44,7 +44,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@5742e2a039330cbb23ebf35f046f814d4c6ff811 # v5
with:
distribution: goreleaser
version: ${{ env.GITHUB_REF_NAME }}
+5 -1
View File
@@ -9,7 +9,7 @@ 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.24.0
GOVERSION=1.26.3
DIST_DIR=dist
@@ -51,6 +51,10 @@ 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)
+15 -1
View File
@@ -64,7 +64,8 @@ There are two docker tags available:
If you wan to run the application using docker, configure it using environment variables see example:
```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 \
-v ./save.json:/save.json \
-p 8081:8081 \
@@ -86,6 +87,7 @@ To disable SSL and run Cosmium on HTTP instead, you can use the `-DisableTls` fl
- **-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)
- **-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.
@@ -99,6 +101,18 @@ All mentioned arguments can also be set using environment variables:
- **COSMIUM_PORT** for `-Port`
- **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
This project is [MIT licensed](./LICENSE).
+4 -4
View File
@@ -3,7 +3,7 @@ package api
import (
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
"github.com/pikami/cosmium/internal/datastore"
)
type ApiServer struct {
@@ -11,10 +11,10 @@ type ApiServer struct {
onServerShutdown chan interface{}
isActive bool
router *gin.Engine
config config.ServerConfig
config *config.ServerConfig
}
func NewApiServer(dataRepository *repositories.DataRepository, config config.ServerConfig) *ApiServer {
func NewApiServer(dataStore datastore.DataStore, config *config.ServerConfig) *ApiServer {
stopChan := make(chan interface{})
onServerShutdownChan := make(chan interface{})
@@ -24,7 +24,7 @@ func NewApiServer(dataRepository *repositories.DataRepository, config config.Ser
config: config,
}
apiServer.CreateRouter(dataRepository)
apiServer.CreateRouter(dataStore)
return apiServer
}
+31
View File
@@ -15,9 +15,15 @@ const (
ExplorerBaseUrlLocation = "/_explorer"
)
const (
DataStoreJson = "json"
DataStoreBadger = "badger"
)
func ParseFlags() ServerConfig {
host := flag.String("Host", "localhost", "Hostname")
port := flag.Int("Port", 8081, "Listen port")
rntbdPort := flag.Int("RntbdPort", 10000, "RNTBD listen port")
explorerPath := flag.String("ExplorerDir", "", "Path to cosmos-explorer files")
tlsCertificatePath := flag.String("Cert", "", "Hostname")
tlsCertificateKey := flag.String("CertKey", "", "Hostname")
@@ -28,6 +34,9 @@ func ParseFlags() ServerConfig {
persistDataPath := flag.String("Persist", "", "Saves data to given path on application exit")
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()
setFlagsFromEnvironment()
@@ -35,6 +44,7 @@ func ParseFlags() ServerConfig {
config := ServerConfig{}
config.Host = *host
config.Port = *port
config.RntbdPort = *rntbdPort
config.ExplorerPath = *explorerPath
config.TLS_CertificatePath = *tlsCertificatePath
config.TLS_CertificateKey = *tlsCertificateKey
@@ -44,6 +54,8 @@ func ParseFlags() ServerConfig {
config.DisableTls = *disableTls
config.AccountKey = *accountKey
config.LogLevel = logLevel.value
config.DataStore = dataStore.value
config.EnableRntbd = *enableRntbd
config.PopulateCalculatedFields()
@@ -54,6 +66,7 @@ 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 {
@@ -68,6 +81,24 @@ func (c *ServerConfig) PopulateCalculatedFields() {
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() {
+5
View File
@@ -4,10 +4,12 @@ type ServerConfig struct {
DatabaseAccount string `json:"databaseAccount"`
DatabaseDomain string `json:"databaseDomain"`
DatabaseEndpoint string `json:"databaseEndpoint"`
RntbdEndpoint string `json:"rntbdEndpoint"`
AccountKey string `json:"accountKey"`
ExplorerPath string `json:"explorerPath"`
Port int `json:"port"`
RntbdPort int `json:"rntbdPort"`
Host string `json:"host"`
TLS_CertificatePath string `json:"tlsCertificatePath"`
TLS_CertificateKey string `json:"tlsCertificateKey"`
@@ -17,4 +19,7 @@ type ServerConfig struct {
DisableTls bool `json:"disableTls"`
LogLevel string `json:"logLevel"`
ExplorerBaseUrlLocation string `json:"explorerBaseUrlLocation"`
EnableRntbd bool `json:"enableRntbd"`
DataStore string `json:"dataStore"`
}
+25 -23
View File
@@ -5,17 +5,19 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
)
func (h *Handlers) GetAllCollections(c *gin.Context) {
databaseId := c.Param("databaseId")
collections, status := h.repository.GetAllCollections(databaseId)
if status == repositorymodels.StatusOk {
database, _ := h.repository.GetDatabase(databaseId)
collections, status := h.dataStore.GetAllCollections(databaseId)
if status == datastore.StatusOk {
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{
"_rid": database.ResourceID,
"DocumentCollections": collections,
@@ -24,48 +26,48 @@ func (h *Handlers) GetAllCollections(c *gin.Context) {
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetCollection(c *gin.Context) {
databaseId := c.Param("databaseId")
id := c.Param("collId")
collection, status := h.repository.GetCollection(databaseId, id)
if status == repositorymodels.StatusOk {
collection, status := h.dataStore.GetCollection(databaseId, id)
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, collection)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteCollection(c *gin.Context) {
databaseId := c.Param("databaseId")
id := c.Param("collId")
status := h.repository.DeleteCollection(databaseId, id)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteCollection(databaseId, id)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateCollection(c *gin.Context) {
databaseId := c.Param("databaseId")
var newCollection repositorymodels.Collection
var newCollection datastore.Collection
if err := c.BindJSON(&newCollection); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
@@ -73,20 +75,20 @@ func (h *Handlers) CreateCollection(c *gin.Context) {
}
if newCollection.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"})
c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdCollection, status := h.repository.CreateCollection(databaseId, newCollection)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdCollection, status := h.dataStore.CreateCollection(databaseId, newCollection)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdCollection)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
+2 -2
View File
@@ -7,11 +7,11 @@ import (
)
func (h *Handlers) CosmiumExport(c *gin.Context) {
repositoryState, err := h.repository.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(repositoryState))
c.Data(http.StatusOK, "application/json", []byte(dataStoreState))
}
+24 -22
View File
@@ -5,13 +5,15 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
)
func (h *Handlers) GetAllDatabases(c *gin.Context) {
databases, status := h.repository.GetAllDatabases()
if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(databases)))
databases, status := h.dataStore.GetAllDatabases()
if status == datastore.StatusOk {
c.Header(headers.ItemCount, fmt.Sprintf("%d", len(databases)))
c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "",
"Databases": databases,
@@ -20,45 +22,45 @@ func (h *Handlers) GetAllDatabases(c *gin.Context) {
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetDatabase(c *gin.Context) {
id := c.Param("databaseId")
database, status := h.repository.GetDatabase(id)
if status == repositorymodels.StatusOk {
database, status := h.dataStore.GetDatabase(id)
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, database)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteDatabase(c *gin.Context) {
id := c.Param("databaseId")
status := h.repository.DeleteDatabase(id)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteDatabase(id)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateDatabase(c *gin.Context) {
var newDatabase repositorymodels.Database
var newDatabase datastore.Database
if err := c.BindJSON(&newDatabase); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
@@ -66,20 +68,20 @@ func (h *Handlers) CreateDatabase(c *gin.Context) {
}
if newDatabase.ID == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"})
c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdDatabase, status := h.repository.CreateDatabase(newDatabase)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdDatabase, status := h.dataStore.CreateDatabase(newDatabase)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDatabase)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
+172 -80
View File
@@ -9,20 +9,26 @@ import (
jsonpatch "github.com/cosmiumdev/json-patch/v5"
"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"
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"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/parsers"
"github.com/pikami/cosmium/parsers/nosql"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
)
func (h *Handlers) GetAllDocuments(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
documents, status := h.repository.GetAllDocuments(databaseId, collectionId)
if status == repositorymodels.StatusOk {
collection, _ := h.repository.GetCollection(databaseId, collectionId)
documents, status := h.dataStore.GetAllDocuments(databaseId, collectionId)
if status == datastore.StatusOk {
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{
"_rid": collection.ID,
"Documents": documents,
@@ -31,7 +37,7 @@ func (h *Handlers) GetAllDocuments(c *gin.Context) {
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetDocument(c *gin.Context) {
@@ -39,18 +45,21 @@ func (h *Handlers) GetDocument(c *gin.Context) {
collectionId := c.Param("collId")
documentId := c.Param("docId")
document, status := h.repository.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk {
document, status := h.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == datastore.StatusOk {
if etag, ok := document["_etag"].(string); ok {
c.Header(headers.ETag, etag)
}
c.IndentedJSON(http.StatusOK, document)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteDocument(c *gin.Context) {
@@ -58,21 +67,21 @@ func (h *Handlers) DeleteDocument(c *gin.Context) {
collectionId := c.Param("collId")
documentId := c.Param("docId")
status := h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteDocument(databaseId, collectionId, documentId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
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 (h *Handlers) ReplaceDocument(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
@@ -84,24 +93,42 @@ func (h *Handlers) ReplaceDocument(c *gin.Context) {
return
}
status := h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
existingDocument, status := h.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
if status != datastore.StatusOk {
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
return
}
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
if ifMatch := c.GetHeader(headers.IfMatch); ifMatch != "" {
if existingDocument["_etag"] != ifMatch {
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
}
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)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) PatchDocument(c *gin.Context) {
@@ -109,9 +136,9 @@ func (h *Handlers) PatchDocument(c *gin.Context) {
collectionId := c.Param("collId")
documentId := c.Param("docId")
document, status := h.repository.GetDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
document, status := h.dataStore.GetDocument(databaseId, collectionId, documentId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
@@ -160,24 +187,24 @@ func (h *Handlers) PatchDocument(c *gin.Context) {
return
}
status = h.repository.DeleteDocument(databaseId, collectionId, documentId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
status = h.dataStore.DeleteDocument(databaseId, collectionId, documentId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, modifiedDocument)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, modifiedDocument)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DocumentsPost(c *gin.Context) {
@@ -185,7 +212,7 @@ func (h *Handlers) DocumentsPost(c *gin.Context) {
collectionId := c.Param("collId")
// Handle batch requests
isBatchRequest, _ := strconv.ParseBool(c.GetHeader("x-ms-cosmos-is-batch-request"))
isBatchRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsBatchRequest))
if isBatchRequest {
h.handleBatchRequest(c)
return
@@ -197,34 +224,43 @@ func (h *Handlers) DocumentsPost(c *gin.Context) {
return
}
query := requestBody["query"]
if query != nil {
// Handle query plan requests
isQueryPlanRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsQueryPlanRequest))
if isQueryPlanRequest {
c.IndentedJSON(http.StatusOK, constants.QueryPlanResponse)
return
}
// Handle query requests
isQueryRequest, _ := strconv.ParseBool(c.GetHeader(headers.IsQuery))
isQueryRequestAltHeader, _ := strconv.ParseBool(c.GetHeader(headers.Query))
if isQueryRequest || isQueryRequestAltHeader {
h.handleDocumentQuery(c, requestBody)
return
}
if requestBody["id"] == "" {
c.JSON(http.StatusBadRequest, gin.H{"message": "BadRequest"})
c.JSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
isUpsert, _ := strconv.ParseBool(c.GetHeader("x-ms-documentdb-is-upsert"))
isUpsert, _ := strconv.ParseBool(c.GetHeader(headers.IsUpsert))
if isUpsert {
h.repository.DeleteDocument(databaseId, collectionId, requestBody["id"].(string))
h.dataStore.DeleteDocument(databaseId, collectionId, requestBody["id"].(string))
}
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, requestBody)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, requestBody)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdDocument)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func parametersToMap(pairs []interface{}) map[string]interface{} {
@@ -243,29 +279,55 @@ func (h *Handlers) handleDocumentQuery(c *gin.Context, requestBody map[string]in
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
if c.GetHeader("x-ms-cosmos-is-query-plan-request") != "" {
c.IndentedJSON(http.StatusOK, constants.QueryPlanResponse)
return
}
var queryParameters map[string]interface{}
if paramsArray, ok := requestBody["parameters"].([]interface{}); ok {
queryParameters = parametersToMap(paramsArray)
}
docs, status := h.repository.ExecuteQueryDocuments(databaseId, collectionId, requestBody["query"].(string), queryParameters)
if status != repositorymodels.StatusOk {
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
}
collection, _ := h.repository.GetCollection(databaseId, collectionId)
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(docs)))
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": docs,
"_count": len(docs),
"Documents": executeQueryResult.Rows,
"_count": resultCount,
})
}
@@ -283,9 +345,9 @@ func (h *Handlers) handleBatchRequest(c *gin.Context) {
for idx, operation := range batchOperations {
switch operation.OperationType {
case apimodels.BatchOperationTypeCreate:
createdDocument, status := h.repository.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := repositoryStatusToResponseCode(status)
if status == repositorymodels.StatusOk {
createdDocument, status := h.dataStore.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := dataStoreStatusToResponseCode(status)
if status == datastore.StatusOk {
responseCode = http.StatusCreated
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
@@ -293,25 +355,25 @@ func (h *Handlers) handleBatchRequest(c *gin.Context) {
ResourceBody: createdDocument,
}
case apimodels.BatchOperationTypeDelete:
status := h.repository.DeleteDocument(databaseId, collectionId, operation.Id)
responseCode := repositoryStatusToResponseCode(status)
if status == repositorymodels.StatusOk {
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.repository.DeleteDocument(databaseId, collectionId, operation.Id)
if deleteStatus == repositorymodels.StatusNotFound {
deleteStatus := h.dataStore.DeleteDocument(databaseId, collectionId, operation.Id)
if deleteStatus == datastore.StatusNotFound {
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: http.StatusNotFound,
}
continue
}
createdDocument, createStatus := h.repository.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := repositoryStatusToResponseCode(createStatus)
if createStatus == repositorymodels.StatusOk {
createdDocument, createStatus := h.dataStore.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := dataStoreStatusToResponseCode(createStatus)
if createStatus == datastore.StatusOk {
responseCode = http.StatusCreated
}
batchOperationResults[idx] = apimodels.BatchOperationResult{
@@ -320,10 +382,10 @@ func (h *Handlers) handleBatchRequest(c *gin.Context) {
}
case apimodels.BatchOperationTypeUpsert:
documentId := operation.ResourceBody["id"].(string)
h.repository.DeleteDocument(databaseId, collectionId, documentId)
createdDocument, createStatus := h.repository.CreateDocument(databaseId, collectionId, operation.ResourceBody)
responseCode := repositoryStatusToResponseCode(createStatus)
if createStatus == repositorymodels.StatusOk {
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{
@@ -331,9 +393,9 @@ func (h *Handlers) handleBatchRequest(c *gin.Context) {
ResourceBody: createdDocument,
}
case apimodels.BatchOperationTypeRead:
document, status := h.repository.GetDocument(databaseId, collectionId, operation.Id)
document, status := h.dataStore.GetDocument(databaseId, collectionId, operation.Id)
batchOperationResults[idx] = apimodels.BatchOperationResult{
StatusCode: repositoryStatusToResponseCode(status),
StatusCode: dataStoreStatusToResponseCode(status),
ResourceBody: document,
}
case apimodels.BatchOperationTypePatch:
@@ -352,17 +414,47 @@ func (h *Handlers) handleBatchRequest(c *gin.Context) {
c.JSON(http.StatusOK, batchOperationResults)
}
func repositoryStatusToResponseCode(status repositorymodels.RepositoryStatus) int {
func dataStoreStatusToResponseCode(status datastore.DataStoreStatus) int {
switch status {
case repositorymodels.StatusOk:
case datastore.StatusOk:
return http.StatusOK
case repositorymodels.StatusNotFound:
case datastore.StatusNotFound:
return http.StatusNotFound
case repositorymodels.Conflict:
case datastore.Conflict:
return http.StatusConflict
case repositorymodels.BadRequest:
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
}
+6 -6
View File
@@ -2,17 +2,17 @@ package handlers
import (
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
"github.com/pikami/cosmium/internal/datastore"
)
type Handlers struct {
repository *repositories.DataRepository
config config.ServerConfig
dataStore datastore.DataStore
config *config.ServerConfig
}
func NewHandlers(dataRepository *repositories.DataRepository, config config.ServerConfig) *Handlers {
func NewHandlers(dataStore datastore.DataStore, config *config.ServerConfig) *Handlers {
return &Handlers{
repository: dataRepository,
config: config,
dataStore: dataStore,
config: config,
}
}
+17 -4
View File
@@ -6,11 +6,12 @@ import (
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/authentication"
"github.com/pikami/cosmium/internal/logger"
)
func Authentication(config config.ServerConfig) gin.HandlerFunc {
func Authentication(config *config.ServerConfig) gin.HandlerFunc {
return func(c *gin.Context) {
requestUrl := c.Request.URL.String()
if config.DisableAuth ||
@@ -22,8 +23,8 @@ func Authentication(config config.ServerConfig) gin.HandlerFunc {
resourceType := urlToResourceType(requestUrl)
resourceId := requestToResourceId(c)
authHeader := c.Request.Header.Get("authorization")
date := c.Request.Header.Get("x-ms-date")
authHeader := c.Request.Header.Get(headers.Authorization)
date := c.Request.Header.Get(headers.XDate)
expectedSignature := authentication.GenerateSignature(
c.Request.Method, resourceType, resourceId, date, config.AccountKey)
@@ -60,6 +61,9 @@ func requestToResourceId(c *gin.Context) string {
databaseId, _ := c.Params.Get("databaseId")
collId, _ := c.Params.Get("collId")
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())
var resourceId string
@@ -72,8 +76,17 @@ func requestToResourceId(c *gin.Context) string {
if 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 {
resourceId = collId
}
@@ -7,7 +7,7 @@ import (
"github.com/pikami/cosmium/api/config"
)
func StripTrailingSlashes(r *gin.Engine, config config.ServerConfig) gin.HandlerFunc {
func StripTrailingSlashes(r *gin.Engine, config *config.ServerConfig) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if len(path) > 1 && path[len(path)-1] == '/' && !strings.Contains(path, config.ExplorerBaseUrlLocation) {
+2 -1
View File
@@ -4,10 +4,11 @@ import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/headers"
)
func GetOffers(c *gin.Context) {
c.Header("x-ms-item-count", "0")
c.Header(headers.ItemCount, "0")
c.IndentedJSON(http.StatusOK, gin.H{
"_rid": "",
"_count": 0,
+15 -13
View File
@@ -5,7 +5,9 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
)
@@ -13,21 +15,21 @@ func (h *Handlers) GetPartitionKeyRanges(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
if c.Request.Header.Get("if-none-match") != "" {
if c.Request.Header.Get(headers.IfNoneMatch) != "" {
c.AbortWithStatus(http.StatusNotModified)
return
}
partitionKeyRanges, status := h.repository.GetPartitionKeyRanges(databaseId, collectionId)
if status == repositorymodels.StatusOk {
c.Header("etag", "\"420\"")
c.Header("lsn", "420")
c.Header("x-ms-cosmos-llsn", "420")
c.Header("x-ms-global-committed-lsn", "420")
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(partitionKeyRanges)))
partitionKeyRanges, status := h.dataStore.GetPartitionKeyRanges(databaseId, collectionId)
if status == datastore.StatusOk {
c.Header(headers.ETag, "\"420\"")
c.Header(headers.LSN, "420")
c.Header(headers.CosmosLsn, "420")
c.Header(headers.GlobalCommittedLsn, "420")
c.Header(headers.ItemCount, fmt.Sprintf("%d", len(partitionKeyRanges)))
collectionRid := collectionId
collection, _ := h.repository.GetCollection(databaseId, collectionId)
collection, _ := h.dataStore.GetCollection(databaseId, collectionId)
if collection.ResourceID != "" {
collectionRid = collection.ResourceID
}
@@ -41,10 +43,10 @@ func (h *Handlers) GetPartitionKeyRanges(c *gin.Context) {
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
+47 -1
View File
@@ -3,6 +3,7 @@ package handlers
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
@@ -27,7 +28,9 @@ func (h *Handlers) GetServerInfo(c *gin.Context) {
"databaseAccountEndpoint": h.config.DatabaseEndpoint,
},
},
"enableMultipleWriteLocations": false,
"enableMultipleWriteLocations": false,
"continuousBackupEnabled": false,
"enableNRegionSynchronousCommit": false,
"userReplicationPolicy": map[string]interface{}{
"asyncReplication": false,
"minReplicaSetSize": 1,
@@ -39,3 +42,46 @@ func (h *Handlers) 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}",
})
}
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),
})
}
+34 -32
View File
@@ -5,22 +5,24 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
)
func (h *Handlers) GetAllStoredProcedures(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
sps, status := h.repository.GetAllStoredProcedures(databaseId, collectionId)
sps, status := h.dataStore.GetAllStoredProcedures(databaseId, collectionId)
if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(sps)))
if status == datastore.StatusOk {
c.Header(headers.ItemCount, fmt.Sprintf("%d", len(sps)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "StoredProcedures": sps, "_count": len(sps)})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetStoredProcedure(c *gin.Context) {
@@ -28,19 +30,19 @@ func (h *Handlers) GetStoredProcedure(c *gin.Context) {
collectionId := c.Param("collId")
spId := c.Param("spId")
sp, status := h.repository.GetStoredProcedure(databaseId, collectionId, spId)
sp, status := h.dataStore.GetStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, sp)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteStoredProcedure(c *gin.Context) {
@@ -48,18 +50,18 @@ func (h *Handlers) DeleteStoredProcedure(c *gin.Context) {
collectionId := c.Param("collId")
spId := c.Param("spId")
status := h.repository.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceStoredProcedure(c *gin.Context) {
@@ -67,52 +69,52 @@ func (h *Handlers) ReplaceStoredProcedure(c *gin.Context) {
collectionId := c.Param("collId")
spId := c.Param("spId")
var sp repositorymodels.StoredProcedure
var sp datastore.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.repository.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
status := h.dataStore.DeleteStoredProcedure(databaseId, collectionId, spId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdSP, status := h.repository.CreateStoredProcedure(databaseId, collectionId, sp)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdSP, status := h.dataStore.CreateStoredProcedure(databaseId, collectionId, sp)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateStoredProcedure(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var sp repositorymodels.StoredProcedure
var sp datastore.StoredProcedure
if err := c.BindJSON(&sp); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdSP, status := h.repository.CreateStoredProcedure(databaseId, collectionId, sp)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdSP, status := h.dataStore.CreateStoredProcedure(databaseId, collectionId, sp)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdSP)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
+34 -32
View File
@@ -5,22 +5,24 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
)
func (h *Handlers) GetAllTriggers(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
triggers, status := h.repository.GetAllTriggers(databaseId, collectionId)
triggers, status := h.dataStore.GetAllTriggers(databaseId, collectionId)
if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(triggers)))
if status == datastore.StatusOk {
c.Header(headers.ItemCount, fmt.Sprintf("%d", len(triggers)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "Triggers": triggers, "_count": len(triggers)})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetTrigger(c *gin.Context) {
@@ -28,19 +30,19 @@ func (h *Handlers) GetTrigger(c *gin.Context) {
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
trigger, status := h.repository.GetTrigger(databaseId, collectionId, triggerId)
trigger, status := h.dataStore.GetTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, trigger)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteTrigger(c *gin.Context) {
@@ -48,18 +50,18 @@ func (h *Handlers) DeleteTrigger(c *gin.Context) {
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
status := h.repository.DeleteTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteTrigger(databaseId, collectionId, triggerId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceTrigger(c *gin.Context) {
@@ -67,52 +69,52 @@ func (h *Handlers) ReplaceTrigger(c *gin.Context) {
collectionId := c.Param("collId")
triggerId := c.Param("triggerId")
var trigger repositorymodels.Trigger
var trigger datastore.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.repository.DeleteTrigger(databaseId, collectionId, triggerId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
status := h.dataStore.DeleteTrigger(databaseId, collectionId, triggerId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdTrigger, status := h.repository.CreateTrigger(databaseId, collectionId, trigger)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdTrigger, status := h.dataStore.CreateTrigger(databaseId, collectionId, trigger)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateTrigger(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var trigger repositorymodels.Trigger
var trigger datastore.Trigger
if err := c.BindJSON(&trigger); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdTrigger, status := h.repository.CreateTrigger(databaseId, collectionId, trigger)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdTrigger, status := h.dataStore.CreateTrigger(databaseId, collectionId, trigger)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdTrigger)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
+34 -32
View File
@@ -5,22 +5,24 @@ import (
"net/http"
"github.com/gin-gonic/gin"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/constants"
"github.com/pikami/cosmium/internal/datastore"
)
func (h *Handlers) GetAllUserDefinedFunctions(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
udfs, status := h.repository.GetAllUserDefinedFunctions(databaseId, collectionId)
udfs, status := h.dataStore.GetAllUserDefinedFunctions(databaseId, collectionId)
if status == repositorymodels.StatusOk {
c.Header("x-ms-item-count", fmt.Sprintf("%d", len(udfs)))
if status == datastore.StatusOk {
c.Header(headers.ItemCount, fmt.Sprintf("%d", len(udfs)))
c.IndentedJSON(http.StatusOK, gin.H{"_rid": "", "UserDefinedFunctions": udfs, "_count": len(udfs)})
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) GetUserDefinedFunction(c *gin.Context) {
@@ -28,19 +30,19 @@ func (h *Handlers) GetUserDefinedFunction(c *gin.Context) {
collectionId := c.Param("collId")
udfId := c.Param("udfId")
udf, status := h.repository.GetUserDefinedFunction(databaseId, collectionId, udfId)
udf, status := h.dataStore.GetUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, udf)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) DeleteUserDefinedFunction(c *gin.Context) {
@@ -48,18 +50,18 @@ func (h *Handlers) DeleteUserDefinedFunction(c *gin.Context) {
collectionId := c.Param("collId")
udfId := c.Param("udfId")
status := h.repository.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusOk {
status := h.dataStore.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == datastore.StatusOk {
c.Status(http.StatusNoContent)
return
}
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) ReplaceUserDefinedFunction(c *gin.Context) {
@@ -67,52 +69,52 @@ func (h *Handlers) ReplaceUserDefinedFunction(c *gin.Context) {
collectionId := c.Param("collId")
udfId := c.Param("udfId")
var udf repositorymodels.UserDefinedFunction
var udf datastore.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
status := h.repository.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == repositorymodels.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "NotFound"})
status := h.dataStore.DeleteUserDefinedFunction(databaseId, collectionId, udfId)
if status == datastore.StatusNotFound {
c.IndentedJSON(http.StatusNotFound, constants.NotFoundResponse)
return
}
createdUdf, status := h.repository.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdUdf, status := h.dataStore.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusOK, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
c.IndentedJSON(http.StatusInternalServerError, constants.UnknownErrorResponse)
}
func (h *Handlers) CreateUserDefinedFunction(c *gin.Context) {
databaseId := c.Param("databaseId")
collectionId := c.Param("collId")
var udf repositorymodels.UserDefinedFunction
var udf datastore.UserDefinedFunction
if err := c.BindJSON(&udf); err != nil {
c.IndentedJSON(http.StatusBadRequest, gin.H{"message": "Invalid body"})
c.IndentedJSON(http.StatusBadRequest, constants.BadRequestResponse)
return
}
createdUdf, status := h.repository.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == repositorymodels.Conflict {
c.IndentedJSON(http.StatusConflict, gin.H{"message": "Conflict"})
createdUdf, status := h.dataStore.CreateUserDefinedFunction(databaseId, collectionId, udf)
if status == datastore.Conflict {
c.IndentedJSON(http.StatusConflict, constants.ConflictResponse)
return
}
if status == repositorymodels.StatusOk {
if status == datastore.StatusOk {
c.IndentedJSON(http.StatusCreated, createdUdf)
return
}
c.IndentedJSON(http.StatusInternalServerError, gin.H{"message": "Unknown error"})
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"
)
+5 -3
View File
@@ -10,15 +10,15 @@ import (
"github.com/gin-gonic/gin"
"github.com/pikami/cosmium/api/handlers"
"github.com/pikami/cosmium/api/handlers/middleware"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/repositories"
tlsprovider "github.com/pikami/cosmium/internal/tls_provider"
)
var ginMux sync.Mutex
func (s *ApiServer) CreateRouter(repository *repositories.DataRepository) {
routeHandlers := handlers.NewHandlers(repository, s.config)
func (s *ApiServer) CreateRouter(dataStore datastore.DataStore) {
routeHandlers := handlers.NewHandlers(dataStore, s.config)
ginMux.Lock()
gin.DefaultWriter = logger.InfoWriter()
@@ -31,6 +31,7 @@ func (s *ApiServer) CreateRouter(repository *repositories.DataRepository) {
router := gin.Default(func(e *gin.Engine) {
e.RedirectTrailingSlash = false
e.RemoveExtraSlash = true
})
if s.config.LogLevel == "debug" {
@@ -79,6 +80,7 @@ func (s *ApiServer) CreateRouter(repository *repositories.DataRepository) {
router.GET("/offers", handlers.GetOffers)
router.GET("/", routeHandlers.GetServerInfo)
router.GET("//addresses", routeHandlers.GetAddresses)
router.GET("/cosmium/export", routeHandlers.CosmiumExport)
+12 -15
View File
@@ -2,13 +2,11 @@ package tests_test
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config"
"github.com/stretchr/testify/assert"
@@ -19,9 +17,9 @@ func Test_Authentication(t *testing.T) {
defer ts.Server.Close()
t.Run("Should get 200 when correct account key is used", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
formatConnectionString(ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
@@ -35,9 +33,9 @@ func Test_Authentication(t *testing.T) {
})
t.Run("Should get 401 when wrong account key is used", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"),
formatConnectionString(ts.URL, "AAAA"),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
@@ -47,12 +45,7 @@ func Test_Authentication(t *testing.T) {
azcosmos.DatabaseProperties{ID: testDatabaseName},
&azcosmos.CreateDatabaseOptions{})
var respErr *azcore.ResponseError
if errors.As(err, &respErr) {
assert.Equal(t, respErr.StatusCode, http.StatusUnauthorized)
} else {
panic(err)
}
assert.Contains(t, err.Error(), "401 Unauthorized")
})
t.Run("Should allow unauthorized requests to /_explorer", func(t *testing.T) {
@@ -68,7 +61,7 @@ func Test_Authentication(t *testing.T) {
}
func Test_Authentication_Disabled(t *testing.T) {
ts := runTestServerCustomConfig(config.ServerConfig{
ts := runTestServerCustomConfig(&config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
@@ -77,9 +70,9 @@ func Test_Authentication_Disabled(t *testing.T) {
defer ts.Server.Close()
t.Run("Should get 200 when wrong account key is used, but authentication is dissabled", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, "AAAA"),
formatConnectionString(ts.URL, "AAAA"),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
@@ -92,3 +85,7 @@ func Test_Authentication_Disabled(t *testing.T) {
assert.Equal(t, createResponse.DatabaseProperties.ID, testDatabaseName)
})
}
func formatConnectionString(endpoint, key string) string {
return fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", endpoint, key)
}
+43 -21
View File
@@ -3,32 +3,29 @@ package tests_test
import (
"context"
"errors"
"fmt"
"net/http"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
"github.com/stretchr/testify/assert"
)
func Test_Collections(t *testing.T) {
ts := runTestServer()
defer ts.Server.Close()
presets := []testPreset{PresetJsonStore, PresetBadgerStore}
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
setUp := func(ts *TestServer, client *azcosmos.Client) *azcosmos.DatabaseClient {
ts.DataStore.CreateDatabase(datastore.Database{ID: testDatabaseName})
databaseClient, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err)
ts.Repository.CreateDatabase(repositorymodels.Database{ID: testDatabaseName})
databaseClient, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err)
return databaseClient
}
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) {
createResponse, err := databaseClient.CreateContainer(context.TODO(), azcosmos.ContainerProperties{
ID: testCollectionName,
@@ -39,7 +36,7 @@ func Test_Collections(t *testing.T) {
})
t.Run("Should return conflict when collection exists", func(t *testing.T) {
ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName,
})
@@ -57,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) {
ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName,
})
@@ -73,7 +72,7 @@ func Test_Collections(t *testing.T) {
})
t.Run("Should return not found when collection does not exist", func(t *testing.T) {
ts.Repository.DeleteCollection(testDatabaseName, testCollectionName)
ts.DataStore.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err)
@@ -90,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) {
ts.Repository.CreateCollection(testDatabaseName, repositorymodels.Collection{
ts.DataStore.CreateCollection(testDatabaseName, datastore.Collection{
ID: testCollectionName,
})
@@ -105,7 +106,7 @@ func Test_Collections(t *testing.T) {
})
t.Run("Should return not found when collection does not exist", func(t *testing.T) {
ts.Repository.DeleteCollection(testDatabaseName, testCollectionName)
ts.DataStore.DeleteCollection(testDatabaseName, testCollectionName)
collectionResponse, err := databaseClient.NewContainer(testCollectionName)
assert.Nil(t, err)
@@ -120,5 +121,26 @@ func Test_Collections(t *testing.T) {
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")
})
})
}
+78 -15
View File
@@ -1,40 +1,59 @@
package tests_test
import (
"fmt"
"net/http/httptest"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"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/repositories"
"github.com/stretchr/testify/assert"
)
type TestServer struct {
Server *httptest.Server
Repository *repositories.DataRepository
URL string
Server *httptest.Server
DataStore datastore.DataStore
URL string
}
func runTestServerCustomConfig(config config.ServerConfig) *TestServer {
repository := repositories.NewDataRepository(repositories.RepositoryOptions{})
func getDefaultTestServerConfig() *config.ServerConfig {
return &config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
DataStore: "json",
}
}
api := api.NewApiServer(repository, config)
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,
Repository: repository,
URL: server.URL,
Server: server,
DataStore: dataStore,
URL: server.URL,
}
}
func runTestServer() *TestServer {
config := config.ServerConfig{
AccountKey: config.DefaultAccountKey,
ExplorerPath: "/tmp/nothing",
ExplorerBaseUrlLocation: config.ExplorerBaseUrlLocation,
}
config := getDefaultTestServerConfig()
config.LogLevel = "debug"
logger.SetLogLevel(logger.LogLevelDebug)
@@ -47,3 +66,47 @@ const (
testDatabaseName = "test-db"
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 -20
View File
@@ -3,30 +3,21 @@ package tests_test
import (
"context"
"errors"
"fmt"
"net/http"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
"github.com/pikami/cosmium/api/config"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
"github.com/stretchr/testify/assert"
)
func Test_Databases(t *testing.T) {
ts := runTestServer()
defer ts.Server.Close()
presets := []testPreset{PresetJsonStore, PresetBadgerStore}
client, err := azcosmos.NewClientFromConnectionString(
fmt.Sprintf("AccountEndpoint=%s;AccountKey=%s", ts.URL, config.DefaultAccountKey),
&azcosmos.ClientOptions{},
)
assert.Nil(t, err)
t.Run("Database Create", func(t *testing.T) {
runTestsWithPresets(t, "Database Create", presets, func(t *testing.T, ts *TestServer, client *azcosmos.Client) {
t.Run("Should create database", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
createResponse, err := client.CreateDatabase(context.TODO(), azcosmos.DatabaseProperties{
ID: testDatabaseName,
@@ -37,7 +28,7 @@ func Test_Databases(t *testing.T) {
})
t.Run("Should return conflict when database exists", func(t *testing.T) {
ts.Repository.CreateDatabase(repositorymodels.Database{
ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName,
})
@@ -55,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) {
ts.Repository.CreateDatabase(repositorymodels.Database{
ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName,
})
@@ -71,7 +62,7 @@ func Test_Databases(t *testing.T) {
})
t.Run("Should return not found when database does not exist", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err)
@@ -88,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) {
ts.Repository.CreateDatabase(repositorymodels.Database{
ts.DataStore.CreateDatabase(datastore.Database{
ID: testDatabaseName,
})
@@ -103,7 +94,7 @@ func Test_Databases(t *testing.T) {
})
t.Run("Should return not found when database does not exist", func(t *testing.T) {
ts.Repository.DeleteDatabase(testDatabaseName)
ts.DataStore.DeleteDatabase(testDatabaseName)
databaseResponse, err := client.NewDatabase(testDatabaseName)
assert.Nil(t, err)
@@ -118,5 +109,26 @@ func Test_Databases(t *testing.T) {
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,
},
},
)
})
})
}
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -8,13 +8,15 @@ import (
"time"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/api/headers"
"github.com/pikami/cosmium/internal/authentication"
"github.com/stretchr/testify/assert"
)
// Request document with trailing slash like python cosmosdb client does.
func Test_Documents_Read_Trailing_Slash(t *testing.T) {
ts, _ := documents_InitializeDb(t)
ts := runTestServer()
documents_InitializeDb(t, ts)
defer ts.Server.Close()
t.Run("Read doc with client that appends slash to path", func(t *testing.T) {
@@ -25,8 +27,8 @@ func Test_Documents_Read_Trailing_Slash(t *testing.T) {
signature := authentication.GenerateSignature("GET", "docs", path, date, config.DefaultAccountKey)
httpClient := &http.Client{}
req, _ := http.NewRequest("GET", testUrl, nil)
req.Header.Add("x-ms-date", date)
req.Header.Add("authorization", "sig="+url.QueryEscape(signature))
req.Header.Add(headers.XDate, date)
req.Header.Add(headers.Authorization, "sig="+url.QueryEscape(signature))
res, err := httpClient.Do(req)
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)
}
}
}
+33 -12
View File
@@ -7,27 +7,51 @@ import (
"github.com/pikami/cosmium/api"
"github.com/pikami/cosmium/api/config"
"github.com/pikami/cosmium/internal/repositories"
"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()
repository := repositories.NewDataRepository(repositories.RepositoryOptions{
InitialDataFilePath: configuration.InitialDataFilePath,
PersistDataFilePath: configuration.PersistDataFilePath,
})
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(repository, configuration)
server := api.NewApiServer(dataStore, &configuration)
err := server.Start()
if err != nil {
panic(err)
}
waitForExit(server, repository, configuration)
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, repository *repositories.DataRepository, config config.ServerConfig) {
func waitForExit(server *api.ApiServer, dataStore datastore.DataStore) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@@ -36,8 +60,5 @@ func waitForExit(server *api.ApiServer, repository *repositories.DataRepository,
// Stop the server
server.Stop()
if config.PersistDataFilePath != "" {
repository.SaveStateFS(config.PersistDataFilePath)
}
dataStore.Close()
}
+1 -1
View File
@@ -79,7 +79,7 @@ Cosmium strives to support the core features of Cosmos DB, including:
| Function | Implemented |
| -------- | ----------- |
| IIF | No |
| IIF | Yes |
### Date and time Functions
+44 -26
View File
@@ -1,46 +1,64 @@
module github.com/pikami/cosmium
go 1.24.0
go 1.26.3
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6
github.com/cosmiumdev/json-patch/v5 v5.9.3
github.com/gin-gonic/gin v1.10.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.4.2
github.com/cosmiumdev/json-patch/v5 v5.9.11
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/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
github.com/stretchr/testify v1.11.1
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/exp v0.0.0-20260603202125-055de637280b
)
require (
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.1 // 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/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.4.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/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/go-playground/validator/v10 v10.30.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/klauspost/cpuid/v2 v2.2.9 // 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/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/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // 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/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // 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
)
+103 -68
View File
@@ -1,58 +1,76 @@
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/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6 h1:oBqQLSI1pZwGOdXJAoJJSzmff9tlfD4KroVfjQQmd0g=
github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v0.3.6/go.mod h1:Beh5cHIXJ0oWEDWk9lNFtuklCojLLQ5hl+LqSNTTs0I=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cosmiumdev/json-patch/v5 v5.9.3 h1:l+Og3+5edqV2NHDo58sz72eS733lbXVYP61seYK43Do=
github.com/cosmiumdev/json-patch/v5 v5.9.3/go.mod h1:WzSTCdia0WrlZtjnL19P4RiwWtfdyArm/E7stgEeP5g=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
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.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
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 v1.4.2 h1:zqxnp53f5Jn5PFU5Av4mvyWEbZ7whg72AoOCEzlXFKc=
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.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
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 v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo=
github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/dgraph-io/badger/v4 v4.9.2 h1:Wb5qw8gElqwV1a8msHTeQKova9b1V10heFKMIiPd80E=
github.com/dgraph-io/badger/v4 v4.9.2/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
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/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/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/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
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/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
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=
@@ -61,58 +79,75 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
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/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.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/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.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.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.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.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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ=
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 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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=
+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{}
}
@@ -1,52 +1,52 @@
package repositories
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"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 *DataRepository) GetAllCollections(databaseId string) ([]repositorymodels.Collection, repositorymodels.RepositoryStatus) {
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([]repositorymodels.Collection, 0), repositorymodels.StatusNotFound
return make([]datastore.Collection, 0), datastore.StatusNotFound
}
return maps.Values(r.storeState.Collections[databaseId]), repositorymodels.StatusOk
return maps.Values(r.storeState.Collections[databaseId]), datastore.StatusOk
}
func (r *DataRepository) GetCollection(databaseId string, collectionId string) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
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 repositorymodels.Collection{}, repositorymodels.StatusNotFound
return datastore.Collection{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound
return datastore.Collection{}, datastore.StatusNotFound
}
return r.storeState.Collections[databaseId][collectionId], repositorymodels.StatusOk
return r.storeState.Collections[databaseId][collectionId], datastore.StatusOk
}
func (r *DataRepository) DeleteCollection(databaseId string, collectionId string) repositorymodels.RepositoryStatus {
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 repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
delete(r.storeState.Collections[databaseId], collectionId)
@@ -55,24 +55,24 @@ func (r *DataRepository) DeleteCollection(databaseId string, collectionId string
delete(r.storeState.StoredProcedures[databaseId], collectionId)
delete(r.storeState.UserDefinedFunctions[databaseId], collectionId)
return repositorymodels.StatusOk
return datastore.StatusOk
}
func (r *DataRepository) CreateCollection(databaseId string, newCollection repositorymodels.Collection) (repositorymodels.Collection, repositorymodels.RepositoryStatus) {
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 repositorymodels.Database
var database datastore.Database
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Collection{}, repositorymodels.StatusNotFound
return datastore.Collection{}, datastore.StatusNotFound
}
if _, ok = r.storeState.Collections[databaseId][newCollection.ID]; ok {
return repositorymodels.Collection{}, repositorymodels.Conflict
return datastore.Collection{}, datastore.Conflict
}
newCollection = structhidrators.Hidrate(newCollection).(repositorymodels.Collection)
newCollection = structhidrators.Hidrate(newCollection).(datastore.Collection)
newCollection.TimeStamp = time.Now().Unix()
newCollection.ResourceID = resourceid.NewCombined(database.ResourceID, resourceid.New(resourceid.ResourceTypeCollection))
@@ -80,10 +80,10 @@ func (r *DataRepository) CreateCollection(databaseId string, newCollection repos
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]repositorymodels.Document)
r.storeState.Triggers[databaseId][newCollection.ID] = make(map[string]repositorymodels.Trigger)
r.storeState.StoredProcedures[databaseId][newCollection.ID] = make(map[string]repositorymodels.StoredProcedure)
r.storeState.UserDefinedFunctions[databaseId][newCollection.ID] = make(map[string]repositorymodels.UserDefinedFunction)
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, repositorymodels.StatusOk
return newCollection, datastore.StatusOk
}
@@ -1,39 +1,39 @@
package repositories
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *DataRepository) GetAllDatabases() ([]repositorymodels.Database, repositorymodels.RepositoryStatus) {
func (r *JsonDataStore) GetAllDatabases() ([]datastore.Database, datastore.DataStoreStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.Databases), repositorymodels.StatusOk
return maps.Values(r.storeState.Databases), datastore.StatusOk
}
func (r *DataRepository) GetDatabase(id string) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
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, repositorymodels.StatusOk
return database, datastore.StatusOk
}
return repositorymodels.Database{}, repositorymodels.StatusNotFound
return datastore.Database{}, datastore.StatusNotFound
}
func (r *DataRepository) DeleteDatabase(id string) repositorymodels.RepositoryStatus {
func (r *JsonDataStore) DeleteDatabase(id string) datastore.DataStoreStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[id]; !ok {
return repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
delete(r.storeState.Databases, id)
@@ -43,15 +43,15 @@ func (r *DataRepository) DeleteDatabase(id string) repositorymodels.RepositorySt
delete(r.storeState.StoredProcedures, id)
delete(r.storeState.UserDefinedFunctions, id)
return repositorymodels.StatusOk
return datastore.StatusOk
}
func (r *DataRepository) CreateDatabase(newDatabase repositorymodels.Database) (repositorymodels.Database, repositorymodels.RepositoryStatus) {
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 repositorymodels.Database{}, repositorymodels.Conflict
return datastore.Database{}, datastore.Conflict
}
newDatabase.TimeStamp = time.Now().Unix()
@@ -60,11 +60,11 @@ func (r *DataRepository) CreateDatabase(newDatabase repositorymodels.Database) (
newDatabase.Self = fmt.Sprintf("dbs/%s/", newDatabase.ResourceID)
r.storeState.Databases[newDatabase.ID] = newDatabase
r.storeState.Collections[newDatabase.ID] = make(map[string]repositorymodels.Collection)
r.storeState.Documents[newDatabase.ID] = make(map[string]map[string]repositorymodels.Document)
r.storeState.Triggers[newDatabase.ID] = make(map[string]map[string]repositorymodels.Trigger)
r.storeState.StoredProcedures[newDatabase.ID] = make(map[string]map[string]repositorymodels.StoredProcedure)
r.storeState.UserDefinedFunctions[newDatabase.ID] = make(map[string]map[string]repositorymodels.UserDefinedFunction)
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, repositorymodels.StatusOk
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,15 +1,15 @@
package repositories
package jsondatastore
import (
"fmt"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
)
// I have no idea what this is tbh
func (r *DataRepository) 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()
@@ -30,7 +30,7 @@ func (r *DataRepository) GetPartitionKeyRanges(databaseId string, collectionId s
pkrSelf := fmt.Sprintf("dbs/%s/colls/%s/pkranges/%s/", databaseRid, collectionRid, pkrResourceId)
etag := fmt.Sprintf("\"%s\"", uuid.New())
return []repositorymodels.PartitionKeyRange{
return []datastore.PartitionKeyRange{
{
ResourceID: pkrResourceId,
ID: "0",
@@ -45,5 +45,5 @@ func (r *DataRepository) GetPartitionKeyRanges(databaseId string, collectionId s
TimeStamp: timestamp,
Lsn: 17,
},
}, repositorymodels.StatusOk
}, datastore.StatusOk
}
@@ -1,16 +1,39 @@
package repositories
package jsondatastore
import (
"encoding/json"
"log"
"os"
"reflect"
"sync"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
func (r *DataRepository) InitializeRepository() {
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
@@ -32,7 +55,7 @@ func (r *DataRepository) InitializeRepository() {
}
}
func (r *DataRepository) LoadStateFS(filePath string) {
func (r *JsonDataStore) LoadStateFS(filePath string) {
data, err := os.ReadFile(filePath)
if err != nil {
log.Fatalf("Error reading state JSON file: %v", err)
@@ -45,11 +68,11 @@ func (r *DataRepository) LoadStateFS(filePath string) {
}
}
func (r *DataRepository) LoadStateJSON(jsonData string) error {
func (r *JsonDataStore) LoadStateJSON(jsonData string) error {
r.storeState.Lock()
defer r.storeState.Unlock()
var state repositorymodels.State
var state State
if err := json.Unmarshal([]byte(jsonData), &state); err != nil {
return err
}
@@ -71,7 +94,7 @@ func (r *DataRepository) LoadStateJSON(jsonData string) error {
return nil
}
func (r *DataRepository) SaveStateFS(filePath string) {
func (r *JsonDataStore) SaveStateFS(filePath string) {
r.storeState.RLock()
defer r.storeState.RUnlock()
@@ -92,7 +115,7 @@ func (r *DataRepository) SaveStateFS(filePath string) {
logger.Infof("User defined functions: %d\n", getLength(r.storeState.UserDefinedFunctions))
}
func (r *DataRepository) GetState() (string, error) {
func (r *JsonDataStore) DumpToJson() (string, error) {
r.storeState.RLock()
defer r.storeState.RUnlock()
@@ -103,16 +126,23 @@ func (r *DataRepository) GetState() (string, error) {
}
return string(data), nil
}
func (r *JsonDataStore) Close() {
if r.persistDataFilePath != "" {
r.SaveStateFS(r.persistDataFilePath)
}
}
func getLength(v interface{}) int {
switch v.(type) {
case repositorymodels.Database,
repositorymodels.Collection,
repositorymodels.Document,
repositorymodels.Trigger,
repositorymodels.StoredProcedure,
repositorymodels.UserDefinedFunction:
case datastore.Database,
datastore.Collection,
datastore.Document,
datastore.Trigger,
datastore.StoredProcedure,
datastore.UserDefinedFunction:
return 1
}
@@ -133,55 +163,55 @@ func getLength(v interface{}) int {
return count
}
func (r *DataRepository) ensureStoreStateNoNullReferences() {
func (r *JsonDataStore) ensureStoreStateNoNullReferences() {
if r.storeState.Databases == nil {
r.storeState.Databases = make(map[string]repositorymodels.Database)
r.storeState.Databases = make(map[string]datastore.Database)
}
if r.storeState.Collections == nil {
r.storeState.Collections = make(map[string]map[string]repositorymodels.Collection)
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]repositorymodels.Document)
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]repositorymodels.Trigger)
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]repositorymodels.StoredProcedure)
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]repositorymodels.UserDefinedFunction)
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]repositorymodels.Collection)
r.storeState.Collections[database] = make(map[string]datastore.Collection)
}
if r.storeState.Documents[database] == nil {
r.storeState.Documents[database] = make(map[string]map[string]repositorymodels.Document)
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]repositorymodels.Trigger)
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]repositorymodels.StoredProcedure)
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]repositorymodels.UserDefinedFunction)
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]repositorymodels.Document)
r.storeState.Documents[database][collection] = make(map[string]datastore.Document)
}
for document := range r.storeState.Documents[database][collection] {
@@ -191,15 +221,15 @@ func (r *DataRepository) ensureStoreStateNoNullReferences() {
}
if r.storeState.Triggers[database][collection] == nil {
r.storeState.Triggers[database][collection] = make(map[string]repositorymodels.Trigger)
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]repositorymodels.StoredProcedure)
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]repositorymodels.UserDefinedFunction)
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
}
@@ -1,83 +1,83 @@
package repositories
package jsondatastore
import (
"fmt"
"time"
"github.com/google/uuid"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/resourceid"
"golang.org/x/exp/maps"
)
func (r *DataRepository) GetAllTriggers(databaseId string, collectionId string) ([]repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
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]), repositorymodels.StatusOk
return maps.Values(r.storeState.Triggers[databaseId][collectionId]), datastore.StatusOk
}
func (r *DataRepository) GetTrigger(databaseId string, collectionId string, triggerId string) (repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
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 repositorymodels.Trigger{}, repositorymodels.StatusNotFound
return datastore.Trigger{}, datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
return datastore.Trigger{}, datastore.StatusNotFound
}
if trigger, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; ok {
return trigger, repositorymodels.StatusOk
return trigger, datastore.StatusOk
}
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
return datastore.Trigger{}, datastore.StatusNotFound
}
func (r *DataRepository) DeleteTrigger(databaseId string, collectionId string, triggerId string) repositorymodels.RepositoryStatus {
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 repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
if _, ok := r.storeState.Triggers[databaseId][collectionId][triggerId]; !ok {
return repositorymodels.StatusNotFound
return datastore.StatusNotFound
}
delete(r.storeState.Triggers[databaseId][collectionId], triggerId)
return repositorymodels.StatusOk
return datastore.StatusOk
}
func (r *DataRepository) CreateTrigger(databaseId string, collectionId string, trigger repositorymodels.Trigger) (repositorymodels.Trigger, repositorymodels.RepositoryStatus) {
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 repositorymodels.Database
var collection repositorymodels.Collection
var database datastore.Database
var collection datastore.Collection
if trigger.ID == "" {
return repositorymodels.Trigger{}, repositorymodels.BadRequest
return datastore.Trigger{}, datastore.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
return datastore.Trigger{}, datastore.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Trigger{}, repositorymodels.StatusNotFound
return datastore.Trigger{}, datastore.StatusNotFound
}
if _, ok = r.storeState.Triggers[databaseId][collectionId][trigger.ID]; ok {
return repositorymodels.Trigger{}, repositorymodels.Conflict
return datastore.Trigger{}, datastore.Conflict
}
trigger.TimeStamp = time.Now().Unix()
@@ -87,5 +87,5 @@ func (r *DataRepository) CreateTrigger(databaseId string, collectionId string, t
r.storeState.Triggers[databaseId][collectionId][trigger.ID] = trigger
return trigger, repositorymodels.StatusOk
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,6 +1,4 @@
package repositorymodels
import "sync"
package datastore
type Database struct {
ID string `json:"id"`
@@ -10,13 +8,15 @@ type Database struct {
Self string `json:"_self"`
}
type RepositoryStatus int
type DataStoreStatus int
const (
StatusOk = 1
StatusNotFound = 2
Conflict = 3
BadRequest = 4
StatusOk DataStoreStatus = 1
StatusNotFound DataStoreStatus = 2
Conflict DataStoreStatus = 3
BadRequest DataStoreStatus = 4
IterEOF DataStoreStatus = 5
Unknown DataStoreStatus = 6
)
type TriggerOperation string
@@ -117,25 +117,3 @@ type PartitionKeyRange struct {
TimeStamp int64 `json:"_ts"`
Lsn int `json:"lsn"`
}
type State struct {
sync.RWMutex
// 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"`
}
-130
View File
@@ -1,130 +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 (r *DataRepository) GetAllDocuments(databaseId string, collectionId string) ([]repositorymodels.Document, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return make([]repositorymodels.Document, 0), repositorymodels.StatusNotFound
}
return maps.Values(r.storeState.Documents[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetDocument(databaseId string, collectionId string, documentId string) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
return r.storeState.Documents[databaseId][collectionId][documentId], repositorymodels.StatusOk
}
func (r *DataRepository) DeleteDocument(databaseId string, collectionId string, documentId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.Documents[databaseId][collectionId], documentId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateDocument(databaseId string, collectionId string, document map[string]interface{}) (repositorymodels.Document, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var 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 = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.Document{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Documents[databaseId][collectionId][documentId]; ok {
return repositorymodels.Document{}, repositorymodels.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, repositorymodels.StatusOk
}
func (r *DataRepository) 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 := r.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.ExecuteQuery(typedQuery, covDocs), repositorymodels.StatusOk
}
return nil, repositorymodels.BadRequest
}
-34
View File
@@ -1,34 +0,0 @@
package repositories
import repositorymodels "github.com/pikami/cosmium/internal/repository_models"
type DataRepository struct {
storeState repositorymodels.State
initialDataFilePath string
persistDataFilePath string
}
type RepositoryOptions struct {
InitialDataFilePath string
PersistDataFilePath string
}
func NewDataRepository(options RepositoryOptions) *DataRepository {
repository := &DataRepository{
storeState: repositorymodels.State{
Databases: make(map[string]repositorymodels.Database),
Collections: make(map[string]map[string]repositorymodels.Collection),
Documents: make(map[string]map[string]map[string]repositorymodels.Document),
Triggers: make(map[string]map[string]map[string]repositorymodels.Trigger),
StoredProcedures: make(map[string]map[string]map[string]repositorymodels.StoredProcedure),
UserDefinedFunctions: make(map[string]map[string]map[string]repositorymodels.UserDefinedFunction),
},
initialDataFilePath: options.InitialDataFilePath,
persistDataFilePath: options.PersistDataFilePath,
}
repository.InitializeRepository()
return repository
}
@@ -1,91 +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 (r *DataRepository) GetAllStoredProcedures(databaseId string, collectionId string) ([]repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.StoredProcedures[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetStoredProcedure(databaseId string, collectionId string, spId string) (repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if sp, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; ok {
return sp, repositorymodels.StatusOk
}
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
func (r *DataRepository) DeleteStoredProcedure(databaseId string, collectionId string, spId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.StoredProcedures[databaseId][collectionId][spId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.StoredProcedures[databaseId][collectionId], spId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateStoredProcedure(databaseId string, collectionId string, sp repositorymodels.StoredProcedure) (repositorymodels.StoredProcedure, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database repositorymodels.Database
var collection repositorymodels.Collection
if sp.ID == "" {
return repositorymodels.StoredProcedure{}, repositorymodels.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StoredProcedure{}, repositorymodels.StatusNotFound
}
if _, ok = r.storeState.StoredProcedures[databaseId][collectionId][sp.ID]; ok {
return repositorymodels.StoredProcedure{}, repositorymodels.Conflict
}
sp.TimeStamp = time.Now().Unix()
sp.ResourceID = resourceid.NewCombined(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, repositorymodels.StatusOk
}
@@ -1,91 +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 (r *DataRepository) GetAllUserDefinedFunctions(databaseId string, collectionId string) ([]repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
return maps.Values(r.storeState.UserDefinedFunctions[databaseId][collectionId]), repositorymodels.StatusOk
}
func (r *DataRepository) GetUserDefinedFunction(databaseId string, collectionId string, udfId string) (repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.RLock()
defer r.storeState.RUnlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if udf, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; ok {
return udf, repositorymodels.StatusOk
}
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
func (r *DataRepository) DeleteUserDefinedFunction(databaseId string, collectionId string, udfId string) repositorymodels.RepositoryStatus {
r.storeState.Lock()
defer r.storeState.Unlock()
if _, ok := r.storeState.Databases[databaseId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udfId]; !ok {
return repositorymodels.StatusNotFound
}
delete(r.storeState.UserDefinedFunctions[databaseId][collectionId], udfId)
return repositorymodels.StatusOk
}
func (r *DataRepository) CreateUserDefinedFunction(databaseId string, collectionId string, udf repositorymodels.UserDefinedFunction) (repositorymodels.UserDefinedFunction, repositorymodels.RepositoryStatus) {
r.storeState.Lock()
defer r.storeState.Unlock()
var ok bool
var database repositorymodels.Database
var collection repositorymodels.Collection
if udf.ID == "" {
return repositorymodels.UserDefinedFunction{}, repositorymodels.BadRequest
}
if database, ok = r.storeState.Databases[databaseId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if collection, ok = r.storeState.Collections[databaseId][collectionId]; !ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.StatusNotFound
}
if _, ok := r.storeState.UserDefinedFunctions[databaseId][collectionId][udf.ID]; ok {
return repositorymodels.UserDefinedFunction{}, repositorymodels.Conflict
}
udf.TimeStamp = time.Now().Unix()
udf.ResourceID = resourceid.NewCombined(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, repositorymodels.StatusOk
}
+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
import (
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
)
import "github.com/pikami/cosmium/internal/datastore"
var defaultCollection repositorymodels.Collection = repositorymodels.Collection{
IndexingPolicy: repositorymodels.CollectionIndexingPolicy{
var defaultCollection datastore.Collection = datastore.Collection{
IndexingPolicy: datastore.CollectionIndexingPolicy{
IndexingMode: "consistent",
Automatic: true,
IncludedPaths: []repositorymodels.CollectionIndexingPolicyPath{
IncludedPaths: []datastore.CollectionIndexingPolicyPath{
{Path: "/*"},
},
ExcludedPaths: []repositorymodels.CollectionIndexingPolicyPath{
ExcludedPaths: []datastore.CollectionIndexingPolicyPath{
{Path: "/\"_etag\"/?"},
},
},
PartitionKey: repositorymodels.CollectionPartitionKey{
PartitionKey: datastore.CollectionPartitionKey{
Paths: []string{"/_partitionKey"},
Kind: "Hash",
Version: 2,
+2 -2
View File
@@ -3,11 +3,11 @@ package structhidrators
import (
"reflect"
repositorymodels "github.com/pikami/cosmium/internal/repository_models"
"github.com/pikami/cosmium/internal/datastore"
)
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 input
+12
View File
@@ -34,6 +34,8 @@ const (
SelectItemTypeConstant
SelectItemTypeFunctionCall
SelectItemTypeSubQuery
SelectItemTypeExpression
SelectItemTypeBinaryExpression
)
type SelectItem struct {
@@ -42,6 +44,7 @@ type SelectItem struct {
SelectItems []SelectItem
Type SelectItemType
Value interface{}
Invert bool
IsTopLevel bool
}
@@ -63,6 +66,12 @@ type ComparisonExpression struct {
Operation string
}
type BinaryExpression struct {
Left interface{}
Right interface{}
Operation string
}
type ConstantType int
const (
@@ -98,6 +107,7 @@ const (
FunctionCallContains FunctionCallType = "Contains"
FunctionCallEndsWith FunctionCallType = "EndsWith"
FunctionCallStartsWith FunctionCallType = "StartsWith"
FunctionCallRegexMatch FunctionCallType = "RegexMatch"
FunctionCallIndexOf FunctionCallType = "IndexOf"
FunctionCallToString FunctionCallType = "ToString"
FunctionCallUpper FunctionCallType = "Upper"
@@ -133,6 +143,8 @@ const (
FunctionCallSetIntersect FunctionCallType = "SetIntersect"
FunctionCallSetUnion FunctionCallType = "SetUnion"
FunctionCallIif FunctionCallType = "Iif"
FunctionCallMathAbs FunctionCallType = "MathAbs"
FunctionCallMathAcos FunctionCallType = "MathAcos"
FunctionCallMathAsin FunctionCallType = "MathAsin"
+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),
},
},
)
})
}
+114
View File
@@ -112,6 +112,97 @@ func Test_Parse(t *testing.T) {
)
})
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,
@@ -131,4 +222,27 @@ func Test_Parse(t *testing.T) {
},
)
})
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")},
},
)
})
}
+2746 -1899
View File
File diff suppressed because it is too large Load Diff
+241 -119
View File
@@ -4,137 +4,137 @@ package nosql
import "github.com/pikami/cosmium/parsers"
func makeSelectStmt(
columns, fromClause, joinItems,
whereClause interface{}, distinctClause interface{},
count interface{}, groupByClause interface{}, orderList interface{},
offsetClause interface{},
columns, fromClause, joinItems,
whereClause interface{}, distinctClause interface{},
count interface{}, groupByClause interface{}, orderList interface{},
offsetClause interface{},
) (parsers.SelectStmt, error) {
selectStmt := parsers.SelectStmt{
SelectItems: columns.([]parsers.SelectItem),
}
selectStmt := parsers.SelectStmt{
SelectItems: columns.([]parsers.SelectItem),
}
if fromTable, ok := fromClause.(parsers.Table); ok {
selectStmt.Table = fromTable
}
if joinItemsArray, ok := joinItems.([]interface{}); ok && len(joinItemsArray) > 0 {
selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray))
for i, joinItem := range joinItemsArray {
selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem)
}
}
selectStmt.JoinItems = make([]parsers.JoinItem, len(joinItemsArray))
for i, joinItem := range joinItemsArray {
selectStmt.JoinItems[i] = joinItem.(parsers.JoinItem)
}
}
switch v := whereClause.(type) {
case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem:
selectStmt.Filters = v
}
switch v := whereClause.(type) {
case parsers.ComparisonExpression, parsers.LogicalExpression, parsers.Constant, parsers.SelectItem:
selectStmt.Filters = v
}
if distinctClause != nil {
selectStmt.Distinct = true
}
if distinctClause != nil {
selectStmt.Distinct = true
}
if n, ok := count.(int); ok {
selectStmt.Count = n
}
if n, ok := count.(int); ok {
selectStmt.Count = n
}
if offsetArr, ok := offsetClause.([]interface{}); ok && len(offsetArr) == 2 {
if n, ok := offsetArr[0].(int); ok {
selectStmt.Offset = n
}
if offsetArr, ok := offsetClause.([]interface{}); ok && len(offsetArr) == 2 {
if n, ok := offsetArr[0].(int); ok {
selectStmt.Offset = n
}
if n, ok := offsetArr[1].(int); ok {
selectStmt.Count = n
}
}
if n, ok := offsetArr[1].(int); ok {
selectStmt.Count = n
}
}
if orderExpressions, ok := orderList.([]parsers.OrderExpression); ok {
selectStmt.OrderExpressions = orderExpressions
}
if orderExpressions, ok := orderList.([]parsers.OrderExpression); ok {
selectStmt.OrderExpressions = orderExpressions
}
if groupByClause != nil {
selectStmt.GroupBy = groupByClause.([]parsers.SelectItem)
}
if groupByClause != nil {
selectStmt.GroupBy = groupByClause.([]parsers.SelectItem)
}
return selectStmt, nil
return selectStmt, nil
}
func makeJoin(table interface{}, column interface{}) (parsers.JoinItem, error) {
joinItem := parsers.JoinItem{}
joinItem := parsers.JoinItem{}
if selectItem, isSelectItem := column.(parsers.SelectItem); isSelectItem {
joinItem.SelectItem = selectItem
joinItem.Table.Value = selectItem.Alias
}
if selectItem, isSelectItem := column.(parsers.SelectItem); isSelectItem {
joinItem.SelectItem = selectItem
joinItem.Table.Value = selectItem.Alias
}
if tableTyped, isTable := table.(parsers.Table); isTable {
joinItem.Table = tableTyped
}
if tableTyped, isTable := table.(parsers.Table); isTable {
joinItem.Table = tableTyped
}
return joinItem, nil
return joinItem, nil
}
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)
for _, p := range ps {
for _, p := range ps {
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) {
collsAsArray := other_columns.([]interface{})
columnList := make([]parsers.SelectItem, len(collsAsArray) + 1)
columnList[0] = column.(parsers.SelectItem)
columnList := make([]parsers.SelectItem, len(collsAsArray) + 1)
columnList[0] = column.(parsers.SelectItem)
for i, v := range collsAsArray {
for i, v := range collsAsArray {
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) {
return parsers.SelectItem{
SelectItems: columns.([]parsers.SelectItem),
Type: parsers.SelectItemTypeArray,
}, nil
return parsers.SelectItem{
SelectItems: columns.([]parsers.SelectItem),
Type: parsers.SelectItemTypeArray,
}, nil
}
func makeSelectObject(field interface{}, other_fields interface{}) (parsers.SelectItem, error) {
fieldsAsArray := other_fields.([]interface{})
fieldsList := make([]parsers.SelectItem, len(fieldsAsArray)+1)
fieldsList[0] = field.(parsers.SelectItem)
fieldsList := make([]parsers.SelectItem, len(fieldsAsArray)+1)
fieldsList[0] = field.(parsers.SelectItem)
for i, v := range fieldsAsArray {
if col, ok := v.(parsers.SelectItem); ok {
fieldsList[i+1] = col
}
}
for i, v := range fieldsAsArray {
if col, ok := v.(parsers.SelectItem); ok {
fieldsList[i+1] = col
}
}
return parsers.SelectItem{
SelectItems: fieldsList,
Type: parsers.SelectItemTypeObject,
}, nil
return parsers.SelectItem{
SelectItems: fieldsList,
Type: parsers.SelectItemTypeObject,
}, nil
}
func makeOrderByClause(ex1 interface{}, others interface{}) ([]parsers.OrderExpression, error) {
othersArray := others.([]interface{})
orderList := make([]parsers.OrderExpression, len(othersArray)+1)
orderList[0] = ex1.(parsers.OrderExpression)
orderList := make([]parsers.OrderExpression, len(othersArray)+1)
orderList[0] = ex1.(parsers.OrderExpression)
for i, v := range othersArray {
if col, ok := v.(parsers.OrderExpression); ok {
orderList[i+1] = col
}
}
for i, v := range othersArray {
if col, ok := v.(parsers.OrderExpression); ok {
orderList[i+1] = col
}
}
return orderList, nil
return orderList, nil
}
func makeOrderExpression(field interface{}, order interface{}) (parsers.OrderExpression, error) {
@@ -144,8 +144,8 @@ func makeOrderExpression(field interface{}, order interface{}) (parsers.OrderExp
}
if orderValue, ok := order.(parsers.OrderDirection); ok {
value.Direction = orderValue
}
value.Direction = orderValue
}
return value, nil
}
@@ -169,13 +169,39 @@ func joinStrings(array []interface{}) string {
func combineExpressions(ex1 interface{}, exs interface{}, operation parsers.LogicalExpressionType) (interface{}, error) {
if exs == nil || len(exs.([]interface{})) < 1 {
return ex1, nil
}
return ex1, nil
}
return parsers.LogicalExpression{
Expressions: append([]interface{}{ex1}, exs.([]interface{})...),
Operation: operation,
}, nil
return parsers.LogicalExpression{
Expressions: append([]interface{}{ex1}, exs.([]interface{})...),
Operation: operation,
}, 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
}
}
@@ -204,16 +230,16 @@ TopClause <- Top ws count:Integer {
return count, nil
}
FromClause <- From ws table:TableName selectItem:(ws In ws column:SelectItem { return column, nil }) {
FromClause <- From ws table:TableName selectItem:(ws In ws column:SelectItemWithAlias { return column, nil }) {
tableTyped := table.(parsers.Table)
if selectItem != nil {
tableTyped.SelectItem = selectItem.(parsers.SelectItem)
tableTyped.SelectItem = selectItem.(parsers.SelectItem)
tableTyped.IsInSelect = true
}
}
return tableTyped, nil
} / From ws column:SelectItem {
return tableTyped, nil
} / From ws column:SelectItemWithAlias {
tableSelectItem := column.(parsers.SelectItem)
table := parsers.Table{
Value: tableSelectItem.Alias,
@@ -222,11 +248,11 @@ FromClause <- From ws table:TableName selectItem:(ws In ws column:SelectItem { r
return table, nil
} / From ws subQuery:SubQuerySelectItem {
subQueryTyped := subQuery.(parsers.SelectItem)
table := parsers.Table{
Value: subQueryTyped.Alias,
SelectItem: subQueryTyped,
}
return table, nil
table := parsers.Table{
Value: subQueryTyped.Alias,
SelectItem: subQueryTyped,
}
return table, nil
}
SubQuery <- exists:(exists:Exists ws { return exists, nil })? "(" ws selectStmt:SelectStmt ws ")" {
@@ -251,7 +277,7 @@ SubQuerySelectItem <- subQuery:SubQuery asClause:(ws alias:AsClause { return ali
return selectItem, nil
}
JoinClause <- Join ws table:TableName ws In ws column:SelectItem {
JoinClause <- Join ws table:TableName ws In ws column:SelectItemWithAlias {
return makeJoin(table, column)
} / Join ws subQuery:SubQuerySelectItem {
return makeJoin(nil, subQuery)
@@ -265,17 +291,40 @@ Selection <- SelectValueSpec / ColumnList / SelectAsterisk
SelectAsterisk <- "*" {
selectItem, _ := makeSelectItem("c", make([]interface{}, 0), parsers.SelectItemTypeField)
selectItem.IsTopLevel = true
selectItem.IsTopLevel = true
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)
}
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.IsTopLevel = true
selectItem.IsTopLevel = true
return makeColumnList(selectItem, make([]interface{}, 0))
}
@@ -289,19 +338,32 @@ SelectArray <- "[" ws columns:ColumnList ws "]" {
SelectObject <- "{" ws field:SelectObjectField ws other_fields:(ws "," ws coll:SelectObjectField {return coll, nil })* ws "}" {
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 {
item := selectItem.(parsers.SelectItem)
item.Alias = name.(string)
return item, nil
item.Alias = name.(string)
return item, nil
}
SelectProperty <- name:Identifier path:(DotFieldAccess / ArrayFieldAccess)* {
return makeSelectItem(name, path, parsers.SelectItemTypeField)
}
SelectItem <- selectItem:(SubQuerySelectItem / 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
switch typedValue := selectItem.(type) {
case parsers.SelectItem:
@@ -318,18 +380,14 @@ SelectItem <- selectItem:(SubQuerySelectItem / Literal / FunctionCall / SelectAr
}
}
if aliasValue, ok := asClause.(string); ok {
itemResult.Alias = aliasValue
}
return itemResult, nil
return itemResult, nil
}
AsClause <- (ws As)? ws !ExcludedKeywords alias:Identifier {
return alias, nil
}
ExcludedKeywords <- Select / Top / As / From / In / Join / Exists / Where / And / Or / GroupBy / OrderBy / Offset
ExcludedKeywords <- Select / Top / As / From / In / Join / Exists / Where / And / Or / Not / GroupBy / OrderBy / Offset
DotFieldAccess <- "." id:Identifier {
return id, nil
@@ -355,11 +413,35 @@ AndExpression <- ex1:ComparisonExpression ex2:(ws And ws ex:ComparisonExpression
return combineExpressions(ex1, ex2, parsers.LogicalExpressionTypeAnd)
}
ComparisonExpression <- "(" ws ex:OrExpression ws ")" { return ex, nil }
/ left:SelectItem ws op:ComparisonOperator ws right:SelectItem {
ComparisonExpression <- left:AddSubExpression ws op:ComparisonOperator ws right:AddSubExpression {
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:SelectItem { return ex, nil }
OrderByClause <- OrderBy ws ex1:OrderExpression others:(ws "," ws ex:OrderExpression { return ex, nil })* {
return makeOrderByClause(ex1, others)
@@ -371,10 +453,10 @@ OrderExpression <- field:SelectProperty ws order:OrderDirection? {
OrderDirection <- ("ASC"i / "DESC"i) {
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
@@ -397,6 +479,8 @@ And <- "AND"i
Or <- "OR"i wss
Not <- "NOT"i
GroupBy <- "GROUP"i ws "BY"i
OrderBy <- "ORDER"i ws "BY"i
@@ -407,6 +491,10 @@ ComparisonOperator <- ("<=" / ">=" / "=" / "!=" / "<" / ">") {
return string(c.text), nil
}
AddOrSubtractOperation <- ("+" / "-") { return string(c.text), nil }
MultiplyOrDivideOperation <- ("*" / "/") { return string(c.text), nil }
Literal <- FloatLiteral / IntegerLiteral / StringLiteral / BooleanLiteral / ParameterConstant / NullConstant
ParameterConstant <- "@" Identifier {
@@ -434,6 +522,7 @@ BooleanLiteral <- ("true"i / "false"i) {
FunctionCall <- StringFunctions
/ TypeCheckingFunctions
/ ArrayFunctions
/ ConditionalFunctions
/ InFunction
/ AggregateFunctions
/ MathFunctions
@@ -481,6 +570,8 @@ ArrayFunctions <- ArrayConcatExpression
/ SetIntersectExpression
/ SetUnionExpression
ConditionalFunctions <- IifExpression
MathFunctions <- MathAbsExpression
/ MathAcosExpression
/ MathAsinExpression
@@ -590,6 +681,8 @@ ThreeArgumentStringFunctionExpression <- function:ThreeArgumentStringFunction ws
functionType = parsers.FunctionCallEndsWith
case "STARTSWITH":
functionType = parsers.FunctionCallStartsWith
case "REGEXMATCH":
functionType = parsers.FunctionCallRegexMatch
case "INDEX_OF":
functionType = parsers.FunctionCallIndexOf
}
@@ -597,7 +690,7 @@ ThreeArgumentStringFunctionExpression <- function:ThreeArgumentStringFunction ws
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
}
@@ -673,6 +766,10 @@ SetUnionExpression <- "SetUnion"i ws "(" ws set1:SelectItem ws "," ws set2:Selec
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}) }
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}) }
@@ -716,8 +813,33 @@ MathNumberBinExpression <- "NumberBin"i ws "(" ws ex1:SelectItem others:(ws ","
MathPiExpression <- "PI"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathPi, []interface{}{}) }
MathRandExpression <- "RAND"i ws "(" ws ")" { return createFunctionCall(parsers.FunctionCallMathRand, []interface{}{}) }
InFunction <- ex1:SelectProperty ws In ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* ws ")" {
return createFunctionCall(parsers.FunctionCallIn, append([]interface{}{ex1, ex2}, others.([]interface{})...))
InFunction <- ex1:SelectProperty ws notIn:("NOT"i ws)? In ws "(" ws ex2:SelectItem others:(ws "," ws ex:SelectItem { return ex, nil })* 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
}
/ "(" 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 ")" {
+86
View File
@@ -178,4 +178,90 @@ func Test_Parse_Select(t *testing.T) {
},
)
})
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")},
},
)
})
}
+26
View File
@@ -168,6 +168,32 @@ func Test_Execute_StringFunctions(t *testing.T) {
)
})
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")},
},
)
})
t.Run("Should parse function INDEX_OF()", func(t *testing.T) {
testQueryParse(
t,
+17
View File
@@ -148,4 +148,21 @@ func Test_Parse_Were(t *testing.T) {
},
)
})
t.Run("Should correctly parse NOT conditions", func(t *testing.T) {
testQueryParse(
t,
`select c.id
FROM c
WHERE NOT c.boolean`,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{{Path: []string{"c", "id"}, Alias: ""}},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
Filters: parsers.SelectItem{
Path: []string{"c", "boolean"},
Invert: true,
},
},
)
})
}
@@ -0,0 +1,91 @@
package memoryexecutor_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_Arithmetics(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": 1, "a": 420},
map[string]interface{}{"id": 2, "a": 6.9},
map[string]interface{}{"id": 3},
}
t.Run("Should execute simple arithmetics", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
{
Type: parsers.SelectItemTypeBinaryExpression,
Alias: "result",
Value: parsers.BinaryExpression{
Operation: "+",
Left: testutils.SelectItem_Path("c", "a"),
Right: parsers.SelectItem{
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Constant_Float(2.0),
Right: testutils.SelectItem_Constant_Int(3),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": 1, "result": 426.0},
map[string]interface{}{"id": 2, "result": 12.9},
map[string]interface{}{"id": 3, "result": nil},
},
)
})
t.Run("Should execute arithmetics in WHERE clause", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
{
Alias: "result",
Type: parsers.SelectItemTypeBinaryExpression,
Value: parsers.BinaryExpression{
Operation: "*",
Left: testutils.SelectItem_Path("c", "a"),
Right: testutils.SelectItem_Constant_Int(2),
},
},
},
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", "a"),
Right: testutils.SelectItem_Constant_Int(2),
},
},
Right: testutils.SelectItem_Constant_Int(500),
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": 1, "result": 840.0},
},
)
})
}
@@ -25,7 +25,7 @@ func (r rowContext) array_Contains(arguments []interface{}) bool {
exprToSearch := r.resolveSelectItem(arguments[1].(parsers.SelectItem))
partialSearch := false
if len(arguments) > 2 {
if len(arguments) > 2 && arguments[2] != nil {
boolExpr := r.resolveSelectItem(arguments[2].(parsers.SelectItem))
if boolValue, ok := boolExpr.(bool); ok {
partialSearch = boolValue
@@ -196,6 +196,10 @@ func (r rowContext) parseArray(argument interface{}) []interface{} {
ex := r.resolveSelectItem(exItem)
arrValue := reflect.ValueOf(ex)
if arrValue.Kind() == reflect.Invalid {
return nil
}
if arrValue.Kind() != reflect.Slice {
logger.ErrorLn("parseArray got parameters of wrong type")
return nil
@@ -0,0 +1,27 @@
package memoryexecutor
import "github.com/pikami/cosmium/internal/datastore"
type rowArrayIterator struct {
documents []rowContext
index int
}
func NewRowArrayIterator(documents []rowContext) *rowArrayIterator {
return &rowArrayIterator{
documents: documents,
index: -1,
}
}
func (i *rowArrayIterator) Next() (rowContext, datastore.DataStoreStatus) {
i.index++
if i.index >= len(i.documents) {
return rowContext{}, datastore.IterEOF
}
row := i.documents[i.index]
i.documents[i.index] = rowContext{} // Help GC reclaim memory
return row, datastore.StatusOk
}
+461
View File
@@ -0,0 +1,461 @@
package memoryexecutor
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/parsers"
)
type RowType interface{}
type rowContext struct {
tables map[string]RowType
parameters map[string]interface{}
grouppedRows []rowContext
}
type rowIterator interface {
Next() (rowContext, datastore.DataStoreStatus)
}
type rowTypeIterator interface {
Next() (RowType, datastore.DataStoreStatus)
}
func resolveDestinationColumnName(selectItem parsers.SelectItem, itemIndex int, queryParameters map[string]interface{}) string {
if selectItem.Alias != "" {
return selectItem.Alias
}
destinationName := fmt.Sprintf("$%d", itemIndex+1)
if len(selectItem.Path) > 0 {
destinationName = selectItem.Path[len(selectItem.Path)-1]
}
if destinationName[0] == '@' {
destinationName = queryParameters[destinationName].(string)
}
return destinationName
}
func (r rowContext) resolveSelectItem(selectItem parsers.SelectItem) interface{} {
if selectItem.Type == parsers.SelectItemTypeArray {
return r.selectItem_SelectItemTypeArray(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeObject {
return r.selectItem_SelectItemTypeObject(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeConstant {
return r.selectItem_SelectItemTypeConstant(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeSubQuery {
return r.selectItem_SelectItemTypeSubQuery(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedFunctionCall, ok := selectItem.Value.(parsers.FunctionCall); ok {
return r.selectItem_SelectItemTypeFunctionCall(typedFunctionCall)
}
logger.ErrorLn("parsers.SelectItem has incorrect Value type (expected parsers.FunctionCall)")
return nil
}
if selectItem.Type == parsers.SelectItemTypeExpression {
if typedExpression, ok := selectItem.Value.(parsers.ComparisonExpression); ok {
return r.filters_ComparisonExpression(typedExpression)
}
if typedExpression, ok := selectItem.Value.(parsers.LogicalExpression); ok {
return r.filters_LogicalExpression(typedExpression)
}
logger.ErrorLn("parsers.SelectItem has incorrect Value type (expected parsers.ComparisonExpression)")
return nil
}
if selectItem.Type == parsers.SelectItemTypeBinaryExpression {
if typedSelectItem, ok := selectItem.Value.(parsers.BinaryExpression); ok {
return r.selectItem_SelectItemTypeBinaryExpression(typedSelectItem)
}
logger.ErrorLn("parsers.SelectItem has incorrect Value type (expected parsers.BinaryExpression)")
return nil
}
return r.selectItem_SelectItemTypeField(selectItem)
}
func (r rowContext) selectItem_SelectItemTypeArray(selectItem parsers.SelectItem) interface{} {
arrayValue := make([]interface{}, 0)
for _, subSelectItem := range selectItem.SelectItems {
arrayValue = append(arrayValue, r.resolveSelectItem(subSelectItem))
}
return arrayValue
}
func (r rowContext) selectItem_SelectItemTypeObject(selectItem parsers.SelectItem) interface{} {
objectValue := make(map[string]interface{})
for _, subSelectItem := range selectItem.SelectItems {
objectValue[subSelectItem.Alias] = r.resolveSelectItem(subSelectItem)
}
return objectValue
}
func (r rowContext) selectItem_SelectItemTypeConstant(selectItem parsers.SelectItem) interface{} {
var typedValue parsers.Constant
var ok bool
if typedValue, ok = selectItem.Value.(parsers.Constant); !ok {
// TODO: Handle error
logger.ErrorLn("parsers.Constant has incorrect Value type")
}
if typedValue.Type == parsers.ConstantTypeParameterConstant &&
r.parameters != nil {
if key, ok := typedValue.Value.(string); ok {
return r.parameters[key]
}
}
return typedValue.Value
}
func (r rowContext) selectItem_SelectItemTypeSubQuery(selectItem parsers.SelectItem) interface{} {
subQuery := selectItem.Value.(parsers.SelectStmt)
subQueryResult := executeQuery(
subQuery,
NewRowArrayIterator([]rowContext{r}),
)
if subQuery.Exists {
_, status := subQueryResult.Next()
return status == datastore.StatusOk
}
allDocuments := make([]RowType, 0)
for {
row, status := subQueryResult.Next()
if status != datastore.StatusOk {
break
}
allDocuments = append(allDocuments, row)
}
return allDocuments
}
func (r rowContext) selectItem_SelectItemTypeFunctionCall(functionCall parsers.FunctionCall) interface{} {
switch functionCall.Type {
case parsers.FunctionCallStringEquals:
return r.strings_StringEquals(functionCall.Arguments)
case parsers.FunctionCallContains:
return r.strings_Contains(functionCall.Arguments)
case parsers.FunctionCallEndsWith:
return r.strings_EndsWith(functionCall.Arguments)
case parsers.FunctionCallStartsWith:
return r.strings_StartsWith(functionCall.Arguments)
case parsers.FunctionCallRegexMatch:
return r.strings_RegexMatch(functionCall.Arguments)
case parsers.FunctionCallConcat:
return r.strings_Concat(functionCall.Arguments)
case parsers.FunctionCallIndexOf:
return r.strings_IndexOf(functionCall.Arguments)
case parsers.FunctionCallToString:
return r.strings_ToString(functionCall.Arguments)
case parsers.FunctionCallUpper:
return r.strings_Upper(functionCall.Arguments)
case parsers.FunctionCallLower:
return r.strings_Lower(functionCall.Arguments)
case parsers.FunctionCallLeft:
return r.strings_Left(functionCall.Arguments)
case parsers.FunctionCallLength:
return r.strings_Length(functionCall.Arguments)
case parsers.FunctionCallLTrim:
return r.strings_LTrim(functionCall.Arguments)
case parsers.FunctionCallReplace:
return r.strings_Replace(functionCall.Arguments)
case parsers.FunctionCallReplicate:
return r.strings_Replicate(functionCall.Arguments)
case parsers.FunctionCallReverse:
return r.strings_Reverse(functionCall.Arguments)
case parsers.FunctionCallRight:
return r.strings_Right(functionCall.Arguments)
case parsers.FunctionCallRTrim:
return r.strings_RTrim(functionCall.Arguments)
case parsers.FunctionCallSubstring:
return r.strings_Substring(functionCall.Arguments)
case parsers.FunctionCallTrim:
return r.strings_Trim(functionCall.Arguments)
case parsers.FunctionCallIsDefined:
return r.typeChecking_IsDefined(functionCall.Arguments)
case parsers.FunctionCallIsArray:
return r.typeChecking_IsArray(functionCall.Arguments)
case parsers.FunctionCallIsBool:
return r.typeChecking_IsBool(functionCall.Arguments)
case parsers.FunctionCallIsFiniteNumber:
return r.typeChecking_IsFiniteNumber(functionCall.Arguments)
case parsers.FunctionCallIsInteger:
return r.typeChecking_IsInteger(functionCall.Arguments)
case parsers.FunctionCallIsNull:
return r.typeChecking_IsNull(functionCall.Arguments)
case parsers.FunctionCallIsNumber:
return r.typeChecking_IsNumber(functionCall.Arguments)
case parsers.FunctionCallIsObject:
return r.typeChecking_IsObject(functionCall.Arguments)
case parsers.FunctionCallIsPrimitive:
return r.typeChecking_IsPrimitive(functionCall.Arguments)
case parsers.FunctionCallIsString:
return r.typeChecking_IsString(functionCall.Arguments)
case parsers.FunctionCallArrayConcat:
return r.array_Concat(functionCall.Arguments)
case parsers.FunctionCallArrayContains:
return r.array_Contains(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAny:
return r.array_Contains_Any(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAll:
return r.array_Contains_All(functionCall.Arguments)
case parsers.FunctionCallArrayLength:
return r.array_Length(functionCall.Arguments)
case parsers.FunctionCallArraySlice:
return r.array_Slice(functionCall.Arguments)
case parsers.FunctionCallSetIntersect:
return r.set_Intersect(functionCall.Arguments)
case parsers.FunctionCallSetUnion:
return r.set_Union(functionCall.Arguments)
case parsers.FunctionCallIif:
return r.misc_Iif(functionCall.Arguments)
case parsers.FunctionCallMathAbs:
return r.math_Abs(functionCall.Arguments)
case parsers.FunctionCallMathAcos:
return r.math_Acos(functionCall.Arguments)
case parsers.FunctionCallMathAsin:
return r.math_Asin(functionCall.Arguments)
case parsers.FunctionCallMathAtan:
return r.math_Atan(functionCall.Arguments)
case parsers.FunctionCallMathCeiling:
return r.math_Ceiling(functionCall.Arguments)
case parsers.FunctionCallMathCos:
return r.math_Cos(functionCall.Arguments)
case parsers.FunctionCallMathCot:
return r.math_Cot(functionCall.Arguments)
case parsers.FunctionCallMathDegrees:
return r.math_Degrees(functionCall.Arguments)
case parsers.FunctionCallMathExp:
return r.math_Exp(functionCall.Arguments)
case parsers.FunctionCallMathFloor:
return r.math_Floor(functionCall.Arguments)
case parsers.FunctionCallMathIntBitNot:
return r.math_IntBitNot(functionCall.Arguments)
case parsers.FunctionCallMathLog10:
return r.math_Log10(functionCall.Arguments)
case parsers.FunctionCallMathRadians:
return r.math_Radians(functionCall.Arguments)
case parsers.FunctionCallMathRound:
return r.math_Round(functionCall.Arguments)
case parsers.FunctionCallMathSign:
return r.math_Sign(functionCall.Arguments)
case parsers.FunctionCallMathSin:
return r.math_Sin(functionCall.Arguments)
case parsers.FunctionCallMathSqrt:
return r.math_Sqrt(functionCall.Arguments)
case parsers.FunctionCallMathSquare:
return r.math_Square(functionCall.Arguments)
case parsers.FunctionCallMathTan:
return r.math_Tan(functionCall.Arguments)
case parsers.FunctionCallMathTrunc:
return r.math_Trunc(functionCall.Arguments)
case parsers.FunctionCallMathAtn2:
return r.math_Atn2(functionCall.Arguments)
case parsers.FunctionCallMathIntAdd:
return r.math_IntAdd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitAnd:
return r.math_IntBitAnd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitLeftShift:
return r.math_IntBitLeftShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitOr:
return r.math_IntBitOr(functionCall.Arguments)
case parsers.FunctionCallMathIntBitRightShift:
return r.math_IntBitRightShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitXor:
return r.math_IntBitXor(functionCall.Arguments)
case parsers.FunctionCallMathIntDiv:
return r.math_IntDiv(functionCall.Arguments)
case parsers.FunctionCallMathIntMod:
return r.math_IntMod(functionCall.Arguments)
case parsers.FunctionCallMathIntMul:
return r.math_IntMul(functionCall.Arguments)
case parsers.FunctionCallMathIntSub:
return r.math_IntSub(functionCall.Arguments)
case parsers.FunctionCallMathPower:
return r.math_Power(functionCall.Arguments)
case parsers.FunctionCallMathLog:
return r.math_Log(functionCall.Arguments)
case parsers.FunctionCallMathNumberBin:
return r.math_NumberBin(functionCall.Arguments)
case parsers.FunctionCallMathPi:
return r.math_Pi()
case parsers.FunctionCallMathRand:
return r.math_Rand()
case parsers.FunctionCallAggregateAvg:
return r.aggregate_Avg(functionCall.Arguments)
case parsers.FunctionCallAggregateCount:
return r.aggregate_Count(functionCall.Arguments)
case parsers.FunctionCallAggregateMax:
return r.aggregate_Max(functionCall.Arguments)
case parsers.FunctionCallAggregateMin:
return r.aggregate_Min(functionCall.Arguments)
case parsers.FunctionCallAggregateSum:
return r.aggregate_Sum(functionCall.Arguments)
case parsers.FunctionCallIn:
return r.misc_In(functionCall.Arguments)
}
logger.Errorf("Unknown function call type: %v", functionCall.Type)
return nil
}
func (r rowContext) selectItem_SelectItemTypeBinaryExpression(binaryExpression parsers.BinaryExpression) interface{} {
if binaryExpression.Left == nil || binaryExpression.Right == nil {
logger.Debug("parsers.BinaryExpression has nil Left or Right value")
return nil
}
leftValue := r.resolveSelectItem(binaryExpression.Left.(parsers.SelectItem))
rightValue := r.resolveSelectItem(binaryExpression.Right.(parsers.SelectItem))
if leftValue == nil || rightValue == nil {
return nil
}
leftNumber, leftIsNumber := numToFloat64(leftValue)
rightNumber, rightIsNumber := numToFloat64(rightValue)
if !leftIsNumber || !rightIsNumber {
logger.Debug("Binary expression operands are not numbers, returning nil")
return nil
}
switch binaryExpression.Operation {
case "+":
return leftNumber + rightNumber
case "-":
return leftNumber - rightNumber
case "*":
return leftNumber * rightNumber
case "/":
if rightNumber == 0 {
logger.Debug("Division by zero in binary expression")
return nil
}
return leftNumber / rightNumber
default:
return nil
}
}
func (r rowContext) selectItem_SelectItemTypeField(selectItem parsers.SelectItem) interface{} {
value := r.tables[selectItem.Path[0]]
if len(selectItem.Path) > 1 {
for _, pathSegment := range selectItem.Path[1:] {
if pathSegment[0] == '@' {
pathSegment = r.parameters[pathSegment].(string)
}
switch nestedValue := value.(type) {
case map[string]interface{}:
value = nestedValue[pathSegment]
case map[string]RowType:
value = nestedValue[pathSegment]
case datastore.Document:
value = nestedValue[pathSegment]
case map[string]datastore.Document:
value = nestedValue[pathSegment]
case []int, []string, []interface{}:
slice := reflect.ValueOf(nestedValue)
if arrayIndex, err := strconv.Atoi(pathSegment); err == nil && slice.Len() > arrayIndex {
value = slice.Index(arrayIndex).Interface()
} else {
return nil
}
default:
return nil
}
}
}
return value
}
func compareValues(val1, val2 interface{}) int {
// Handle nil values
if val1 == nil && val2 == nil {
return 0
} else if val1 == nil {
return -1
} else if val2 == nil {
return 1
}
// Handle number values
val1Number, val1IsNumber := numToFloat64(val1)
val2Number, val2IsNumber := numToFloat64(val2)
if val1IsNumber && val2IsNumber {
if val1Number < val2Number {
return -1
} else if val1Number > val2Number {
return 1
}
return 0
}
// Handle different types
if reflect.TypeOf(val1) != reflect.TypeOf(val2) {
return 1
}
switch val1 := val1.(type) {
case string:
val2 := val2.(string)
return strings.Compare(val1, val2)
case bool:
val2 := val2.(bool)
if val1 == val2 {
return 0
} else if val1 {
return 1
} else {
return -1
}
// TODO: Add more types
default:
if reflect.DeepEqual(val1, val2) {
return 0
}
return 1
}
}
func copyMap[T RowType | []RowType](originalMap map[string]T) map[string]T {
targetMap := make(map[string]T)
for k, v := range originalMap {
targetMap[k] = v
}
return targetMap
}
@@ -0,0 +1,92 @@
package memoryexecutor_test
import (
"testing"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
func Test_Execute_Expressions(t *testing.T) {
mockData := []memoryexecutor.RowType{
map[string]interface{}{"id": "123", "age": 10, "isCool": true},
map[string]interface{}{"id": "456", "age": 20, "isCool": false},
map[string]interface{}{"id": "789", "age": 30, "isCool": true},
}
t.Run("Should execute comparison expressions in SELECT", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
{
Alias: "isAdult",
Type: parsers.SelectItemTypeExpression,
Value: parsers.ComparisonExpression{
Operation: ">=",
Left: testutils.SelectItem_Path("c", "age"),
Right: testutils.SelectItem_Constant_Int(18),
},
},
{
Alias: "isNotCool",
Type: parsers.SelectItemTypeExpression,
Value: parsers.ComparisonExpression{
Operation: "!=",
Left: testutils.SelectItem_Path("c", "isCool"),
Right: testutils.SelectItem_Constant_Bool(true),
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "123", "isAdult": false, "isNotCool": false},
map[string]interface{}{"id": "456", "isAdult": true, "isNotCool": true},
map[string]interface{}{"id": "789", "isAdult": true, "isNotCool": false},
},
)
})
t.Run("Should execute logical expressions in SELECT", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
{
Path: []string{"c", "id"},
Type: parsers.SelectItemTypeField,
},
{
Alias: "isCoolAndAdult",
Type: parsers.SelectItemTypeExpression,
Value: parsers.LogicalExpression{
Operation: parsers.LogicalExpressionTypeAnd,
Expressions: []interface{}{
testutils.SelectItem_Path("c", "isCool"),
parsers.ComparisonExpression{
Operation: ">=",
Left: testutils.SelectItem_Path("c", "age"),
Right: testutils.SelectItem_Constant_Int(18),
},
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "123", "isCoolAndAdult": false},
map[string]interface{}{"id": "456", "isCoolAndAdult": false},
map[string]interface{}{"id": "789", "isCoolAndAdult": true},
},
)
})
}
@@ -0,0 +1,36 @@
package memoryexecutor
import "github.com/pikami/cosmium/internal/datastore"
type distinctIterator struct {
documents rowTypeIterator
seenDocs []RowType
}
func (di *distinctIterator) Next() (RowType, datastore.DataStoreStatus) {
if di.documents == nil {
return rowContext{}, datastore.IterEOF
}
for {
row, status := di.documents.Next()
if status != datastore.StatusOk {
di.documents = nil
return rowContext{}, status
}
if !di.seen(row) {
di.seenDocs = append(di.seenDocs, row)
return row, status
}
}
}
func (di *distinctIterator) seen(row RowType) bool {
for _, seenRow := range di.seenDocs {
if compareValues(seenRow, row) == 0 {
return true
}
}
return false
}
@@ -0,0 +1,143 @@
package memoryexecutor
import (
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/parsers"
)
type filterIterator struct {
documents rowIterator
filters interface{}
}
func (fi *filterIterator) Next() (rowContext, datastore.DataStoreStatus) {
if fi.documents == nil {
return rowContext{}, datastore.IterEOF
}
for {
row, status := fi.documents.Next()
if status != datastore.StatusOk {
fi.documents = nil
return rowContext{}, status
}
if fi.evaluateFilters(row) {
return row, status
}
}
}
func (fi *filterIterator) evaluateFilters(row rowContext) bool {
if fi.filters == nil {
return true
}
switch typedFilters := fi.filters.(type) {
case parsers.ComparisonExpression:
return row.filters_ComparisonExpression(typedFilters)
case parsers.LogicalExpression:
return row.filters_LogicalExpression(typedFilters)
case parsers.Constant:
if value, ok := typedFilters.Value.(bool); ok {
return value
}
return false
case parsers.SelectItem:
resolvedValue := row.resolveSelectItem(typedFilters)
if value, ok := resolvedValue.(bool); ok {
if typedFilters.Invert {
return !value
}
return value
}
}
return false
}
func (r rowContext) applyFilters(filters interface{}) bool {
if filters == nil {
return true
}
switch typedFilters := filters.(type) {
case parsers.ComparisonExpression:
return r.filters_ComparisonExpression(typedFilters)
case parsers.LogicalExpression:
return r.filters_LogicalExpression(typedFilters)
case parsers.Constant:
if value, ok := typedFilters.Value.(bool); ok {
return value
}
return false
case parsers.SelectItem:
resolvedValue := r.resolveSelectItem(typedFilters)
if value, ok := resolvedValue.(bool); ok {
if typedFilters.Invert {
return !value
}
return value
}
}
return false
}
func (r rowContext) filters_ComparisonExpression(expression parsers.ComparisonExpression) bool {
leftExpression, leftExpressionOk := expression.Left.(parsers.SelectItem)
rightExpression, rightExpressionOk := expression.Right.(parsers.SelectItem)
if !leftExpressionOk || !rightExpressionOk {
logger.ErrorLn("ComparisonExpression has incorrect Left or Right type")
return false
}
leftValue := r.resolveSelectItem(leftExpression)
rightValue := r.resolveSelectItem(rightExpression)
cmp := compareValues(leftValue, rightValue)
switch expression.Operation {
case "=":
return cmp == 0
case "!=":
return cmp != 0
case "<":
return cmp < 0
case ">":
return cmp > 0
case "<=":
return cmp <= 0
case ">=":
return cmp >= 0
}
return false
}
func (r rowContext) filters_LogicalExpression(expression parsers.LogicalExpression) bool {
var result bool
for i, subExpression := range expression.Expressions {
expressionResult := r.applyFilters(subExpression)
if i == 0 {
result = expressionResult
}
switch expression.Operation {
case parsers.LogicalExpressionTypeAnd:
result = result && expressionResult
if !result {
return false
}
case parsers.LogicalExpressionTypeOr:
result = result || expressionResult
if result {
return true
}
}
}
return result
}
@@ -0,0 +1,73 @@
package memoryexecutor
import (
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
)
type fromIterator struct {
documents rowIterator
table parsers.Table
buffer []rowContext
bufferIndex int
}
func (fi *fromIterator) Next() (rowContext, datastore.DataStoreStatus) {
if fi.documents == nil {
return rowContext{}, datastore.IterEOF
}
// Return from buffer if available
if fi.bufferIndex < len(fi.buffer) {
result := fi.buffer[fi.bufferIndex]
fi.buffer[fi.bufferIndex] = rowContext{}
fi.bufferIndex++
return result, datastore.StatusOk
}
// Resolve next row from documents
row, status := fi.documents.Next()
if status != datastore.StatusOk {
fi.documents = nil
return row, status
}
if fi.table.SelectItem.Path != nil || fi.table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
destinationTableName := fi.table.SelectItem.Alias
if destinationTableName == "" {
destinationTableName = fi.table.Value
}
if destinationTableName == "" {
destinationTableName = resolveDestinationColumnName(fi.table.SelectItem, 0, row.parameters)
}
if fi.table.IsInSelect || fi.table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := row.parseArray(fi.table.SelectItem)
rowContexts := make([]rowContext, len(selectValue))
for i, newRowData := range selectValue {
rowContexts[i].parameters = row.parameters
rowContexts[i].tables = copyMap(row.tables)
rowContexts[i].tables[destinationTableName] = newRowData
}
fi.buffer = rowContexts
fi.bufferIndex = 0
return fi.Next()
}
if len(fi.table.SelectItem.Path) > 0 {
sourceTableName := fi.table.SelectItem.Path[0]
sourceTableData := row.tables[sourceTableName]
if sourceTableData == nil {
// When source table is not found, assume it's root document
row.tables[sourceTableName] = row.tables["$root"]
}
}
newRowData := row.resolveSelectItem(fi.table.SelectItem)
row.tables[destinationTableName] = newRowData
return row, status
}
return row, status
}
@@ -0,0 +1,69 @@
package memoryexecutor
import (
"fmt"
"strings"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
)
type groupByIterator struct {
documents rowIterator
groupBy []parsers.SelectItem
groupedRows []rowContext
}
func (gi *groupByIterator) Next() (rowContext, datastore.DataStoreStatus) {
if gi.groupedRows != nil {
if len(gi.groupedRows) == 0 {
return rowContext{}, datastore.IterEOF
}
row := gi.groupedRows[0]
gi.groupedRows = gi.groupedRows[1:]
return row, datastore.StatusOk
}
documents := make([]rowContext, 0)
for {
row, status := gi.documents.Next()
if status != datastore.StatusOk {
break
}
documents = append(documents, row)
}
gi.documents = nil
groupedRows := make(map[string][]rowContext)
groupedKeys := make([]string, 0)
for _, row := range documents {
key := row.generateGroupByKey(gi.groupBy)
if _, ok := groupedRows[key]; !ok {
groupedKeys = append(groupedKeys, key)
}
groupedRows[key] = append(groupedRows[key], row)
}
gi.groupedRows = make([]rowContext, 0)
for _, key := range groupedKeys {
gi.groupedRows = append(gi.groupedRows, rowContext{
tables: groupedRows[key][0].tables,
parameters: groupedRows[key][0].parameters,
grouppedRows: groupedRows[key],
})
}
return gi.Next()
}
func (r rowContext) generateGroupByKey(groupBy []parsers.SelectItem) string {
var keyBuilder strings.Builder
for _, selectItem := range groupBy {
value := r.resolveSelectItem(selectItem)
keyBuilder.WriteString(fmt.Sprintf("%v", value))
keyBuilder.WriteString(":")
}
return keyBuilder.String()
}
@@ -0,0 +1,62 @@
package memoryexecutor
import (
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
)
type joinIterator struct {
documents rowIterator
query parsers.SelectStmt
buffer []rowContext
}
func (ji *joinIterator) Next() (rowContext, datastore.DataStoreStatus) {
if ji.documents == nil {
return rowContext{}, datastore.IterEOF
}
if len(ji.buffer) > 0 {
row := ji.buffer[0]
ji.buffer = ji.buffer[1:]
return row, datastore.StatusOk
}
doc, status := ji.documents.Next()
if status != datastore.StatusOk {
ji.documents = nil
return rowContext{}, status
}
ji.buffer = []rowContext{doc}
for _, joinItem := range ji.query.JoinItems {
nextDocuments := make([]rowContext, 0)
for _, row := range ji.buffer {
joinedItems := row.resolveJoinItemSelect(joinItem.SelectItem)
for _, joinedItem := range joinedItems {
tablesCopy := copyMap(row.tables)
tablesCopy[joinItem.Table.Value] = joinedItem
nextDocuments = append(nextDocuments, rowContext{
parameters: row.parameters,
tables: tablesCopy,
})
}
}
ji.buffer = nextDocuments
}
return ji.Next()
}
func (r rowContext) resolveJoinItemSelect(selectItem parsers.SelectItem) []RowType {
if selectItem.Path != nil || selectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := r.parseArray(selectItem)
documents := make([]RowType, len(selectValue))
for i, newRowData := range selectValue {
documents[i] = newRowData
}
return documents
}
return []RowType{}
}
@@ -0,0 +1,19 @@
package memoryexecutor
import "github.com/pikami/cosmium/internal/datastore"
type limitIterator struct {
documents rowTypeIterator
limit int
count int
}
func (li *limitIterator) Next() (RowType, datastore.DataStoreStatus) {
if li.count >= li.limit {
li.documents = nil
return rowContext{}, datastore.IterEOF
}
li.count++
return li.documents.Next()
}
@@ -605,10 +605,30 @@ func numToInt(ex interface{}) (int, bool) {
func numToFloat64(num interface{}) (float64, bool) {
switch val := num.(type) {
case float64:
return val, true
case int:
return float64(val), true
case int8:
return float64(val), true
case int16:
return float64(val), true
case int32:
return float64(val), true
case int64:
return float64(val), true
case uint:
return float64(val), true
case uint8:
return float64(val), true
case uint16:
return float64(val), true
case uint32:
return float64(val), true
case uint64:
return float64(val), true
case float32:
return float64(val), true
case float64:
return val, true
default:
return 0, false
}
@@ -1,740 +1,120 @@
package memoryexecutor
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/pikami/cosmium/internal/logger"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
"golang.org/x/exp/slices"
)
type RowType interface{}
type rowContext struct {
tables map[string]RowType
parameters map[string]interface{}
grouppedRows []rowContext
type ExecuteQueryResult struct {
Rows []RowType
HasMorePages bool
}
func ExecuteQuery(query parsers.SelectStmt, documents []RowType) []RowType {
currentDocuments := make([]rowContext, 0)
for _, doc := range documents {
currentDocuments = append(currentDocuments, resolveFrom(query, doc)...)
func ExecuteQuery(
query parsers.SelectStmt,
documents rowTypeIterator,
offset int,
limit int,
) ExecuteQueryResult {
resultIter := executeQuery(query, &rowTypeToRowContextIterator{documents: documents, query: query})
result := &ExecuteQueryResult{
Rows: make([]RowType, 0),
HasMorePages: false,
}
// Handle JOINS
nextDocuments := make([]rowContext, 0)
for _, currentDocument := range currentDocuments {
rowContexts := currentDocument.handleJoin(query)
nextDocuments = append(nextDocuments, rowContexts...)
}
currentDocuments = nextDocuments
// Apply filters
nextDocuments = make([]rowContext, 0)
for _, currentDocument := range currentDocuments {
if currentDocument.applyFilters(query.Filters) {
nextDocuments = append(nextDocuments, currentDocument)
for i := 0; i < offset; i++ {
_, status := resultIter.Next()
if status != datastore.StatusOk {
break
}
}
currentDocuments = nextDocuments
// Apply order
for i := 0; i < limit; i++ {
row, status := resultIter.Next()
if status != datastore.StatusOk {
break
}
result.Rows = append(result.Rows, row)
}
_, status := resultIter.Next()
if status == datastore.StatusOk {
result.HasMorePages = true
}
return *result
}
func executeQuery(query parsers.SelectStmt, documents rowIterator) rowTypeIterator {
// Resolve FROM
var iter rowIterator = &fromIterator{
documents: documents,
table: query.Table,
}
// Apply JOIN
if len(query.JoinItems) > 0 {
iter = &joinIterator{
documents: iter,
query: query,
}
}
// Apply WHERE
if query.Filters != nil {
iter = &filterIterator{
documents: iter,
filters: query.Filters,
}
}
// Apply ORDER BY
if len(query.OrderExpressions) > 0 {
applyOrder(currentDocuments, query.OrderExpressions)
iter = &orderIterator{
documents: iter,
orderExpressions: query.OrderExpressions,
}
}
// Apply group by
// Apply GROUP BY
if len(query.GroupBy) > 0 {
currentDocuments = applyGroupBy(currentDocuments, query.GroupBy)
iter = &groupByIterator{
documents: iter,
groupBy: query.GroupBy,
}
}
// Apply select
projectedDocuments := applyProjection(currentDocuments, query.SelectItems, query.GroupBy)
// Apply SELECT
var projectedIterator rowTypeIterator = &projectIterator{
documents: iter,
selectItems: query.SelectItems,
groupBy: query.GroupBy,
}
// Apply distinct
// Apply DISTINCT
if query.Distinct {
projectedDocuments = deduplicate(projectedDocuments)
projectedIterator = &distinctIterator{
documents: projectedIterator,
}
}
// Apply offset
// Apply OFFSET
if query.Offset > 0 {
if query.Offset < len(projectedDocuments) {
projectedDocuments = projectedDocuments[query.Offset:]
} else {
projectedDocuments = []RowType{}
projectedIterator = &offsetIterator{
documents: projectedIterator,
offset: query.Offset,
}
}
// Apply result limit
if query.Count > 0 && len(projectedDocuments) > query.Count {
projectedDocuments = projectedDocuments[:query.Count]
// Apply LIMIT
if query.Count > 0 {
projectedIterator = &limitIterator{
documents: projectedIterator,
limit: query.Count,
}
}
return projectedDocuments
}
func resolveFrom(query parsers.SelectStmt, doc RowType) []rowContext {
initialRow, gotParentContext := doc.(rowContext)
if !gotParentContext {
var initialTableName string
if query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
initialTableName = query.Table.SelectItem.Value.(parsers.SelectStmt).Table.Value
}
if initialTableName == "" {
initialTableName = query.Table.Value
}
if initialTableName == "" {
initialTableName = resolveDestinationColumnName(query.Table.SelectItem, 0, query.Parameters)
}
initialRow = rowContext{
parameters: query.Parameters,
tables: map[string]RowType{
initialTableName: doc,
"$root": doc,
},
}
}
if query.Table.SelectItem.Path != nil || query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
destinationTableName := query.Table.SelectItem.Alias
if destinationTableName == "" {
destinationTableName = query.Table.Value
}
if destinationTableName == "" {
destinationTableName = resolveDestinationColumnName(query.Table.SelectItem, 0, initialRow.parameters)
}
if query.Table.IsInSelect || query.Table.SelectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := initialRow.parseArray(query.Table.SelectItem)
rowContexts := make([]rowContext, len(selectValue))
for i, newRowData := range selectValue {
rowContexts[i].parameters = initialRow.parameters
rowContexts[i].tables = copyMap(initialRow.tables)
rowContexts[i].tables[destinationTableName] = newRowData
}
return rowContexts
}
if len(query.Table.SelectItem.Path) > 0 {
sourceTableName := query.Table.SelectItem.Path[0]
sourceTableData := initialRow.tables[sourceTableName]
if sourceTableData == nil {
// When source table is not found, assume it's root document
initialRow.tables[sourceTableName] = initialRow.tables["$root"]
}
}
newRowData := initialRow.resolveSelectItem(query.Table.SelectItem)
initialRow.tables[destinationTableName] = newRowData
return []rowContext{initialRow}
}
return []rowContext{initialRow}
}
func (r rowContext) handleJoin(query parsers.SelectStmt) []rowContext {
currentDocuments := []rowContext{r}
for _, joinItem := range query.JoinItems {
nextDocuments := make([]rowContext, 0)
for _, currentDocument := range currentDocuments {
joinedItems := currentDocument.resolveJoinItemSelect(joinItem.SelectItem)
for _, joinedItem := range joinedItems {
tablesCopy := copyMap(currentDocument.tables)
tablesCopy[joinItem.Table.Value] = joinedItem
nextDocuments = append(nextDocuments, rowContext{
parameters: currentDocument.parameters,
tables: tablesCopy,
})
}
}
currentDocuments = nextDocuments
}
return currentDocuments
}
func (r rowContext) resolveJoinItemSelect(selectItem parsers.SelectItem) []RowType {
if selectItem.Path != nil || selectItem.Type == parsers.SelectItemTypeSubQuery {
selectValue := r.parseArray(selectItem)
documents := make([]RowType, len(selectValue))
for i, newRowData := range selectValue {
documents[i] = newRowData
}
return documents
}
return []RowType{}
}
func (r rowContext) applyFilters(filters interface{}) bool {
if filters == nil {
return true
}
switch typedFilters := filters.(type) {
case parsers.ComparisonExpression:
return r.filters_ComparisonExpression(typedFilters)
case parsers.LogicalExpression:
return r.filters_LogicalExpression(typedFilters)
case parsers.Constant:
if value, ok := typedFilters.Value.(bool); ok {
return value
}
return false
case parsers.SelectItem:
resolvedValue := r.resolveSelectItem(typedFilters)
if value, ok := resolvedValue.(bool); ok {
return value
}
}
return false
}
func (r rowContext) filters_ComparisonExpression(expression parsers.ComparisonExpression) bool {
leftExpression, leftExpressionOk := expression.Left.(parsers.SelectItem)
rightExpression, rightExpressionOk := expression.Right.(parsers.SelectItem)
if !leftExpressionOk || !rightExpressionOk {
logger.ErrorLn("ComparisonExpression has incorrect Left or Right type")
return false
}
leftValue := r.resolveSelectItem(leftExpression)
rightValue := r.resolveSelectItem(rightExpression)
cmp := compareValues(leftValue, rightValue)
switch expression.Operation {
case "=":
return cmp == 0
case "!=":
return cmp != 0
case "<":
return cmp < 0
case ">":
return cmp > 0
case "<=":
return cmp <= 0
case ">=":
return cmp >= 0
}
return false
}
func (r rowContext) filters_LogicalExpression(expression parsers.LogicalExpression) bool {
var result bool
for i, subExpression := range expression.Expressions {
expressionResult := r.applyFilters(subExpression)
if i == 0 {
result = expressionResult
}
switch expression.Operation {
case parsers.LogicalExpressionTypeAnd:
result = result && expressionResult
if !result {
return false
}
case parsers.LogicalExpressionTypeOr:
result = result || expressionResult
if result {
return true
}
}
}
return result
}
func applyOrder(documents []rowContext, orderExpressions []parsers.OrderExpression) {
less := func(i, j int) bool {
for _, order := range orderExpressions {
val1 := documents[i].resolveSelectItem(order.SelectItem)
val2 := documents[j].resolveSelectItem(order.SelectItem)
cmp := compareValues(val1, val2)
if cmp != 0 {
if order.Direction == parsers.OrderDirectionDesc {
return cmp > 0
}
return cmp < 0
}
}
return i < j
}
sort.SliceStable(documents, less)
}
func applyGroupBy(documents []rowContext, groupBy []parsers.SelectItem) []rowContext {
groupedRows := make(map[string][]rowContext)
groupedKeys := make([]string, 0)
for _, row := range documents {
key := row.generateGroupByKey(groupBy)
if _, ok := groupedRows[key]; !ok {
groupedKeys = append(groupedKeys, key)
}
groupedRows[key] = append(groupedRows[key], row)
}
grouppedRows := make([]rowContext, 0)
for _, key := range groupedKeys {
grouppedRowContext := rowContext{
tables: groupedRows[key][0].tables,
parameters: groupedRows[key][0].parameters,
grouppedRows: groupedRows[key],
}
grouppedRows = append(grouppedRows, grouppedRowContext)
}
return grouppedRows
}
func (r rowContext) generateGroupByKey(groupBy []parsers.SelectItem) string {
var keyBuilder strings.Builder
for _, selectItem := range groupBy {
value := r.resolveSelectItem(selectItem)
keyBuilder.WriteString(fmt.Sprintf("%v", value))
keyBuilder.WriteString(":")
}
return keyBuilder.String()
}
func applyProjection(documents []rowContext, selectItems []parsers.SelectItem, groupBy []parsers.SelectItem) []RowType {
if len(documents) == 0 {
return []RowType{}
}
if hasAggregateFunctions(selectItems) && len(groupBy) == 0 {
// When can have aggregate functions without GROUP BY clause,
// we should aggregate all rows in that case
rowContext := rowContext{
tables: documents[0].tables,
parameters: documents[0].parameters,
grouppedRows: documents,
}
return []RowType{rowContext.applyProjection(selectItems)}
}
projectedDocuments := make([]RowType, len(documents))
for index, row := range documents {
projectedDocuments[index] = row.applyProjection(selectItems)
}
return projectedDocuments
}
func (r rowContext) applyProjection(selectItems []parsers.SelectItem) RowType {
// When the first value is top level, select it instead
if len(selectItems) > 0 && selectItems[0].IsTopLevel {
return r.resolveSelectItem(selectItems[0])
}
// Construct a new row based on the selected columns
row := make(map[string]interface{})
for index, selectItem := range selectItems {
destinationName := resolveDestinationColumnName(selectItem, index, r.parameters)
row[destinationName] = r.resolveSelectItem(selectItem)
}
return row
}
func resolveDestinationColumnName(selectItem parsers.SelectItem, itemIndex int, queryParameters map[string]interface{}) string {
if selectItem.Alias != "" {
return selectItem.Alias
}
destinationName := fmt.Sprintf("$%d", itemIndex+1)
if len(selectItem.Path) > 0 {
destinationName = selectItem.Path[len(selectItem.Path)-1]
}
if destinationName[0] == '@' {
destinationName = queryParameters[destinationName].(string)
}
return destinationName
}
func (r rowContext) resolveSelectItem(selectItem parsers.SelectItem) interface{} {
if selectItem.Type == parsers.SelectItemTypeArray {
return r.selectItem_SelectItemTypeArray(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeObject {
return r.selectItem_SelectItemTypeObject(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeConstant {
return r.selectItem_SelectItemTypeConstant(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeSubQuery {
return r.selectItem_SelectItemTypeSubQuery(selectItem)
}
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedFunctionCall, ok := selectItem.Value.(parsers.FunctionCall); ok {
return r.selectItem_SelectItemTypeFunctionCall(typedFunctionCall)
}
logger.ErrorLn("parsers.SelectItem has incorrect Value type (expected parsers.FunctionCall)")
return nil
}
return r.selectItem_SelectItemTypeField(selectItem)
}
func (r rowContext) selectItem_SelectItemTypeArray(selectItem parsers.SelectItem) interface{} {
arrayValue := make([]interface{}, 0)
for _, subSelectItem := range selectItem.SelectItems {
arrayValue = append(arrayValue, r.resolveSelectItem(subSelectItem))
}
return arrayValue
}
func (r rowContext) selectItem_SelectItemTypeObject(selectItem parsers.SelectItem) interface{} {
objectValue := make(map[string]interface{})
for _, subSelectItem := range selectItem.SelectItems {
objectValue[subSelectItem.Alias] = r.resolveSelectItem(subSelectItem)
}
return objectValue
}
func (r rowContext) selectItem_SelectItemTypeConstant(selectItem parsers.SelectItem) interface{} {
var typedValue parsers.Constant
var ok bool
if typedValue, ok = selectItem.Value.(parsers.Constant); !ok {
// TODO: Handle error
logger.ErrorLn("parsers.Constant has incorrect Value type")
}
if typedValue.Type == parsers.ConstantTypeParameterConstant &&
r.parameters != nil {
if key, ok := typedValue.Value.(string); ok {
return r.parameters[key]
}
}
return typedValue.Value
}
func (r rowContext) selectItem_SelectItemTypeSubQuery(selectItem parsers.SelectItem) interface{} {
subQuery := selectItem.Value.(parsers.SelectStmt)
subQueryResult := ExecuteQuery(
subQuery,
[]RowType{r},
)
if subQuery.Exists {
return len(subQueryResult) > 0
}
return subQueryResult
}
func (r rowContext) selectItem_SelectItemTypeFunctionCall(functionCall parsers.FunctionCall) interface{} {
switch functionCall.Type {
case parsers.FunctionCallStringEquals:
return r.strings_StringEquals(functionCall.Arguments)
case parsers.FunctionCallContains:
return r.strings_Contains(functionCall.Arguments)
case parsers.FunctionCallEndsWith:
return r.strings_EndsWith(functionCall.Arguments)
case parsers.FunctionCallStartsWith:
return r.strings_StartsWith(functionCall.Arguments)
case parsers.FunctionCallConcat:
return r.strings_Concat(functionCall.Arguments)
case parsers.FunctionCallIndexOf:
return r.strings_IndexOf(functionCall.Arguments)
case parsers.FunctionCallToString:
return r.strings_ToString(functionCall.Arguments)
case parsers.FunctionCallUpper:
return r.strings_Upper(functionCall.Arguments)
case parsers.FunctionCallLower:
return r.strings_Lower(functionCall.Arguments)
case parsers.FunctionCallLeft:
return r.strings_Left(functionCall.Arguments)
case parsers.FunctionCallLength:
return r.strings_Length(functionCall.Arguments)
case parsers.FunctionCallLTrim:
return r.strings_LTrim(functionCall.Arguments)
case parsers.FunctionCallReplace:
return r.strings_Replace(functionCall.Arguments)
case parsers.FunctionCallReplicate:
return r.strings_Replicate(functionCall.Arguments)
case parsers.FunctionCallReverse:
return r.strings_Reverse(functionCall.Arguments)
case parsers.FunctionCallRight:
return r.strings_Right(functionCall.Arguments)
case parsers.FunctionCallRTrim:
return r.strings_RTrim(functionCall.Arguments)
case parsers.FunctionCallSubstring:
return r.strings_Substring(functionCall.Arguments)
case parsers.FunctionCallTrim:
return r.strings_Trim(functionCall.Arguments)
case parsers.FunctionCallIsDefined:
return r.typeChecking_IsDefined(functionCall.Arguments)
case parsers.FunctionCallIsArray:
return r.typeChecking_IsArray(functionCall.Arguments)
case parsers.FunctionCallIsBool:
return r.typeChecking_IsBool(functionCall.Arguments)
case parsers.FunctionCallIsFiniteNumber:
return r.typeChecking_IsFiniteNumber(functionCall.Arguments)
case parsers.FunctionCallIsInteger:
return r.typeChecking_IsInteger(functionCall.Arguments)
case parsers.FunctionCallIsNull:
return r.typeChecking_IsNull(functionCall.Arguments)
case parsers.FunctionCallIsNumber:
return r.typeChecking_IsNumber(functionCall.Arguments)
case parsers.FunctionCallIsObject:
return r.typeChecking_IsObject(functionCall.Arguments)
case parsers.FunctionCallIsPrimitive:
return r.typeChecking_IsPrimitive(functionCall.Arguments)
case parsers.FunctionCallIsString:
return r.typeChecking_IsString(functionCall.Arguments)
case parsers.FunctionCallArrayConcat:
return r.array_Concat(functionCall.Arguments)
case parsers.FunctionCallArrayContains:
return r.array_Contains(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAny:
return r.array_Contains_Any(functionCall.Arguments)
case parsers.FunctionCallArrayContainsAll:
return r.array_Contains_All(functionCall.Arguments)
case parsers.FunctionCallArrayLength:
return r.array_Length(functionCall.Arguments)
case parsers.FunctionCallArraySlice:
return r.array_Slice(functionCall.Arguments)
case parsers.FunctionCallSetIntersect:
return r.set_Intersect(functionCall.Arguments)
case parsers.FunctionCallSetUnion:
return r.set_Union(functionCall.Arguments)
case parsers.FunctionCallMathAbs:
return r.math_Abs(functionCall.Arguments)
case parsers.FunctionCallMathAcos:
return r.math_Acos(functionCall.Arguments)
case parsers.FunctionCallMathAsin:
return r.math_Asin(functionCall.Arguments)
case parsers.FunctionCallMathAtan:
return r.math_Atan(functionCall.Arguments)
case parsers.FunctionCallMathCeiling:
return r.math_Ceiling(functionCall.Arguments)
case parsers.FunctionCallMathCos:
return r.math_Cos(functionCall.Arguments)
case parsers.FunctionCallMathCot:
return r.math_Cot(functionCall.Arguments)
case parsers.FunctionCallMathDegrees:
return r.math_Degrees(functionCall.Arguments)
case parsers.FunctionCallMathExp:
return r.math_Exp(functionCall.Arguments)
case parsers.FunctionCallMathFloor:
return r.math_Floor(functionCall.Arguments)
case parsers.FunctionCallMathIntBitNot:
return r.math_IntBitNot(functionCall.Arguments)
case parsers.FunctionCallMathLog10:
return r.math_Log10(functionCall.Arguments)
case parsers.FunctionCallMathRadians:
return r.math_Radians(functionCall.Arguments)
case parsers.FunctionCallMathRound:
return r.math_Round(functionCall.Arguments)
case parsers.FunctionCallMathSign:
return r.math_Sign(functionCall.Arguments)
case parsers.FunctionCallMathSin:
return r.math_Sin(functionCall.Arguments)
case parsers.FunctionCallMathSqrt:
return r.math_Sqrt(functionCall.Arguments)
case parsers.FunctionCallMathSquare:
return r.math_Square(functionCall.Arguments)
case parsers.FunctionCallMathTan:
return r.math_Tan(functionCall.Arguments)
case parsers.FunctionCallMathTrunc:
return r.math_Trunc(functionCall.Arguments)
case parsers.FunctionCallMathAtn2:
return r.math_Atn2(functionCall.Arguments)
case parsers.FunctionCallMathIntAdd:
return r.math_IntAdd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitAnd:
return r.math_IntBitAnd(functionCall.Arguments)
case parsers.FunctionCallMathIntBitLeftShift:
return r.math_IntBitLeftShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitOr:
return r.math_IntBitOr(functionCall.Arguments)
case parsers.FunctionCallMathIntBitRightShift:
return r.math_IntBitRightShift(functionCall.Arguments)
case parsers.FunctionCallMathIntBitXor:
return r.math_IntBitXor(functionCall.Arguments)
case parsers.FunctionCallMathIntDiv:
return r.math_IntDiv(functionCall.Arguments)
case parsers.FunctionCallMathIntMod:
return r.math_IntMod(functionCall.Arguments)
case parsers.FunctionCallMathIntMul:
return r.math_IntMul(functionCall.Arguments)
case parsers.FunctionCallMathIntSub:
return r.math_IntSub(functionCall.Arguments)
case parsers.FunctionCallMathPower:
return r.math_Power(functionCall.Arguments)
case parsers.FunctionCallMathLog:
return r.math_Log(functionCall.Arguments)
case parsers.FunctionCallMathNumberBin:
return r.math_NumberBin(functionCall.Arguments)
case parsers.FunctionCallMathPi:
return r.math_Pi()
case parsers.FunctionCallMathRand:
return r.math_Rand()
case parsers.FunctionCallAggregateAvg:
return r.aggregate_Avg(functionCall.Arguments)
case parsers.FunctionCallAggregateCount:
return r.aggregate_Count(functionCall.Arguments)
case parsers.FunctionCallAggregateMax:
return r.aggregate_Max(functionCall.Arguments)
case parsers.FunctionCallAggregateMin:
return r.aggregate_Min(functionCall.Arguments)
case parsers.FunctionCallAggregateSum:
return r.aggregate_Sum(functionCall.Arguments)
case parsers.FunctionCallIn:
return r.misc_In(functionCall.Arguments)
}
logger.Errorf("Unknown function call type: %v", functionCall.Type)
return nil
}
func (r rowContext) selectItem_SelectItemTypeField(selectItem parsers.SelectItem) interface{} {
value := r.tables[selectItem.Path[0]]
if len(selectItem.Path) > 1 {
for _, pathSegment := range selectItem.Path[1:] {
if pathSegment[0] == '@' {
pathSegment = r.parameters[pathSegment].(string)
}
switch nestedValue := value.(type) {
case map[string]interface{}:
value = nestedValue[pathSegment]
case map[string]RowType:
value = nestedValue[pathSegment]
case []int, []string, []interface{}:
slice := reflect.ValueOf(nestedValue)
if arrayIndex, err := strconv.Atoi(pathSegment); err == nil && slice.Len() > arrayIndex {
value = slice.Index(arrayIndex).Interface()
} else {
return nil
}
default:
return nil
}
}
}
return value
}
func hasAggregateFunctions(selectItems []parsers.SelectItem) bool {
if selectItems == nil {
return false
}
for _, selectItem := range selectItems {
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedValue, ok := selectItem.Value.(parsers.FunctionCall); ok && slices.Contains[[]parsers.FunctionCallType](parsers.AggregateFunctions, typedValue.Type) {
return true
}
}
if hasAggregateFunctions(selectItem.SelectItems) {
return true
}
}
return false
}
func compareValues(val1, val2 interface{}) int {
if reflect.TypeOf(val1) != reflect.TypeOf(val2) {
return 1
}
switch val1 := val1.(type) {
case int:
val2 := val2.(int)
if val1 < val2 {
return -1
} else if val1 > val2 {
return 1
}
return 0
case float64:
val2 := val2.(float64)
if val1 < val2 {
return -1
} else if val1 > val2 {
return 1
}
return 0
case string:
val2 := val2.(string)
return strings.Compare(val1, val2)
case bool:
val2 := val2.(bool)
if val1 == val2 {
return 0
} else if val1 {
return 1
} else {
return -1
}
// TODO: Add more types
default:
if reflect.DeepEqual(val1, val2) {
return 0
}
return 1
}
}
func deduplicate[T RowType | interface{}](slice []T) []T {
var result []T
result = make([]T, 0)
for i := 0; i < len(slice); i++ {
unique := true
for j := 0; j < len(result); j++ {
if compareValues(slice[i], result[j]) == 0 {
unique = false
break
}
}
if unique {
result = append(result, slice[i])
}
}
return result
}
func copyMap[T RowType | []RowType](originalMap map[string]T) map[string]T {
targetMap := make(map[string]T)
for k, v := range originalMap {
targetMap[k] = v
}
return targetMap
return projectedIterator
}
@@ -16,3 +16,16 @@ func (r rowContext) misc_In(arguments []interface{}) bool {
return false
}
func (r rowContext) misc_Iif(arguments []interface{}) interface{} {
if len(arguments) != 3 {
return nil
}
condition := r.resolveSelectItem(arguments[0].(parsers.SelectItem))
if condition != nil && condition == true {
return r.resolveSelectItem(arguments[1].(parsers.SelectItem))
}
return r.resolveSelectItem(arguments[2].(parsers.SelectItem))
}
+128 -3
View File
@@ -4,21 +4,44 @@ import (
"reflect"
"testing"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
memoryexecutor "github.com/pikami/cosmium/query_executors/memory_executor"
testutils "github.com/pikami/cosmium/test_utils"
)
type TestDocumentIterator struct {
documents []memoryexecutor.RowType
index int
}
func NewTestDocumentIterator(documents []memoryexecutor.RowType) *TestDocumentIterator {
return &TestDocumentIterator{
documents: documents,
index: -1,
}
}
func (i *TestDocumentIterator) Next() (memoryexecutor.RowType, datastore.DataStoreStatus) {
i.index++
if i.index >= len(i.documents) {
return nil, datastore.IterEOF
}
return i.documents[i.index], datastore.StatusOk
}
func testQueryExecute(
t *testing.T,
query parsers.SelectStmt,
data []memoryexecutor.RowType,
expectedData []memoryexecutor.RowType,
) {
result := memoryexecutor.ExecuteQuery(query, data)
iter := NewTestDocumentIterator(data)
result := memoryexecutor.ExecuteQuery(query, iter, 0, 1000)
if !reflect.DeepEqual(result, expectedData) {
t.Errorf("execution result does not match expected data.\nExpected: %+v\nGot: %+v", expectedData, result)
if !reflect.DeepEqual(result.Rows, expectedData) {
t.Errorf("execution result does not match expected data.\nExpected: %+v\nGot: %+v", expectedData, result.Rows)
}
}
@@ -126,6 +149,77 @@ func Test_Execute(t *testing.T) {
)
})
t.Run("Should execute NOT IN function", func(t *testing.T) {
testQueryExecute(
t,
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"),
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "12345"},
map[string]interface{}{"id": "67890"},
},
)
})
t.Run("Should execute IN function with function call", func(t *testing.T) {
testQueryExecute(
t,
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"),
},
},
},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "456"},
map[string]interface{}{"id": "123"},
},
)
})
t.Run("Should execute IN selector", func(t *testing.T) {
testQueryExecute(
t,
@@ -151,4 +245,35 @@ func Test_Execute(t *testing.T) {
},
)
})
t.Run("Should execute function IIF()", func(t *testing.T) {
testQueryExecute(
t,
parsers.SelectStmt{
SelectItems: []parsers.SelectItem{
testutils.SelectItem_Path("c", "id"),
{
Alias: "coolness",
Type: parsers.SelectItemTypeFunctionCall,
Value: parsers.FunctionCall{
Type: parsers.FunctionCallIif,
Arguments: []interface{}{
testutils.SelectItem_Path("c", "isCool"),
testutils.SelectItem_Constant_String("real cool"),
testutils.SelectItem_Constant_String("not cool"),
},
},
},
},
Table: parsers.Table{SelectItem: testutils.SelectItem_Path("c")},
},
mockData,
[]memoryexecutor.RowType{
map[string]interface{}{"id": "12345", "coolness": "not cool"},
map[string]interface{}{"id": "67890", "coolness": "real cool"},
map[string]interface{}{"id": "456", "coolness": "real cool"},
map[string]interface{}{"id": "123", "coolness": "real cool"},
},
)
})
}
@@ -0,0 +1,22 @@
package memoryexecutor
import "github.com/pikami/cosmium/internal/datastore"
type offsetIterator struct {
documents rowTypeIterator
offset int
skipped bool
}
func (oi *offsetIterator) Next() (RowType, datastore.DataStoreStatus) {
if oi.skipped {
return oi.documents.Next()
}
for i := 0; i < oi.offset; i++ {
oi.documents.Next()
}
oi.skipped = true
return oi.Next()
}
@@ -0,0 +1,63 @@
package memoryexecutor
import (
"sort"
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
)
type orderIterator struct {
documents rowIterator
orderExpressions []parsers.OrderExpression
orderedDocs []rowContext
docsIndex int
}
func (oi *orderIterator) Next() (rowContext, datastore.DataStoreStatus) {
if oi.orderedDocs != nil {
if oi.docsIndex >= len(oi.orderedDocs) {
return rowContext{}, datastore.IterEOF
}
row := oi.orderedDocs[oi.docsIndex]
oi.orderedDocs[oi.docsIndex] = rowContext{}
oi.docsIndex++
return row, datastore.StatusOk
}
oi.orderedDocs = make([]rowContext, 0)
for {
row, status := oi.documents.Next()
if status != datastore.StatusOk {
break
}
oi.orderedDocs = append(oi.orderedDocs, row)
}
oi.documents = nil
less := func(i, j int) bool {
for _, order := range oi.orderExpressions {
val1 := oi.orderedDocs[i].resolveSelectItem(order.SelectItem)
val2 := oi.orderedDocs[j].resolveSelectItem(order.SelectItem)
cmp := compareValues(val1, val2)
if cmp != 0 {
if order.Direction == parsers.OrderDirectionDesc {
return cmp > 0
}
return cmp < 0
}
}
return i < j
}
sort.SliceStable(oi.orderedDocs, less)
if len(oi.orderedDocs) == 0 {
return rowContext{}, datastore.IterEOF
}
oi.docsIndex = 1
return oi.orderedDocs[0], datastore.StatusOk
}
@@ -0,0 +1,90 @@
package memoryexecutor
import (
"github.com/pikami/cosmium/internal/datastore"
"github.com/pikami/cosmium/parsers"
"golang.org/x/exp/slices"
)
type projectIterator struct {
documents rowIterator
selectItems []parsers.SelectItem
groupBy []parsers.SelectItem
}
func (pi *projectIterator) Next() (RowType, datastore.DataStoreStatus) {
if pi.documents == nil {
return rowContext{}, datastore.IterEOF
}
row, status := pi.documents.Next()
if status != datastore.StatusOk {
pi.documents = nil
return rowContext{}, status
}
if hasAggregateFunctions(pi.selectItems) && len(pi.groupBy) == 0 {
// When can have aggregate functions without GROUP BY clause,
// we should aggregate all rows in that case.
allDocuments := []rowContext{row}
for {
row, status := pi.documents.Next()
if status != datastore.StatusOk {
break
}
allDocuments = append(allDocuments, row)
}
if len(allDocuments) == 0 {
return rowContext{}, datastore.IterEOF
}
aggRow := rowContext{
tables: row.tables,
parameters: row.parameters,
grouppedRows: allDocuments,
}
return aggRow.applyProjection(pi.selectItems), datastore.StatusOk
}
return row.applyProjection(pi.selectItems), datastore.StatusOk
}
func (r rowContext) applyProjection(selectItems []parsers.SelectItem) RowType {
// When the first value is top level, select it instead
if len(selectItems) > 0 && selectItems[0].IsTopLevel {
return r.resolveSelectItem(selectItems[0])
}
// Construct a new row based on the selected columns
row := make(map[string]interface{})
for index, selectItem := range selectItems {
destinationName := resolveDestinationColumnName(selectItem, index, r.parameters)
row[destinationName] = r.resolveSelectItem(selectItem)
}
return row
}
func hasAggregateFunctions(selectItems []parsers.SelectItem) bool {
if selectItems == nil {
return false
}
for _, selectItem := range selectItems {
if selectItem.Type == parsers.SelectItemTypeFunctionCall {
if typedValue, ok := selectItem.Value.(parsers.FunctionCall); ok && slices.Contains[[]parsers.FunctionCallType](parsers.AggregateFunctions, typedValue.Type) {
return true
}
}
if hasAggregateFunctions(selectItem.SelectItems) {
return true
}
}
return false
}

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