mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
Compare commits
No commits in common. "master" and "v3.35.0" have entirely different histories.
118 changed files with 2528 additions and 8913 deletions
|
|
@ -1,45 +0,0 @@
|
|||
## User Contribution Guidelines for FeatureBase
|
||||
|
||||
Thank you for your interest in contributing to FeatureBase! We appreciate your support in making this open-source project even better. Here are some guidelines to help you get started with contributing to FeatureBase:
|
||||
|
||||
1. Familiarize Yourself with the Project:
|
||||
- Visit the FeatureBase website at www.featurebase.com to understand the project's goals, capabilities, and features.
|
||||
- Read the documentation available on the website, including the installation guide, configuration options, and data modeling concepts.
|
||||
- Explore the codebase by cloning the repository and reviewing the source code.
|
||||
|
||||
2. Join the Community:
|
||||
- Visit the FeatureBase community page at https://www.featurebase.com/community to learn more about the project's community and how to get involved.
|
||||
- Join the Discord server at https://discord.gg/FBn2vEp7Na to chat with other contributors and users, ask questions, and share your ideas.
|
||||
|
||||
3. Set Up Your Development Environment:
|
||||
- Ensure you have Go installed on your machine. Make sure your shell's search path includes the go/bin directory.
|
||||
- Clone the FeatureBase repository or download it as a zip file from the repository's page.
|
||||
- Follow the "Build FeatureBase Server from source" instructions in the README file to compile the server binary and the ingester binaries.
|
||||
|
||||
4. Choose a Contribution Area:
|
||||
- Identify the area you'd like to contribute to, such as bug fixes, new features, performance improvements, documentation updates, or community support.
|
||||
- Check the issue tracker on the repository or the FeatureBase community for open issues or feature requests that align with your interests and skills. Alternatively, propose your own idea by creating a new issue.
|
||||
|
||||
5. Create a New Branch:
|
||||
- Before making any changes, create a new branch in the repository's Git repository. This branch will contain your contributions.
|
||||
- Give your branch a descriptive name that reflects the nature of your contribution.
|
||||
|
||||
6. Make Your Changes:
|
||||
- Follow the coding style and conventions used in the existing codebase.
|
||||
- Write clear and concise commit messages for each logical change.
|
||||
- If you're introducing new features or modifying existing behavior, make sure to update the documentation to reflect the changes.
|
||||
|
||||
7. Test Your Changes:
|
||||
- Run the existing test suite to ensure that your modifications do not introduce any regressions.
|
||||
- If applicable, write additional tests to cover the changes you made.
|
||||
- Document any new testing procedures required for your contribution.
|
||||
|
||||
8. Submitting Your Contribution:
|
||||
- Push your branch to the main repository or create a fork and submit a pull request to the main repository.
|
||||
- Provide a detailed description of your changes, including the problem you solved and the approach you took.
|
||||
- Be responsive to any feedback or suggestions provided by the project maintainers or other contributors.
|
||||
- Once your contribution is approved, it will be reviewed and merged into the main codebase.
|
||||
|
||||
Please note that by contributing to FeatureBase, you agree that your contributions will be licensed under the Apache 2.0 license, which governs the project.
|
||||
|
||||
Thank you for considering contributing to FeatureBase! Your contributions are valuable and help improve the project for everyone.
|
||||
53
README.md
53
README.md
|
|
@ -1,10 +1,4 @@
|
|||
# FeatureBase Community
|
||||
|
||||
FeatureBase Community is now archived and no longer maintained.
|
||||
|
||||
* [FeatureBase Community Help](https://github.com/FeatureBaseDB/FB-community-help)
|
||||
|
||||
|
||||
# FeatureBase
|
||||
|
||||
## Pilosa is now FeatureBase
|
||||
|
||||
|
|
@ -16,8 +10,6 @@ For more information about FeatureBase, please visit [www.featurebase.com][HomeP
|
|||
|
||||
## Getting Started
|
||||
|
||||
* [Learn how to install FeatureBase Community](https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/community/com-getstart/com-getstart-home.md)
|
||||
|
||||
### Build FeatureBase Server from source
|
||||
|
||||
0. Install go. Ensure that your shell's search path includes the go/bin directory.
|
||||
|
|
@ -27,20 +19,42 @@ For more information about FeatureBase, please visit [www.featurebase.com][HomeP
|
|||
4. Run `featurebase server --handler.allowed-origins=http://localhost:3000` to run FeatureBase server with default settings (learn more about configuring FeatureBase at the link below). The `--handler.allowed-origins` parameter allows the standalone web UI to talk to the server; this can be omitted if the web UI is not needed.
|
||||
5. Run `curl localhost:10101/status` to verify the server is running and accessible.
|
||||
|
||||
### Ingest Data and Query
|
||||
|
||||
1. Run
|
||||
```
|
||||
molecula-consumer-csv \
|
||||
--index repository \
|
||||
--header "language__ID_F,project_id__ID_F" \
|
||||
--id-field project_id \
|
||||
--batch-size 1000 \
|
||||
--files example.csv
|
||||
```
|
||||
|
||||
This will ingest the `example.csv` file into a FeatureBase table called `repository`. If the table does not exist, it will be automatically created. Learn more about [ingesting data into FeatureBase][Ingest]
|
||||
|
||||
2. Query your data.
|
||||
```
|
||||
curl localhost:10101/index/repository/query \
|
||||
-X POST \
|
||||
-d 'Row(example=5)'
|
||||
```
|
||||
Learn about supported [SQL][SQL], native [Pilosa Query Language (PQL)][PQL].
|
||||
|
||||
### Data Model
|
||||
|
||||
Because FeatureBase is built on bitmaps, there is bit of a learning curve to grasp how your data is represented.
|
||||
[Learn about Data Modeling][DataModel].
|
||||
|
||||
* [Learn about Data Modeling](https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/concepts/concepts-home.md)
|
||||
### More Information
|
||||
|
||||
[Installation][Install]
|
||||
|
||||
### Ingest Data and Query
|
||||
|
||||
* [Learn how to ingest data from multiple data sources](https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/community/com-ingest/com-ingest-manage.md)
|
||||
[Configuration][Config]
|
||||
|
||||
## Community
|
||||
|
||||
You can email us at community@featurebase.com and [learn more about contributing](https://github.com/FeatureBaseDB/featurebase/blob/master/OPENSOURCE.md).
|
||||
You can email us at community@featurebase.com or learn more about contributing at [https://www.featurebase.com/community][Community].
|
||||
|
||||
Chat with us: [https://discord.gg/FBn2vEp7Na][Discord]
|
||||
|
||||
|
|
@ -59,14 +73,13 @@ A lot has changed since the days of Pilosa. This list highlights some new capabi
|
|||
|
||||
FeatureBase is licensed under the [Apache License, Version 2.0][License]
|
||||
|
||||
[Community]: https://github.com/FeatureBaseDB/FB-community-help/tree/main
|
||||
[Install]:https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/community/com-getstart/com-getstart-home.md
|
||||
[Config]: https://github.com/FeatureBaseDB/FB-community-help/tree/main/docs/community/com-config
|
||||
[DataModel]: https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/concepts/concepts-home.md
|
||||
[Community]: http://www.featurebase.com/community?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[Config]: https://docs.featurebase.com/docs/community/com-config/old-config-flags/?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[DataModel]: https://docs.featurebase.com/docs/concepts/overview-data-modeling/?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[Discord]: https://discord.gg/FBn2vEp7Na
|
||||
[HomePage]: http://featurebase.com?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[Ingest]: https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/community/com-ingest/com-ingest-manage.md
|
||||
|
||||
[Ingest]: https://docs.featurebase.com/docs/community/com-ingest/old-ingesters/?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[Install]: https://docs.featurebase.com/docs/community/com-home/#install-featurebase-community?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[License]: http://www.apache.org/licenses/LICENSE-2.0
|
||||
[PQL]: https://docs.featurebase.com/docs/pql-guide/pql-home/?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
[SQL]: https://docs.featurebase.com/docs/sql-guide/sql-guide-home/?utm_campaign=Open%20Source&utm_source=GitHub
|
||||
|
|
|
|||
|
|
@ -35,22 +35,6 @@ func (api *API) ApplyDirective(ctx context.Context, d *dax.Directive) error {
|
|||
// Handle the operations based on the directive method.
|
||||
switch d.Method {
|
||||
case dax.DirectiveMethodDiff:
|
||||
// In order to prevent adding too much code specific to handling a diff
|
||||
// directive (e.g. adding something like an `enactDirectiveDiff()`
|
||||
// method), we are instead going to build a full Directive based on the
|
||||
// diff, and then proceed normally as if we had received a full
|
||||
// Directive. We do that by copying the previous Directive and then
|
||||
// applying the diffs to the copy.
|
||||
newD := previousDirective.Copy()
|
||||
|
||||
// Apply the diffs from the incoming Directive to the new, copied
|
||||
// Directive.
|
||||
newD.ApplyDiff(d)
|
||||
|
||||
// Now proceed with the new diff as if we had received it as a full diff.
|
||||
d = newD
|
||||
|
||||
case dax.DirectiveMethodFull:
|
||||
// pass: normal operation
|
||||
|
||||
case dax.DirectiveMethodReset:
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func TestAPI_Directive(t *testing.T) {
|
|||
// Empty directive (and empty holder).
|
||||
{
|
||||
d := &dax.Directive{
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Version: 1,
|
||||
}
|
||||
err := api.ApplyDirective(ctx, d)
|
||||
|
|
@ -41,7 +41,7 @@ func TestAPI_Directive(t *testing.T) {
|
|||
// Add a new table.
|
||||
{
|
||||
d := &dax.Directive{
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl1,
|
||||
},
|
||||
|
|
@ -55,7 +55,7 @@ func TestAPI_Directive(t *testing.T) {
|
|||
// Add a new table, and keep the existing table.
|
||||
{
|
||||
d := &dax.Directive{
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl1,
|
||||
tbl2,
|
||||
|
|
@ -70,7 +70,7 @@ func TestAPI_Directive(t *testing.T) {
|
|||
// Add a new table and remove one of the existing tables.
|
||||
{
|
||||
d := &dax.Directive{
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl2,
|
||||
tbl3,
|
||||
|
|
|
|||
15
cli/meta.go
15
cli/meta.go
|
|
@ -364,7 +364,7 @@ Input/Output
|
|||
\warn [-n] [STRING] write string to standard error (-n for no newline)
|
||||
|
||||
Informational
|
||||
\d list tables, including system tables
|
||||
\d list tables
|
||||
\d NAME describe table
|
||||
\dt list tables
|
||||
\dv list views
|
||||
|
|
@ -504,17 +504,8 @@ func (m *metaDescribe) execute(cmd *Command) (responseAction, error) {
|
|||
switch len(m.args) {
|
||||
case 0:
|
||||
// Describe with no args should list all relations (tables, views,
|
||||
// etc.). For now, we're just going to list the tables, including system
|
||||
// tables.
|
||||
qry := []queryPart{
|
||||
newPartRaw("SHOW TABLES WITH SYSTEM"),
|
||||
}
|
||||
|
||||
if err := cmd.executeAndWriteQuery(qry); err != nil {
|
||||
return actionNone, errors.Wrap(err, "executing query")
|
||||
}
|
||||
|
||||
return actionReset, nil
|
||||
// etc.). For now, we're just going to list the tables.
|
||||
return newMetaListTables().execute(cmd)
|
||||
|
||||
case 1:
|
||||
// Describe with a single arg will assume the arg is a table name, so it
|
||||
|
|
|
|||
68
cli/testdata/table
vendored
68
cli/testdata/table
vendored
|
|
@ -1,44 +1,27 @@
|
|||
// Show tables for database using SHOW TABLES WITH SYSTEM.
|
||||
SEND:SHOW TABLES WITH SYSTEM;
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:
|
||||
|
||||
// Show tables for database using \d.
|
||||
SEND:\d
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:
|
||||
|
||||
// Show tables for database using SHOW TABLES.
|
||||
SEND:SHOW TABLES;
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:
|
||||
|
||||
|
||||
// Show tables for database using \dt.
|
||||
SEND:\dt
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:+-----+------+-------+------------+------------+------------+------+------------+-------------+
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:
|
||||
|
||||
// Create a table. That can be used for general testing.
|
||||
|
|
@ -50,11 +33,16 @@ EXPECT:
|
|||
|
||||
// Show tables for database to get the newly created table.
|
||||
SEND:\dt
|
||||
EXPECT:+-------+-------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------+-------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| users | users | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------+-------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECTCOMP:WithFormat:| users | users | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ | | | {timestamp} | {timestamp} | false | 0 | |
|
||||
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
|
||||
EXPECT:
|
||||
|
||||
// We don't select from users until AFTER we check SHOW TABLES above because
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ type Balancer interface {
|
|||
// be either transferred to other workers or placed on the free job list.
|
||||
RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.WorkerDiff, error)
|
||||
|
||||
// ReleaseWorkers dissociates the given workers from a database.
|
||||
ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error
|
||||
// FreeWorkers dissociates the given workers from a database.
|
||||
FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error
|
||||
|
||||
// AddJobs adds new jobs for the given database.
|
||||
AddJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID, jobs ...dax.Job) ([]dax.WorkerDiff, error)
|
||||
|
|
@ -64,7 +64,7 @@ func (b *NopBalancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.Worke
|
|||
func (b *NopBalancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.WorkerDiff, error) {
|
||||
return []dax.WorkerDiff{}, nil
|
||||
}
|
||||
func (b *NopBalancer) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
func (b *NopBalancer) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
return nil
|
||||
}
|
||||
func (b *NopBalancer) AddJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID, jobs ...dax.Job) ([]dax.WorkerDiff, error) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ type Balancer struct {
|
|||
// current represents the current state of worker/job assigments.
|
||||
current WorkerJobService
|
||||
|
||||
workerRegistry controller.WorkerRegistry
|
||||
nodeService controller.NodeService
|
||||
|
||||
// freeJobs is the set of jobs which have yet to be assigned to a worker.
|
||||
// This could be because there are no available workers, or because a worker
|
||||
|
|
@ -44,37 +44,47 @@ type Balancer struct {
|
|||
}
|
||||
|
||||
// New returns a new instance of Balancer.
|
||||
func New(wr controller.WorkerRegistry, fjs FreeJobService, wjs WorkerJobService, fws FreeWorkerService, schemar schemar.Schemar, logger logger.Logger) *Balancer {
|
||||
func New(ns controller.NodeService, fjs FreeJobService, wjs WorkerJobService, fws FreeWorkerService, schemar schemar.Schemar, logger logger.Logger) *Balancer {
|
||||
return &Balancer{
|
||||
current: wjs,
|
||||
workerRegistry: wr,
|
||||
freeJobs: fjs,
|
||||
freeWorkers: fws,
|
||||
schemar: schemar,
|
||||
logger: logger,
|
||||
current: wjs,
|
||||
nodeService: ns,
|
||||
freeJobs: fjs,
|
||||
freeWorkers: fws,
|
||||
schemar: schemar,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// AddWorker adds the given Node to the Balancer's available worker pool. Note
|
||||
// that a node is used for ALL of the role types specified. In other words,
|
||||
// specifying roleTypes = {compute, translate}, does not mean that the node can
|
||||
// be used as either a compute worker or a translate worker. It means that it
|
||||
// will be used as both.
|
||||
// AddWorker adds the given Node to the Balancer's available worker pool.
|
||||
// TODO(tlt): this method takes a Node (as opposed to a Worker) because in the
|
||||
// future we may want to maintain separate worker pools based on RoleType
|
||||
// (compute, translate, etc.).
|
||||
func (b *Balancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.WorkerDiff, error) {
|
||||
b.logger.Debugf("AddWorker(%s)", node.Address)
|
||||
addr := node.Address
|
||||
b.logger.Debugf("AddWorker(%s)", addr)
|
||||
|
||||
if err := b.workerRegistry.AddWorker(tx, node); err != nil {
|
||||
return nil, errors.Wrapf(err, "creating node on node service: %s", node.Address)
|
||||
if err := b.nodeService.CreateNode(tx, addr, node); err != nil {
|
||||
return nil, errors.Wrapf(err, "creating node on node service: %s", addr)
|
||||
}
|
||||
|
||||
diffs := NewInternalDiffs()
|
||||
|
||||
// Process the newly added workers.
|
||||
// This logic means that a node is used for ALL of the role types specified.
|
||||
// In other words, specifying roleTypes = {compute, translate}, does not
|
||||
// mean that the node can be used as either a compute worker or a translate
|
||||
// worker. It means that it will be used as both.
|
||||
for _, rt := range node.RoleTypes {
|
||||
if err := b.addWorker(tx, rt, addr); err != nil {
|
||||
return nil, errors.Wrapf(err, "adding worker: (%s) %s", rt, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the freeWorkers.
|
||||
// TODO(tlt): this is a little heavy-handed. I'm sure we'll need to be more
|
||||
// intentional about knowing which databases need workers, as opposed to
|
||||
// intentional about knowing which databases needs workers, as opposed to
|
||||
// this brute force loop over all databases every time.
|
||||
if diff, err := b.balance(tx); err != nil {
|
||||
return nil, errors.Wrapf(err, "balancing new worker: %s", node.Address)
|
||||
return nil, errors.Wrapf(err, "balancing new worker: %s", addr)
|
||||
} else {
|
||||
diffs.Merge(diff)
|
||||
}
|
||||
|
|
@ -82,9 +92,23 @@ func (b *Balancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.WorkerDi
|
|||
return diffs.Output(), nil
|
||||
}
|
||||
|
||||
func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (InternalDiffs, error) {
|
||||
b.logger.Debugf("assigning min workers for '%s', '%s'", roleType, qdbid)
|
||||
// addWorker adds a worker to the free worker list. From there, it can be used
|
||||
// by any database which needs a worker.
|
||||
func (b *Balancer) addWorker(tx dax.Transaction, roleType dax.RoleType, addr dax.Address) error {
|
||||
// If this worker already exists, don't do anything.
|
||||
if dbkey := b.current.DatabaseForWorker(tx, addr); dbkey != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := b.freeWorkers.AddWorkers(tx, roleType, addr); err != nil {
|
||||
return errors.Wrap(err, "adding free worker")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType) (InternalDiffs, error) {
|
||||
b.logger.Debugf("assigning min workers for '%s'", roleType)
|
||||
// Find out how many free workers we have.
|
||||
freeWorkers, err := b.freeWorkers.ListWorkers(tx, roleType)
|
||||
if err != nil {
|
||||
|
|
@ -98,12 +122,10 @@ func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType, q
|
|||
return InternalDiffs{}, nil
|
||||
}
|
||||
|
||||
// Get database and its minWorkerCount (Database.Options.WorkersMin). This
|
||||
// used to get all databases, but now this method is specific to a single
|
||||
// database. That's why we just get the one here.
|
||||
qdbs, err := b.schemar.Databases(tx, qdbid.OrganizationID, qdbid.DatabaseID)
|
||||
// Get all database and their minWorkerCount (Database.Options.WorkersMin).
|
||||
qdbs, err := b.schemar.Databases(tx, "")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting database")
|
||||
return nil, errors.Wrap(err, "getting all database")
|
||||
}
|
||||
|
||||
// Create a map[database]int where int is the number of workers required to
|
||||
|
|
@ -147,7 +169,7 @@ func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType, q
|
|||
|
||||
diffs := NewInternalDiffs()
|
||||
|
||||
// Create an ordered slice of map keys so that tests are predictable.
|
||||
// Create an ordered slice of map keys so that tests are predicatable.
|
||||
qdbids := make([]dax.QualifiedDatabaseID, 0, len(m))
|
||||
for qdbid := range m {
|
||||
qdbids = append(qdbids, qdbid)
|
||||
|
|
@ -222,12 +244,13 @@ func (b *Balancer) databaseHasJobs(tx dax.Transaction, roleType dax.RoleType, qd
|
|||
func (b *Balancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.WorkerDiff, error) {
|
||||
diffs := NewInternalDiffs()
|
||||
|
||||
// See if the worker is assigned to a database. If it is, disassociate the
|
||||
// worker from all of its jobs for the database.
|
||||
////// The rest is database specific. ////////////
|
||||
|
||||
// See if the worker is assigned to a database. If it's not, return early.
|
||||
dbkey := b.current.DatabaseForWorker(tx, addr)
|
||||
if dbkey != "" {
|
||||
qdbid := dbkey.QualifiedDatabaseID()
|
||||
for _, rt := range dax.AllRoleTypes {
|
||||
for _, rt := range []dax.RoleType{dax.RoleTypeCompute, dax.RoleTypeTranslate} {
|
||||
if diff, err := b.removeDatabaseWorker(tx, rt, qdbid, addr); err != nil {
|
||||
return nil, errors.Wrapf(err, "removing worker: (%s) %s", rt, addr)
|
||||
} else {
|
||||
|
|
@ -236,8 +259,15 @@ func (b *Balancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.Wor
|
|||
}
|
||||
}
|
||||
|
||||
// Remove the worker from the worker registry.
|
||||
if err := b.workerRegistry.RemoveWorker(tx, addr); err != nil {
|
||||
// Remove the worker from the free worker list (if it's there).
|
||||
for _, rt := range []dax.RoleType{dax.RoleTypeCompute, dax.RoleTypeTranslate} {
|
||||
if err := b.freeWorkers.RemoveWorker(tx, rt, addr); err != nil {
|
||||
return nil, errors.Wrapf(err, "removing worker from free list: (%s) %s", rt, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the worker (i.e. Node) from the node service.
|
||||
if err := b.nodeService.DeleteNode(tx, addr); err != nil {
|
||||
return nil, errors.Wrapf(err, "deleting node from node service: %s", addr)
|
||||
}
|
||||
|
||||
|
|
@ -254,8 +284,6 @@ func (b *Balancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.Wor
|
|||
return diffs.Output(), nil
|
||||
}
|
||||
|
||||
// removeDatabaseWorker is used to remove a worker that has been associated with
|
||||
// a database. The worker here is determined by address.
|
||||
func (b *Balancer) removeDatabaseWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) (InternalDiffs, error) {
|
||||
jobs, err := b.current.ListJobs(tx, roleType, qdbid, addr)
|
||||
if err != nil {
|
||||
|
|
@ -263,8 +291,13 @@ func (b *Balancer) removeDatabaseWorker(tx dax.Transaction, roleType dax.RoleTyp
|
|||
}
|
||||
|
||||
// Before removing the worker, mark its jobs as free.
|
||||
if err := b.freeJobs.MarkJobsAsFree(tx, roleType, qdbid, jobs); err != nil {
|
||||
return nil, errors.Wrap(err, "marking jobs as free")
|
||||
if err := b.freeJobs.MergeJobs(tx, roleType, qdbid, jobs); err != nil {
|
||||
return nil, errors.Wrap(err, "merging free jobs")
|
||||
}
|
||||
|
||||
// Remove the worker.
|
||||
if err := b.current.DeleteWorker(tx, roleType, qdbid, addr); err != nil {
|
||||
return nil, errors.Wrap(err, "deleting worker")
|
||||
}
|
||||
|
||||
// Even though this may not be useful to the caller (for example, in the
|
||||
|
|
@ -278,8 +311,8 @@ func (b *Balancer) removeDatabaseWorker(tx dax.Transaction, roleType dax.RoleTyp
|
|||
return diff, nil
|
||||
}
|
||||
|
||||
func (b *Balancer) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
return errors.Wrap(b.current.ReleaseWorkers(tx, addrs...), "freeing workers")
|
||||
func (b *Balancer) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
return errors.Wrap(b.current.FreeWorkers(tx, addrs...), "freeing workers")
|
||||
}
|
||||
|
||||
func (b *Balancer) AddJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID, jobs ...dax.Job) ([]dax.WorkerDiff, error) {
|
||||
|
|
@ -333,7 +366,7 @@ func (b *Balancer) addJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.
|
|||
// assigned workers until it has at least one job (which this database
|
||||
// now has).
|
||||
if diff, err := b.balanceDatabaseForRole(tx, roleType, qdbid); err != nil {
|
||||
return nil, errors.Wrapf(err, "balancing database for role: (%s)", roleType)
|
||||
return nil, errors.Wrapf(err, "assigning min workers: (%s)", roleType)
|
||||
} else {
|
||||
diffs.Merge(diff)
|
||||
}
|
||||
|
|
@ -363,7 +396,6 @@ func (b *Balancer) addDatabaseJobs(tx dax.Transaction, roleType dax.RoleType, qd
|
|||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "getting workers jobs: %s", roleType)
|
||||
}
|
||||
|
||||
jset := dax.NewSet[dax.Job]()
|
||||
for _, workerInfo := range workerJobs {
|
||||
jset.Merge(dax.NewSet(workerInfo.Jobs...))
|
||||
|
|
@ -405,12 +437,12 @@ func (b *Balancer) addDatabaseJobs(tx dax.Transaction, roleType dax.RoleType, qd
|
|||
jobCounts[lowWorker]++
|
||||
}
|
||||
|
||||
for addr, jobs := range jobsToCreate {
|
||||
if err := b.current.AssignWorkerToJobs(tx, roleType, qdbid, addr, jobs...); err != nil {
|
||||
for worker, jobs := range jobsToCreate {
|
||||
if err := b.current.CreateJobs(tx, roleType, qdbid, worker, jobs...); err != nil {
|
||||
return nil, errors.Wrap(err, "creating job")
|
||||
}
|
||||
for _, job := range jobs {
|
||||
diffs.Added(addr, job)
|
||||
diffs.Added(worker, job)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -495,7 +527,7 @@ func (b *Balancer) BalanceDatabase(tx dax.Transaction, qdbid dax.QualifiedDataba
|
|||
func (b *Balancer) balanceDatabase(tx dax.Transaction, qdbid dax.QualifiedDatabaseID) (InternalDiffs, error) {
|
||||
diffs := NewInternalDiffs()
|
||||
|
||||
for _, role := range dax.AllRoleTypes {
|
||||
for _, role := range []dax.RoleType{dax.RoleTypeCompute, dax.RoleTypeTranslate} {
|
||||
diff, err := b.balanceDatabaseForRole(tx, role, qdbid)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "getting worker count: (%s) %s", role, qdbid)
|
||||
|
|
@ -512,7 +544,8 @@ func (b *Balancer) balanceDatabaseForRole(tx dax.Transaction, roleType dax.RoleT
|
|||
|
||||
// Before balancing, make sure the database has its minimum number of
|
||||
// workers satisfied.
|
||||
if diff, err := b.assignMinWorkers(tx, roleType, qdbid); err != nil {
|
||||
// TODO(tlt): make assignMinWorkers database specific.
|
||||
if diff, err := b.assignMinWorkers(tx, roleType); err != nil {
|
||||
return nil, errors.Wrapf(err, "assigning min workers: (%s) %s", roleType, qdbid)
|
||||
} else {
|
||||
diffs.Merge(diff)
|
||||
|
|
@ -776,11 +809,11 @@ func (b *Balancer) workerForJob(tx dax.Transaction, roleType dax.RoleType, qdbid
|
|||
}
|
||||
|
||||
func (b *Balancer) ReadNode(tx dax.Transaction, addr dax.Address) (*dax.Node, error) {
|
||||
return b.workerRegistry.Worker(tx, addr)
|
||||
return b.nodeService.ReadNode(tx, addr)
|
||||
}
|
||||
|
||||
func (b *Balancer) Nodes(tx dax.Transaction) ([]*dax.Node, error) {
|
||||
return b.workerRegistry.Workers(tx)
|
||||
return b.nodeService.Nodes(tx)
|
||||
}
|
||||
|
||||
type WorkerJobService interface {
|
||||
|
|
@ -790,9 +823,10 @@ type WorkerJobService interface {
|
|||
ListWorkers(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Addresses, error)
|
||||
|
||||
CreateWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error
|
||||
ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error
|
||||
DeleteWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error
|
||||
FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error
|
||||
|
||||
AssignWorkerToJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error
|
||||
CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error
|
||||
DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job dax.Job) error
|
||||
DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) (InternalDiffs, error)
|
||||
JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr ...dax.Address) (map[dax.Address]int, error)
|
||||
|
|
@ -806,10 +840,12 @@ type FreeJobService interface {
|
|||
DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job dax.Job) error
|
||||
DeleteJobsForTable(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID) error
|
||||
ListJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (dax.Jobs, error)
|
||||
MarkJobsAsFree(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error
|
||||
MergeJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error
|
||||
}
|
||||
|
||||
type FreeWorkerService interface {
|
||||
AddWorkers(tx dax.Transaction, roleType dax.RoleType, addrs ...dax.Address) error
|
||||
RemoveWorker(tx dax.Transaction, roleType dax.RoleType, addr dax.Address) error
|
||||
PopWorkers(tx dax.Transaction, roleType dax.RoleType, num int) ([]dax.Address, error)
|
||||
ListWorkers(tx dax.Transaction, roleType dax.RoleType) (dax.Addresses, error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,11 +39,6 @@ func TestFreeJobService(t *testing.T) {
|
|||
job2 := dax.Job(qtid.Key() + "job2")
|
||||
job3 := dax.Job(qtid.Key() + "job3")
|
||||
|
||||
node := &dax.Node{
|
||||
Address: nodeAddr,
|
||||
RoleTypes: []dax.RoleType{role},
|
||||
}
|
||||
|
||||
err = fjSvc.CreateJobs(tx, role, qdbid, job1, job2, job3)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -54,22 +49,22 @@ func TestFreeJobService(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, dax.Jobs{job1, job3}, jobs)
|
||||
|
||||
workerReg := sqldb.NewWorkerRegistry(nil)
|
||||
err = workerReg.AddWorker(tx, node)
|
||||
fwSvc := sqldb.NewFreeWorkerService(nil)
|
||||
err = fwSvc.AddWorkers(tx, role, nodeAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
wjSvc := sqldb.NewWorkerJobService(nil)
|
||||
err = wjSvc.CreateWorker(tx, role, qdbid, nodeAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = wjSvc.AssignWorkerToJobs(tx, role, qdbid, nodeAddr, job1)
|
||||
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job1)
|
||||
require.NoError(t, err)
|
||||
|
||||
jobs, err = fjSvc.ListJobs(tx, role, qdbid)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, dax.Jobs{job3}, jobs)
|
||||
|
||||
err = fjSvc.MarkJobsAsFree(tx, role, qdbid, dax.Jobs{job1})
|
||||
err = fjSvc.MergeJobs(tx, role, qdbid, dax.Jobs{job1})
|
||||
require.NoError(t, err)
|
||||
|
||||
jobs, err = fjSvc.ListJobs(tx, role, qdbid)
|
||||
|
|
|
|||
|
|
@ -20,25 +20,12 @@ func TestFreeWorkerService(t *testing.T) {
|
|||
}
|
||||
}()
|
||||
|
||||
node1 := &dax.Node{Address: nodeAddr, RoleTypes: dax.AllRoleTypes}
|
||||
node2 := &dax.Node{Address: nodeAddr2, RoleTypes: dax.AllRoleTypes}
|
||||
node3 := &dax.Node{Address: nodeAddr3, RoleTypes: dax.AllRoleTypes}
|
||||
node4 := &dax.Node{Address: nodeAddr4, RoleTypes: dax.AllRoleTypes}
|
||||
node5 := &dax.Node{Address: nodeAddr5, RoleTypes: dax.AllRoleTypes}
|
||||
|
||||
workerReg := sqldb.NewWorkerRegistry(nil)
|
||||
|
||||
// Add some workers.
|
||||
require.NoError(t, workerReg.AddWorker(tx, node1))
|
||||
require.NoError(t, workerReg.AddWorker(tx, node2))
|
||||
require.NoError(t, workerReg.AddWorker(tx, node3))
|
||||
require.NoError(t, workerReg.AddWorker(tx, node4))
|
||||
require.NoError(t, workerReg.AddWorker(tx, node5))
|
||||
|
||||
// Remove one of the workers.
|
||||
require.NoError(t, workerReg.RemoveWorker(tx, node2.Address))
|
||||
|
||||
fwSvc := sqldb.NewFreeWorkerService(nil)
|
||||
err = fwSvc.AddWorkers(tx, role, nodeAddr, nodeAddr2, nodeAddr3, nodeAddr4, nodeAddr5)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fwSvc.RemoveWorker(tx, role, nodeAddr2)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err := fwSvc.ListWorkers(tx, role)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const (
|
|||
nodeAddr5 = "myaddress5"
|
||||
)
|
||||
|
||||
func TestWorkerRegistry(t *testing.T) {
|
||||
func TestNodeService(t *testing.T) {
|
||||
tx, err := SQLTransactor.BeginTx(context.Background(), true)
|
||||
require.NoError(t, err, "getting transaction")
|
||||
|
||||
|
|
@ -29,34 +29,34 @@ func TestWorkerRegistry(t *testing.T) {
|
|||
}
|
||||
}()
|
||||
|
||||
workerReg := sqldb.NewWorkerRegistry(nil)
|
||||
nodeSvc := sqldb.NewNodeService(nil)
|
||||
|
||||
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr, RoleTypes: []dax.RoleType{dax.RoleTypeCompute}})
|
||||
err = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr, RoleTypes: []dax.RoleType{"compute"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
node, err := workerReg.Worker(tx, nodeAddr)
|
||||
node, err := nodeSvc.ReadNode(tx, nodeAddr)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, nodeAddr, node.Address)
|
||||
require.EqualValues(t, 1, len(node.RoleTypes))
|
||||
require.EqualValues(t, "compute", node.RoleTypes[0])
|
||||
|
||||
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr2, RoleTypes: []dax.RoleType{dax.RoleTypeTranslate, dax.RoleTypeCompute}})
|
||||
err = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr2, RoleTypes: []dax.RoleType{"translate", "compute"}})
|
||||
require.NoError(t, err, "create node 2")
|
||||
|
||||
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr3, RoleTypes: []dax.RoleType{dax.RoleTypeCompute}})
|
||||
err = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr3, RoleTypes: []dax.RoleType{"compute"}})
|
||||
require.NoError(t, err, "create node 3")
|
||||
|
||||
nodes, err := workerReg.Workers(tx)
|
||||
nodes, err := nodeSvc.Nodes(tx)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 3, len(nodes))
|
||||
for _, node := range nodes {
|
||||
assert.Contains(t, node.RoleTypes, dax.RoleType("compute"), "node should have compute role but is: %+v", node)
|
||||
}
|
||||
|
||||
err = workerReg.RemoveWorker(tx, nodeAddr2)
|
||||
err = nodeSvc.DeleteNode(tx, nodeAddr2)
|
||||
require.NoError(t, err, "deleting node")
|
||||
|
||||
nodes, err = workerReg.Workers(tx)
|
||||
nodes, err = nodeSvc.Nodes(tx)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, len(nodes))
|
||||
for _, node := range nodes {
|
||||
|
|
|
|||
|
|
@ -40,14 +40,9 @@ func TestWorkerJobService(t *testing.T) {
|
|||
wjSvc := sqldb.NewWorkerJobService(nil)
|
||||
qdbid := dax.QualifiedDatabaseID{OrganizationID: orgID, DatabaseID: dbID}
|
||||
|
||||
node := &dax.Node{
|
||||
Address: nodeAddr,
|
||||
RoleTypes: []dax.RoleType{role},
|
||||
}
|
||||
|
||||
// have to create a free worker before you can create a worker job worker
|
||||
workerReg := sqldb.NewWorkerRegistry(nil)
|
||||
err = workerReg.AddWorker(tx, node)
|
||||
fwSvc := sqldb.NewFreeWorkerService(nil)
|
||||
err = fwSvc.AddWorkers(tx, role, nodeAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = wjSvc.CreateWorker(tx, role, qdbid, nodeAddr)
|
||||
|
|
@ -70,7 +65,7 @@ func TestWorkerJobService(t *testing.T) {
|
|||
err = fjSvc.CreateJobs(tx, role, qdbid, job1, job2, job3)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = wjSvc.AssignWorkerToJobs(tx, role, qdbid, nodeAddr, job1, job2)
|
||||
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job1, job2)
|
||||
require.NoError(t, err)
|
||||
|
||||
jobs, err := wjSvc.ListJobs(tx, role, qdbid, nodeAddr)
|
||||
|
|
@ -90,7 +85,7 @@ func TestWorkerJobService(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, dax.Addresses{nodeAddr}, addrs)
|
||||
|
||||
err = wjSvc.AssignWorkerToJobs(tx, role, qdbid, nodeAddr, job3)
|
||||
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job3)
|
||||
require.NoError(t, err)
|
||||
|
||||
jcs, err := wjSvc.JobCounts(tx, role, qdbid, nodeAddr)
|
||||
|
|
@ -115,7 +110,7 @@ func TestWorkerJobService(t *testing.T) {
|
|||
dk := wjSvc.DatabaseForWorker(tx, nodeAddr)
|
||||
require.EqualValues(t, "db__orgid__blah", dk)
|
||||
|
||||
err = wjSvc.ReleaseWorkers(tx, nodeAddr)
|
||||
err = wjSvc.DeleteWorker(tx, role, qdbid, nodeAddr)
|
||||
require.NoError(t, err)
|
||||
|
||||
addrs, err = wjSvc.ListWorkers(tx, role, qdbid)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -159,16 +159,14 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(0),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 2,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 2,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -196,7 +194,7 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{},
|
||||
ComputeRoles: []dax.ComputeRole{},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 1,
|
||||
Version: 3,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -218,7 +216,7 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{},
|
||||
ComputeRoles: []dax.ComputeRole{},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 1,
|
||||
Version: 4,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -226,32 +224,53 @@ func TestController(t *testing.T) {
|
|||
assert.Equal(t, exp, got)
|
||||
|
||||
// Add more shards.
|
||||
// Because addShards is a helper function which actually adds each shard
|
||||
// one at a time, the controller is actually building separate
|
||||
// directives for each call to IngestShard. In other words, this test is
|
||||
// ensuring that the directive which are sent are what you would get if
|
||||
// you added one shard at a time. So here, we just send in 3 at a time.
|
||||
// We don't want more that one directive per address in the same test
|
||||
// check, otherwise we can't guarantee an order.
|
||||
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3)...)
|
||||
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3, 5, 8)...)
|
||||
|
||||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(1),
|
||||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 5,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(2),
|
||||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 6,
|
||||
},
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(3),
|
||||
Shards: dax.NewShardNums(0, 3),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 3,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 7,
|
||||
},
|
||||
{
|
||||
Address: node1.Address,
|
||||
|
|
@ -259,16 +278,14 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(1),
|
||||
Shards: dax.NewShardNums(1, 5),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 2,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 8,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
|
|
@ -276,56 +293,14 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(2),
|
||||
Shards: dax.NewShardNums(2, 8),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 2,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
||||
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(5, 8)...)
|
||||
|
||||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(5),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 3,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbl0.Key(),
|
||||
Shards: dax.NewShardNums(8),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 3,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 9,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -336,14 +311,11 @@ func TestController(t *testing.T) {
|
|||
tbl1 := daxtest.TestQualifiedTable(t, qdbid, "bar", 0, false)
|
||||
assert.NoError(t, con.CreateTable(ctx, tbl1))
|
||||
|
||||
exp = []*dax.Directive{}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
||||
tbls = append(tbls, tbl1)
|
||||
sort.Sort(tbls)
|
||||
|
||||
// Add more shards.
|
||||
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(3, 5, 8)...)
|
||||
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(3, 5, 8, 13)...)
|
||||
|
||||
exp = []*dax.Directive{
|
||||
{
|
||||
|
|
@ -351,76 +323,80 @@ func TestController(t *testing.T) {
|
|||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(3),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(0, 3),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 4,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 10,
|
||||
},
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(5),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(1, 5),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 4,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 11,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(8),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(2, 8),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 4,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 12,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
require.Equal(t, len(exp), len(got))
|
||||
require.Equal(t, exp, got)
|
||||
|
||||
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(13)...)
|
||||
|
||||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(13),
|
||||
Shards: dax.NewShardNums(3, 13),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(0, 3),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 5,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 13,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -435,18 +411,21 @@ func TestController(t *testing.T) {
|
|||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(3, 13),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(1),
|
||||
Shards: dax.NewShardNums(0, 1, 3),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 6,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 14,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
|
|
@ -455,20 +434,18 @@ func TestController(t *testing.T) {
|
|||
tbls[0],
|
||||
tbls[1],
|
||||
},
|
||||
ComputeRolesAdded: []dax.ComputeRole{
|
||||
ComputeRoles: []dax.ComputeRole{
|
||||
{
|
||||
TableKey: tbls[0].Key(),
|
||||
Shards: dax.NewShardNums(5),
|
||||
Shards: dax.NewShardNums(5, 8),
|
||||
},
|
||||
{
|
||||
TableKey: tbls[1].Key(),
|
||||
Shards: dax.NewShardNums(5),
|
||||
Shards: dax.NewShardNums(2, 5, 8),
|
||||
},
|
||||
},
|
||||
ComputeRolesRemoved: []dax.ComputeRole{},
|
||||
TranslateRolesAdded: []dax.TranslateRole{},
|
||||
TranslateRolesRemoved: []dax.TranslateRole{},
|
||||
Version: 5,
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 15,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -481,7 +458,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -497,7 +474,7 @@ func TestController(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 6,
|
||||
Version: 16,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -544,7 +521,7 @@ func TestController(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 1,
|
||||
Version: 17,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -557,7 +534,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node3.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -573,7 +550,7 @@ func TestController(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 2,
|
||||
Version: 18,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -588,7 +565,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node3.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -604,7 +581,7 @@ func TestController(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 3,
|
||||
Version: 19,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -617,7 +594,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node3.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
},
|
||||
|
|
@ -628,7 +605,7 @@ func TestController(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 4,
|
||||
Version: 20,
|
||||
},
|
||||
}
|
||||
got = director.flush()
|
||||
|
|
@ -713,7 +690,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
|
|
@ -750,7 +727,7 @@ func TestController(t *testing.T) {
|
|||
Tables: []*dax.QualifiedTable{},
|
||||
ComputeRoles: []dax.ComputeRole{},
|
||||
TranslateRoles: []dax.TranslateRole{},
|
||||
Version: 1,
|
||||
Version: 3,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
|
@ -766,7 +743,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
|
|
@ -777,11 +754,11 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(0, 1, 2),
|
||||
},
|
||||
},
|
||||
Version: 3,
|
||||
Version: 4,
|
||||
},
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbl0,
|
||||
},
|
||||
|
|
@ -792,7 +769,7 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(3, 5, 7),
|
||||
},
|
||||
},
|
||||
Version: 2,
|
||||
Version: 5,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
|
|
@ -807,7 +784,7 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(4, 6),
|
||||
},
|
||||
},
|
||||
Version: 1,
|
||||
Version: 6,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
|
@ -826,7 +803,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -842,11 +819,11 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(0, 1, 2),
|
||||
},
|
||||
},
|
||||
Version: 4,
|
||||
Version: 7,
|
||||
},
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -862,11 +839,11 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(3, 5, 7),
|
||||
},
|
||||
},
|
||||
Version: 3,
|
||||
Version: 8,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
tbls[1],
|
||||
|
|
@ -882,7 +859,7 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(4, 6),
|
||||
},
|
||||
},
|
||||
Version: 2,
|
||||
Version: 9,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
|
@ -894,7 +871,7 @@ func TestController(t *testing.T) {
|
|||
exp = []*dax.Directive{
|
||||
{
|
||||
Address: node0.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
},
|
||||
|
|
@ -905,11 +882,11 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(1, 4, 7, 10, 13, 16, 19, 22),
|
||||
},
|
||||
},
|
||||
Version: 5,
|
||||
Version: 10,
|
||||
},
|
||||
{
|
||||
Address: node1.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
},
|
||||
|
|
@ -920,11 +897,11 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(2, 5, 8, 11, 14, 17, 20, 23),
|
||||
},
|
||||
},
|
||||
Version: 4,
|
||||
Version: 11,
|
||||
},
|
||||
{
|
||||
Address: node2.Address,
|
||||
Method: dax.DirectiveMethodFull,
|
||||
Method: dax.DirectiveMethodDiff,
|
||||
Tables: []*dax.QualifiedTable{
|
||||
tbls[0],
|
||||
},
|
||||
|
|
@ -935,7 +912,7 @@ func TestController(t *testing.T) {
|
|||
Partitions: dax.NewPartitionNums(0, 3, 6, 9, 12, 15, 18, 21),
|
||||
},
|
||||
},
|
||||
Version: 3,
|
||||
Version: 12,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, exp, director.flush())
|
||||
|
|
|
|||
36
dax/controller/node.go
Normal file
36
dax/controller/node.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
)
|
||||
|
||||
// NodeService represents a service for managing Nodes.
|
||||
type NodeService interface {
|
||||
CreateNode(dax.Transaction, dax.Address, *dax.Node) error
|
||||
ReadNode(dax.Transaction, dax.Address) (*dax.Node, error)
|
||||
DeleteNode(dax.Transaction, dax.Address) error
|
||||
Nodes(dax.Transaction) ([]*dax.Node, error)
|
||||
}
|
||||
|
||||
// Ensure type implements interface.
|
||||
var _ NodeService = &nopNodeService{}
|
||||
|
||||
// nopNoder is a no-op implementation of the Noder interface.
|
||||
type nopNodeService struct{}
|
||||
|
||||
func NewNopNodeService() *nopNodeService {
|
||||
return &nopNodeService{}
|
||||
}
|
||||
|
||||
func (n *nopNodeService) CreateNode(dax.Transaction, dax.Address, *dax.Node) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopNodeService) ReadNode(dax.Transaction, dax.Address) (*dax.Node, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (n *nopNodeService) DeleteNode(dax.Transaction, dax.Address) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopNodeService) Nodes(dax.Transaction) ([]*dax.Node, error) {
|
||||
return []*dax.Node{}, nil
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
type Config struct {
|
||||
AddressManager dax.AddressManager
|
||||
WorkerRegistry dax.WorkerRegistry
|
||||
NodeService dax.NodeService
|
||||
NodePoller NodePoller
|
||||
PollInterval time.Duration
|
||||
Logger logger.Logger
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type Poller struct {
|
|||
|
||||
addressManager dax.AddressManager
|
||||
|
||||
workerRegistry dax.WorkerRegistry
|
||||
nodeService dax.NodeService
|
||||
|
||||
nodePoller NodePoller
|
||||
pollInterval time.Duration
|
||||
|
|
@ -30,7 +30,7 @@ type Poller struct {
|
|||
func New(cfg Config) *Poller {
|
||||
p := &Poller{
|
||||
addressManager: dax.NewNopAddressManager(),
|
||||
workerRegistry: dax.NewNopWorkerRegistry(),
|
||||
nodeService: dax.NewNopNodeService(),
|
||||
nodePoller: NewNopNodePoller(),
|
||||
pollInterval: time.Second,
|
||||
logger: logger.NopLogger,
|
||||
|
|
@ -40,8 +40,8 @@ func New(cfg Config) *Poller {
|
|||
if cfg.AddressManager != nil {
|
||||
p.addressManager = cfg.AddressManager
|
||||
}
|
||||
if cfg.WorkerRegistry != nil {
|
||||
p.workerRegistry = cfg.WorkerRegistry
|
||||
if cfg.NodeService != nil {
|
||||
p.nodeService = cfg.NodeService
|
||||
}
|
||||
if cfg.NodePoller != nil {
|
||||
p.nodePoller = cfg.NodePoller
|
||||
|
|
@ -57,7 +57,7 @@ func New(cfg Config) *Poller {
|
|||
}
|
||||
|
||||
func (p *Poller) Addresses() []dax.Address {
|
||||
nodes, err := p.workerRegistry.Workers(context.Background())
|
||||
nodes, err := p.nodeService.Nodes(context.Background())
|
||||
if err != nil {
|
||||
p.logger.Errorf("POLLER: unable to get nodes from node service: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import (
|
|||
func TestPoller(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
workerRegistry := newMemWorkerRegistry()
|
||||
nodeService := newMemNodeService()
|
||||
|
||||
// node 1
|
||||
node1 := newMockNode(t, "health", 0)
|
||||
|
|
@ -44,7 +44,7 @@ func TestPoller(t *testing.T) {
|
|||
}
|
||||
|
||||
// manager
|
||||
manager := newMockManager(t, ctx, "deregister-nodes", workerRegistry)
|
||||
manager := newMockManager(t, ctx, "deregister-nodes", nodeService)
|
||||
defer manager.Close()
|
||||
managerAddr := dax.Address(manager.URL())
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ func TestPoller(t *testing.T) {
|
|||
cfg := poller.Config{
|
||||
AddressManager: controllerhttp.NewAddressManager(managerAddr),
|
||||
NodePoller: poller.NewHTTPNodePoller(logger.NopLogger),
|
||||
WorkerRegistry: workerRegistry,
|
||||
NodeService: nodeService,
|
||||
}
|
||||
p := poller.New(cfg)
|
||||
|
||||
|
|
@ -62,9 +62,9 @@ func TestPoller(t *testing.T) {
|
|||
close(done)
|
||||
}()
|
||||
|
||||
// Add workers to workerRegistry so they are available to the poller.
|
||||
workerRegistry.AddWorker(ctx, addr1, daxNode1)
|
||||
workerRegistry.AddWorker(ctx, addr2, daxNode2)
|
||||
// Add nodes to nodeService so they are available to the poller.
|
||||
nodeService.CreateNode(ctx, addr1, daxNode1)
|
||||
nodeService.CreateNode(ctx, addr2, daxNode2)
|
||||
|
||||
go p.Run()
|
||||
defer p.Stop()
|
||||
|
|
@ -83,13 +83,13 @@ type mockManager struct {
|
|||
t *testing.T
|
||||
server *httptest.Server
|
||||
|
||||
workerRegistry dax.WorkerRegistry
|
||||
nodeService dax.NodeService
|
||||
}
|
||||
|
||||
func newMockManager(t *testing.T, ctx context.Context, deregisterPath string, wr dax.WorkerRegistry) *mockManager {
|
||||
func newMockManager(t *testing.T, ctx context.Context, deregisterPath string, nodeService dax.NodeService) *mockManager {
|
||||
mm := &mockManager{
|
||||
t: t,
|
||||
workerRegistry: wr,
|
||||
t: t,
|
||||
nodeService: nodeService,
|
||||
}
|
||||
|
||||
// deregister is a function used in this mock to remove the address from the
|
||||
|
|
@ -97,7 +97,7 @@ func newMockManager(t *testing.T, ctx context.Context, deregisterPath string, wr
|
|||
// the Poller.
|
||||
deregister := func(addrs ...dax.Address) {
|
||||
for _, addr := range addrs {
|
||||
mm.workerRegistry.RemoveWorker(context.Background(), addr)
|
||||
mm.nodeService.DeleteNode(context.Background(), addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,25 +176,25 @@ func (m *mockNode) Close() {
|
|||
}
|
||||
}
|
||||
|
||||
type memWorkerRegistry struct {
|
||||
type memNodeService struct {
|
||||
mu sync.RWMutex
|
||||
addresses map[dax.Address]*dax.Node
|
||||
}
|
||||
|
||||
func newMemWorkerRegistry() *memWorkerRegistry {
|
||||
return &memWorkerRegistry{
|
||||
func newMemNodeService() *memNodeService {
|
||||
return &memNodeService{
|
||||
addresses: make(map[dax.Address]*dax.Node),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memWorkerRegistry) AddWorker(ctx context.Context, addr dax.Address, node *dax.Node) error {
|
||||
func (m *memNodeService) CreateNode(ctx context.Context, addr dax.Address, node *dax.Node) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.addresses[addr] = node
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memWorkerRegistry) Worker(ctx context.Context, addr dax.Address) (*dax.Node, error) {
|
||||
func (m *memNodeService) ReadNode(ctx context.Context, addr dax.Address) (*dax.Node, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
node, ok := m.addresses[addr]
|
||||
|
|
@ -204,14 +204,14 @@ func (m *memWorkerRegistry) Worker(ctx context.Context, addr dax.Address) (*dax.
|
|||
return node, nil
|
||||
}
|
||||
|
||||
func (m *memWorkerRegistry) RemoveWorker(ctx context.Context, addr dax.Address) error {
|
||||
func (m *memNodeService) DeleteNode(ctx context.Context, addr dax.Address) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.addresses, addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memWorkerRegistry) Workers(ctx context.Context) ([]*dax.Node, error) {
|
||||
func (m *memNodeService) Nodes(ctx context.Context) ([]*dax.Node, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func NewErrDatabaseIDInvalid(databaseID dax.DatabaseID) error {
|
|||
func NewErrDatabaseNameInvalid(databaseName dax.DatabaseName) error {
|
||||
return errors.New(
|
||||
ErrCodeDatabaseNameInvalid,
|
||||
fmt.Sprintf("invalid database name %s", databaseName),
|
||||
fmt.Sprintf("database name '%s' is invalid", databaseName),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func NewBalancer(log logger.Logger) *balancer.Balancer {
|
|||
fjs := NewFreeJobService(log)
|
||||
wjs := NewWorkerJobService(log)
|
||||
fws := NewFreeWorkerService(log)
|
||||
ns := NewWorkerRegistry(log)
|
||||
ns := NewNodeService(log)
|
||||
|
||||
return balancer.New(ns, fjs, wjs, fws, schemar, log)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,51 +20,16 @@ type directiveVersion struct {
|
|||
log logger.Logger
|
||||
}
|
||||
|
||||
func (d *directiveVersion) GetCurrent(tx dax.Transaction, addr dax.Address) (uint64, error) {
|
||||
func (d *directiveVersion) Increment(tx dax.Transaction, delta uint64) (uint64, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return 0, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
// table is pre-populated w/ a single record w/ ID=1 during schema migration
|
||||
dv := &models.DirectiveVersion{}
|
||||
err := dt.C.Find(dv, addr)
|
||||
if err == nil {
|
||||
return uint64(dv.Version), nil
|
||||
}
|
||||
|
||||
// If there is not yet a record for address, create one and return 0 as the
|
||||
// "current version".
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
dv.ID = string(addr)
|
||||
if err := dt.C.Create(dv); err != nil {
|
||||
return 0, errors.Wrapf(err, "creating directive_version for address: %s", addr)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
return 0, errors.Wrapf(err, "finding directive_version for address: %s", addr)
|
||||
}
|
||||
|
||||
func (d *directiveVersion) SetNext(tx dax.Transaction, addr dax.Address, current, next uint64) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
dv := &models.DirectiveVersion{}
|
||||
|
||||
// Table is assumed to be pre-populated by a previous call to GetCurrent. We
|
||||
// use the postgres specific "RETURNING" along with `.First()` to ensure
|
||||
// that a record was updated. If no record matches the WHERE clause, then
|
||||
// RETURNING would return a result set with 0 records, which causes
|
||||
// `.First()` to return an error.
|
||||
err := dt.C.RawQuery(`
|
||||
UPDATE directive_versions
|
||||
SET version = ?, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
AND version = ?
|
||||
RETURNING id, version`, next, addr, current).First(dv)
|
||||
err := dt.C.RawQuery("UPDATE directive_versions SET version = version + ? WHERE id = ? RETURNING id, version", delta, 1).First(dv)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "updating directive_version for address: %s", addr)
|
||||
return 0, errors.Wrap(err, "updating directive_version")
|
||||
}
|
||||
return nil
|
||||
return uint64(dv.Version), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
package sqldb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDirectiveVersion(t *testing.T) {
|
||||
t.Run("GetAndSet", func(t *testing.T) {
|
||||
trans, err := sqldb.NewTransactor(sqldb.GetTestConfigRandomDB("directive_version"), logger.StderrLogger) // TODO running migrations takes kind of a long time, consolidate w/ other SQL tests
|
||||
require.NoError(t, err, "connecting")
|
||||
require.NoError(t, trans.Start())
|
||||
|
||||
tx, err := trans.BeginTx(context.Background(), true)
|
||||
require.NoError(t, err, "getting transaction")
|
||||
|
||||
defer func() {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
t.Logf("rolling back: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
addr := dax.Address("address1")
|
||||
|
||||
dvSvc := sqldb.NewDirectiveVersion(nil)
|
||||
|
||||
// Get the current version; this returns 0 because a record for addr
|
||||
// didn't exist and so it was created.
|
||||
n, err := dvSvc.GetCurrent(tx, addr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(0), n)
|
||||
|
||||
// Set next version to n+1 = 1.
|
||||
require.NoError(t, dvSvc.SetNext(tx, addr, n, n+1))
|
||||
|
||||
// Get the version again and make sure we get the 1 that was set.
|
||||
n, err = dvSvc.GetCurrent(tx, addr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), n)
|
||||
|
||||
// Try to set next version with an incorrect current version (999) and
|
||||
// ensure we get an error.
|
||||
require.Error(t, dvSvc.SetNext(tx, addr, 999, n+1))
|
||||
})
|
||||
}
|
||||
|
|
@ -28,38 +28,16 @@ func (fj *freeJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType,
|
|||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
// jobNames is used as input to the "name in (...)" query.
|
||||
jobNames := make([]interface{}, 0, len(job))
|
||||
for i := range job {
|
||||
jobNames = append(jobNames, job[i].Job())
|
||||
}
|
||||
|
||||
// existing will contain the list of jobs which already exist.
|
||||
existing := &models.Jobs{}
|
||||
if err := dt.C.Where("name in (?)", jobNames...).All(existing); err != nil {
|
||||
return errors.Wrap(err, "getting existing jobs")
|
||||
}
|
||||
|
||||
jobs := make(models.Jobs, 0, len(job))
|
||||
for _, j := range job {
|
||||
// Check to be sure this job doesn't already exist.
|
||||
if existing.Contains(j) {
|
||||
continue
|
||||
}
|
||||
jobs = append(jobs, models.Job{
|
||||
jobs := make(models.Jobs, len(job))
|
||||
for i, j := range job {
|
||||
jobs[i] = models.Job{
|
||||
Name: j,
|
||||
Role: roleType,
|
||||
DatabaseID: qdbid.DatabaseID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := dt.C.Create(jobs)
|
||||
return errors.Wrap(err, "creating free jobs")
|
||||
return errors.Wrap(err, "creating jobs")
|
||||
}
|
||||
|
||||
func (fj *freeJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job dax.Job) error {
|
||||
|
|
@ -102,9 +80,8 @@ func (fj *freeJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, qd
|
|||
return djs, nil
|
||||
}
|
||||
|
||||
// MarkJobsAsFree disassociates any worker that was previously assigned to this
|
||||
// job.
|
||||
func (fj *freeJobService) MarkJobsAsFree(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error {
|
||||
// MergeJobs - AFAICT this means "mark these jobs as free"
|
||||
func (fj *freeJobService) MergeJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package sqldb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller/balancer"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/models"
|
||||
|
|
@ -23,6 +21,34 @@ type freeWorkerService struct {
|
|||
log logger.Logger
|
||||
}
|
||||
|
||||
func (fw *freeWorkerService) AddWorkers(tx dax.Transaction, roleType dax.RoleType, addrs ...dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
workers := make(models.Workers, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
workers[i] = models.Worker{
|
||||
Address: addr,
|
||||
Role: roleType,
|
||||
}
|
||||
}
|
||||
|
||||
err := dt.C.Create(workers)
|
||||
return errors.Wrap(err, "creating workers")
|
||||
}
|
||||
|
||||
func (fw *freeWorkerService) RemoveWorker(tx dax.Transaction, roleType dax.RoleType, addr dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
err := dt.C.RawQuery("DELETE from workers where database_id is null and role = ? and address = ?", roleType, addr).Exec()
|
||||
return errors.Wrap(err, "deleting")
|
||||
}
|
||||
|
||||
func (fw *freeWorkerService) PopWorkers(tx dax.Transaction, roleType dax.RoleType, num int) ([]dax.Address, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
|
|
@ -32,8 +58,7 @@ func (fw *freeWorkerService) PopWorkers(tx dax.Transaction, roleType dax.RoleTyp
|
|||
results := make([]struct {
|
||||
Address dax.Address `db:"address"`
|
||||
}, 0, num)
|
||||
sel := fmt.Sprintf("select address from workers where role_%s = true and database_id is NULL limit ?", roleType)
|
||||
err := dt.C.RawQuery(sel, num).All(&results)
|
||||
err := dt.C.RawQuery("select address from workers where role = ? and database_id is NULL limit ?", roleType, num).All(&results)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "querying")
|
||||
}
|
||||
|
|
@ -56,10 +81,9 @@ func (fw *freeWorkerService) ListWorkers(tx dax.Transaction, roleType dax.RoleTy
|
|||
}
|
||||
|
||||
workers := make(models.Workers, 0)
|
||||
where := fmt.Sprintf("role_%s = true and database_id is NULL", roleType)
|
||||
err := dt.C.Select("address").Where(where).Order("address asc").All(&workers)
|
||||
err := dt.C.Select("address").Where("role = ? and database_id is NULL", roleType).Order("address asc").All(&workers)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "querying for free workers")
|
||||
return nil, errors.Wrap(err, "querying for workers")
|
||||
}
|
||||
|
||||
ret := make(dax.Addresses, len(workers))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/errors"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/gobuffalo/pop/v6"
|
||||
)
|
||||
|
|
@ -59,45 +58,39 @@ func NewEmbedMigrator(fs fs.FS, c *pop.Connection, log logger.Logger) (*EmbedMig
|
|||
|
||||
func (fm *EmbedMigrator) findMigrations(runner func(mf pop.Migration, tx *pop.Connection) error) error {
|
||||
return fs.WalkDir(fm.FS, "migrations", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "walking dir")
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
match, err := pop.ParseMigrationFilename(d.Name())
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "unsupported dialect") {
|
||||
fm.log.Warnf("ignoring migration file with %s", err.Error())
|
||||
fmt.Printf("walking path: %s, d: %v, err: %v\n", path, d, err)
|
||||
if !d.IsDir() {
|
||||
match, err := pop.ParseMigrationFilename(d.Name())
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "unsupported dialect") {
|
||||
fm.log.Warnf("ignoring migration file with %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if match == nil {
|
||||
fm.log.Warnf("ignoring file %s because it does not match the migration file pattern", d.Name())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
mf := pop.Migration{
|
||||
Path: path,
|
||||
Version: match.Version,
|
||||
Name: match.Name,
|
||||
DBType: match.DBType,
|
||||
Direction: match.Direction,
|
||||
Type: match.Type,
|
||||
Runner: runner,
|
||||
}
|
||||
switch mf.Direction {
|
||||
case "up":
|
||||
fm.UpMigrations.Migrations = append(fm.UpMigrations.Migrations, mf)
|
||||
case "down":
|
||||
fm.DownMigrations.Migrations = append(fm.DownMigrations.Migrations, mf)
|
||||
default:
|
||||
// the regex only matches `(up|down)` for direction, so a panic here is appropriate
|
||||
panic("got unknown migration direction " + mf.Direction)
|
||||
}
|
||||
}
|
||||
if match == nil {
|
||||
fm.log.Warnf("ignoring file %s because it does not match the migration file pattern", d.Name())
|
||||
return nil
|
||||
}
|
||||
mf := pop.Migration{
|
||||
Path: path,
|
||||
Version: match.Version,
|
||||
Name: match.Name,
|
||||
DBType: match.DBType,
|
||||
Direction: match.Direction,
|
||||
Type: match.Type,
|
||||
Runner: runner,
|
||||
}
|
||||
switch mf.Direction {
|
||||
case "up":
|
||||
fm.UpMigrations.Migrations = append(fm.UpMigrations.Migrations, mf)
|
||||
case "down":
|
||||
fm.DownMigrations.Migrations = append(fm.DownMigrations.Migrations, mf)
|
||||
default:
|
||||
// the regex only matches `(up|down)` for direction, so a panic here is appropriate
|
||||
panic("got unknown migration direction " + mf.Direction)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
116
dax/controller/sqldb/node.go
Normal file
116
dax/controller/sqldb/node.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
package sqldb
|
||||
|
||||
import (
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/models"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ controller.NodeService = (*nodeService)(nil)
|
||||
|
||||
func NewNodeService(log logger.Logger) *nodeService {
|
||||
if log == nil {
|
||||
log = logger.NopLogger
|
||||
}
|
||||
return &nodeService{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type nodeService struct {
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (n *nodeService) CreateNode(tx dax.Transaction, addr dax.Address, node *dax.Node) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
mnode := &models.Node{Address: node.Address}
|
||||
err := dt.C.Create(mnode)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating node")
|
||||
}
|
||||
|
||||
nodeRoles := make(models.NodeRoles, len(node.RoleTypes))
|
||||
mnode.NodeRoles = nodeRoles
|
||||
for i, rt := range node.RoleTypes {
|
||||
mnode.NodeRoles[i] = models.NodeRole{
|
||||
NodeID: mnode.ID,
|
||||
Role: rt,
|
||||
}
|
||||
}
|
||||
err = dt.C.Create(&(mnode.NodeRoles))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating node roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nodeService) ReadNode(tx dax.Transaction, addr dax.Address) (*dax.Node, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
node := &models.Node{}
|
||||
err := dt.C.Eager().Where("address = ?", addr).First(node)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting node")
|
||||
}
|
||||
|
||||
roleTypes := make([]dax.RoleType, len(node.NodeRoles))
|
||||
for i, nr := range node.NodeRoles {
|
||||
roleTypes[i] = nr.Role
|
||||
}
|
||||
|
||||
return &dax.Node{
|
||||
Address: node.Address,
|
||||
RoleTypes: roleTypes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *nodeService) DeleteNode(tx dax.Transaction, addr dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
node := &models.Node{}
|
||||
err := dt.C.Eager().Where("address = ?", addr).First(node)
|
||||
if isNoRowsError(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "finding node")
|
||||
}
|
||||
|
||||
err = dt.C.Destroy(node)
|
||||
return errors.Wrap(err, "destroying node")
|
||||
}
|
||||
|
||||
func (n *nodeService) Nodes(tx dax.Transaction) ([]*dax.Node, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
nodes := []*models.Node{}
|
||||
dt.C.Eager().Order("address asc").All(&nodes)
|
||||
|
||||
ret := make([]*dax.Node, len(nodes))
|
||||
for i, node := range nodes {
|
||||
ret[i] = &dax.Node{
|
||||
Address: node.Address,
|
||||
RoleTypes: make([]dax.RoleType, len(node.NodeRoles)),
|
||||
}
|
||||
for j, nr := range node.NodeRoles {
|
||||
ret[i].RoleTypes[j] = nr.Role
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
featurebase "github.com/featurebasedb/featurebase/v3"
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/models"
|
||||
|
|
@ -35,8 +34,8 @@ func (s *Schemar) CreateDatabase(tx dax.Transaction, qdb *dax.QualifiedDatabase)
|
|||
return schemar.NewErrDatabaseIDInvalid(qdb.ID)
|
||||
}
|
||||
|
||||
// Sanitizing database name
|
||||
if err := featurebase.ValidateName(string(qdb.Name)); err != nil {
|
||||
// Ensure the database name is not blank.
|
||||
if qdb.Name == "" {
|
||||
return schemar.NewErrDatabaseNameInvalid(qdb.Name)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package sqldb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"database/sql"
|
||||
|
||||
|
|
@ -66,14 +67,16 @@ func (t Transactor) Start() error {
|
|||
return errors.Wrap(err, "migrating DB")
|
||||
}
|
||||
|
||||
err := t.RawQuery("INSERT INTO directive_versions (id, version, created_at, updated_at) VALUES (1, 0, '1970-01-01T00:00', '1970-01-01T00:00')").Exec()
|
||||
if err != nil && !strings.Contains(err.Error(), "duplicate key value violates unique constraint") {
|
||||
return errors.Wrap(err, "unexpected error (re)inserting directive_version record")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t Transactor) BeginTx(ctx context.Context, writable bool) (dax.Transaction, error) {
|
||||
cn, err := t.NewTransactionContextOptions(ctx, &sql.TxOptions{
|
||||
Isolation: sql.LevelRepeatableRead,
|
||||
ReadOnly: !writable,
|
||||
})
|
||||
cn, err := t.NewTransactionContextOptions(ctx, &sql.TxOptions{ReadOnly: !writable})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting SQL transaction")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
package sqldb
|
||||
|
||||
import (
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/models"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ controller.WorkerRegistry = (*workerRegistry)(nil)
|
||||
|
||||
func NewWorkerRegistry(log logger.Logger) *workerRegistry {
|
||||
if log == nil {
|
||||
log = logger.NopLogger
|
||||
}
|
||||
return &workerRegistry{
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type workerRegistry struct {
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
func (w *workerRegistry) AddWorker(tx dax.Transaction, node *dax.Node) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
workers := models.Workers{}
|
||||
|
||||
// Determine if a worker for this address already exists. We use `All()`
|
||||
// here instead of `First()` because `First()` returns an error if there's
|
||||
// no match.
|
||||
if err := dt.C.Where("address = ?", node.Address).All(&workers); err != nil {
|
||||
return errors.Wrapf(err, "getting workers by address: %s", node.Address)
|
||||
}
|
||||
|
||||
switch len(workers) {
|
||||
case 0:
|
||||
// Continue on to create.
|
||||
case 1:
|
||||
// Since a worker for this address already exists, just update it and
|
||||
// return.
|
||||
worker := workers[0]
|
||||
for _, roleType := range node.RoleTypes {
|
||||
if err := worker.SetRole(roleType); err != nil {
|
||||
return errors.Wrapf(err, "setting role: %s", roleType)
|
||||
}
|
||||
}
|
||||
return dt.C.Update(worker)
|
||||
default:
|
||||
return errors.Errorf("found more than one worker for address: %s", node.Address)
|
||||
}
|
||||
|
||||
worker := &models.Worker{
|
||||
Address: node.Address,
|
||||
}
|
||||
for _, roleType := range node.RoleTypes {
|
||||
if err := worker.SetRole(roleType); err != nil {
|
||||
return errors.Wrapf(err, "setting role: %s", roleType)
|
||||
}
|
||||
}
|
||||
|
||||
return dt.C.Create(worker)
|
||||
}
|
||||
|
||||
func (w *workerRegistry) Worker(tx dax.Transaction, addr dax.Address) (*dax.Node, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
err := dt.C.Eager().Where("address = ?", addr).First(worker)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "getting worker: %s", addr)
|
||||
}
|
||||
|
||||
return &dax.Node{
|
||||
Address: worker.Address,
|
||||
RoleTypes: workerRoleTypes(worker),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *workerRegistry) RemoveWorker(tx dax.Transaction, addr dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
err := dt.C.Eager().Where("address = ?", addr).First(worker)
|
||||
if isNoRowsError(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "finding worker: %s", addr)
|
||||
}
|
||||
|
||||
err = dt.C.Destroy(worker)
|
||||
return errors.Wrap(err, "destroying worker")
|
||||
}
|
||||
|
||||
func (w *workerRegistry) Workers(tx dax.Transaction) ([]*dax.Node, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
workers := []*models.Worker{}
|
||||
dt.C.Eager().Order("address asc").All(&workers)
|
||||
|
||||
ret := make([]*dax.Node, len(workers))
|
||||
for i, worker := range workers {
|
||||
ret[i] = &dax.Node{
|
||||
Address: worker.Address,
|
||||
RoleTypes: workerRoleTypes(worker),
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func workerRoleTypes(worker *models.Worker) []dax.RoleType {
|
||||
roleTypes := make([]dax.RoleType, 0)
|
||||
if worker.RoleCompute {
|
||||
roleTypes = append(roleTypes, dax.RoleTypeCompute)
|
||||
}
|
||||
if worker.RoleTranslate {
|
||||
roleTypes = append(roleTypes, dax.RoleTypeTranslate)
|
||||
}
|
||||
if worker.RoleQuery {
|
||||
roleTypes = append(roleTypes, dax.RoleTypeQuery)
|
||||
}
|
||||
|
||||
return roleTypes
|
||||
}
|
||||
|
|
@ -24,59 +24,37 @@ type workerJobService struct {
|
|||
log logger.Logger
|
||||
}
|
||||
|
||||
// WorkersJobs returns all the workers for the database along with the jobs
|
||||
// associated to each worker, even if the number of jobs is 0.
|
||||
func (w *workerJobService) WorkersJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) ([]dax.WorkerInfo, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
// First, get all workers for the database.
|
||||
workers := models.Workers{}
|
||||
sql := fmt.Sprintf("role_%s = true and database_id = ?", roleType)
|
||||
err := dt.C.Where(sql, qdbid.DatabaseID).Order("address asc").All(&workers)
|
||||
err := dt.C.Eager().Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Order("address asc").All(&workers)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting workers")
|
||||
}
|
||||
|
||||
// Then, get the jobs for each worker. Ideally, we would do this in a single
|
||||
// sql query, but it wasn't clear how to do an Eager() LeftJoin() where
|
||||
// there is a where clause condition on the right side of the join (in this
|
||||
// case, `jobs.role = ?`).
|
||||
ret := make([]dax.WorkerInfo, len(workers))
|
||||
for i, worker := range workers {
|
||||
ret[i].Address = worker.Address
|
||||
jobs, err := jobsForWorker(dt, &worker, roleType)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting jobs for worker")
|
||||
ret[i].Jobs = make([]dax.Job, len(worker.Jobs))
|
||||
for j, job := range worker.Jobs {
|
||||
ret[i].Jobs[j] = job.Name
|
||||
}
|
||||
ret[i].Jobs = jobs
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func jobsForWorker(dt *DaxTransaction, worker *models.Worker, roleType dax.RoleType) ([]dax.Job, error) {
|
||||
jobs := models.Jobs{}
|
||||
if err := dt.C.Where("worker_id = ? and role = ?", worker.ID, roleType).Order("name asc").All(&jobs); err != nil {
|
||||
return nil, errors.Wrapf(err, "getting jobs for worker: %s", worker.ID)
|
||||
}
|
||||
ret := make([]dax.Job, len(jobs))
|
||||
for i := range jobs {
|
||||
ret[i] = jobs[i].Name
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (w *workerJobService) WorkerCount(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID) (int, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return 0, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
worker := &models.Worker{}
|
||||
sql := fmt.Sprintf("role_%s = true and database_id = ?", roleType)
|
||||
cnt, err := dt.C.Where(sql, qdbid.DatabaseID).Count(worker)
|
||||
cnt, err := dt.C.Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Count(worker)
|
||||
return cnt, errors.Wrap(err, "getting count")
|
||||
}
|
||||
|
||||
|
|
@ -87,8 +65,7 @@ func (w *workerJobService) ListWorkers(tx dax.Transaction, roleType dax.RoleType
|
|||
}
|
||||
|
||||
workers := models.Workers{}
|
||||
sql := fmt.Sprintf("role_%s = true and database_id = ?", roleType)
|
||||
err := dt.C.Select("address").Where(sql, qdbid.DatabaseID).Order("address asc").All(&workers)
|
||||
err := dt.C.Select("address").Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Order("address asc").All(&workers)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting workers")
|
||||
}
|
||||
|
|
@ -108,13 +85,12 @@ func (w *workerJobService) CreateWorker(tx dax.Transaction, roleType dax.RoleTyp
|
|||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
sql := fmt.Sprintf("UPDATE workers SET database_id = ? WHERE role_%s = true and address = ? RETURNING workers.ID", roleType)
|
||||
err := dt.C.RawQuery(sql, qdbid.DatabaseID, addr).First(worker)
|
||||
err := dt.C.RawQuery("UPDATE workers SET database_id = ? WHERE role = ? and address = ? RETURNING workers.ID", qdbid.DatabaseID, roleType, addr).First(worker)
|
||||
|
||||
return errors.Wrap(err, "associating worker to database")
|
||||
}
|
||||
|
||||
func (w *workerJobService) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
func (w *workerJobService) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
|
|
@ -128,17 +104,34 @@ func (w *workerJobService) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Addre
|
|||
return errors.Wrap(err, "updating workers")
|
||||
}
|
||||
|
||||
func (w *workerJobService) AssignWorkerToJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error {
|
||||
func (w *workerJobService) DeleteWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
sql := fmt.Sprintf("address = ? and role_%s = true", roleType)
|
||||
err := dt.C.Where(sql, addr).First(worker)
|
||||
err := dt.C.Where("address = ? and role = ? and database_id = ?", addr, roleType, qdbid.DatabaseID).First(worker)
|
||||
if isNoRowsError(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return errors.Wrap(err, "getting worker")
|
||||
}
|
||||
|
||||
err = dt.C.Destroy(worker)
|
||||
return errors.Wrap(err, "deleting worker")
|
||||
}
|
||||
|
||||
func (w *workerJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
err := dt.C.Where("address = ? and role = ?", addr, roleType).First(worker)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "getting worker: (%s) %s", roleType, addr)
|
||||
return errors.Wrap(err, "getting worker")
|
||||
}
|
||||
|
||||
jobs := models.Jobs{}
|
||||
|
|
@ -147,35 +140,35 @@ func (w *workerJobService) AssignWorkerToJobs(tx dax.Transaction, roleType dax.R
|
|||
return errors.Wrap(err, "updating jobs")
|
||||
}
|
||||
|
||||
// Assign jobs not in "jobs", and therefore didn't get updated by the
|
||||
// previous sql statement.
|
||||
toBeAssigned := jobsNotAssigned(job, jobs, roleType, worker)
|
||||
// create jobs not in "jobs"
|
||||
toCreate := jobsNotUpdated(job, jobs, worker)
|
||||
|
||||
if err := dt.C.Create(toBeAssigned); err != nil {
|
||||
err = dt.C.Create(toCreate)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "creating jobs")
|
||||
}
|
||||
|
||||
return nil
|
||||
return errors.Wrap(err, "creating jobs")
|
||||
}
|
||||
|
||||
func jobsNotAssigned(incomingJobs []dax.Job, assigned models.Jobs, roleType dax.RoleType, worker *models.Worker) (toBeAssigned models.Jobs) {
|
||||
func jobsNotUpdated(incomingJobs []dax.Job, created models.Jobs, worker *models.Worker) (toCreate models.Jobs) {
|
||||
outer:
|
||||
for _, incJob := range incomingJobs {
|
||||
for _, assignedJob := range assigned {
|
||||
if assignedJob.Name == incJob {
|
||||
for _, createdJob := range created {
|
||||
if createdJob.Name == incJob {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
toBeAssigned = append(toBeAssigned,
|
||||
toCreate = append(toCreate,
|
||||
models.Job{
|
||||
Name: incJob,
|
||||
Role: roleType,
|
||||
Role: worker.Role,
|
||||
DatabaseID: dax.DatabaseID(worker.DatabaseID.String),
|
||||
Worker: worker,
|
||||
},
|
||||
)
|
||||
}
|
||||
return toBeAssigned
|
||||
return toCreate
|
||||
}
|
||||
|
||||
func (w *workerJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job dax.Job) error {
|
||||
|
|
@ -185,8 +178,7 @@ func (w *workerJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType,
|
|||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
sql := fmt.Sprintf("role_%s = true and database_id = ? and address = ?", roleType)
|
||||
err := dt.C.Select("id").Where(sql, qdbid.DatabaseID, addr).First(worker)
|
||||
err := dt.C.Select("id").Where("role = ? and database_id = ? and address = ?", roleType, qdbid.DatabaseID, addr).First(worker)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting worker")
|
||||
}
|
||||
|
|
@ -235,7 +227,7 @@ func (w *workerJobService) DeleteJobsForTable(tx dax.Transaction, roleType dax.R
|
|||
return idiffs, errors.Wrap(err, "deleting jobs")
|
||||
}
|
||||
|
||||
func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addrs ...dax.Address) (map[dax.Address]int, error) {
|
||||
func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr ...dax.Address) (map[dax.Address]int, error) {
|
||||
dt, ok := tx.(*DaxTransaction)
|
||||
if !ok {
|
||||
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
|
||||
|
|
@ -246,23 +238,18 @@ func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType,
|
|||
Count int `db:"count"`
|
||||
}{}
|
||||
var err error
|
||||
if len(addrs) == 0 {
|
||||
if len(addr) == 0 {
|
||||
qstring := `select address, count(*) as count
|
||||
from workers w inner join jobs j on j.worker_id = w.id
|
||||
where w.database_id = ? and w.role_%s = true
|
||||
and j.role = ?
|
||||
where w.database_id = ? and w.role = ?
|
||||
group by w.address`
|
||||
sql := fmt.Sprintf(qstring, roleType)
|
||||
err = dt.C.RawQuery(sql, qdbid.DatabaseID, roleType).All(&results)
|
||||
err = dt.C.RawQuery(qstring, qdbid.DatabaseID, roleType).All(&results)
|
||||
} else {
|
||||
qstring := `select address, count(*) as count
|
||||
from workers w inner join jobs j on j.worker_id = w.id
|
||||
where w.database_id = ? and w.role_%s = true
|
||||
and j.role = ?
|
||||
and w.address in (?)
|
||||
where w.address in (?) and w.database_id = ? and w.role = ?
|
||||
group by w.address`
|
||||
sql := fmt.Sprintf(qstring, roleType)
|
||||
err = dt.C.RawQuery(sql, qdbid.DatabaseID, roleType, addrs).All(&results)
|
||||
err = dt.C.RawQuery(qstring, addr, qdbid.DatabaseID, roleType).All(&results)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "querying for jobs")
|
||||
|
|
@ -282,15 +269,20 @@ func (w *workerJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, q
|
|||
}
|
||||
|
||||
worker := &models.Worker{}
|
||||
sql := fmt.Sprintf("role_%s = true and database_id = ? and address = ?", roleType)
|
||||
err := dt.C.Where(sql, qdbid.DatabaseID, addr).First(worker)
|
||||
err := dt.C.Eager().Where("role = ? and database_id = ? and address = ?", roleType, qdbid.DatabaseID, addr).First(worker)
|
||||
if isNoRowsError(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrap(err, "getting worker")
|
||||
}
|
||||
|
||||
return jobsForWorker(dt, worker, roleType)
|
||||
ret := make(dax.Jobs, len(worker.Jobs))
|
||||
// jobs are ordered by "name asc" defined on the worker model.
|
||||
for i, job := range worker.Jobs {
|
||||
ret[i] = job.Name
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (w *workerJobService) DatabaseForWorker(tx dax.Transaction, addr dax.Address) dax.DatabaseKey {
|
||||
|
|
|
|||
|
|
@ -21,19 +21,19 @@ func TestJobsNotUpdated(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
toCreate := jobsNotAssigned(incJobs, created, dax.RoleTypeCompute, &models.Worker{
|
||||
ID: u2,
|
||||
RoleCompute: true,
|
||||
DatabaseID: nulls.NewString("dbid"),
|
||||
toCreate := jobsNotUpdated(incJobs, created, &models.Worker{
|
||||
ID: u2,
|
||||
Role: "compute",
|
||||
DatabaseID: nulls.NewString("dbid"),
|
||||
})
|
||||
|
||||
require.Equal(t, 3, len(toCreate))
|
||||
|
||||
// Test when 0 jobs are updated
|
||||
toCreate = jobsNotAssigned(incJobs, models.Jobs{}, dax.RoleTypeCompute, &models.Worker{
|
||||
ID: u2,
|
||||
RoleCompute: true,
|
||||
DatabaseID: nulls.NewString("dbid"),
|
||||
toCreate = jobsNotUpdated(incJobs, models.Jobs{}, &models.Worker{
|
||||
ID: u2,
|
||||
Role: "compute",
|
||||
DatabaseID: nulls.NewString("dbid"),
|
||||
})
|
||||
|
||||
require.Equal(t, 4, len(toCreate))
|
||||
|
|
|
|||
17
dax/controller/transactor.go
Normal file
17
dax/controller/transactor.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
)
|
||||
|
||||
type Transactor interface {
|
||||
// Start is useful for Transactor implementations which need to establish a
|
||||
// connection. We don't want to do that in the NewImplementation() function;
|
||||
// we want that to happen upon Start().
|
||||
Start() error
|
||||
|
||||
BeginTx(ctx context.Context, writable bool) (dax.Transaction, error)
|
||||
Close() error
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
)
|
||||
|
||||
// WorkerRegistry represents a service for managing Nodes. Note that this
|
||||
// interface mirrors the dax.WorkerRegistry interface, but its methods take
|
||||
// dax.Transactions rather than Contexts. That's because the dax version of this
|
||||
// interface is meant to be a the API boundary, where this is an interface for
|
||||
// use within the Controller.
|
||||
type WorkerRegistry interface {
|
||||
AddWorker(dax.Transaction, *dax.Node) error
|
||||
Worker(dax.Transaction, dax.Address) (*dax.Node, error)
|
||||
RemoveWorker(dax.Transaction, dax.Address) error
|
||||
Workers(dax.Transaction) ([]*dax.Node, error)
|
||||
}
|
||||
|
||||
// Ensure type implements interface.
|
||||
var _ WorkerRegistry = &nopWorkerRegistry{}
|
||||
|
||||
// nopWorkerRegistry is a no-op implementation of the WorkerRegistry interface.
|
||||
type nopWorkerRegistry struct{}
|
||||
|
||||
func NewNopWorkerRegistry() *nopWorkerRegistry {
|
||||
return &nopWorkerRegistry{}
|
||||
}
|
||||
|
||||
func (n *nopWorkerRegistry) AddWorker(dax.Transaction, *dax.Node) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) Worker(dax.Transaction, dax.Address) (*dax.Node, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) RemoveWorker(dax.Transaction, dax.Address) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) Workers(dax.Transaction) ([]*dax.Node, error) {
|
||||
return []*dax.Node{}, nil
|
||||
}
|
||||
265
dax/directive.go
265
dax/directive.go
|
|
@ -1,7 +1,5 @@
|
|||
package dax
|
||||
|
||||
import "sort"
|
||||
|
||||
// Directive contains the instructions, sent from the Controller, which a
|
||||
// compute node is to follow. A Directive is typically JSON-encoded and POSTed
|
||||
// to a compute node's `/directive` endpoint.
|
||||
|
|
@ -18,30 +16,11 @@ type Directive struct {
|
|||
ComputeRoles []ComputeRole `json:"compute-roles"`
|
||||
TranslateRoles []TranslateRole `json:"translate-roles"`
|
||||
|
||||
// The following members are used by DirectiveMethodDiff. They inlude only
|
||||
// those roles which have changed, as opposed to the entire role set for the
|
||||
// worker.
|
||||
ComputeRolesAdded []ComputeRole `json:"compute-roles-added"`
|
||||
ComputeRolesRemoved []ComputeRole `json:"compute-roles-removed"`
|
||||
TranslateRolesAdded []TranslateRole `json:"translate-roles-added"`
|
||||
TranslateRolesRemoved []TranslateRole `json:"translate-roles-removed"`
|
||||
|
||||
Version uint64 `json:"version"`
|
||||
}
|
||||
|
||||
// DirectiveVersion defines how the buildDirective step of the controller gets
|
||||
// the next directive version. It's important that the two methods on this
|
||||
// interface are not consolidated into a single step, because we use each method
|
||||
// as a sort of lock/unlock to ensure that only one directive (per address) is
|
||||
// built at a time. Since we always to the `GetCurrent()` call at the beginning
|
||||
// of buildDirective, if two directives are being build for the same address
|
||||
// concurrently, then when one of the calls `SetNext()`, the RepeatableRead
|
||||
// isolation level enforced on the transaction will cause the latest call to
|
||||
// fail since the value of version will have changed since it was first read at
|
||||
// the beginning of its transaction.
|
||||
type DirectiveVersion interface {
|
||||
GetCurrent(tx Transaction, addr Address) (uint64, error)
|
||||
SetNext(tx Transaction, addr Address, current, next uint64) error
|
||||
Increment(tx Transaction, delta uint64) (uint64, error)
|
||||
}
|
||||
|
||||
// DirectiveMethod is used to tell the compute node how it should handle the
|
||||
|
|
@ -49,15 +28,8 @@ type DirectiveVersion interface {
|
|||
type DirectiveMethod string
|
||||
|
||||
const (
|
||||
// DirectiveMethodFull tells the compute node consider the Directive as the
|
||||
// full, complete state to which it should adhere. It should diff the
|
||||
// Directive with its local, cached Directive and only apply the
|
||||
// differences.
|
||||
DirectiveMethodFull DirectiveMethod = "full"
|
||||
|
||||
// DirectiveMethodFull includes only diffs. The compute node should keep
|
||||
// everything about its existing state the same, and just apply the diffs in
|
||||
// the Directive.
|
||||
// DirectiveMethodDiff tells the compute node to diff the Directive with its
|
||||
// local, cached Directive and only apply the differences.
|
||||
DirectiveMethodDiff DirectiveMethod = "diff"
|
||||
|
||||
// DirectiveMethodReset tells the compute node to delete all of its existing
|
||||
|
|
@ -119,23 +91,21 @@ func (d *Directive) ComputeShardsMap() map[TableKey]ShardNums {
|
|||
return m
|
||||
}
|
||||
|
||||
// computeShardsMapOfMaps returns a map of TableKey to a map of ShardNum in
|
||||
// order to support adding and removing shards as distinct values. This map can
|
||||
// then be converted back to a slice of ShardNum.
|
||||
func (d *Directive) computeShardsMapOfMaps() map[TableKey]map[ShardNum]struct{} {
|
||||
m := make(map[TableKey]map[ShardNum]struct{})
|
||||
if d == nil || d.ComputeRoles == nil {
|
||||
return m
|
||||
// TranslatePartitions returns the list of partitions, for the given table, for
|
||||
// which this translate node is responsible. It assumes that the Directive does
|
||||
// not contain more than one TranslateRole for the same table; in that case, we
|
||||
// would need to return the union of Shards.
|
||||
func (d *Directive) TranslatePartitions(tbl TableKey) PartitionNums {
|
||||
if d == nil || d.TranslateRoles == nil {
|
||||
return PartitionNums{}
|
||||
}
|
||||
|
||||
for _, cr := range d.ComputeRoles {
|
||||
m[cr.TableKey] = make(map[ShardNum]struct{})
|
||||
for _, shardNum := range cr.Shards {
|
||||
m[cr.TableKey][shardNum] = struct{}{}
|
||||
for _, tr := range d.TranslateRoles {
|
||||
if tr.TableKey == tbl {
|
||||
return tr.Partitions
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
return PartitionNums{}
|
||||
}
|
||||
|
||||
// TranslatePartitionsMap returns a map of table to partitions. It assumes that
|
||||
|
|
@ -160,53 +130,6 @@ func (d *Directive) TranslatePartitionsMap() map[TableKey]PartitionNums {
|
|||
return m
|
||||
}
|
||||
|
||||
// translatePartitionsMapOfMaps returns a map of TableKey to a map of
|
||||
// PartitionNum in order to support adding and removing partitions as distinct
|
||||
// values. This map can then be converted back to a slice of PartitionNum.
|
||||
func (d *Directive) translatePartitionsMapOfMaps() map[TableKey]map[PartitionNum]struct{} {
|
||||
m := make(map[TableKey]map[PartitionNum]struct{})
|
||||
if d == nil || d.TranslateRoles == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
for _, tr := range d.TranslateRoles {
|
||||
// Since we added FieldVersions to the TranslateRole, it's possible for
|
||||
// a TranslateRole to have an empty Partitions list. In that case, we
|
||||
// want to exclude that from the map.
|
||||
if len(tr.Partitions) == 0 {
|
||||
continue
|
||||
}
|
||||
m[tr.TableKey] = make(map[PartitionNum]struct{})
|
||||
for _, partitionNum := range tr.Partitions {
|
||||
m[tr.TableKey][partitionNum] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// translateFieldsMapOfMaps returns a map of TableKey to a map of FieldName in
|
||||
// order to support adding and removing fields as distinct values. This map can
|
||||
// then be converted back to a slice of FieldName.
|
||||
func (d *Directive) translateFieldsMapOfMaps() map[TableKey]map[FieldName]struct{} {
|
||||
m := make(map[TableKey]map[FieldName]struct{})
|
||||
if d == nil || d.TranslateRoles == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
for _, tr := range d.TranslateRoles {
|
||||
if len(tr.Fields) == 0 {
|
||||
continue
|
||||
}
|
||||
m[tr.TableKey] = make(map[FieldName]struct{})
|
||||
for _, fname := range tr.Fields {
|
||||
m[tr.TableKey][fname] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// TranslateFieldsMap returns a map of table to fields. It assumes that
|
||||
// the Directive does not contain more than one TranslateRole for the same
|
||||
// table; in that case, we would need to return the union of FieldValues.
|
||||
|
|
@ -248,167 +171,9 @@ func (d *Directive) IsEmpty() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// Copy returns a copy of Directive.
|
||||
func (d *Directive) Copy() *Directive {
|
||||
ret := &Directive{
|
||||
Address: d.Address,
|
||||
Method: d.Method,
|
||||
Version: d.Version,
|
||||
}
|
||||
ret.Tables = append(ret.Tables, d.Tables...)
|
||||
ret.ComputeRoles = append(ret.ComputeRoles, d.ComputeRoles...)
|
||||
ret.TranslateRoles = append(ret.TranslateRoles, d.TranslateRoles...)
|
||||
// We intenionally do not copy the `Added` and `Removed` members because
|
||||
// those are not necessary to keep in the cached Directive (which just needs
|
||||
// to include the full Directive); they are only required when sending the
|
||||
// diff Directive.
|
||||
return ret
|
||||
}
|
||||
|
||||
// ApplyDiff applies the diffs specified in diff to d.
|
||||
func (d *Directive) ApplyDiff(diff *Directive) *Directive {
|
||||
// Add any tables which are included in diff but not in d. We don't remove
|
||||
// tables based on a diff.
|
||||
for _, qtbl := range diff.Tables {
|
||||
if t, _ := d.Table(qtbl.QualifiedID()); t == nil {
|
||||
d.Tables = append(d.Tables, qtbl)
|
||||
}
|
||||
}
|
||||
|
||||
// cmap is a map of map used to apply the directive diffs. We will convert
|
||||
// the final map to the ComputeRoles member in the returned Directive.
|
||||
cmap := d.computeShardsMapOfMaps()
|
||||
|
||||
// Handle ComputeRolesAdded
|
||||
for _, crole := range diff.ComputeRolesAdded {
|
||||
if _, ok := cmap[crole.TableKey]; !ok {
|
||||
cmap[crole.TableKey] = make(map[ShardNum]struct{})
|
||||
}
|
||||
for _, shardNum := range crole.Shards {
|
||||
cmap[crole.TableKey][shardNum] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ComputeRolesRemoved
|
||||
for _, crole := range diff.ComputeRolesRemoved {
|
||||
if _, ok := cmap[crole.TableKey]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, shardNum := range crole.Shards {
|
||||
delete(cmap[crole.TableKey], shardNum)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert cmap back to d.ComputeRoles.
|
||||
croles := make([]ComputeRole, 0, len(cmap))
|
||||
for tkey, smap := range cmap {
|
||||
shards := make([]ShardNum, 0, len(smap))
|
||||
for s := range smap {
|
||||
shards = append(shards, s)
|
||||
}
|
||||
sort.Slice(shards, func(i, j int) bool { return shards[i] < shards[j] })
|
||||
croles = append(croles, ComputeRole{
|
||||
TableKey: tkey,
|
||||
Shards: shards,
|
||||
})
|
||||
}
|
||||
// Sort croles by table.
|
||||
sort.Slice(croles, func(i, j int) bool { return croles[i].TableKey < croles[j].TableKey })
|
||||
d.ComputeRoles = croles
|
||||
|
||||
// tmap is a map of map used to apply the directive diffs. We will convert
|
||||
// the final map to the TranslateRoles member in the returned Directive.
|
||||
tmap := d.translatePartitionsMapOfMaps()
|
||||
|
||||
// tmapf is a map of map, specific to translate fields, used to apply the
|
||||
// directive diffs. We will convert the final map to the TranslateRoles
|
||||
// member in the returned Directive.
|
||||
tmapf := d.translateFieldsMapOfMaps()
|
||||
|
||||
// Handle TransateRolesAdded
|
||||
for _, trole := range diff.TranslateRolesAdded {
|
||||
if len(trole.Fields) > 0 {
|
||||
// Fields.
|
||||
if _, ok := tmapf[trole.TableKey]; !ok {
|
||||
tmapf[trole.TableKey] = make(map[FieldName]struct{})
|
||||
}
|
||||
for _, fname := range trole.Fields {
|
||||
tmapf[trole.TableKey][fname] = struct{}{}
|
||||
}
|
||||
} else {
|
||||
// Partitions.
|
||||
if _, ok := tmap[trole.TableKey]; !ok {
|
||||
tmap[trole.TableKey] = make(map[PartitionNum]struct{})
|
||||
}
|
||||
for _, partitionNum := range trole.Partitions {
|
||||
tmap[trole.TableKey][partitionNum] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle TranslateRolesRemoved
|
||||
for _, trole := range diff.TranslateRolesRemoved {
|
||||
if len(trole.Fields) > 0 {
|
||||
// Fields.
|
||||
if _, ok := tmapf[trole.TableKey]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, fname := range trole.Fields {
|
||||
delete(tmapf[trole.TableKey], fname)
|
||||
}
|
||||
} else {
|
||||
// Partitions.
|
||||
if _, ok := tmap[trole.TableKey]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, partitionNum := range trole.Partitions {
|
||||
delete(tmap[trole.TableKey], partitionNum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tmap back to d.TranslateRoles.
|
||||
troles := make([]TranslateRole, 0, len(tmap)+len(tmapf))
|
||||
for tkey, pmap := range tmap {
|
||||
partitions := make([]PartitionNum, 0, len(pmap))
|
||||
for p := range pmap {
|
||||
partitions = append(partitions, p)
|
||||
}
|
||||
sort.Slice(partitions, func(i, j int) bool { return partitions[i] < partitions[j] })
|
||||
troles = append(troles, TranslateRole{
|
||||
TableKey: tkey,
|
||||
Partitions: partitions,
|
||||
})
|
||||
}
|
||||
for tkey, fmap := range tmapf {
|
||||
fields := make([]FieldName, 0, len(fmap))
|
||||
for f := range fmap {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool { return fields[i] < fields[j] })
|
||||
troles = append(troles, TranslateRole{
|
||||
TableKey: tkey,
|
||||
Fields: fields,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort troles by table.
|
||||
sort.Slice(troles, func(i, j int) bool { return troles[i].TableKey < troles[j].TableKey })
|
||||
d.TranslateRoles = troles
|
||||
|
||||
// It doesn't really matter that we set method on the directive to be
|
||||
// cached, but we do it just for informational purposes.
|
||||
d.Method = diff.Method
|
||||
|
||||
// Finally, be sure to use the incoming version, not the version from d.
|
||||
d.Version = diff.Version
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Directives is a sortable slice of Directive.
|
||||
type Directives []*Directive
|
||||
|
||||
func (d Directives) Len() int { return len(d) }
|
||||
func (d Directives) Less(i, j int) bool { return d[i].Address < d[j].Address }
|
||||
func (d Directives) Less(i, j int) bool { return d[i].Version < d[j].Version }
|
||||
func (d Directives) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
||||
|
|
|
|||
32
dax/directive_version_test.go
Normal file
32
dax/directive_version_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package dax_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDirectiveVersion(t *testing.T) {
|
||||
trans, err := sqldb.NewTransactor(sqldb.GetTestConfigRandomDB("directive_version"), logger.StderrLogger) // TODO running migrations takes kind of a long time, consolidate w/ other SQL tests
|
||||
require.NoError(t, err, "connecting")
|
||||
require.NoError(t, trans.Start())
|
||||
|
||||
tx, err := trans.BeginTx(context.Background(), true)
|
||||
require.NoError(t, err, "getting transaction")
|
||||
|
||||
defer func() {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
t.Logf("rolling back: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
dvSvc := sqldb.NewDirectiveVersion(nil)
|
||||
|
||||
n, err := dvSvc.Increment(tx, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint64(1), n)
|
||||
}
|
||||
|
|
@ -1,9 +1,4 @@
|
|||
drop_table("organizations")
|
||||
drop_table("databases")
|
||||
drop_table("tables")
|
||||
drop_table("columns")
|
||||
drop_table("nodes")
|
||||
drop_table("node_roles")
|
||||
drop_table("workers")
|
||||
drop_table("jobs")
|
||||
drop_table("directive_versions")
|
||||
drop_table("tables")
|
||||
drop_table("databases")
|
||||
drop_table("organizations")
|
||||
|
|
@ -43,42 +43,40 @@ create_table("columns") {
|
|||
}
|
||||
|
||||
create_table("nodes") {
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("address", "string")
|
||||
t.Timestamps()
|
||||
}
|
||||
|
||||
create_table("node_roles") {
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("node_id", "uuid")
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("node_id", "uuid")
|
||||
t.Column("role", "string")
|
||||
t.ForeignKey("node_id", {"nodes": ["id"]}, {"on_delete": "cascade"})
|
||||
t.Timestamps()
|
||||
}
|
||||
|
||||
create_table("workers") {
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("address", "string")
|
||||
t.Column("role", "string")
|
||||
t.Column("database_id", "string", {"null": true})
|
||||
t.Column("database_id", "string", {"null": true})
|
||||
t.ForeignKey("database_id", {"databases": ["id"]}, {"null": true})
|
||||
}
|
||||
|
||||
create_table("jobs") {
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("id", "uuid", {primary: true})
|
||||
t.Column("name", "string")
|
||||
t.Column("role", "string")
|
||||
t.Column("worker_id", "uuid", {"null": true})
|
||||
t.Column("worker_id", "uuid", {"null": true})
|
||||
t.ForeignKey("worker_id", {"workers": ["id"]}, {"null": true})
|
||||
t.Column("database_id", "string")
|
||||
t.Column("database_id", "string")
|
||||
t.ForeignKey("database_id", {"databases": ["id"]}, {"on_delete": "cascade"})
|
||||
t.Timestamps()
|
||||
}
|
||||
|
||||
add_index("jobs", ["database_id", "name"], {"unique": true})
|
||||
|
||||
create_table("directive_versions") {
|
||||
t.Column("id", "int", {primary: true})
|
||||
t.Column("id", "int", {primary: true})
|
||||
t.Column("version", "int")
|
||||
t.Timestamps()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
create_table("directive_versions_tmp") {
|
||||
t.Column("id", "string", {primary: true})
|
||||
t.Column("version", "int")
|
||||
t.Timestamps()
|
||||
}
|
||||
|
||||
sql("insert into directive_versions_tmp (id, version, created_at, updated_at) select address, 0, created_at, updated_at from workers where role = 'compute';")
|
||||
|
||||
sql("update directive_versions_tmp set version = (select version from directive_versions where id = 1);")
|
||||
|
||||
drop_table("directive_versions")
|
||||
|
||||
rename_table("directive_versions_tmp", "directive_versions")
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
add_column("workers", "role_compute", "bool", {"default": false})
|
||||
add_column("workers", "role_translate", "bool", {"default": false})
|
||||
add_column("workers", "role_query", "bool", {"default": false})
|
||||
|
||||
sql("update jobs set worker_id = wc.id from workers wc inner join workers wt on wc.address = wt.address and wc.database_id = wt.database_id and wc.role = 'compute' and wt.role = 'translate' where worker_id = wt.id;")
|
||||
|
||||
sql("update workers set role_compute = true where role = 'compute';")
|
||||
|
||||
sql("update workers set role_translate = true from workers wt where workers.address = wt.address and workers.role = 'compute' and wt.role = 'translate';")
|
||||
|
||||
sql("delete from workers where role = 'translate';")
|
||||
|
||||
drop_column("workers", "role")
|
||||
|
||||
drop_table("node_roles")
|
||||
drop_table("nodes")
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
|
||||
// DirectiveVersion holds what version the current directive is
|
||||
type DirectiveVersion struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ID int `json:"id" db:"id"`
|
||||
Version int `json:"version" db:"version"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
|
|
|
|||
|
|
@ -39,16 +39,6 @@ func (t Jobs) String() string {
|
|||
return string(jt)
|
||||
}
|
||||
|
||||
// Contains returns true if j is in Jobs.
|
||||
func (t Jobs) Contains(j dax.Job) bool {
|
||||
for i := range t {
|
||||
if t[i].Name == j {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
|
||||
// This method is not required and may be deleted.
|
||||
func (t *Job) Validate(tx *pop.Connection) (*validate.Errors, error) {
|
||||
|
|
|
|||
56
dax/models/node.go
Normal file
56
dax/models/node.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/gobuffalo/pop/v6"
|
||||
"github.com/gobuffalo/validate/v3"
|
||||
"github.com/gobuffalo/validate/v3/validators"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// Node represents a host or server that is available to work on jobs.
|
||||
type Node struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Address dax.Address `json:"address" db:"address"`
|
||||
NodeRoles NodeRoles `json:"node_roles" has_many:"node_roles" order_by:"created_at asc"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// String is not required by pop and may be deleted
|
||||
func (t *Node) String() string {
|
||||
jt, _ := json.MarshalIndent(t, " ", " ") //nolint:errchkjson
|
||||
return string(jt)
|
||||
}
|
||||
|
||||
// Nodes is not required by pop and may be deleted
|
||||
type Nodes []*Node
|
||||
|
||||
// String is not required by pop and may be deleted
|
||||
func (t Nodes) String() string {
|
||||
jt, _ := json.MarshalIndent(t, " ", " ") //nolint:errchkjson
|
||||
return string(jt)
|
||||
}
|
||||
|
||||
// Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method.
|
||||
// This method is not required and may be deleted.
|
||||
func (t *Node) Validate(tx *pop.Connection) (*validate.Errors, error) {
|
||||
return validate.Validate(
|
||||
&validators.StringIsPresent{Field: string(t.Address), Name: "Address"},
|
||||
), nil
|
||||
}
|
||||
|
||||
// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
|
||||
// This method is not required and may be deleted.
|
||||
func (t *Node) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
|
||||
return validate.NewErrors(), nil
|
||||
}
|
||||
|
||||
// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
|
||||
// This method is not required and may be deleted.
|
||||
func (t *Node) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
|
||||
return validate.NewErrors(), nil
|
||||
}
|
||||
31
dax/models/node_roles.go
Normal file
31
dax/models/node_roles.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// NodeRole holds information about what types of jobs (roles) each node can perform.
|
||||
type NodeRole struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
NodeID uuid.UUID `json:"node_id" db:"node_id"`
|
||||
Role dax.RoleType `json:"role" db:"role"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// String is not required by pop and may be deleted
|
||||
func (t *NodeRole) String() string {
|
||||
jt, _ := json.MarshalIndent(t, " ", " ") //nolint:errchkjson
|
||||
return string(jt)
|
||||
}
|
||||
|
||||
type NodeRoles []NodeRole
|
||||
|
||||
func (t NodeRoles) String() string {
|
||||
jt, _ := json.MarshalIndent(t, " ", " ") //nolint:errchkjson
|
||||
return string(jt)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/errors"
|
||||
"github.com/gobuffalo/nulls"
|
||||
"github.com/gobuffalo/pop/v6"
|
||||
"github.com/gobuffalo/validate/v3"
|
||||
|
|
@ -16,15 +15,13 @@ import (
|
|||
// Worker is a node plus a role that gets assigned to a database and
|
||||
// can be assigned jobs for that database.
|
||||
type Worker struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Address dax.Address `json:"address" db:"address"`
|
||||
DatabaseID nulls.String `json:"database_id" db:"database_id"` // this can be empty which means the worker is unassigned
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
Jobs Jobs `json:"jobs" has_many:"jobs" order_by:"name asc"`
|
||||
RoleCompute bool `json:"role_compute" db:"role_compute"`
|
||||
RoleTranslate bool `json:"role_translate" db:"role_translate"`
|
||||
RoleQuery bool `json:"role_query" db:"role_query"`
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
Address dax.Address `json:"address" db:"address"`
|
||||
Role dax.RoleType `json:"role" db:"role"`
|
||||
DatabaseID nulls.String `json:"database_id" db:"database_id"` // this can be empty which means the worker is unassigned
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
Jobs Jobs `json:"jobs" has_many:"jobs" order_by:"name asc"`
|
||||
}
|
||||
|
||||
// String is not required by pop and may be deleted
|
||||
|
|
@ -33,23 +30,6 @@ func (t *Worker) String() string {
|
|||
return string(jt)
|
||||
}
|
||||
|
||||
// SetRole applies a dax.RoleType to one of the boolean fields on the Worker
|
||||
// model. It returns an error if the model does not support that role type.
|
||||
func (t *Worker) SetRole(role dax.RoleType) error {
|
||||
switch role {
|
||||
case dax.RoleTypeCompute:
|
||||
t.RoleCompute = true
|
||||
case dax.RoleTypeTranslate:
|
||||
t.RoleTranslate = true
|
||||
case dax.RoleTypeQuery:
|
||||
t.RoleQuery = true
|
||||
default:
|
||||
errors.Errorf("invalid role type for worker: %s", role)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Workers is not required by pop and may be deleted
|
||||
type Workers []Worker
|
||||
|
||||
|
|
|
|||
|
|
@ -53,34 +53,34 @@ type AssignedNode struct {
|
|||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
// WorkerRegistry represents a service for managing Workers.
|
||||
type WorkerRegistry interface {
|
||||
AddWorker(context.Context, Address, *Node) error
|
||||
Worker(context.Context, Address) (*Node, error)
|
||||
RemoveWorker(context.Context, Address) error
|
||||
Workers(context.Context) ([]*Node, error)
|
||||
// NodeService represents a service for managing Nodes.
|
||||
type NodeService interface {
|
||||
CreateNode(context.Context, Address, *Node) error
|
||||
ReadNode(context.Context, Address) (*Node, error)
|
||||
DeleteNode(context.Context, Address) error
|
||||
Nodes(context.Context) ([]*Node, error)
|
||||
}
|
||||
|
||||
// Ensure type implements interface.
|
||||
var _ WorkerRegistry = &nopWorkerRegistry{}
|
||||
var _ NodeService = &nopNodeService{}
|
||||
|
||||
// nopWorkerRegistry is a no-op implementation of the WorkerRegistry interface.
|
||||
type nopWorkerRegistry struct{}
|
||||
// nopNoder is a no-op implementation of the Noder interface.
|
||||
type nopNodeService struct{}
|
||||
|
||||
func NewNopWorkerRegistry() *nopWorkerRegistry {
|
||||
return &nopWorkerRegistry{}
|
||||
func NewNopNodeService() *nopNodeService {
|
||||
return &nopNodeService{}
|
||||
}
|
||||
|
||||
func (n *nopWorkerRegistry) AddWorker(context.Context, Address, *Node) error {
|
||||
func (n *nopNodeService) CreateNode(context.Context, Address, *Node) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) Worker(context.Context, Address) (*Node, error) {
|
||||
func (n *nopNodeService) ReadNode(context.Context, Address) (*Node, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) RemoveWorker(context.Context, Address) error {
|
||||
func (n *nopNodeService) DeleteNode(context.Context, Address) error {
|
||||
return nil
|
||||
}
|
||||
func (n *nopWorkerRegistry) Workers(context.Context) ([]*Node, error) {
|
||||
func (n *nopNodeService) Nodes(context.Context) ([]*Node, error) {
|
||||
return []*Node{}, nil
|
||||
}
|
||||
|
||||
|
|
@ -894,297 +894,145 @@ func (o *orchestrator) executeMax(ctx context.Context, tableKeyer dax.TableKeyer
|
|||
return other, nil
|
||||
}
|
||||
|
||||
// executePercentile executes a Percentile() call. This logic is mirrored from
|
||||
// featurebase executor, but we should probably replace it with a smarter algorithm.
|
||||
func (o *orchestrator) executePercentile(ctx context.Context, tableKeyer dax.TableKeyer, c *pql.Call, shards []uint64, opt *featurebase.ExecOptions) (_ interface{}, err error) {
|
||||
// TODO(jaffee) fix this... valcountize assumes access to field details like base
|
||||
// executePercentile executes a Percentile() call.
|
||||
func (o *orchestrator) executePercentile(ctx context.Context, tableKeyer dax.TableKeyer, c *pql.Call, shards []uint64, opt *featurebase.ExecOptions) (_ featurebase.ValCount, err error) {
|
||||
span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executePercentile")
|
||||
defer span.Finish()
|
||||
|
||||
// get nth
|
||||
var nthFloat float64
|
||||
nthArg := c.Args["nth"]
|
||||
nthArg, ok := c.Args["nth"]
|
||||
if !ok {
|
||||
return featurebase.ValCount{}, errors.New(errors.ErrUncoded, "Percentile(): nth required")
|
||||
}
|
||||
switch nthArg := nthArg.(type) {
|
||||
case pql.Decimal:
|
||||
nthFloat = nthArg.Float64()
|
||||
case int64:
|
||||
nthFloat = float64(nthArg)
|
||||
case nil:
|
||||
return nil, errors.New(errors.ErrUncoded, "Percentile(): nth required")
|
||||
default:
|
||||
return nil, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
|
||||
return featurebase.ValCount{}, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
|
||||
}
|
||||
if nthFloat < 0 || nthFloat > 100.0 {
|
||||
return nil, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
|
||||
return featurebase.ValCount{}, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
|
||||
}
|
||||
|
||||
// get field
|
||||
fieldName, err := c.FirstStringArg("field", "_field")
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.ErrUncoded, "Percentile(): field required")
|
||||
return featurebase.ValCount{}, errors.New(errors.ErrUncoded, "Percentile(): field required")
|
||||
}
|
||||
field, err := o.schemaFieldInfo(ctx, tableKeyer, fieldName)
|
||||
if err != nil {
|
||||
return nil, ErrFieldNotFound
|
||||
return featurebase.ValCount{}, ErrFieldNotFound
|
||||
}
|
||||
|
||||
// filter call for min & max
|
||||
var filterCall *pql.Call
|
||||
|
||||
// We want to know the total number of values, so that when we check
|
||||
// for values <X, or >X, we are also able to infer the number of values
|
||||
// equal to X.
|
||||
var totalCountCall *pql.Call
|
||||
// check if filter provided
|
||||
if filterArg, ok := c.Args["filter"].(*pql.Call); ok && filterArg != nil {
|
||||
// You could supply a filter like `Not(x=3)` which would yield values
|
||||
// which exist in the database but are null in this field, we don't
|
||||
// want that.
|
||||
filterCall = filterArg
|
||||
totalCountCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Intersect",
|
||||
Children: []*pql.Call{
|
||||
filterCall,
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &pql.Condition{
|
||||
Op: pql.NEQ,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// request a count of IS NOT NULL, aka Row(field!=null). We care about
|
||||
// the actual number of results that should exist.
|
||||
totalCountCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &pql.Condition{
|
||||
Op: pql.NEQ,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
// total values matched by the filter (if it exists) or that aren't null
|
||||
totalCountInterface, err := o.executeCall(ctx, tableKeyer, totalCountCall, shards, opt)
|
||||
totalCount, ok := totalCountInterface.(uint64)
|
||||
if !ok || totalCount == 0 {
|
||||
// it's not an error, but the median of nothing is NULL.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We have totalCount values. If nth is 50, we want half the values to be
|
||||
// above us, and half below us. So for instance, if we have 6 values, we want
|
||||
// 3 above us, and 3 below us. For odd numbers, we can round these *both*
|
||||
// down -- for 7 values, we'd want 3 higher, and 3 lower.
|
||||
desiredLess := uint64((float64(totalCount) * nthFloat) / 100.0)
|
||||
desiredGreater := uint64((float64(totalCount) * (100 - nthFloat)) / 100.0)
|
||||
|
||||
// get min
|
||||
var minVal featurebase.ValCount
|
||||
|
||||
if desiredGreater != 0 {
|
||||
q, err := pql.ParseString(fmt.Sprintf(`Min(field="%s")`, fieldName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing max call for Percentile")
|
||||
}
|
||||
minCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
minCall.Children = append(minCall.Children, filterCall)
|
||||
}
|
||||
minVal, err = o.executeMin(ctx, tableKeyer, minCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Min call for Percentile")
|
||||
}
|
||||
|
||||
if desiredLess == 0 {
|
||||
if minVal.DecimalVal != nil {
|
||||
minVal.FloatVal = minVal.DecimalVal.Float64()
|
||||
}
|
||||
return minVal, nil
|
||||
}
|
||||
q, _ := pql.ParseString(fmt.Sprintf(`Min(field="%s")`, fieldName))
|
||||
minCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
minCall.Children = append(minCall.Children, filterCall)
|
||||
}
|
||||
minVal, err := o.executeMin(ctx, tableKeyer, minCall, shards, opt)
|
||||
if err != nil {
|
||||
return featurebase.ValCount{}, errors.Wrap(err, "executing Min call for Percentile")
|
||||
}
|
||||
if nthFloat == 0.0 {
|
||||
return minVal, nil
|
||||
}
|
||||
|
||||
// get max
|
||||
q, err := pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing max call for Percentile")
|
||||
}
|
||||
q, _ = pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
|
||||
maxCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
maxCall.Children = append(maxCall.Children, filterCall)
|
||||
}
|
||||
maxVal, err := o.executeMax(ctx, tableKeyer, maxCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Max call for Percentile")
|
||||
return featurebase.ValCount{}, errors.Wrap(err, "executing Max call for Percentile")
|
||||
}
|
||||
|
||||
if desiredGreater == 0 {
|
||||
if maxVal.DecimalVal != nil {
|
||||
maxVal.FloatVal = maxVal.DecimalVal.Float64()
|
||||
}
|
||||
return maxVal, nil
|
||||
}
|
||||
|
||||
// o.executeCount(ctx, tableKeyer, countCall, shards, opt)
|
||||
// cookValCount(possibleNthVal, 1, field), nil
|
||||
// the logic here is basically identical whether we're doing a decimal field
|
||||
// or an integer field, but the actual code used to compare maximum and minimum
|
||||
// values, or extract values from valCount objects, differs.
|
||||
// So we set up generic functions which will produce the right values.
|
||||
var averageMinMax func() interface{}
|
||||
var minLessthanMax func() bool
|
||||
var maxValueUnder func(interface{})
|
||||
var minValueOver func(interface{})
|
||||
|
||||
if field.Options.Type == FieldTypeDecimal {
|
||||
minPtr := minVal.DecimalVal
|
||||
maxPtr := maxVal.DecimalVal
|
||||
if minPtr == nil {
|
||||
return nil, fmt.Errorf("unexpectedly nil min value in percentile")
|
||||
}
|
||||
if maxPtr == nil {
|
||||
return nil, fmt.Errorf("unexpectedly nil max value in percentile")
|
||||
}
|
||||
min := *minPtr
|
||||
max := *maxPtr
|
||||
two := pql.NewDecimal(2, 0)
|
||||
one := pql.NewDecimal(1, field.Options.Scale)
|
||||
averageMinMax = func() interface{} {
|
||||
return pql.DivideDecimal(pql.AddDecimal(min, max), two)
|
||||
}
|
||||
minLessthanMax = func() bool {
|
||||
return min.LessThan(max)
|
||||
}
|
||||
maxValueUnder = func(v interface{}) {
|
||||
max = pql.SubtractDecimal(v.(pql.Decimal), one)
|
||||
}
|
||||
minValueOver = func(v interface{}) {
|
||||
min = pql.AddDecimal(v.(pql.Decimal), one)
|
||||
}
|
||||
} else {
|
||||
// plain BSI field
|
||||
min := minVal.Val
|
||||
max := maxVal.Val
|
||||
averageMinMax = func() interface{} {
|
||||
// min+max could overflow, in theory, but if they're both odd, we want one
|
||||
// higher than min/2 + max/2.
|
||||
return (min / 2) + (max / 2) + (((min % 2) + (max % 2)) / 2)
|
||||
}
|
||||
minLessthanMax = func() bool {
|
||||
return min < max
|
||||
}
|
||||
maxValueUnder = func(v interface{}) {
|
||||
max = v.(int64) - 1
|
||||
}
|
||||
minValueOver = func(v interface{}) {
|
||||
min = v.(int64) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// set up reusable pql.Call objects representing a count (or intersectioncount,
|
||||
// if we have a filter) with a condition we can alter.
|
||||
// set up reusables
|
||||
var countCall, rangeCall *pql.Call
|
||||
rangeCondition := pql.Condition{
|
||||
Op: pql.LT,
|
||||
Value: nil,
|
||||
}
|
||||
rangeCall = &pql.Call{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &rangeCondition,
|
||||
},
|
||||
}
|
||||
if filterCall == nil {
|
||||
countCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{rangeCall},
|
||||
}
|
||||
countQuery, _ := pql.ParseString(fmt.Sprintf("Count(Row(%s < 0))", fieldName))
|
||||
countCall = countQuery.Calls[0]
|
||||
rangeCall = countCall.Children[0]
|
||||
} else {
|
||||
countCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Intersect",
|
||||
Children: []*pql.Call{rangeCall, filterCall},
|
||||
},
|
||||
},
|
||||
}
|
||||
countQuery, _ := pql.ParseString(fmt.Sprintf(`Count(Intersect(Row(%s < 0)))`, fieldName))
|
||||
countCall = countQuery.Calls[0]
|
||||
intersectCall := countCall.Children[0]
|
||||
intersectCall.Children = append(intersectCall.Children, filterCall)
|
||||
rangeCall = intersectCall.Children[0]
|
||||
}
|
||||
|
||||
k := (100 - nthFloat) / nthFloat
|
||||
|
||||
min, max := minVal.Val, maxVal.Val
|
||||
// estimate nth val, eg median when nth=0.5
|
||||
// we start with a blind guess of minVal, so if min and max are equal,
|
||||
// we just fall out of the loop. If they're not, we compute the middle value
|
||||
// of whatever range we're looking at, and compare it to our expectations of
|
||||
// how many
|
||||
var possibleNthVal interface{}
|
||||
if minVal.DecimalVal != nil {
|
||||
possibleNthVal = minVal.DecimalVal
|
||||
} else {
|
||||
possibleNthVal = minVal.Val
|
||||
}
|
||||
for minLessthanMax() {
|
||||
for min < max {
|
||||
// compute average without integer overflow, then correct for division of
|
||||
// odd numbers by 2
|
||||
possibleNthVal = averageMinMax()
|
||||
rangeCondition.Value = possibleNthVal
|
||||
rangeCondition.Op = pql.LT
|
||||
leftCount, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
|
||||
possibleNthVal := ((max / 2) + (min / 2)) + (((max % 2) + (min % 2)) / 2)
|
||||
// possibleNthVal = (max + min) / 2
|
||||
// get left count
|
||||
rangeCall.Args[fieldName] = &pql.Condition{
|
||||
Op: pql.Token(pql.LT),
|
||||
Value: possibleNthVal,
|
||||
}
|
||||
leftCountUint64, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Count call L for Percentile")
|
||||
return featurebase.ValCount{}, errors.Wrap(err, "executing Count call L for Percentile")
|
||||
}
|
||||
leftCount := int64(leftCountUint64)
|
||||
|
||||
// If there's more things less than possibleNthVal than our desired number
|
||||
// of things less, we need to look at the left side of this.
|
||||
if leftCount > desiredLess {
|
||||
maxValueUnder(possibleNthVal)
|
||||
continue
|
||||
// get right count
|
||||
rangeCall.Args[fieldName] = &pql.Condition{
|
||||
Op: pql.Token(pql.GT),
|
||||
Value: possibleNthVal,
|
||||
}
|
||||
|
||||
rangeCondition.Op = pql.GT
|
||||
rightCount, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
|
||||
rightCountUint64, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Count call R for Percentile")
|
||||
}
|
||||
// If there's more things greater than the desired number, we need to look to the right.
|
||||
if rightCount > desiredGreater {
|
||||
minValueOver(possibleNthVal)
|
||||
continue
|
||||
return featurebase.ValCount{}, errors.Wrap(err, "executing Count call R for Percentile")
|
||||
}
|
||||
rightCount := int64(rightCountUint64)
|
||||
|
||||
// min and max may be different, but the number of values above and below this
|
||||
// value are both reasonable. For instance, with 7 items and looking for median,
|
||||
// we'd have 3 less and 3 greater, and we can't really do better than that.
|
||||
break
|
||||
// 'weight' the left count as per k
|
||||
leftCountWeighted := int64(math.Round(k * float64(leftCount)))
|
||||
|
||||
// binary search
|
||||
if leftCountWeighted > rightCount {
|
||||
max = possibleNthVal - 1
|
||||
} else if leftCountWeighted < rightCount {
|
||||
min = possibleNthVal + 1
|
||||
} else {
|
||||
return cookValCount(possibleNthVal, 1, field), nil
|
||||
}
|
||||
}
|
||||
switch v := possibleNthVal.(type) {
|
||||
case int64:
|
||||
return featurebase.ValCount{
|
||||
Val: v,
|
||||
Count: 1,
|
||||
}, nil
|
||||
case pql.Decimal:
|
||||
return featurebase.ValCount{
|
||||
DecimalVal: &v,
|
||||
FloatVal: v.Float64(),
|
||||
Count: 1,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected percentile Nth value type %T", possibleNthVal)
|
||||
|
||||
return cookValCount(min, 1, field), nil
|
||||
}
|
||||
|
||||
func cookValCount(val int64, cnt uint64, field *featurebase.FieldInfo) featurebase.ValCount {
|
||||
valCount := featurebase.ValCount{Count: int64(cnt)}
|
||||
base := field.Options.Base
|
||||
switch field.Options.Type {
|
||||
case featurebase.FieldTypeDecimal:
|
||||
dec := pql.NewDecimal(val+base, field.Options.Scale)
|
||||
valCount.DecimalVal = &dec
|
||||
case FieldTypeTimestamp:
|
||||
valCount.TimestampVal = time.Unix(0, (val+base)*featurebase.TimeUnitNanos(field.Options.TimeUnit)).UTC()
|
||||
}
|
||||
valCount.Val = val + base
|
||||
return valCount
|
||||
}
|
||||
|
||||
// executeMinRow executes a MinRow() call.
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ type Queryer struct {
|
|||
|
||||
controller dax.Controller
|
||||
|
||||
systemLayer *systemlayer.SystemLayer
|
||||
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
|
|
@ -49,7 +47,6 @@ func New(cfg Config) *Queryer {
|
|||
q := &Queryer{
|
||||
controller: dax.NewNopController(),
|
||||
orchestrators: make(map[dax.QualifiedDatabaseID]*qualifiedOrchestrator),
|
||||
systemLayer: systemlayer.NewSystemLayer(),
|
||||
logger: logger.NopLogger,
|
||||
}
|
||||
|
||||
|
|
@ -201,14 +198,17 @@ func (q *Queryer) QuerySQL(ctx context.Context, qdbid dax.QualifiedDatabaseID, s
|
|||
// Importer
|
||||
imp := idkserverless.NewImporter(q.controller, qdbid, nil)
|
||||
|
||||
// TODO(tlt): We need a serverless-compatible implementation of the
|
||||
// SystemAPI.
|
||||
sysapi := newSystemAPI(q.controller, qdbid)
|
||||
sysapi := &featurebase.NopSystemAPI{}
|
||||
|
||||
systemLayer := systemlayer.NewSystemLayer()
|
||||
|
||||
// We intentionally don't pass the sql argument here because we're working
|
||||
// with an io.Reader rather than a string and it's just not necessary to
|
||||
// send it as a string to this method. Also, what happens if the sql is a
|
||||
// large BULK INSERT?
|
||||
pl := planner.NewExecutionPlanner(q.Orchestrator(qdbid), sapi, sysapi, q.systemLayer, imp, q.logger, "")
|
||||
pl := planner.NewExecutionPlanner(q.Orchestrator(qdbid), sapi, sysapi, systemLayer, imp, q.logger, "")
|
||||
|
||||
planOp, err := pl.CompilePlan(ctx, st)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package queryer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
featurebase "github.com/featurebasedb/featurebase/v3"
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
)
|
||||
|
||||
// systemAPI is an implementation of the systemAPI.
|
||||
type systemAPI struct {
|
||||
featurebase.NopSystemAPI
|
||||
controller dax.Controller
|
||||
qdbid dax.QualifiedDatabaseID
|
||||
}
|
||||
|
||||
func newSystemAPI(c dax.Controller, qdbid dax.QualifiedDatabaseID) *systemAPI {
|
||||
return &systemAPI{
|
||||
controller: c,
|
||||
qdbid: qdbid,
|
||||
}
|
||||
}
|
||||
|
||||
// ClusterNodes returns a list of featurebase.ClusterNodes
|
||||
// with length of the minimum number of workers
|
||||
func (s *systemAPI) ClusterNodes() []featurebase.ClusterNode {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
qdb, err := s.controller.DatabaseByID(ctx, s.qdbid)
|
||||
if err != nil {
|
||||
return []featurebase.ClusterNode{}
|
||||
}
|
||||
out := make([]featurebase.ClusterNode, qdb.Options.WorkersMin)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *systemAPI) PlatformDescription() string {
|
||||
return "Serverless"
|
||||
}
|
||||
|
||||
func (s *systemAPI) ClusterName() string {
|
||||
return "Serverless"
|
||||
}
|
||||
|
||||
func (s *systemAPI) ClusterNodeCount() int {
|
||||
ctx := context.Background()
|
||||
|
||||
qdb, err := s.controller.DatabaseByID(ctx, s.qdbid)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return qdb.Options.WorkersMin
|
||||
}
|
||||
|
||||
func (s *systemAPI) ClusterState() string {
|
||||
return "NORMAL"
|
||||
}
|
||||
|
|
@ -6,11 +6,6 @@ type RoleType string
|
|||
const (
|
||||
RoleTypeCompute RoleType = "compute"
|
||||
RoleTypeTranslate RoleType = "translate"
|
||||
RoleTypeQuery RoleType = "query"
|
||||
)
|
||||
|
||||
var (
|
||||
AllRoleTypes = []RoleType{RoleTypeCompute, RoleTypeTranslate, RoleTypeQuery}
|
||||
)
|
||||
|
||||
// RoleTypes is a list of RoleType, used primarily to introduce helper methods
|
||||
|
|
|
|||
|
|
@ -256,11 +256,19 @@ func MustRunManagedCommand(tb testing.TB, opts ...server.CommandOption) *Managed
|
|||
// after). This has the advantage that if the tests fail partway
|
||||
// through, you can inspect the state of the database for
|
||||
// debugging purposes.
|
||||
if err := mc.trans.TruncateAll(); err != nil {
|
||||
err := mc.trans.TruncateAll()
|
||||
if err != nil {
|
||||
tb.Fatalf("truncating DB: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.trans.Close(); err != nil {
|
||||
// The migrations contain an insert, but since we just truncated everything we need to redo that insert.
|
||||
err = mc.trans.RawQuery("INSERT INTO directive_versions (id, version, created_at, updated_at) VALUES (1, 1, '1970-01-01T00:00', '1970-01-01T00:00')").Exec()
|
||||
if err != nil {
|
||||
tb.Fatalf("reinserting directive_version record after truncation: %v", err)
|
||||
}
|
||||
|
||||
err = mc.trans.Close()
|
||||
if err != nil {
|
||||
tb.Fatalf("Closing conn after truncating all tables: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,11 +142,10 @@ func TestDAXIntegration(t *testing.T) {
|
|||
// need to get these passing before alpha.
|
||||
skips := []string{
|
||||
"testinsert/test-5", // error messages differ
|
||||
"percentile_test/test-6", // related to TODO in orchestrator.executePercentile
|
||||
"alterTable/alterTableBadTable", // looks like table does not exist is a different error in DAX
|
||||
"top-limit-tests/test-2", // don't know why this is failing at all
|
||||
"top-limit-tests/test-3", // don't know why this is failing at all
|
||||
"top-tests/test-1", // don't know why this is failing at all
|
||||
"delete_tests",
|
||||
"groupby_set_test", // no idea why this has ceased to work
|
||||
"viewtests/drop-view", // drop view does a delete
|
||||
"viewtests/drop-view-if-exists-after-drop",
|
||||
"viewtests/select-view-after-drop",
|
||||
|
|
@ -275,7 +274,7 @@ func TestDAXIntegration(t *testing.T) {
|
|||
// TODO: implement this without a sleep.
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// ensure partitions are still covered
|
||||
// ensure paritions are still covered
|
||||
nodes, err = controllerClient.TranslateNodes(context.Background(), qtid, append(partitions0, partitions1...)...)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, nodes, 1) {
|
||||
|
|
@ -372,7 +371,7 @@ func TestDAXIntegration(t *testing.T) {
|
|||
qtid, err := controllerClient.TableID(ctx, qdbid, dax.TableName(defs.Keyed.Name(0)))
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, controllerClient.SnapshotTable(ctx, qtid))
|
||||
controllerClient.SnapshotTable(ctx, qtid)
|
||||
|
||||
// Ingest more data.
|
||||
t.Run("ingest and query more data", func(t *testing.T) {
|
||||
|
|
@ -465,7 +464,7 @@ func TestDAXIntegration(t *testing.T) {
|
|||
assert.NoError(t, svcmgr.ControllerStart())
|
||||
assert.True(t, mc.Healthy(controllerKey))
|
||||
|
||||
// ensure partitions are still covered
|
||||
// ensure paritions are still covered
|
||||
nodes, err = controllerClient.TranslateNodes(context.Background(), qtid, partitions...)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, nodes, 1) {
|
||||
|
|
@ -693,7 +692,7 @@ func TestDAXIntegration(t *testing.T) {
|
|||
cfg.Computer.N = 4
|
||||
opt := server.OptCommandConfig(cfg)
|
||||
mc := test.MustRunManagedCommand(t, opt)
|
||||
defer mc.Close()
|
||||
|
||||
svcmgr := mc.Manage()
|
||||
|
||||
// Set up Controller client.
|
||||
|
|
|
|||
|
|
@ -1,103 +1,9 @@
|
|||
package dax
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/errors"
|
||||
)
|
||||
import "context"
|
||||
|
||||
type Transaction interface {
|
||||
Commit() error
|
||||
Context() context.Context
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
type Transactor interface {
|
||||
// Start is useful for Transactor implementations which need to establish a
|
||||
// connection. We don't want to do that in the NewImplementation() function;
|
||||
// we want that to happen upon Start().
|
||||
Start() error
|
||||
|
||||
BeginTx(ctx context.Context, writable bool) (Transaction, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
const (
|
||||
// postgresTxConflictError occurs any time a transaction violates the
|
||||
// repeatable isolation level.
|
||||
postgresTxConflictError = "(SQLSTATE 40001)"
|
||||
|
||||
// postgresDuplicateKeyContraint occurs when two concurrent transactions try
|
||||
// to create the same record, causing one of them to violate a key
|
||||
// constraint.
|
||||
postgresDuplicateKeyContraint = "(SQLSTATE 23505)"
|
||||
)
|
||||
|
||||
// txFunc is the function signature for a function which can be retried using
|
||||
// the RetryWithTx function.
|
||||
type txFunc func(tx Transaction, writable bool) error
|
||||
|
||||
// RetryWithTx will retry the txFunc up to maxTries, or a try succeeds,
|
||||
// whichever comes first. If writable is set to true, RetryWithTx will use a
|
||||
// writable transaction for each try, and attempt to Commit the transaction. If
|
||||
// the transaction fails with an error related to invalid serialization, and
|
||||
// there are still tries remaining, the transaction will be retried.
|
||||
func RetryWithTx(ctx context.Context, trans Transactor, fn txFunc, writable bool, maxTries int) error {
|
||||
// stopRetry can be set to true to abort the retry loop. This is useful when
|
||||
// a transaction completes successfully, but maxTries has not been reached;
|
||||
// i.e, because the transaction succeeded, there's no reason to keep trying.
|
||||
var stopRetry bool
|
||||
|
||||
for maxTries >= 1 && !stopRetry {
|
||||
maxTries--
|
||||
|
||||
if err := func() error {
|
||||
// Begin a read transaction.
|
||||
tx, err := trans.BeginTx(ctx, writable)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "beginning tx, writable: %v", writable)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Call the function with the transaction. We pass in writable in
|
||||
// case the function operates differently based on whether it is a
|
||||
// read or write transaction.
|
||||
if err := fn(tx, writable); err != nil {
|
||||
return errors.Wrapf(err, "calling function with tx, writable: %v", writable)
|
||||
}
|
||||
|
||||
if writable {
|
||||
if err := tx.Commit(); err != nil {
|
||||
return errors.Wrap(err, "committing tx")
|
||||
}
|
||||
}
|
||||
|
||||
stopRetry = true
|
||||
return nil
|
||||
}(); err != nil {
|
||||
// If we get a serialization error, and we still have some write
|
||||
// attempts remaining, then continue trying.
|
||||
if maxTries > 0 && containsAny(err.Error(), []string{
|
||||
postgresTxConflictError,
|
||||
postgresDuplicateKeyContraint,
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsAny returns true if s contains at least one of the strings in
|
||||
// substrs.
|
||||
func containsAny(s string, substrs []string) bool {
|
||||
for _, substr := range substrs {
|
||||
if strings.Contains(s, substr) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
package dax_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
||||
"github.com/featurebasedb/featurebase/v3/logger"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTransaction(t *testing.T) {
|
||||
t.Run("retryWithTx", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
log := logger.StderrLogger
|
||||
trans, err := sqldb.NewTransactor(sqldb.GetTestConfigRandomDB("retry_with_tx"), log) // TODO running migrations takes kind of a long time, consolidate w/ other SQL tests
|
||||
require.NoError(t, err, "connecting")
|
||||
require.NoError(t, trans.Start())
|
||||
|
||||
orgID1 := dax.OrganizationID("acme")
|
||||
db1 := &dax.Database{
|
||||
ID: "db1id",
|
||||
Name: "db1",
|
||||
Options: dax.DatabaseOptions{
|
||||
WorkersMin: 1,
|
||||
},
|
||||
}
|
||||
qdb1 := dax.NewQualifiedDatabase(orgID1, db1)
|
||||
qdbid1 := qdb1.QualifiedID()
|
||||
schemar := sqldb.NewSchemar(log)
|
||||
|
||||
// The purpose of this test is to ensure that we're enforcing repeatable
|
||||
// read isolation level on writes. It tests the RetryWithTx function by
|
||||
// retrying a write that fails and ensuring that it succeeds on the next
|
||||
// retry. It performs the following steps:
|
||||
//
|
||||
// tx1: create db1 with units 1
|
||||
// tx2: read db1 (units should be 1)
|
||||
// wait... on chan "wait2"
|
||||
// read db1 again (units should still be 1) << repeatableread
|
||||
// set units to 2
|
||||
// tx3 set units to 3
|
||||
// tx4 read db1 (units should be 3)
|
||||
// close "wait2"
|
||||
// tx5 read db1 (units should be 2)
|
||||
|
||||
wait2 := make(chan struct{})
|
||||
wait3 := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
// tx1
|
||||
tx1 := func(tx dax.Transaction, writable bool) error {
|
||||
dt, ok := tx.(*sqldb.DaxTransaction)
|
||||
require.True(t, ok)
|
||||
|
||||
require.NoError(t, schemar.CreateDatabase(dt, qdb1))
|
||||
|
||||
return nil
|
||||
}
|
||||
require.NoError(t, dax.RetryWithTx(ctx, trans, tx1, true, 1))
|
||||
|
||||
// tx2
|
||||
|
||||
// tx2cnt tracks the number of times that tx2 has been called. We need
|
||||
// this because we expect it to read different values depending on which
|
||||
// call it's on. And we only want it to close channels the first time
|
||||
// through.
|
||||
var tx2cnt int
|
||||
|
||||
// exp contains the values that we expect tx2 to read (for
|
||||
// "workers-min") on the respective call.
|
||||
exp := map[int]int{
|
||||
0: 1,
|
||||
1: 3,
|
||||
}
|
||||
tx2 := func(tx dax.Transaction, writable bool) error {
|
||||
dt, ok := tx.(*sqldb.DaxTransaction)
|
||||
require.True(t, ok)
|
||||
|
||||
qdb, err := schemar.DatabaseByID(dt, qdbid1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)
|
||||
if tx2cnt == 0 {
|
||||
close(wait3)
|
||||
}
|
||||
|
||||
// Wait until tx3 commits before trying to do anything else.
|
||||
<-wait2
|
||||
|
||||
qdb, err = schemar.DatabaseByID(dt, qdbid1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, exp[tx2cnt], qdb.Options.WorkersMin)
|
||||
|
||||
// Increment tx2cnt for the next time tx2 gets called.
|
||||
tx2cnt++
|
||||
|
||||
return schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "2")
|
||||
}
|
||||
|
||||
// Run the calls to tx2 in a go routine because we want to mimic
|
||||
// concurrent attempt to read/write the same data.
|
||||
go func() {
|
||||
require.NoError(t, dax.RetryWithTx(ctx, trans, tx2, true, 2))
|
||||
|
||||
// After the second call of tx2 completes, close the done channel
|
||||
// so that tx5 can proceed and verify that tx2 eventually got to
|
||||
// commit its transaction.
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Wait until tx2 does its first read of the data before allowing tx3 to
|
||||
// begin.
|
||||
select {
|
||||
case <-wait3:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("expected close of channel: wait3")
|
||||
}
|
||||
|
||||
// tx3
|
||||
tx3 := func(tx dax.Transaction, writable bool) error {
|
||||
dt, ok := tx.(*sqldb.DaxTransaction)
|
||||
require.True(t, ok)
|
||||
|
||||
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, qdb.Options.WorkersMin)
|
||||
|
||||
require.NoError(t, schemar.SetDatabaseOption(dt, qdbid1, dax.DatabaseOptionWorkersMin, "3"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
require.NoError(t, dax.RetryWithTx(ctx, trans, tx3, true, 1))
|
||||
|
||||
// tx4
|
||||
tx4 := func(tx dax.Transaction, writable bool) error {
|
||||
dt, ok := tx.(*sqldb.DaxTransaction)
|
||||
require.True(t, ok)
|
||||
|
||||
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 3, qdb.Options.WorkersMin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
require.NoError(t, dax.RetryWithTx(ctx, trans, tx4, false, 1))
|
||||
|
||||
// Close wait2 so that tx2 can continue retrying transactions.
|
||||
close(wait2)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("expected close of channel: done")
|
||||
}
|
||||
|
||||
// tx5
|
||||
tx5 := func(tx dax.Transaction, writable bool) error {
|
||||
dt, ok := tx.(*sqldb.DaxTransaction)
|
||||
require.True(t, ok)
|
||||
|
||||
qdb, err := schemar.DatabaseByID(dt, qdb1.QualifiedID())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, qdb.Options.WorkersMin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
require.NoError(t, dax.RetryWithTx(ctx, trans, tx5, false, 1))
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ type WorkerDiff struct {
|
|||
RemovedJobs []Job
|
||||
}
|
||||
|
||||
// Add adds w2 to w. It panics if w and w2 don't have the same worker
|
||||
// Add adds w2 to w. It panics of w and w2 don't have teh same worker
|
||||
// ID. Any job that is added and then removed or removed and then
|
||||
// added cancels out and won't be present after add is called.
|
||||
func (w *WorkerDiff) Add(w2 WorkerDiff) {
|
||||
|
|
@ -55,15 +55,14 @@ func (w *WorkerDiff) Add(w2 WorkerDiff) {
|
|||
r2 := NewSet(w2.RemovedJobs...)
|
||||
|
||||
// final Added is (a1 - r2) + (a2 - r1)
|
||||
// this is because anything that is removed and then added, or added and
|
||||
// then removed cancels out
|
||||
// this is because anything that is removed and then added, or added and then removed cancels out
|
||||
added := a1.Minus(r2).Plus(a2.Minus(r1))
|
||||
|
||||
// final removed is (r1 - a2) + (r2 - a1)
|
||||
removed := r1.Minus(a2).Plus(r2.Minus(a1))
|
||||
|
||||
w.AddedJobs = added.Sorted()
|
||||
w.RemovedJobs = removed.Sorted()
|
||||
w.AddedJobs = added.Slice()
|
||||
w.RemovedJobs = removed.Slice()
|
||||
}
|
||||
|
||||
// WorkerDiffs is a sortable slice of WorkerDiff.
|
||||
|
|
@ -73,34 +72,6 @@ func (w WorkerDiffs) Len() int { return len(w) }
|
|||
func (w WorkerDiffs) Less(i, j int) bool { return w[i].Address < w[j].Address }
|
||||
func (w WorkerDiffs) Swap(i, j int) { w[i], w[j] = w[j], w[i] }
|
||||
|
||||
func (w WorkerDiffs) Apply(o WorkerDiffs) WorkerDiffs {
|
||||
out := make(WorkerDiffs, len(w))
|
||||
|
||||
// m is a map which makes it easy for us to match on Address between the two
|
||||
// WorkerDiffs; i.e. without doing nested loops.
|
||||
m := make(map[Address]int)
|
||||
for i := range w {
|
||||
out[i] = w[i]
|
||||
m[out[i].Address] = i
|
||||
}
|
||||
|
||||
for i := range o {
|
||||
oAddr := o[i].Address
|
||||
if idx, ok := m[oAddr]; ok {
|
||||
// merge these
|
||||
out[idx].Add(o[i])
|
||||
continue
|
||||
}
|
||||
|
||||
// Append any items from o which don't exist in w.
|
||||
out = append(out, o[i])
|
||||
}
|
||||
|
||||
sort.Sort(out)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Set is a set of stringy items.
|
||||
type Set[K ~string] map[K]struct{}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,54 +79,3 @@ func TestWorkerDiffAdd(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerDiffsApply(t *testing.T) {
|
||||
|
||||
a := []WorkerDiff{
|
||||
{
|
||||
Address: "addr2",
|
||||
AddedJobs: []Job{"j10", "j11"},
|
||||
RemovedJobs: []Job{"j99", "j100"},
|
||||
},
|
||||
{
|
||||
Address: "addr1",
|
||||
AddedJobs: []Job{"j1", "j2"},
|
||||
RemovedJobs: []Job{"j86"},
|
||||
},
|
||||
}
|
||||
|
||||
b := []WorkerDiff{
|
||||
{
|
||||
Address: "addr2",
|
||||
AddedJobs: []Job{"j3", "j99"},
|
||||
RemovedJobs: []Job{"j2", "j10"},
|
||||
},
|
||||
{
|
||||
Address: "addr3",
|
||||
AddedJobs: []Job{"j777"},
|
||||
RemovedJobs: []Job{},
|
||||
},
|
||||
}
|
||||
|
||||
out := WorkerDiffs(a).Apply(b)
|
||||
|
||||
exp := []WorkerDiff{
|
||||
{
|
||||
Address: "addr1",
|
||||
AddedJobs: []Job{"j1", "j2"},
|
||||
RemovedJobs: []Job{"j86"},
|
||||
},
|
||||
{
|
||||
Address: "addr2",
|
||||
AddedJobs: []Job{"j11", "j3"},
|
||||
RemovedJobs: []Job{"j100", "j2"},
|
||||
},
|
||||
{
|
||||
Address: "addr3",
|
||||
AddedJobs: []Job{"j777"},
|
||||
RemovedJobs: []Job{},
|
||||
},
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, exp, out)
|
||||
}
|
||||
|
|
|
|||
311
executor.go
311
executor.go
|
|
@ -1294,310 +1294,129 @@ func (e *executor) executeMax(ctx context.Context, qcx *Qcx, index string, c *pq
|
|||
}
|
||||
|
||||
// executePercentile executes a Percentile() call.
|
||||
//
|
||||
// To compute the percentile, we find the maximum and minimum values that match
|
||||
// our filter (or an implicit filter of "value isn't null"), and also count values
|
||||
// matching our filter. We convert our percentile to an approximate number of
|
||||
// values that should be higher or lower than the desired value. If either of those
|
||||
// is zero, we return the minimum/maximum; otherwise, we do a binary search of
|
||||
// the range between minimum and maximum, looking for a value which has the
|
||||
// desired number of values higher or lower than it.
|
||||
//
|
||||
// Unfortunately, each step in this process is its own, separate, cluster-wide
|
||||
// query. this should be replaced with a modern probabilistic algorithm, which
|
||||
// could accumulate statistical information per shard and combine that information
|
||||
// in a single pass.
|
||||
func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (result interface{}, err error) {
|
||||
// defer func() {
|
||||
// fmt.Fprintf(os.Stderr, "executePercentile %s: %#v, %v\n",
|
||||
// c.String(), result, err)
|
||||
// }()
|
||||
func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) {
|
||||
span, ctx := tracing.StartSpanFromContext(ctx, "executor.executePercentile")
|
||||
defer span.Finish()
|
||||
|
||||
// get nth
|
||||
var nthFloat float64
|
||||
nthArg := c.Args["nth"]
|
||||
nthArg, ok := c.Args["nth"]
|
||||
if !ok {
|
||||
return ValCount{}, errors.New("Percentile(): nth required")
|
||||
}
|
||||
switch nthArg := nthArg.(type) {
|
||||
case pql.Decimal:
|
||||
nthFloat = nthArg.Float64()
|
||||
case int64:
|
||||
nthFloat = float64(nthArg)
|
||||
case nil:
|
||||
return nil, errors.New("Percentile(): nth required")
|
||||
default:
|
||||
return nil, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
|
||||
return ValCount{}, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
|
||||
}
|
||||
if nthFloat < 0 || nthFloat > 100.0 {
|
||||
return nil, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
|
||||
return ValCount{}, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
|
||||
}
|
||||
|
||||
// get field
|
||||
fieldName, err := c.FirstStringArg("field", "_field")
|
||||
if err != nil {
|
||||
return nil, errors.New("Percentile(): field required")
|
||||
return ValCount{}, errors.New("Percentile(): field required")
|
||||
}
|
||||
field := e.Holder.Field(index, fieldName)
|
||||
if field == nil {
|
||||
return nil, ErrFieldNotFound
|
||||
return ValCount{}, ErrFieldNotFound
|
||||
}
|
||||
|
||||
// filter call for min & max
|
||||
var filterCall *pql.Call
|
||||
|
||||
// We want to know the total number of values, so that when we check
|
||||
// for values <X, or >X, we are also able to infer the number of values
|
||||
// equal to X.
|
||||
var totalCountCall *pql.Call
|
||||
// check if filter provided
|
||||
if filterArg, ok := c.Args["filter"].(*pql.Call); ok && filterArg != nil {
|
||||
// You could supply a filter like `Not(x=3)` which would yield values
|
||||
// which exist in the database but are null in this field, we don't
|
||||
// want that.
|
||||
filterCall = filterArg
|
||||
totalCountCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Intersect",
|
||||
Children: []*pql.Call{
|
||||
filterCall,
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &pql.Condition{
|
||||
Op: pql.NEQ,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// request a count of IS NOT NULL, aka Row(field!=null). We care about
|
||||
// the actual number of results that should exist.
|
||||
totalCountCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &pql.Condition{
|
||||
Op: pql.NEQ,
|
||||
Value: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
// total values matched by the filter (if it exists) or that aren't null
|
||||
totalCountInterface, err := e.executeCall(ctx, qcx, index, totalCountCall, shards, opt)
|
||||
totalCount, ok := totalCountInterface.(uint64)
|
||||
if !ok || totalCount == 0 {
|
||||
// it's not an error, but the median of nothing is NULL.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We have totalCount values. If nth is 50, we want half the values to be
|
||||
// above us, and half below us. So for instance, if we have 6 values, we want
|
||||
// 3 above us, and 3 below us. For odd numbers, we can round these *both*
|
||||
// down -- for 7 values, we'd want 3 higher, and 3 lower.
|
||||
desiredLess := uint64((float64(totalCount) * nthFloat) / 100.0)
|
||||
desiredGreater := uint64((float64(totalCount) * (100 - nthFloat)) / 100.0)
|
||||
|
||||
// get min
|
||||
var minVal ValCount
|
||||
|
||||
if desiredGreater != 0 {
|
||||
q, err := pql.ParseString(fmt.Sprintf(`Min(field="%s")`, fieldName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing max call for Percentile")
|
||||
}
|
||||
minCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
minCall.Children = append(minCall.Children, filterCall)
|
||||
}
|
||||
minVal, err = e.executeMin(ctx, qcx, index, minCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Min call for Percentile")
|
||||
}
|
||||
|
||||
if desiredLess == 0 {
|
||||
if minVal.DecimalVal != nil {
|
||||
minVal.FloatVal = minVal.DecimalVal.Float64()
|
||||
}
|
||||
return minVal, nil
|
||||
}
|
||||
q, _ := pql.ParseString(fmt.Sprintf(`Min(field="%s")`, fieldName))
|
||||
minCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
minCall.Children = append(minCall.Children, filterCall)
|
||||
}
|
||||
minVal, err := e.executeMin(ctx, qcx, index, minCall, shards, opt)
|
||||
if err != nil {
|
||||
return ValCount{}, errors.Wrap(err, "executing Min call for Percentile")
|
||||
}
|
||||
if nthFloat == 0.0 {
|
||||
return minVal, nil
|
||||
}
|
||||
|
||||
// get max
|
||||
q, err := pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing max call for Percentile")
|
||||
}
|
||||
q, _ = pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
|
||||
maxCall := q.Calls[0]
|
||||
if filterCall != nil {
|
||||
maxCall.Children = append(maxCall.Children, filterCall)
|
||||
}
|
||||
maxVal, err := e.executeMax(ctx, qcx, index, maxCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Max call for Percentile")
|
||||
return ValCount{}, errors.Wrap(err, "executing Max call for Percentile")
|
||||
}
|
||||
|
||||
if desiredGreater == 0 {
|
||||
if maxVal.DecimalVal != nil {
|
||||
maxVal.FloatVal = maxVal.DecimalVal.Float64()
|
||||
}
|
||||
return maxVal, nil
|
||||
}
|
||||
|
||||
// the logic here is basically identical whether we're doing a decimal field
|
||||
// or an integer field, but the actual code used to compare maximum and minimum
|
||||
// values, or extract values from valCount objects, differs.
|
||||
// So we set up generic functions which will produce the right values.
|
||||
var averageMinMax func() interface{}
|
||||
var minLessthanMax func() bool
|
||||
var maxValueUnder func(interface{})
|
||||
var minValueOver func(interface{})
|
||||
|
||||
if field.options.Type == FieldTypeDecimal {
|
||||
minPtr := minVal.DecimalVal
|
||||
maxPtr := maxVal.DecimalVal
|
||||
if minPtr == nil {
|
||||
return nil, fmt.Errorf("unexpectedly nil min value in percentile")
|
||||
}
|
||||
if maxPtr == nil {
|
||||
return nil, fmt.Errorf("unexpectedly nil max value in percentile")
|
||||
}
|
||||
min := *minPtr
|
||||
max := *maxPtr
|
||||
two := pql.NewDecimal(2, 0)
|
||||
one := pql.NewDecimal(1, field.options.Scale)
|
||||
averageMinMax = func() interface{} {
|
||||
return pql.DivideDecimal(pql.AddDecimal(min, max), two)
|
||||
}
|
||||
minLessthanMax = func() bool {
|
||||
return min.LessThan(max)
|
||||
}
|
||||
maxValueUnder = func(v interface{}) {
|
||||
max = pql.SubtractDecimal(v.(pql.Decimal), one)
|
||||
}
|
||||
minValueOver = func(v interface{}) {
|
||||
min = pql.AddDecimal(v.(pql.Decimal), one)
|
||||
}
|
||||
} else {
|
||||
// plain BSI field
|
||||
min := minVal.Val
|
||||
max := maxVal.Val
|
||||
averageMinMax = func() interface{} {
|
||||
// min+max could overflow, in theory, but if they're both odd, we want one
|
||||
// higher than min/2 + max/2.
|
||||
return (min / 2) + (max / 2) + (((min % 2) + (max % 2)) / 2)
|
||||
}
|
||||
minLessthanMax = func() bool {
|
||||
return min < max
|
||||
}
|
||||
maxValueUnder = func(v interface{}) {
|
||||
max = v.(int64) - 1
|
||||
}
|
||||
minValueOver = func(v interface{}) {
|
||||
min = v.(int64) + 1
|
||||
}
|
||||
}
|
||||
|
||||
// set up reusable pql.Call objects representing a count (or intersectioncount,
|
||||
// if we have a filter) with a condition we can alter.
|
||||
// set up reusables
|
||||
var countCall, rangeCall *pql.Call
|
||||
rangeCondition := pql.Condition{
|
||||
Op: pql.LT,
|
||||
Value: nil,
|
||||
}
|
||||
rangeCall = &pql.Call{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
fieldName: &rangeCondition,
|
||||
},
|
||||
}
|
||||
if filterCall == nil {
|
||||
countCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{rangeCall},
|
||||
}
|
||||
countQuery, _ := pql.ParseString(fmt.Sprintf("Count(Row(%s < 0))", fieldName))
|
||||
countCall = countQuery.Calls[0]
|
||||
rangeCall = countCall.Children[0]
|
||||
} else {
|
||||
countCall = &pql.Call{
|
||||
Name: "Count",
|
||||
Children: []*pql.Call{
|
||||
{
|
||||
Name: "Intersect",
|
||||
Children: []*pql.Call{rangeCall, filterCall},
|
||||
},
|
||||
},
|
||||
}
|
||||
countQuery, _ := pql.ParseString(fmt.Sprintf(`Count(Intersect(Row(%s < 0)))`, fieldName))
|
||||
countCall = countQuery.Calls[0]
|
||||
intersectCall := countCall.Children[0]
|
||||
intersectCall.Children = append(intersectCall.Children, filterCall)
|
||||
rangeCall = intersectCall.Children[0]
|
||||
}
|
||||
|
||||
k := (100 - nthFloat) / nthFloat
|
||||
|
||||
min, max := minVal.Val, maxVal.Val
|
||||
// estimate nth val, eg median when nth=0.5
|
||||
// we start with a blind guess of minVal, so if min and max are equal,
|
||||
// we just fall out of the loop. If they're not, we compute the middle value
|
||||
// of whatever range we're looking at, and compare it to our expectations of
|
||||
// how many
|
||||
var possibleNthVal interface{}
|
||||
if minVal.DecimalVal != nil {
|
||||
possibleNthVal = minVal.DecimalVal
|
||||
} else {
|
||||
possibleNthVal = minVal.Val
|
||||
}
|
||||
for minLessthanMax() {
|
||||
for min < max {
|
||||
// compute average without integer overflow, then correct for division of
|
||||
// odd numbers by 2
|
||||
possibleNthVal = averageMinMax()
|
||||
rangeCondition.Value = possibleNthVal
|
||||
rangeCondition.Op = pql.LT
|
||||
leftCount, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
|
||||
possibleNthVal := ((max / 2) + (min / 2)) + (((max % 2) + (min % 2)) / 2)
|
||||
// possibleNthVal = (max + min) / 2
|
||||
// get left count
|
||||
rangeCall.Args[fieldName] = &pql.Condition{
|
||||
Op: pql.Token(pql.LT),
|
||||
Value: possibleNthVal,
|
||||
}
|
||||
leftCountUint64, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Count call L for Percentile")
|
||||
return ValCount{}, errors.Wrap(err, "executing Count call L for Percentile")
|
||||
}
|
||||
leftCount := int64(leftCountUint64)
|
||||
|
||||
// If there's more things less than possibleNthVal than our desired number
|
||||
// of things less, we need to look at the left side of this.
|
||||
if leftCount > desiredLess {
|
||||
maxValueUnder(possibleNthVal)
|
||||
continue
|
||||
// get right count
|
||||
rangeCall.Args[fieldName] = &pql.Condition{
|
||||
Op: pql.Token(pql.GT),
|
||||
Value: possibleNthVal,
|
||||
}
|
||||
|
||||
rangeCondition.Op = pql.GT
|
||||
rightCount, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
|
||||
rightCountUint64, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "executing Count call R for Percentile")
|
||||
}
|
||||
// If there's more things greater than the desired number, we need to look to the right.
|
||||
if rightCount > desiredGreater {
|
||||
minValueOver(possibleNthVal)
|
||||
continue
|
||||
return ValCount{}, errors.Wrap(err, "executing Count call R for Percentile")
|
||||
}
|
||||
rightCount := int64(rightCountUint64)
|
||||
|
||||
// min and max may be different, but the number of values above and below this
|
||||
// value are both reasonable. For instance, with 7 items and looking for median,
|
||||
// we'd have 3 less and 3 greater, and we can't really do better than that.
|
||||
break
|
||||
}
|
||||
switch v := possibleNthVal.(type) {
|
||||
case int64:
|
||||
return ValCount{
|
||||
Val: v,
|
||||
Count: 1,
|
||||
}, nil
|
||||
case pql.Decimal:
|
||||
return ValCount{
|
||||
DecimalVal: &v,
|
||||
FloatVal: v.Float64(),
|
||||
Count: 1,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected percentile Nth value type %T", possibleNthVal)
|
||||
// 'weight' the left count as per k
|
||||
leftCountWeighted := int64(math.Round(k * float64(leftCount)))
|
||||
|
||||
// binary search
|
||||
if leftCountWeighted > rightCount {
|
||||
max = possibleNthVal - 1
|
||||
} else if leftCountWeighted < rightCount {
|
||||
min = possibleNthVal + 1
|
||||
} else {
|
||||
return field.valCountize(possibleNthVal, 1, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return field.valCountize(min, 1, nil)
|
||||
}
|
||||
|
||||
// executeMinRow executes a MinRow() call.
|
||||
|
|
|
|||
|
|
@ -7641,22 +7641,13 @@ func variousQueriesOnPercentiles(t *testing.T, c *test.Cluster) {
|
|||
if nth == 0.0 {
|
||||
return min
|
||||
}
|
||||
if nth == 100.0 {
|
||||
return max
|
||||
}
|
||||
k := (100 - nth) / nth
|
||||
|
||||
possibleNthVal := int64(0)
|
||||
desiredLess := int((float64(len(nums)) * nth) / 100.0)
|
||||
desiredGreater := int((float64(len(nums)) * (100 - nth)) / 100.0)
|
||||
if desiredLess == 0 {
|
||||
return min
|
||||
}
|
||||
if desiredGreater == 0 {
|
||||
return max
|
||||
}
|
||||
// bin search
|
||||
for min < max {
|
||||
possibleNthVal = ((max / 2) + (min / 2)) + (((max % 2) + (min % 2)) / 2)
|
||||
leftCount, rightCount := 0, 0
|
||||
leftCount, rightCount := int64(0), int64(0)
|
||||
for _, num := range nums {
|
||||
if num < possibleNthVal {
|
||||
leftCount++
|
||||
|
|
@ -7665,9 +7656,11 @@ func variousQueriesOnPercentiles(t *testing.T, c *test.Cluster) {
|
|||
}
|
||||
}
|
||||
|
||||
if leftCount > desiredLess {
|
||||
leftCountWeighted := int64(math.Round(k * float64(leftCount)))
|
||||
|
||||
if leftCountWeighted > rightCount {
|
||||
max = possibleNthVal - 1
|
||||
} else if rightCount > desiredGreater {
|
||||
} else if leftCountWeighted < rightCount {
|
||||
min = possibleNthVal + 1
|
||||
} else { // perfectly balanced, as all things should be
|
||||
return possibleNthVal
|
||||
|
|
|
|||
9
field.go
9
field.go
|
|
@ -1627,11 +1627,6 @@ func (f *Field) MinForShard(qcx *Qcx, shard uint64, filter *Row) (ValCount, erro
|
|||
// includes the int64 "Val\" value to make comparisons easier in the
|
||||
// executor (at time of writing, Percentile takes advantage of this,
|
||||
// but we might be able to simplify logic in other places as well).
|
||||
//
|
||||
// Note that the ValCount returned has bsig.Base included, or if
|
||||
// you specify a nil bsig, includes the field's bsig.Base. Which is
|
||||
// to say, don't use this if you have a value that's already been
|
||||
// adjusted by base.
|
||||
func (f *Field) valCountize(val int64, cnt uint64, bsig *bsiGroup) (ValCount, error) {
|
||||
if bsig == nil {
|
||||
bsig = f.bsiGroup(f.name)
|
||||
|
|
@ -1642,10 +1637,10 @@ func (f *Field) valCountize(val int64, cnt uint64, bsig *bsiGroup) (ValCount, er
|
|||
}
|
||||
valCount := ValCount{Count: int64(cnt)}
|
||||
|
||||
if f.options.Type == FieldTypeDecimal {
|
||||
if f.Options().Type == FieldTypeDecimal {
|
||||
dec := pql.NewDecimal(val+bsig.Base, bsig.Scale)
|
||||
valCount.DecimalVal = &dec
|
||||
} else if f.options.Type == FieldTypeTimestamp {
|
||||
} else if f.Options().Type == FieldTypeTimestamp {
|
||||
ts, err := ValToTimestamp(f.options.TimeUnit, val+bsig.Base)
|
||||
if err != nil {
|
||||
return ValCount{}, errors.Wrap(err, "translating value to timestamp")
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -137,7 +137,6 @@ require (
|
|||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
|
||||
github.com/tinylib/msgp v1.1.2 // indirect
|
||||
gonum.org/v1/gonum v0.11.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -198,7 +197,6 @@ require (
|
|||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/sajari/regression v1.0.1
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
|
|
|
|||
3
go.sum
3
go.sum
|
|
@ -1056,8 +1056,6 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sajari/regression v1.0.1 h1:iTVc6ZACGCkoXC+8NdqH5tIreslDTT/bXxT6OmHR5PE=
|
||||
github.com/sajari/regression v1.0.1/go.mod h1:NeG/XTW1lYfGY7YV/Z0nYDV/RGh3wxwd1yW46835flM=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
|
|
@ -1686,7 +1684,6 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq
|
|||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
|
||||
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
|
|
|
|||
|
|
@ -191,11 +191,13 @@ func (m *importer) EncodeImportValues(ctx context.Context, tid dax.TableID, fld
|
|||
return "", nil, errors.Wrapf(err, "getting qtbl")
|
||||
}
|
||||
|
||||
// Since we're calling EncodeImportValues on the client, we don't actually
|
||||
// need a valid client (that method doesn't actually use the client).
|
||||
// Really, that method should be a function on the client package rather
|
||||
// than a method on the Client type.
|
||||
fbClient, err := m.fbClient("")
|
||||
address, err := m.controller.IngestShard(context.Background(), qtbl.QualifiedID(), dax.ShardNum(shard))
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "calling ingest-shard")
|
||||
}
|
||||
|
||||
// Set up a FeatureBase client with address.
|
||||
fbClient, err := m.fbClient(address)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "getting featurebase client")
|
||||
}
|
||||
|
|
@ -214,11 +216,13 @@ func (m *importer) EncodeImport(ctx context.Context, tid dax.TableID, fld *dax.F
|
|||
return "", nil, errors.Wrapf(err, "getting qtbl")
|
||||
}
|
||||
|
||||
// Since we're calling EncodeImportValues on the client, we don't actually
|
||||
// need a valid client (that method doesn't actually use the client).
|
||||
// Really, that method should be a function on the client package rather
|
||||
// than a method on the Client type.
|
||||
fbClient, err := m.fbClient("")
|
||||
address, err := m.controller.IngestShard(context.Background(), qtbl.QualifiedID(), dax.ShardNum(shard))
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "calling ingest-shard")
|
||||
}
|
||||
|
||||
// Set up a FeatureBase client with address.
|
||||
fbClient, err := m.fbClient(address)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "getting featurebase client")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,10 +135,6 @@ func newPreconditionFailedError(err error) PreconditionFailedError {
|
|||
}
|
||||
|
||||
// Regular expression to validate index and field names.
|
||||
// The lowest limitation I've seen on any filesystem we care about is 255
|
||||
// characters. 230 leaves enough space that an index or field could be
|
||||
// backed up and have a timestamp and file extension appended while
|
||||
// still allowing for much longer index and field names. --Jaffee
|
||||
var nameRegexp = regexp.MustCompile(`^[a-z][a-z0-9Θ_-]{0,229}$`)
|
||||
|
||||
// TimeFormat is the go-style time format used to parse string dates.
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@ const (
|
|||
ErrUnsupported errors.Code = "ErrUnsupported"
|
||||
ErrCacheKeyNotFound errors.Code = "ErrCacheKeyNotFound"
|
||||
|
||||
// syntax/semantic errors
|
||||
ErrDuplicateColumn errors.Code = "ErrDuplicateColumn"
|
||||
ErrUnknownType errors.Code = "ErrUnknownType"
|
||||
ErrUnknownIdentifier errors.Code = "ErrUnknownIdentifier"
|
||||
ErrTopLimitCannotCoexist errors.Code = "ErrTopLimitCannotCoexist"
|
||||
ErrDuplicateColumn errors.Code = "ErrDuplicateColumn"
|
||||
ErrUnknownType errors.Code = "ErrUnknownType"
|
||||
ErrUnknownIdentifier errors.Code = "ErrUnknownIdentifier"
|
||||
|
||||
// type related errors
|
||||
ErrTypeIncompatibleWithBitwiseOperator errors.Code = "ErrTypeIncompatibleWithBitwiseOperator"
|
||||
ErrTypeIncompatibleWithLogicalOperator errors.Code = "ErrTypeIncompatibleWithLogicalOperator"
|
||||
ErrTypeIncompatibleWithEqualityOperator errors.Code = "ErrTypeIncompatibleWithEqualityOperator"
|
||||
|
|
@ -43,6 +40,8 @@ const (
|
|||
ErrTimeQuantumExpressionExpected errors.Code = "ErrTimeQuantumExpressionExpected"
|
||||
ErrSingleRowExpected errors.Code = "ErrSingleRowExpected"
|
||||
|
||||
// type related errors
|
||||
|
||||
// decimal
|
||||
ErrDecimalScaleExpected errors.Code = "ErrDecimalScaleExpected"
|
||||
|
||||
|
|
@ -95,9 +94,6 @@ const (
|
|||
ErrViewExists errors.Code = "ErrViewExists"
|
||||
ErrViewNotFound errors.Code = "ErrViewNotFound"
|
||||
|
||||
ErrModelExists errors.Code = "ErrModelExists"
|
||||
ErrModelNotFound errors.Code = "ErrModelNotFound"
|
||||
|
||||
ErrBadColumnConstraint errors.Code = "ErrBadColumnConstraint"
|
||||
ErrConflictingColumnConstraint errors.Code = "ErrConflictingColumnConstraint"
|
||||
|
||||
|
|
@ -145,16 +141,6 @@ const (
|
|||
ErrInvalidDatetimePart errors.Code = "ErrInvalidDatetimePart"
|
||||
ErrOutputValueOutOfRange errors.Code = "ErrOutputValueOutOfRange"
|
||||
ErrDivideByZero errors.Code = "ErrDivideByZero"
|
||||
|
||||
// remote execution
|
||||
ErrRemoteUnauthorized errors.Code = "ErrRemoteUnauthorized"
|
||||
|
||||
// query hints
|
||||
ErrUnknownQueryHint errors.Code = "ErrInvalidQueryHint"
|
||||
ErrInvalidQueryHintParameterCount errors.Code = "ErrInvalidQueryHintParameterCount"
|
||||
|
||||
// show options
|
||||
ErrUnknownShowOption errors.Code = "ErrUnknownShowOption"
|
||||
)
|
||||
|
||||
func NewErrDuplicateColumn(line int, col int, column string) error {
|
||||
|
|
@ -178,13 +164,6 @@ func NewErrUnknownIdentifier(line int, col int, ident string) error {
|
|||
)
|
||||
}
|
||||
|
||||
func NewErrErrTopLimitCannotCoexist(line int, col int) error {
|
||||
return errors.New(
|
||||
ErrTopLimitCannotCoexist,
|
||||
fmt.Sprintf("[%d:%d] TOP and LIMIT cannot cannot be used at the same time (TOP will be deprecated in a future release)", line, col),
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrInternal(msg string) error {
|
||||
preamble := "internal error"
|
||||
_, filename, line, ok := runtime.Caller(1)
|
||||
|
|
@ -668,20 +647,6 @@ func NewErrViewExists(line, col int, viewName string) error {
|
|||
)
|
||||
}
|
||||
|
||||
func NewErrModelNotFound(line, col int, viewName string) error {
|
||||
return errors.New(
|
||||
ErrModelNotFound,
|
||||
fmt.Sprintf("[%d:%d] model '%s' not found", line, col, viewName),
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrModelExists(line, col int, viewName string) error {
|
||||
return errors.New(
|
||||
ErrModelExists,
|
||||
fmt.Sprintf("[%d:%d] model '%s' already exists", line, col, viewName),
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrBadColumnConstraint(line, col int, constraint, columnType string) error {
|
||||
return errors.New(
|
||||
ErrBadColumnConstraint,
|
||||
|
|
@ -911,35 +876,3 @@ func NewErrDivideByZero(line, col int) error {
|
|||
fmt.Sprintf("[%d:%d] divisor is equal to zero", line, col),
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrRemoteUnauthorized(line, col int, remoteUrl string) error {
|
||||
return errors.New(
|
||||
ErrRemoteUnauthorized,
|
||||
fmt.Sprintf("unauthorized on remote server '%s'", remoteUrl),
|
||||
)
|
||||
}
|
||||
|
||||
// query hints
|
||||
|
||||
func NewErrUnknownQueryHint(line, col int, hintName string) error {
|
||||
return errors.New(
|
||||
ErrUnknownQueryHint,
|
||||
fmt.Sprintf("[%d:%d] unknown query hint '%s'", line, col, hintName),
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrInvalidQueryHintParameterCount(line, col int, hintName string, desiredList string, desiredCount int, actualCount int) error {
|
||||
return errors.New(
|
||||
ErrInvalidQueryHintParameterCount,
|
||||
fmt.Sprintf("[%d:%d] query hint '%s' expected %d parameter(s) (%s), got %d parameters", line, col, hintName, desiredCount, desiredList, actualCount),
|
||||
)
|
||||
}
|
||||
|
||||
// show options
|
||||
|
||||
func NewErrUnknownShowOption(line, col int, optionName string) error {
|
||||
return errors.New(
|
||||
ErrUnknownShowOption,
|
||||
fmt.Sprintf("[%d:%d] unknown show option '%s'", line, col, optionName),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,13 +34,11 @@ func (*CaseExpr) node() {}
|
|||
func (*CastExpr) node() {}
|
||||
func (*CheckConstraint) node() {}
|
||||
func (*ColumnDefinition) node() {}
|
||||
func (*CopyStatement) node() {}
|
||||
func (*CommitStatement) node() {}
|
||||
func (*CreateDatabaseStatement) node() {}
|
||||
func (*CreateIndexStatement) node() {}
|
||||
func (*CreateTableStatement) node() {}
|
||||
func (*CreateFunctionStatement) node() {}
|
||||
func (*CreateModelStatement) node() {}
|
||||
func (*CreateViewStatement) node() {}
|
||||
func (*DateLit) node() {}
|
||||
func (*DefaultConstraint) node() {}
|
||||
|
|
@ -50,7 +48,6 @@ func (*DropIndexStatement) node() {}
|
|||
func (*DropTableStatement) node() {}
|
||||
func (*DropFunctionStatement) node() {}
|
||||
func (*DropViewStatement) node() {}
|
||||
func (*DropModelStatement) node() {}
|
||||
func (*Exists) node() {}
|
||||
func (*ExplainStatement) node() {}
|
||||
func (*ExprList) node() {}
|
||||
|
|
@ -76,21 +73,18 @@ func (*OnConstraint) node() {}
|
|||
func (*OrderingTerm) node() {}
|
||||
func (*OverClause) node() {}
|
||||
func (*ParenExpr) node() {}
|
||||
func (*PredictStatement) node() {}
|
||||
func (*SetLiteralExpr) node() {}
|
||||
func (*ParenSource) node() {}
|
||||
func (*PrimaryKeyConstraint) node() {}
|
||||
func (*QualifiedRef) node() {}
|
||||
func (*QualifiedTableName) node() {}
|
||||
func (*Range) node() {}
|
||||
func (*ReturnStatement) node() {}
|
||||
func (*ReleaseStatement) node() {}
|
||||
func (*ResultColumn) node() {}
|
||||
func (*RollbackStatement) node() {}
|
||||
func (*SavepointStatement) node() {}
|
||||
func (*SelectStatement) node() {}
|
||||
func (*StringLit) node() {}
|
||||
func (*TableQueryOption) node() {}
|
||||
func (*TableValuedFunction) node() {}
|
||||
func (*TimeUnitConstraint) node() {}
|
||||
func (*TimeQuantumConstraint) node() {}
|
||||
|
|
@ -117,7 +111,6 @@ func (*AlterTableStatement) stmt() {}
|
|||
func (*AlterViewStatement) stmt() {}
|
||||
func (*AnalyzeStatement) stmt() {}
|
||||
func (*BeginStatement) stmt() {}
|
||||
func (*CopyStatement) stmt() {}
|
||||
func (*BulkInsertStatement) stmt() {}
|
||||
func (*ShowDatabasesStatement) stmt() {}
|
||||
func (*ShowTablesStatement) stmt() {}
|
||||
|
|
@ -128,7 +121,6 @@ func (*CreateDatabaseStatement) stmt() {}
|
|||
func (*CreateIndexStatement) stmt() {}
|
||||
func (*CreateTableStatement) stmt() {}
|
||||
func (*CreateFunctionStatement) stmt() {}
|
||||
func (*CreateModelStatement) stmt() {}
|
||||
func (*CreateViewStatement) stmt() {}
|
||||
func (*DeleteStatement) stmt() {}
|
||||
func (*DropDatabaseStatement) stmt() {}
|
||||
|
|
@ -136,12 +128,9 @@ func (*DropIndexStatement) stmt() {}
|
|||
func (*DropTableStatement) stmt() {}
|
||||
func (*DropFunctionStatement) stmt() {}
|
||||
func (*DropViewStatement) stmt() {}
|
||||
func (*DropModelStatement) stmt() {}
|
||||
func (*PredictStatement) stmt() {}
|
||||
func (*ExplainStatement) stmt() {}
|
||||
func (*InsertStatement) stmt() {}
|
||||
func (*ReleaseStatement) stmt() {}
|
||||
func (*ReturnStatement) stmt() {}
|
||||
func (*RollbackStatement) stmt() {}
|
||||
func (*SavepointStatement) stmt() {}
|
||||
func (*SelectStatement) stmt() {}
|
||||
|
|
@ -188,14 +177,10 @@ func CloneStatement(stmt Statement) Statement {
|
|||
return stmt.Clone()
|
||||
case *DropViewStatement:
|
||||
return stmt.Clone()
|
||||
case *DropModelStatement:
|
||||
return stmt.Clone()
|
||||
case *ExplainStatement:
|
||||
return stmt.Clone()
|
||||
case *InsertStatement:
|
||||
return stmt.Clone()
|
||||
case *BulkInsertStatement:
|
||||
return stmt.Clone()
|
||||
case *ReleaseStatement:
|
||||
return stmt.Clone()
|
||||
case *RollbackStatement:
|
||||
|
|
@ -320,12 +305,6 @@ func CloneExpr(expr Expr) Expr {
|
|||
return expr.Clone()
|
||||
case *Variable:
|
||||
return expr.Clone()
|
||||
case *SysVariable:
|
||||
return expr.Clone()
|
||||
case *DateLit:
|
||||
return expr.Clone()
|
||||
case *SetLiteralExpr:
|
||||
return expr.Clone()
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid expr type: %T", expr))
|
||||
}
|
||||
|
|
@ -540,19 +519,11 @@ func (s *ShowDatabasesStatement) Clone() *ShowDatabasesStatement {
|
|||
type ShowTablesStatement struct {
|
||||
Show Pos // position of SHOW
|
||||
Tables Pos // position of TABLES
|
||||
With Pos
|
||||
System *Ident
|
||||
}
|
||||
|
||||
// String returns the string representation of the statement.
|
||||
func (s *ShowTablesStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("SHOW TABLES")
|
||||
if s.With.IsValid() {
|
||||
buf.WriteString(" WITH")
|
||||
fmt.Fprintf(&buf, " %s", s.System.String())
|
||||
}
|
||||
return buf.String()
|
||||
return "SHOW TABLES"
|
||||
}
|
||||
|
||||
func (s *ShowTablesStatement) Clone() *ShowTablesStatement {
|
||||
|
|
@ -1266,8 +1237,10 @@ func (c *CacheTypeConstraint) String() string {
|
|||
}
|
||||
|
||||
type TimeUnitConstraint struct {
|
||||
TimeUnit Pos // position of TIMEUNIT keyword
|
||||
Expr Expr // expression
|
||||
TimeUnit Pos // position of TIMEUNIT keyword
|
||||
Expr Expr // expression
|
||||
Epoch Pos // position of TIMEUNIT keyword
|
||||
EpochExpr Expr // expression
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of c.
|
||||
|
|
@ -1285,6 +1258,10 @@ func (c *TimeUnitConstraint) String() string {
|
|||
var buf bytes.Buffer
|
||||
buf.WriteString("TIMEUNIT ")
|
||||
buf.WriteString(c.Expr.String())
|
||||
if c.Epoch.IsValid() {
|
||||
buf.WriteString(" EPOCH ")
|
||||
buf.WriteString(c.EpochExpr.String())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
|
|
@ -1713,10 +1690,8 @@ func IdentName(ident *Ident) string {
|
|||
return ident.Name
|
||||
}
|
||||
|
||||
// SysVariable represents built-in system variables that can be referenced in
|
||||
// the sql for current date, current time and other potential system determinable
|
||||
// values. In SQL these system provided data elements are referenced using
|
||||
// keywords such as CURRENT_DATE & CURRENT_TIMESTAMP, etc.
|
||||
// SysVariable represents built-in system variables that can be referenced in the sql for current date, current time and other potential pre-determinable values.
|
||||
// In SQL these system provided data elements are referenced using keywords such as CURRENT_DATE & CURRENT_TIMESTAMP, etc.
|
||||
type SysVariable struct {
|
||||
NamePos Pos // variable position in sql
|
||||
Token Token // parser token mapped to the variable's name/keyword
|
||||
|
|
@ -1736,11 +1711,11 @@ func (svar *SysVariable) Clone() *SysVariable {
|
|||
return &other
|
||||
}
|
||||
func (svar *SysVariable) Name() string {
|
||||
return svar.String()
|
||||
return tokens[svar.Token]
|
||||
}
|
||||
|
||||
func (svar *SysVariable) String() string {
|
||||
return svar.Token.String()
|
||||
return svar.Name()
|
||||
}
|
||||
|
||||
func (svar *SysVariable) DataType() ExprDataType {
|
||||
|
|
@ -2931,35 +2906,6 @@ func (s *DropViewStatement) String() string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
type DropModelStatement struct {
|
||||
Drop Pos // position of DROP keyword
|
||||
Model Pos // position of MODEL keyword
|
||||
If Pos // position of IF keyword
|
||||
IfExists Pos // position of EXISTS keyword after IF
|
||||
Name *Ident // view name
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of s.
|
||||
func (s *DropModelStatement) Clone() *DropModelStatement {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
other := *s
|
||||
other.Name = s.Name.Clone()
|
||||
return &other
|
||||
}
|
||||
|
||||
// String returns the string representation of the statement.
|
||||
func (s *DropModelStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("DROP MODEL")
|
||||
if s.IfExists.IsValid() {
|
||||
buf.WriteString(" IF EXISTS")
|
||||
}
|
||||
fmt.Fprintf(&buf, " %s", s.Name.String())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type CreateIndexStatement struct {
|
||||
Create Pos // position of CREATE keyword
|
||||
Unique Pos // position of optional UNIQUE keyword
|
||||
|
|
@ -3048,11 +2994,6 @@ func (s *DropIndexStatement) String() string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
type FunctionOptionDefinition struct {
|
||||
Name *Ident // option name
|
||||
OptionExpr Expr // option expression
|
||||
}
|
||||
|
||||
type ParameterDefinition struct {
|
||||
Name *Variable // parameter name
|
||||
Type *Type // data type
|
||||
|
|
@ -3070,11 +3011,8 @@ type CreateFunctionStatement struct {
|
|||
Parameters []*ParameterDefinition // parameters
|
||||
Rparen Pos // position of parameter RParen
|
||||
|
||||
Returns Pos // position of RETURNS keyword
|
||||
ReturnType *Type // return def
|
||||
|
||||
With Pos // position of WITH keyword
|
||||
Options []*FunctionOptionDefinition // options
|
||||
Returns Pos // position of RETURNS keyword
|
||||
ReturnDef *ParameterDefinition // return def
|
||||
|
||||
As Pos // position of AS keyword
|
||||
|
||||
|
|
@ -3115,21 +3053,7 @@ func (s *CreateFunctionStatement) String() string {
|
|||
}
|
||||
|
||||
buf.WriteString(" RETURNS ")
|
||||
fmt.Fprintf(&buf, "%s", s.ReturnType.String())
|
||||
|
||||
if s.With.IsValid() {
|
||||
buf.WriteString(" WITH ")
|
||||
if len(s.Options) > 0 {
|
||||
buf.WriteString(" (")
|
||||
for idx, p := range s.Options {
|
||||
if idx > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%s %s", p.Name.Name, p.OptionExpr.String())
|
||||
}
|
||||
buf.WriteString(")")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&buf, "%s %s", s.ReturnDef.Name, s.ReturnDef.Type.Name)
|
||||
|
||||
buf.WriteString(" AS BEGIN")
|
||||
for i := range s.Body {
|
||||
|
|
@ -3168,86 +3092,6 @@ func (s *DropFunctionStatement) String() string {
|
|||
return buf.String()
|
||||
}
|
||||
|
||||
type ReturnStatement struct {
|
||||
Return Pos // position of RETURN keyword
|
||||
ReturnExpr Expr // what we are returning
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of s.
|
||||
func (s *ReturnStatement) Clone() *ReturnStatement {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
other := *s
|
||||
other.ReturnExpr = CloneExpr(s.ReturnExpr)
|
||||
return &other
|
||||
}
|
||||
|
||||
func (s *ReturnStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("RETURN")
|
||||
fmt.Fprintf(&buf, " %s", s.ReturnExpr.String())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type ModelOptionDefinition struct {
|
||||
Name *Ident // option name
|
||||
OptionExpr Expr // option expression
|
||||
}
|
||||
|
||||
type CreateModelStatement struct {
|
||||
Create Pos // position of CREATE keyword
|
||||
Model Pos // position of MODEL keyword
|
||||
If Pos // position of IF keyword
|
||||
IfNot Pos // position of NOT keyword after IF
|
||||
IfNotExists Pos // position of EXISTS keyword after IF NOT
|
||||
Name *Ident // model name
|
||||
With Pos // position of WITH keyword
|
||||
|
||||
Options []*ModelOptionDefinition // options
|
||||
|
||||
As Pos // position of AS keyword
|
||||
|
||||
ModelQuery *SelectStatement // model query
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of s.
|
||||
func (s *CreateModelStatement) Clone() *CreateModelStatement {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
other := *s
|
||||
other.Name = s.Name.Clone()
|
||||
other.ModelQuery = s.ModelQuery.Clone()
|
||||
return &other
|
||||
}
|
||||
|
||||
// String returns the string representation of the statement.
|
||||
func (s *CreateModelStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("CREATE MODEL")
|
||||
if s.IfNotExists.IsValid() {
|
||||
buf.WriteString(" IF NOT EXISTS")
|
||||
}
|
||||
fmt.Fprintf(&buf, " %s", s.Name.String())
|
||||
|
||||
if len(s.Options) > 0 {
|
||||
buf.WriteString(" (")
|
||||
for idx, p := range s.Options {
|
||||
if idx > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%s %s", p.Name.Name, p.OptionExpr.String())
|
||||
}
|
||||
buf.WriteString(")")
|
||||
}
|
||||
|
||||
buf.WriteString(" AS ")
|
||||
buf.WriteString(s.ModelQuery.String())
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type BulkInsertMapDefinition struct {
|
||||
Name *Ident // map name
|
||||
Type *Type // data type
|
||||
|
|
@ -3271,6 +3115,8 @@ func (c *BulkInsertMapDefinition) String() string {
|
|||
var buf bytes.Buffer
|
||||
buf.WriteString(c.MapExpr.String())
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(c.Name.String())
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(c.Type.String())
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -3356,65 +3202,35 @@ func (s *BulkInsertStatement) String() string {
|
|||
|
||||
buf.WriteString(" FROM ")
|
||||
fmt.Fprintf(&buf, " %s", s.DataSource.String())
|
||||
buf.WriteString(" WITH")
|
||||
buf.WriteString(" WITH ")
|
||||
|
||||
if s.Format != nil {
|
||||
buf.WriteString(" FORMAT ")
|
||||
buf.WriteString("FORMAT ")
|
||||
buf.WriteString(s.Format.String())
|
||||
}
|
||||
|
||||
if s.Input != nil {
|
||||
buf.WriteString(" INPUT ")
|
||||
buf.WriteString("INPUT ")
|
||||
buf.WriteString(s.Input.String())
|
||||
}
|
||||
|
||||
if s.HeaderRow != nil {
|
||||
buf.WriteString(" HEADER_ROW ")
|
||||
buf.WriteString("HEADER_ROW ")
|
||||
}
|
||||
|
||||
if s.BatchSize != nil {
|
||||
buf.WriteString(" BATCHSIZE ")
|
||||
buf.WriteString("BATCHSIZE ")
|
||||
buf.WriteString(s.BatchSize.String())
|
||||
}
|
||||
|
||||
if s.RowsLimit != nil {
|
||||
buf.WriteString(" ROWSLIMIT ")
|
||||
buf.WriteString("ROWSLIMIT ")
|
||||
buf.WriteString(s.RowsLimit.String())
|
||||
}
|
||||
|
||||
if s.AllowMissingValues != nil {
|
||||
buf.WriteString(" ALLOW_MISSING_VALUES ")
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (s *BulkInsertStatement) Clone() *BulkInsertStatement {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
other := *s
|
||||
other.Table = s.Table.Clone()
|
||||
other.Columns = cloneIdents(s.Columns)
|
||||
other.TransformList = cloneExprs(s.TransformList)
|
||||
other.DataSource = CloneExpr(s.DataSource)
|
||||
other.BatchSize = CloneExpr(s.BatchSize)
|
||||
other.RowsLimit = CloneExpr(s.RowsLimit)
|
||||
other.Format = CloneExpr(s.Format)
|
||||
other.Input = CloneExpr(s.Input)
|
||||
other.HeaderRow = CloneExpr(s.HeaderRow)
|
||||
other.AllowMissingValues = CloneExpr(s.AllowMissingValues)
|
||||
other.MapList = cloneBulkInsertMap(s.MapList)
|
||||
return &other
|
||||
}
|
||||
|
||||
func cloneBulkInsertMap(s []*BulkInsertMapDefinition) []*BulkInsertMapDefinition {
|
||||
other := make([]*BulkInsertMapDefinition, len(s))
|
||||
for i := range s {
|
||||
other[i] = s[i].Clone()
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
type InsertStatement struct {
|
||||
//WithClause *WithClause // clause containing CTEs
|
||||
|
||||
|
|
@ -3797,77 +3613,6 @@ func (c *IndexedColumn) String() string {
|
|||
return c.X.String()
|
||||
}
|
||||
|
||||
type CopyStatement struct {
|
||||
Copy Pos // position of COPY keyword
|
||||
Source Source // source table
|
||||
To Pos // position of TO keyword
|
||||
TargetName *Ident // target table name
|
||||
Where Pos // position of WHERE keyword
|
||||
WhereExpr Expr // where clause expression
|
||||
With Pos // position of WITH keyword
|
||||
|
||||
Url Expr // url for target server
|
||||
ApiKey Expr // apikey for target server
|
||||
}
|
||||
|
||||
func (c *CopyStatement) Clone() *CopyStatement {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
other := *c
|
||||
other.Source = CloneSource(c.Source)
|
||||
other.TargetName = c.TargetName.Clone()
|
||||
other.WhereExpr = CloneExpr(c.WhereExpr)
|
||||
other.Url = CloneExpr(c.Url)
|
||||
other.ApiKey = CloneExpr(c.ApiKey)
|
||||
return &other
|
||||
}
|
||||
|
||||
func (c *CopyStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&buf, "COPY %s to %s", c.Source.String(), c.TargetName.String())
|
||||
if c.WhereExpr != nil {
|
||||
fmt.Fprintf(&buf, " WHERE %s", c.WhereExpr.String())
|
||||
}
|
||||
if c.With.IsValid() {
|
||||
fmt.Fprintf(&buf, " WITH")
|
||||
if c.Url != nil {
|
||||
fmt.Fprintf(&buf, " URL %s", c.Url.String())
|
||||
}
|
||||
if c.ApiKey != nil {
|
||||
fmt.Fprintf(&buf, " APIKEY %s", c.Url.String())
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type PredictStatement struct {
|
||||
Predict Pos // position of PREDICT keyword
|
||||
Using Pos // position of USING keyword
|
||||
ModelName *Ident // model name
|
||||
|
||||
InputQuery *SelectStatement // input query
|
||||
}
|
||||
|
||||
func (c *PredictStatement) Clone() *PredictStatement {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
other := *c
|
||||
other.ModelName = c.ModelName.Clone()
|
||||
other.InputQuery = c.InputQuery.Clone()
|
||||
return &other
|
||||
}
|
||||
|
||||
func (c *PredictStatement) String() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&buf, "PREDICT USING %s", c.ModelName.String())
|
||||
fmt.Fprintf(&buf, " %s", c.InputQuery.String())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type SelectStatement struct {
|
||||
WithClause *WithClause // clause containing CTEs
|
||||
|
||||
|
|
@ -3908,8 +3653,6 @@ type SelectStatement struct {
|
|||
OrderBy Pos // position of BY keyword after ORDER
|
||||
OrderingTerms []*OrderingTerm // terms of ORDER BY clause
|
||||
|
||||
Limit Pos // position of LIMIT keyword
|
||||
LimitExpr Expr // LIMIT expr
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of s.
|
||||
|
|
@ -3919,6 +3662,7 @@ func (s *SelectStatement) Clone() *SelectStatement {
|
|||
}
|
||||
other := *s
|
||||
other.WithClause = s.WithClause.Clone()
|
||||
//other.ValueLists = cloneExprLists(s.ValueLists)
|
||||
other.TopExpr = CloneExpr(s.TopExpr)
|
||||
other.Columns = cloneResultColumns(s.Columns)
|
||||
other.Source = CloneSource(s.Source)
|
||||
|
|
@ -3928,7 +3672,6 @@ func (s *SelectStatement) Clone() *SelectStatement {
|
|||
other.Windows = cloneWindows(s.Windows)
|
||||
other.Compound = s.Compound.Clone()
|
||||
other.OrderingTerms = cloneOrderingTerms(s.OrderingTerms)
|
||||
other.LimitExpr = CloneExpr(s.LimitExpr)
|
||||
return &other
|
||||
}
|
||||
|
||||
|
|
@ -3967,10 +3710,29 @@ func (s *SelectStatement) String() string {
|
|||
buf.WriteString(" ")
|
||||
}
|
||||
|
||||
/*if len(s.ValueLists) > 0 {
|
||||
buf.WriteString("VALUES ")
|
||||
for i, exprs := range s.ValueLists {
|
||||
if i != 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
|
||||
buf.WriteString("(")
|
||||
for j, expr := range exprs.Exprs {
|
||||
if j != 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
buf.WriteString(expr.String())
|
||||
}
|
||||
buf.WriteString(")")
|
||||
}
|
||||
} else {*/
|
||||
buf.WriteString("SELECT ")
|
||||
if s.Distinct.IsValid() {
|
||||
buf.WriteString("DISTINCT ")
|
||||
}
|
||||
} //else if s.All.IsValid() {
|
||||
// buf.WriteString("ALL ")
|
||||
//}
|
||||
if s.Top.IsValid() {
|
||||
fmt.Fprintf(&buf, "TOP(%s) ", s.TopExpr.String())
|
||||
}
|
||||
|
|
@ -4016,6 +3778,7 @@ func (s *SelectStatement) String() string {
|
|||
buf.WriteString(window.String())
|
||||
}
|
||||
}
|
||||
// }
|
||||
|
||||
// Write compound operator.
|
||||
if s.Compound != nil {
|
||||
|
|
@ -4045,10 +3808,6 @@ func (s *SelectStatement) String() string {
|
|||
}
|
||||
}
|
||||
|
||||
if s.Limit.IsValid() {
|
||||
fmt.Fprintf(&buf, " LIMIT %s", s.LimitExpr.String())
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
|
|
@ -4148,45 +3907,15 @@ func (c *ResultColumn) String() string {
|
|||
return c.Expr.String()
|
||||
}
|
||||
|
||||
type TableQueryOption struct {
|
||||
OptionName *Ident
|
||||
LParen Pos
|
||||
OptionParams []*Ident
|
||||
RParen Pos
|
||||
}
|
||||
|
||||
func (n *TableQueryOption) Clone() *TableQueryOption {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
other := *n
|
||||
other.OptionName = n.OptionName.Clone()
|
||||
other.OptionParams = cloneIdents(n.OptionParams)
|
||||
return &other
|
||||
}
|
||||
|
||||
func (n *TableQueryOption) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(n.OptionName.String())
|
||||
buf.WriteString("(")
|
||||
for i, o := range n.OptionParams {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, " %s", o.String())
|
||||
}
|
||||
buf.WriteString(")")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type QualifiedTableName struct {
|
||||
Name *Ident // table name
|
||||
As Pos // position of AS keyword
|
||||
Alias *Ident // optional table alias
|
||||
With Pos // position of WITH keyword
|
||||
LParen Pos
|
||||
QueryOptions []*TableQueryOption
|
||||
RParen Pos
|
||||
Name *Ident // table name
|
||||
As Pos // position of AS keyword
|
||||
Alias *Ident // optional table alias
|
||||
Indexed Pos // position of INDEXED keyword
|
||||
IndexedBy Pos // position of BY keyword after INDEXED
|
||||
Not Pos // position of NOT keyword before INDEXED
|
||||
NotIndexed Pos // position of NOT keyword before INDEXED
|
||||
Index *Ident // name of index
|
||||
OutputColumns []*SourceOutputColumn // output columns - populated during analysis
|
||||
}
|
||||
|
||||
|
|
@ -4203,17 +3932,6 @@ func (n *QualifiedTableName) MatchesTablenameOrAlias(match string) bool {
|
|||
return strings.EqualFold(IdentName(n.Alias), match) || strings.EqualFold(IdentName(n.Name), match)
|
||||
}
|
||||
|
||||
func cloneQueryOptions(a []*TableQueryOption) []*TableQueryOption {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
other := make([]*TableQueryOption, len(a))
|
||||
for i := range a {
|
||||
other[i] = a[i].Clone()
|
||||
}
|
||||
return other
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of n.
|
||||
func (n *QualifiedTableName) Clone() *QualifiedTableName {
|
||||
if n == nil {
|
||||
|
|
@ -4222,7 +3940,7 @@ func (n *QualifiedTableName) Clone() *QualifiedTableName {
|
|||
other := *n
|
||||
other.Name = n.Name.Clone()
|
||||
other.Alias = n.Alias.Clone()
|
||||
other.QueryOptions = cloneQueryOptions(n.QueryOptions)
|
||||
other.Index = n.Index.Clone()
|
||||
return &other
|
||||
}
|
||||
|
||||
|
|
@ -4237,15 +3955,10 @@ func (n *QualifiedTableName) String() string {
|
|||
fmt.Fprintf(&buf, " %s", n.Alias.String())
|
||||
}
|
||||
|
||||
if n.With.IsValid() {
|
||||
buf.WriteString(" WITH (")
|
||||
for i, o := range n.QueryOptions {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, " %s", o.String())
|
||||
}
|
||||
buf.WriteString(")")
|
||||
if n.Index != nil {
|
||||
fmt.Fprintf(&buf, " INDEXED BY %s", n.Index.String())
|
||||
} else if n.NotIndexed.IsValid() {
|
||||
buf.WriteString(" NOT INDEXED")
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
|
@ -4426,7 +4139,6 @@ func (c *JoinClause) Clone() *JoinClause {
|
|||
other.X = CloneSource(c.X)
|
||||
other.Y = CloneSource(c.Y)
|
||||
other.Constraint = CloneJoinConstraint(c.Constraint)
|
||||
other.Operator = c.Operator.Clone()
|
||||
return &other
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/go-test/deep"
|
||||
|
|
@ -404,8 +403,11 @@ func TestCreateFunctionStatement_String(t *testing.T) {
|
|||
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
},
|
||||
},
|
||||
ReturnType: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
}, `CREATE FUNCTION func (@param1 int) RETURNS int AS BEGIN END`)
|
||||
ReturnDef: &parser.ParameterDefinition{
|
||||
Name: &parser.Variable{Name: "@scalar"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
},
|
||||
}, `CREATE FUNCTION func (@param1 int) RETURNS @scalar int AS BEGIN END`)
|
||||
|
||||
AssertStatementStringer(t, &parser.CreateFunctionStatement{
|
||||
IfNotExists: pos(0),
|
||||
|
|
@ -416,8 +418,11 @@ func TestCreateFunctionStatement_String(t *testing.T) {
|
|||
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
},
|
||||
},
|
||||
ReturnType: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
}, `CREATE FUNCTION IF NOT EXISTS func (@param1 int) RETURNS int AS BEGIN END`)
|
||||
ReturnDef: &parser.ParameterDefinition{
|
||||
Name: &parser.Variable{Name: "@scalar"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
|
||||
},
|
||||
}, `CREATE FUNCTION IF NOT EXISTS func (@param1 int) RETURNS @scalar int AS BEGIN END`)
|
||||
}
|
||||
|
||||
func TestCreateViewStatement_String(t *testing.T) {
|
||||
|
|
@ -696,88 +701,6 @@ func TestInsertStatement_String(t *testing.T) {
|
|||
UpdateWhereExpr: &parser.BoolLit{Value: false},
|
||||
},
|
||||
}, `INSERT INTO "tbl" DEFAULT VALUES ON CONFLICT ("x" ASC, "y" DESC) WHERE TRUE DO UPDATE SET "x" = 100, ("y", "z") = 200 WHERE FALSE`)*/
|
||||
|
||||
// Testing upsert clause separately until it is enabled in Insert.
|
||||
{
|
||||
upsertast := parser.UpsertClause{
|
||||
DoNothing: pos(0),
|
||||
}
|
||||
upsertsql := `ON CONFLICT DO NOTHING`
|
||||
|
||||
if upsertast.String() != upsertsql {
|
||||
t.Fatalf("parser.UpsertClause.String()=%q, want %q", upsertast.String(), upsertsql)
|
||||
}
|
||||
upsertast = parser.UpsertClause{
|
||||
Columns: []*parser.IndexedColumn{
|
||||
{X: &parser.Ident{Name: "x"}, Asc: pos(0)},
|
||||
{X: &parser.Ident{Name: "y"}, Desc: pos(0)},
|
||||
},
|
||||
WhereExpr: &parser.BoolLit{Value: true},
|
||||
Assignments: []*parser.Assignment{
|
||||
{Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}},
|
||||
{Columns: []*parser.Ident{{Name: "y"}, {Name: "z"}}, Expr: &parser.IntegerLit{Value: "200"}},
|
||||
},
|
||||
UpdateWhereExpr: &parser.BoolLit{Value: false},
|
||||
}
|
||||
upsertsql = "ON CONFLICT (x ASC, y DESC) WHERE TRUE DO UPDATE SET x = 100, (y, z) = 200 WHERE FALSE"
|
||||
|
||||
if upsertast.String() != upsertsql {
|
||||
t.Fatalf("parser.UpsertClause.String()=%q, want %q", upsertast.String(), upsertsql)
|
||||
}
|
||||
|
||||
if upsertast.Clone().String() != upsertsql {
|
||||
t.Fatalf("parser.UpsertClause.Clone().String()=%q, want %q", upsertast.Clone().String(), upsertsql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test Bulk Insert for CSV format
|
||||
func TestBulkInsertStatement_String(t *testing.T) {
|
||||
AssertStatementStringer(t, &parser.BulkInsertStatement{
|
||||
Table: &parser.Ident{Name: "tbl"},
|
||||
Columns: []*parser.Ident{
|
||||
{Name: "string"},
|
||||
{Name: "int"},
|
||||
{Name: "decimal"},
|
||||
{Name: "timestamp"},
|
||||
},
|
||||
MapList: []*parser.BulkInsertMapDefinition{
|
||||
{Name: &parser.Ident{Name: "string"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "STRING"}},
|
||||
MapExpr: &parser.Ident{Name: "1"}},
|
||||
{Name: &parser.Ident{Name: "int"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "INT"}},
|
||||
MapExpr: &parser.Ident{Name: "2"}},
|
||||
{Name: &parser.Ident{Name: "decimal"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "DECIMAL"},
|
||||
Scale: &parser.IntegerLit{Value: "2"}},
|
||||
MapExpr: &parser.Ident{Name: "3"}},
|
||||
{Name: &parser.Ident{Name: "timestamp"},
|
||||
Type: &parser.Type{Name: &parser.Ident{Name: "TIMESTAMP"}},
|
||||
MapExpr: &parser.Ident{Name: "4"}},
|
||||
},
|
||||
TransformList: []parser.Expr{
|
||||
&parser.CaseExpr{
|
||||
//Operand: &parser.Ident{Name: "foo"},
|
||||
Blocks: []*parser.CaseBlock{
|
||||
{Condition: &parser.BinaryExpr{Op: parser.EQ, X: &parser.Variable{Name: "@0", VariableIndex: 0}, Y: &parser.StringLit{Value: "Texas"}}, Body: &parser.StringLit{Value: "TX"}},
|
||||
{Condition: &parser.BinaryExpr{Op: parser.EQ, X: &parser.Variable{Name: "@0", VariableIndex: 0}, Y: &parser.StringLit{Value: "Mass"}}, Body: &parser.StringLit{Value: "MA"}},
|
||||
},
|
||||
ElseExpr: &parser.NullLit{},
|
||||
},
|
||||
&parser.BinaryExpr{Op: parser.STAR, X: &parser.Variable{Name: "@1", VariableIndex: 1}, Y: &parser.IntegerLit{Value: "10"}},
|
||||
&parser.Variable{Name: "@2", VariableIndex: 2},
|
||||
&parser.SysVariable{Token: parser.CURRENT_TIMESTAMP},
|
||||
},
|
||||
DataSource: &parser.StringLit{Value: "csvdata.csv"},
|
||||
BatchSize: &parser.IntegerLit{Value: "100000"},
|
||||
Format: &parser.StringLit{Value: "CSV"},
|
||||
Input: &parser.StringLit{Value: "FILE"},
|
||||
RowsLimit: &parser.IntegerLit{Value: "1000000"},
|
||||
HeaderRow: &parser.BoolLit{Value: false},
|
||||
AllowMissingValues: &parser.BoolLit{Value: true},
|
||||
}, `BULK INSERT INTO tbl(string, int, decimal, timestamp) MAP (1 STRING, 2 INT, 3 DECIMAL(2), 4 TIMESTAMP) TRANSFORM (CASE WHEN @0 = 'Texas' THEN 'TX' WHEN @0 = 'Mass' THEN 'MA' ELSE NULL END, @1 * 10, @2, CURRENT_TIMESTAMP) FROM 'csvdata.csv' WITH FORMAT 'CSV' INPUT 'FILE' HEADER_ROW BATCHSIZE 100000 ROWSLIMIT 1000000 ALLOW_MISSING_VALUES `)
|
||||
|
||||
}
|
||||
|
||||
func TestReleaseStatement_String(t *testing.T) {
|
||||
|
|
@ -814,22 +737,6 @@ func TestSelectStatement_String(t *testing.T) {
|
|||
},
|
||||
}, `SELECT DISTINCT x`)
|
||||
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Top: pos(0),
|
||||
TopExpr: &parser.IntegerLit{Value: "10"},
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{Name: "x"}},
|
||||
},
|
||||
}, `SELECT TOP(10) x`)
|
||||
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
TopN: pos(0),
|
||||
TopExpr: &parser.IntegerLit{Value: "10"},
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{Name: "x"}},
|
||||
},
|
||||
}, `SELECT TOPN(10) x`)
|
||||
|
||||
// AssertStatementStringer(t, &sql.SelectStatement{
|
||||
// All: pos(0),
|
||||
// Columns: []*sql.ResultColumn{
|
||||
|
|
@ -860,34 +767,34 @@ func TestSelectStatement_String(t *testing.T) {
|
|||
},
|
||||
}, `SELECT * FROM (SELECT *)`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
// Windows: []*parser.Window{
|
||||
// {
|
||||
// Name: &parser.Ident{Name: "win1"},
|
||||
// Definition: &parser.WindowDefinition{
|
||||
// Base: &parser.Ident{Name: "base"},
|
||||
// Partitions: []parser.Expr{&parser.Ident{Name: "x"}, &parser.Ident{Name: "y"}},
|
||||
// OrderingTerms: []*parser.OrderingTerm{
|
||||
// {X: &parser.Ident{Name: "x"}, Asc: pos(0), NullsFirst: pos(0)},
|
||||
// {X: &parser.Ident{Name: "y"}, Desc: pos(0), NullsLast: pos(0)},
|
||||
// },
|
||||
// Frame: &parser.FrameSpec{
|
||||
// Range: pos(0),
|
||||
// UnboundedX: pos(0),
|
||||
// PrecedingX: pos(0),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Name: &parser.Ident{Name: "win2"},
|
||||
// Definition: &parser.WindowDefinition{
|
||||
// Base: &parser.Ident{Name: "base2"},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }, `SELECT * FROM tbl WINDOW win1 AS (base PARTITION BY x, y ORDER BY x ASC NULLS FIRST, y DESC NULLS LAST RANGE UNBOUNDED PRECEDING), win2 AS (base2)`)
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
Windows: []*parser.Window{
|
||||
{
|
||||
Name: &parser.Ident{Name: "win1"},
|
||||
Definition: &parser.WindowDefinition{
|
||||
Base: &parser.Ident{Name: "base"},
|
||||
Partitions: []parser.Expr{&parser.Ident{Name: "x"}, &parser.Ident{Name: "y"}},
|
||||
OrderingTerms: []*parser.OrderingTerm{
|
||||
{X: &parser.Ident{Name: "x"}, Asc: pos(0), NullsFirst: pos(0)},
|
||||
{X: &parser.Ident{Name: "y"}, Desc: pos(0), NullsLast: pos(0)},
|
||||
},
|
||||
Frame: &parser.FrameSpec{
|
||||
Range: pos(0),
|
||||
UnboundedX: pos(0),
|
||||
PrecedingX: pos(0),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &parser.Ident{Name: "win2"},
|
||||
Definition: &parser.WindowDefinition{
|
||||
Base: &parser.Ident{Name: "base2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, `SELECT * FROM tbl WINDOW win1 AS (base PARTITION BY x, y ORDER BY x ASC NULLS FIRST, y DESC NULLS LAST RANGE UNBOUNDED PRECEDING), win2 AS (base2)`)
|
||||
|
||||
// AssertStatementStringer(t, &sql.SelectStatement{
|
||||
// WithClause: &sql.WithClause{
|
||||
|
|
@ -908,38 +815,38 @@ func TestSelectStatement_String(t *testing.T) {
|
|||
// },
|
||||
// }, `WITH "cte" ("x", "y") AS (SELECT *) VALUES (1, 2), (3, 4)`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// Union: pos(0),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// },
|
||||
// }, `SELECT * UNION SELECT *`)
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Union: pos(0),
|
||||
Compound: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
},
|
||||
}, `SELECT * UNION SELECT *`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// Union: pos(0),
|
||||
// UnionAll: pos(0),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// },
|
||||
// }, `SELECT * UNION ALL SELECT *`)
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Union: pos(0),
|
||||
UnionAll: pos(0),
|
||||
Compound: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
},
|
||||
}, `SELECT * UNION ALL SELECT *`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// Intersect: pos(0),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// },
|
||||
// }, `SELECT * INTERSECT SELECT *`)
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Intersect: pos(0),
|
||||
Compound: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
},
|
||||
}, `SELECT * INTERSECT SELECT *`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// Except: pos(0),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// },
|
||||
// }, `SELECT * EXCEPT SELECT *`)
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Except: pos(0),
|
||||
Compound: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
},
|
||||
}, `SELECT * EXCEPT SELECT *`)
|
||||
|
||||
AssertStatementStringer(t, &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
|
|
@ -994,228 +901,6 @@ func TestSelectStatement_String(t *testing.T) {
|
|||
// Y: &parser.QualifiedTableName{Name: &parser.Ident{Name: "y"}},
|
||||
// },
|
||||
// }, `SELECT * FROM x CROSS JOIN y`)
|
||||
|
||||
// Test SELECT with WITH clause only upto SQL comparison, skip AssertStatementSanity() until parser can handle WITH clauses.
|
||||
{
|
||||
selectast := parser.SelectStatement{
|
||||
WithClause: &parser.WithClause{
|
||||
CTEs: []*parser.CTE{
|
||||
{
|
||||
TableName: &parser.Ident{Name: "cte"},
|
||||
Columns: []*parser.Ident{
|
||||
{Name: "col1"},
|
||||
{Name: "col2"},
|
||||
},
|
||||
Select: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{Name: "col1"}},
|
||||
{Expr: &parser.Ident{Name: "col2"}},
|
||||
},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "table"}},
|
||||
},
|
||||
As: parser.Pos{Column: 1},
|
||||
}},
|
||||
},
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "cte"}},
|
||||
}
|
||||
selectsql := `WITH cte (col1, col2) AS (SELECT col1, col2 FROM table) SELECT * FROM cte`
|
||||
|
||||
if s := selectast.String(); s != selectsql {
|
||||
t.Fatalf("parser.SelectStatement.String()=%q, want %q", s, selectsql)
|
||||
}
|
||||
if s := selectast.Clone().String(); s != selectsql {
|
||||
t.Fatalf("parser.SelectStatement.Clone().String()=%q, want %q", s, selectsql)
|
||||
}
|
||||
}
|
||||
// Test SelectStatement.HasWildcard()
|
||||
{
|
||||
selectast := &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
}
|
||||
if !selectast.HasWildcard() {
|
||||
t.Fatalf("parser.SelectStatement.HasWildcard()=%v, want %v", false, true)
|
||||
}
|
||||
|
||||
selectast = &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Expr: &parser.QualifiedRef{Star: pos(0)}}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
}
|
||||
if !selectast.HasWildcard() {
|
||||
t.Fatalf("parser.SelectStatement.HasWildcard()=%v, want %v", false, true)
|
||||
}
|
||||
|
||||
selectast = &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Expr: &parser.Ident{Name: "col"}}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
}
|
||||
if selectast.HasWildcard() {
|
||||
t.Fatalf("parser.SelectStatement.HasWildcard()=%v, want %v", true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSources_String(t *testing.T) {
|
||||
// Test helper functions for QualifiedTableName
|
||||
{
|
||||
qtast := parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}}
|
||||
if s := qtast.TableName(); s != "tbl" {
|
||||
t.Fatalf("parser.QualifiedTableName.TableName()=%v, want %v", s, "tbl")
|
||||
}
|
||||
if !qtast.MatchesTablenameOrAlias("tbl") {
|
||||
t.Fatalf("parser.QualifiedTableName.MatchesTablenameOrAlias()=%v, want %v", false, true)
|
||||
}
|
||||
qtast = parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}, Alias: &parser.Ident{Name: "t1"}}
|
||||
if qtast.SourceFromAlias("t1") != qtast.SourceFromAlias("tbl") {
|
||||
t.Fatalf("parser.QualifiedTableName.SourceFromAlias()=%v, want %v", qtast.SourceFromAlias("t1"), qtast.SourceFromAlias("tbl"))
|
||||
}
|
||||
qtast = parser.QualifiedTableName{
|
||||
Name: &parser.Ident{Name: "tbl"},
|
||||
Alias: &parser.Ident{Name: "t1"},
|
||||
OutputColumns: []*parser.SourceOutputColumn{
|
||||
{TableName: "tbl", ColumnName: "col1", ColumnIndex: 1},
|
||||
{TableName: "tbl", ColumnName: "col2", ColumnIndex: 2},
|
||||
},
|
||||
}
|
||||
if n := len(qtast.PossibleOutputColumns()); n != 2 {
|
||||
t.Fatalf("len(parser.QualifiedTableName.PossibleOutputColumns())=%v, want %v", n, 2)
|
||||
}
|
||||
if c, _ := qtast.OutputColumnNamed("col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.QualifiedTableName.OutputColumnNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := qtast.OutputColumnNamed("col99"); c != nil {
|
||||
t.Fatalf("parser.QualifiedTableName.OutputColumnNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if c, _ := qtast.OutputColumnQualifierNamed("tbl", "col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.QualifiedTableName.OutputColumnQualifierNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := qtast.OutputColumnQualifierNamed("t1", "col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.QualifiedTableName.OutputColumnQualifierNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := qtast.OutputColumnQualifierNamed("t9", "col99"); c != nil {
|
||||
t.Fatalf("parser.QualifiedTableName.OutputColumnQualifierNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper functions for JoinClause
|
||||
{
|
||||
jcast := parser.JoinClause{
|
||||
X: &parser.QualifiedTableName{
|
||||
Name: &parser.Ident{Name: "tbl1"},
|
||||
Alias: &parser.Ident{Name: "t1"},
|
||||
OutputColumns: []*parser.SourceOutputColumn{
|
||||
{TableName: "tbl1", ColumnName: "col1", ColumnIndex: 1},
|
||||
{TableName: "tbl1", ColumnName: "col2", ColumnIndex: 2},
|
||||
},
|
||||
},
|
||||
Y: &parser.QualifiedTableName{
|
||||
Name: &parser.Ident{Name: "tbl2"},
|
||||
Alias: &parser.Ident{Name: "t2"},
|
||||
OutputColumns: []*parser.SourceOutputColumn{
|
||||
{TableName: "tbl2", ColumnName: "col3", ColumnIndex: 1},
|
||||
{TableName: "tbl2", ColumnName: "col4", ColumnIndex: 2},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if n := len(jcast.PossibleOutputColumns()); n != 4 {
|
||||
t.Fatalf("len(parser.JoinClause.PossibleOutputColumns())=%v, want %v", n, 4)
|
||||
}
|
||||
if c, _ := jcast.OutputColumnNamed("col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := jcast.OutputColumnNamed("col3"); c.ColumnName != "col3" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnNamed()=%v, want %v", c.ColumnName, "col3")
|
||||
}
|
||||
if c, _ := jcast.OutputColumnNamed("col99"); c != nil {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if c, _ := jcast.OutputColumnQualifierNamed("tbl1", "col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := jcast.OutputColumnQualifierNamed("t2", "col3"); c.ColumnName != "col3" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c.ColumnName, "col3")
|
||||
}
|
||||
if c, _ := jcast.OutputColumnQualifierNamed("t1", "col3"); c != nil {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if c, _ := jcast.OutputColumnQualifierNamed("t2", "col1"); c != nil {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if s := jcast.SourceFromAlias("t1"); s != jcast.X {
|
||||
t.Fatalf("parser.JoinClause.SourceFromAlias()=%v, want %v", s, jcast.X)
|
||||
}
|
||||
if s := jcast.SourceFromAlias("t2"); s != jcast.Y {
|
||||
t.Fatalf("parser.JoinClause.SourceFromAlias()=%v, want %v", s, jcast.Y)
|
||||
}
|
||||
if s := jcast.SourceFromAlias("t3"); s != nil {
|
||||
t.Fatalf("parser.JoinClause.SourceFromAlias()=%v, want %v", s, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// test ParenSource helper functions
|
||||
{
|
||||
psast := parser.ParenSource{
|
||||
X: &parser.QualifiedTableName{
|
||||
Name: &parser.Ident{Name: "tbl1"},
|
||||
OutputColumns: []*parser.SourceOutputColumn{
|
||||
{TableName: "tbl1", ColumnName: "col1", ColumnIndex: 1},
|
||||
{TableName: "tbl1", ColumnName: "col2", ColumnIndex: 2},
|
||||
},
|
||||
},
|
||||
Alias: &parser.Ident{Name: "t1"},
|
||||
}
|
||||
if s := psast.SourceFromAlias("t1"); s.String() != psast.String() {
|
||||
t.Fatalf("parser.ParenSource.SourceFromAlias()=%v, want %v", s, psast)
|
||||
}
|
||||
if s := psast.SourceFromAlias("t3"); s != nil {
|
||||
t.Fatalf("parser.ParenSource.SourceFromAlias()=%v, want %v", s, nil)
|
||||
}
|
||||
if n := len(psast.PossibleOutputColumns()); n != 2 {
|
||||
t.Fatalf("len(parser.JoinClause.PossibleOutputColumns())=%v, want %v", n, 2)
|
||||
}
|
||||
if c, _ := psast.OutputColumnNamed("col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := psast.OutputColumnNamed("col99"); c != nil {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if c, _ := psast.OutputColumnQualifierNamed("t1", "col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := psast.OutputColumnQualifierNamed("t1", "col3"); c != nil {
|
||||
t.Fatalf("parser.JoinClause.OutputColumnQualifierNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Test select statement source helper functions
|
||||
{
|
||||
selectast := &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{Name: "col1"}},
|
||||
{Expr: &parser.Ident{Name: "col2"}},
|
||||
},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "table"}},
|
||||
}
|
||||
|
||||
if s := selectast.SourceFromAlias("table"); s != nil {
|
||||
t.Fatalf("parser.SelectStatement.SourceFromAlias()=%v, want %v", s, nil)
|
||||
}
|
||||
if n := len(selectast.PossibleOutputColumns()); n != 2 {
|
||||
t.Fatalf("len(parser.SelectStatement.PossibleOutputColumns())=%v, want %v", n, 2)
|
||||
}
|
||||
if c, _ := selectast.OutputColumnNamed("col1"); c.ColumnName != "col1" {
|
||||
t.Fatalf("parser.SelectStatement.OutputColumnNamed()=%v, want %v", c.ColumnName, "col1")
|
||||
}
|
||||
if c, _ := selectast.OutputColumnNamed("col99"); c != nil {
|
||||
t.Fatalf("parser.SelectStatement.OutputColumnNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
if c, _ := selectast.OutputColumnQualifierNamed("table", "col1"); c != nil {
|
||||
t.Fatalf("parser.SelectStatement.OutputColumnQualifierNamed()=%v, want %v", c, nil)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateStatement_String(t *testing.T) {
|
||||
|
|
@ -1268,58 +953,20 @@ func TestUpdateStatement_String(t *testing.T) {
|
|||
},
|
||||
}, `UPDATE OR IGNORE tbl SET x = 100`)
|
||||
|
||||
// AssertStatementStringer(t, &parser.UpdateStatement{
|
||||
// WithClause: &parser.WithClause{
|
||||
// CTEs: []*parser.CTE{{
|
||||
// TableName: &parser.Ident{Name: "cte"},
|
||||
// Select: &parser.SelectStatement{
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
// AssertStatementStringer(t, &sql.UpdateStatement{
|
||||
// WithClause: &sql.WithClause{
|
||||
// CTEs: []*sql.CTE{{
|
||||
// TableName: &sql.Ident{Name: "cte"},
|
||||
// Select: &sql.SelectStatement{
|
||||
// Columns: []*sql.ResultColumn{{Star: pos(0)}},
|
||||
// },
|
||||
// As: parser.Pos{Column: 1},
|
||||
// }},
|
||||
// },
|
||||
// Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
// Assignments: []*parser.Assignment{
|
||||
// {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}},
|
||||
// Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}},
|
||||
// Assignments: []*sql.Assignment{
|
||||
// {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}},
|
||||
// },
|
||||
// }, `WITH cte AS (SELECT *) UPDATE tbl SET x = 100`)
|
||||
|
||||
// Testing UPDATE with WITH clause only upto SQL comparison until parser can handle WITH clauses.
|
||||
{
|
||||
updateast := parser.UpdateStatement{
|
||||
WithClause: &parser.WithClause{
|
||||
Recursive: parser.Pos{Column: 1},
|
||||
CTEs: []*parser.CTE{{
|
||||
TableName: &parser.Ident{Name: "cte1"},
|
||||
Select: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "table"}},
|
||||
},
|
||||
As: parser.Pos{Column: 1},
|
||||
},
|
||||
{
|
||||
TableName: &parser.Ident{Name: "cte2"},
|
||||
Select: &parser.SelectStatement{
|
||||
Columns: []*parser.ResultColumn{{Star: pos(0)}},
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{Name: "cte1"}},
|
||||
},
|
||||
As: parser.Pos{Column: 1},
|
||||
}},
|
||||
},
|
||||
Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
|
||||
Assignments: []*parser.Assignment{
|
||||
{Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}},
|
||||
},
|
||||
}
|
||||
updatesql := `WITH RECURSIVE cte1 AS (SELECT * FROM table), cte2 AS (SELECT * FROM cte1) UPDATE tbl SET x = 100`
|
||||
|
||||
if updateast.String() != updatesql {
|
||||
t.Fatalf("parser.UpdateStatement.String()=%q, want %q", updateast.String(), updatesql)
|
||||
}
|
||||
if updateast.Clone().String() != updatesql {
|
||||
t.Fatalf("parser.UpdateStatement.Clone().String()=%q, want %q", updateast.Clone().String(), updatesql)
|
||||
}
|
||||
}
|
||||
// }, `WITH "cte" AS (SELECT *) UPDATE "tbl" SET "x" = 100`)
|
||||
}
|
||||
|
||||
func TestIdent_String(t *testing.T) {
|
||||
|
|
@ -1345,80 +992,6 @@ func TestNullLit_String(t *testing.T) {
|
|||
AssertExprStringer(t, &parser.NullLit{}, `NULL`)
|
||||
}
|
||||
|
||||
// test Date literal type. DateLit.String() will return a quoted string.
|
||||
func TestDateLit_String(t *testing.T) {
|
||||
dl := &parser.DateLit{Value: time.Unix(0, 0).UTC()}
|
||||
AssertExprStringer(t, dl, `'1970-01-01T00:00:00Z'`)
|
||||
}
|
||||
|
||||
// test SetLiteralExpr.
|
||||
func TestSetLiteralExpr_String(t *testing.T) {
|
||||
sl := &parser.SetLiteralExpr{
|
||||
Lbracket: pos(0),
|
||||
Rbracket: pos(0),
|
||||
Members: []parser.Expr{
|
||||
&parser.StringLit{Value: "val1"},
|
||||
&parser.StringLit{Value: "val2"},
|
||||
},
|
||||
}
|
||||
AssertExprStringer(t, sl, `['val1', 'val2']`)
|
||||
}
|
||||
|
||||
// test TupleLiteralExpr.
|
||||
func TestTupleLiteralExpr_String(t *testing.T) {
|
||||
sl := &parser.TupleLiteralExpr{
|
||||
Lbrace: pos(0),
|
||||
Rbrace: pos(0),
|
||||
Members: []parser.Expr{
|
||||
&parser.StringLit{Value: "val1"},
|
||||
&parser.StringLit{Value: "val2"},
|
||||
},
|
||||
}
|
||||
AssertExprStringer(t, sl, `{'val1', 'val2'}`)
|
||||
}
|
||||
|
||||
// Test string literal to timestamp conversion
|
||||
func TestStringLit_ConvertToTimestamp(t *testing.T) {
|
||||
// string value in RFC3339 format
|
||||
sl := &parser.StringLit{Value: "2023-03-24T10:06:01Z"}
|
||||
AssertExprStringer(t, sl.ConvertToTimestamp(), `'2023-03-24T10:06:01Z'`)
|
||||
|
||||
// string value in RFC3339Nano format
|
||||
// DateLit.String() uses time.RFC3339 format, because of that the nano part
|
||||
// will be truncated in the string.
|
||||
sl = &parser.StringLit{Value: "2023-03-24T10:06:01.100000Z"}
|
||||
AssertExprStringer(t, sl.ConvertToTimestamp(), `'2023-03-24T10:06:01Z'`)
|
||||
|
||||
// string value in common date format
|
||||
sl = &parser.StringLit{Value: "2023-03-24"}
|
||||
AssertExprStringer(t, sl.ConvertToTimestamp(), `'2023-03-24T00:00:00Z'`)
|
||||
|
||||
// string value contains a bad date
|
||||
sl = &parser.StringLit{Value: "2023-13-32"}
|
||||
dl := sl.ConvertToTimestamp()
|
||||
if dl != nil {
|
||||
t.Fatalf("StringLit('2023-13-32').ConvertToTimestamp()=%q, want %q", dl.String(), "nil")
|
||||
}
|
||||
}
|
||||
|
||||
// test System Variable type.
|
||||
func TestSysVariable_String(t *testing.T) {
|
||||
// test CURRENT_DATE
|
||||
sv := &parser.SysVariable{Token: parser.CURRENT_DATE}
|
||||
AssertExprStringer(t, sv, parser.CURRENT_DATE.String())
|
||||
// test CURRENTTIMESTAMP
|
||||
sv = &parser.SysVariable{Token: parser.CURRENT_TIMESTAMP}
|
||||
AssertExprStringer(t, sv, parser.CURRENT_TIMESTAMP.String())
|
||||
// test CURRENT_TIMESTAMP's data type and expect it to be timestamp type
|
||||
if sv.DataType() != parser.NewDataTypeTimestamp() {
|
||||
t.Fatalf("SysVariable(CURRENT_TIMESTAMP).DataType()=%q, want %q", sv.DataType().TypeDescription(), parser.NewDataTypeTimestamp().TypeDescription())
|
||||
}
|
||||
// test CURRENT_TIMESTAMP's name and expect it to be CURRENT_TIMESTAMP
|
||||
if sv.Name() != sv.String() {
|
||||
t.Fatalf("SysVariable(CURRENT_TIMESTAMP).Name()=%q, want %q", sv.String(), sv.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestParenExpr_String(t *testing.T) {
|
||||
AssertExprStringer(t, &parser.ParenExpr{X: &parser.NullLit{}}, `(NULL)`)
|
||||
}
|
||||
|
|
@ -1426,7 +999,6 @@ func TestParenExpr_String(t *testing.T) {
|
|||
func TestUnaryExpr_String(t *testing.T) {
|
||||
AssertExprStringer(t, &parser.UnaryExpr{Op: parser.PLUS, X: &parser.IntegerLit{Value: "100"}}, `+100`)
|
||||
AssertExprStringer(t, &parser.UnaryExpr{Op: parser.MINUS, X: &parser.IntegerLit{Value: "100"}}, `-100`)
|
||||
AssertExprStringer(t, &parser.UnaryExpr{Op: parser.BITNOT, X: &parser.BoolLit{Value: true}}, `!TRUE`)
|
||||
AssertNodeStringerPanic(t, &parser.UnaryExpr{X: &parser.IntegerLit{Value: "100"}}, `sql.UnaryExpr.String(): invalid op ILLEGAL`)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ func (p *Parser) ParseStatement() (stmt Statement, err error) {
|
|||
switch tok := p.peek(); tok {
|
||||
case EOF:
|
||||
return nil, io.EOF
|
||||
case EXPLAIN:
|
||||
if stmt, err = p.parseExplainStatement(); err != nil {
|
||||
return stmt, err
|
||||
}
|
||||
//case EXPLAIN:
|
||||
// if stmt, err = p.parseExplainStatement(); err != nil {
|
||||
// return stmt, err
|
||||
// }
|
||||
default:
|
||||
if stmt, err = p.parseNonExplainStatement(); err != nil {
|
||||
return stmt, err
|
||||
|
|
@ -64,6 +64,7 @@ func (p *Parser) ParseStatement() (stmt Statement, err error) {
|
|||
return stmt, nil
|
||||
}
|
||||
|
||||
/*
|
||||
// parseExplain parses EXPLAIN [QUERY PLAN] STMT.
|
||||
func (p *Parser) parseExplainStatement() (_ *ExplainStatement, err error) {
|
||||
var tok Token
|
||||
|
|
@ -88,7 +89,7 @@ func (p *Parser) parseExplainStatement() (_ *ExplainStatement, err error) {
|
|||
return &stmt, err
|
||||
}
|
||||
return &stmt, nil
|
||||
}
|
||||
}*/
|
||||
|
||||
// parseStmt parses all statement types.
|
||||
func (p *Parser) parseNonExplainStatement() (Statement, error) {
|
||||
|
|
@ -101,14 +102,10 @@ func (p *Parser) parseNonExplainStatement() (Statement, error) {
|
|||
return p.parseBulkInsertStatement()
|
||||
case CREATE:
|
||||
return p.parseCreateStatement()
|
||||
case COPY:
|
||||
return p.parseCopyStatement()
|
||||
case DROP:
|
||||
return p.parseDropStatement()
|
||||
case SELECT:
|
||||
return p.parseSelectStatement(false, nil)
|
||||
case PREDICT:
|
||||
return p.parsePredictStatement()
|
||||
case INSERT, REPLACE:
|
||||
return p.parseInsertStatement(nil)
|
||||
case UPDATE:
|
||||
|
|
@ -176,18 +173,12 @@ func (p *Parser) parseShowDatabasesStatement(showPos Pos) (*ShowDatabasesStateme
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Parser) parseShowTablesStatement(showPos Pos) (_ *ShowTablesStatement, err error) {
|
||||
func (p *Parser) parseShowTablesStatement(showPos Pos) (*ShowTablesStatement, error) {
|
||||
switch p.peek() {
|
||||
case TABLES:
|
||||
var stmt ShowTablesStatement
|
||||
stmt.Show = showPos
|
||||
stmt.Tables, _, _ = p.scan()
|
||||
if p.peek() == WITH {
|
||||
stmt.With, _, _ = p.scan()
|
||||
if stmt.System, err = p.parseIdent("show tables option"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
return &stmt, nil
|
||||
default:
|
||||
return nil, p.errorExpected(p.pos, p.tok, "TABLES")
|
||||
|
|
@ -346,10 +337,8 @@ func (p *Parser) parseCreateStatement() (Statement, error) {
|
|||
return p.parseCreateIndexStatement(pos)*/
|
||||
case FUNCTION:
|
||||
return p.parseCreateFunctionStatement(pos)
|
||||
case MODEL:
|
||||
return p.parseCreateModelStatement(pos)
|
||||
default:
|
||||
return nil, p.errorExpected(pos, tok, "DATABASE, TABLE, VIEW, FUNCTION or MODEL")
|
||||
return nil, p.errorExpected(pos, tok, "DATABASE, TABLE, VIEW or FUNCTION")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -384,8 +373,6 @@ func (p *Parser) parseDropStatement() (Statement, error) {
|
|||
return p.parseDropIndexStatement(pos)*/
|
||||
case FUNCTION:
|
||||
return p.parseDropFunctionStatement(pos)
|
||||
case MODEL:
|
||||
return p.parseDropModelStatement(pos)
|
||||
default:
|
||||
return nil, p.errorExpected(pos, tok, "DATABASE, TABLE, VIEW or FUNCTION")
|
||||
}
|
||||
|
|
@ -1171,72 +1158,6 @@ func (p *Parser) parseDropTableStatement(dropPos Pos) (_ *DropTableStatement, er
|
|||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseCopyStatement() (_ *CopyStatement, err error) {
|
||||
assert(p.peek() == COPY)
|
||||
|
||||
var stmt CopyStatement
|
||||
stmt.Copy, _, _ = p.scan()
|
||||
|
||||
ident, err := p.parseIdent("table name")
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
if stmt.Source, err = p.parseQualifiedTableName(ident); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
if p.peek() != TO {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "TO")
|
||||
}
|
||||
stmt.To, _, _ = p.scan()
|
||||
|
||||
if stmt.TargetName, err = p.parseIdent("table name"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
// parse optional "WHERE expr"
|
||||
if p.peek() == WHERE {
|
||||
stmt.Where, _, _ = p.scan()
|
||||
if stmt.WhereExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// options
|
||||
if p.peek() == WITH {
|
||||
stmt.With, _, _ = p.scan()
|
||||
if !isCopyOptionStartToken(p.peek(), p) {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "URL or APIKEY")
|
||||
}
|
||||
|
||||
for {
|
||||
option, err := p.parseIdent("copy option")
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(option.Name) {
|
||||
case "url":
|
||||
stmt.Url, err = p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
case "apikey":
|
||||
stmt.ApiKey, err = p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
if !isCopyOptionStartToken(p.peek(), p) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseCreateViewStatement(createPos Pos) (_ *CreateViewStatement, err error) {
|
||||
assert(p.peek() == VIEW)
|
||||
|
||||
|
|
@ -1365,29 +1286,6 @@ func (p *Parser) parseDropViewStatement(dropPos Pos) (_ *DropViewStatement, err
|
|||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseDropModelStatement(dropPos Pos) (_ *DropModelStatement, err error) {
|
||||
assert(p.peek() == MODEL)
|
||||
|
||||
var stmt DropModelStatement
|
||||
stmt.Drop = dropPos
|
||||
stmt.Model, _, _ = p.scan()
|
||||
|
||||
// Parse optional "IF EXISTS".
|
||||
if p.peek() == IF {
|
||||
stmt.If, _, _ = p.scan()
|
||||
if p.peek() != EXISTS {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "EXISTS")
|
||||
}
|
||||
stmt.IfExists, _, _ = p.scan()
|
||||
}
|
||||
|
||||
if stmt.Name, err = p.parseIdent("view name"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
/*func (p *Parser) parseCreateIndexStatement(createPos Pos) (_ *CreateIndexStatement, err error) {
|
||||
assert(p.peek() == INDEX || p.peek() == UNIQUE)
|
||||
|
||||
|
|
@ -1559,35 +1457,11 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
|
|||
return &stmt, p.errorExpected(p.pos, p.tok, "RETURNS")
|
||||
}
|
||||
stmt.Returns, _, _ = p.scan()
|
||||
stmt.ReturnType, err = p.parseType()
|
||||
stmt.ReturnDef, err = p.parseParameterDefinition()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
// options
|
||||
if p.peek() == WITH {
|
||||
stmt.With, _, _ = p.scan()
|
||||
stmt.Options = make([]*FunctionOptionDefinition, 0)
|
||||
for {
|
||||
option, err := p.parseIdent("function option")
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
expr, err := p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.Options = append(stmt.Options, &FunctionOptionDefinition{
|
||||
Name: option,
|
||||
OptionExpr: expr,
|
||||
})
|
||||
if p.peek() == AS {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.peek() != AS {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "AS")
|
||||
}
|
||||
|
|
@ -1599,7 +1473,7 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
|
|||
stmt.Begin, _, _ = p.scan()
|
||||
|
||||
for {
|
||||
s, err := p.parseFunctionBodyStatement(&stmt)
|
||||
s, err := p.parseFunctionBodyStatement()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
|
@ -1620,15 +1494,8 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
|
|||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseFunctionBodyStatement(cf *CreateFunctionStatement) (stmt Statement, err error) {
|
||||
func (p *Parser) parseFunctionBodyStatement() (stmt Statement, err error) {
|
||||
switch p.peek() {
|
||||
case RETURN:
|
||||
s, err := p.parseReturnStatement()
|
||||
if err != nil {
|
||||
return stmt, err
|
||||
}
|
||||
cf.Body = append(cf.Body, s)
|
||||
|
||||
case END:
|
||||
break
|
||||
default:
|
||||
|
|
@ -1640,20 +1507,6 @@ func (p *Parser) parseFunctionBodyStatement(cf *CreateFunctionStatement) (stmt S
|
|||
return stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseReturnStatement() (_ *ReturnStatement, err error) {
|
||||
assert(p.peek() == RETURN)
|
||||
|
||||
var stmt ReturnStatement
|
||||
stmt.Return, _, _ = p.scan()
|
||||
|
||||
expr, err := p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.ReturnExpr = expr
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseDropFunctionStatement(dropPos Pos) (_ *DropFunctionStatement, err error) {
|
||||
assert(p.peek() == FUNCTION)
|
||||
|
||||
|
|
@ -1677,69 +1530,6 @@ func (p *Parser) parseDropFunctionStatement(dropPos Pos) (_ *DropFunctionStateme
|
|||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseCreateModelStatement(createPos Pos) (_ *CreateModelStatement, err error) {
|
||||
assert(p.peek() == MODEL)
|
||||
|
||||
var stmt CreateModelStatement
|
||||
stmt.Create = createPos
|
||||
stmt.Model, _, _ = p.scan()
|
||||
|
||||
// Parse optional "IF NOT EXISTS".
|
||||
if p.peek() == IF {
|
||||
stmt.If, _, _ = p.scan()
|
||||
|
||||
if p.peek() != NOT {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "NOT")
|
||||
}
|
||||
stmt.IfNot, _, _ = p.scan()
|
||||
|
||||
if p.peek() != EXISTS {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "EXISTS")
|
||||
}
|
||||
stmt.IfNotExists, _, _ = p.scan()
|
||||
}
|
||||
|
||||
if stmt.Name, err = p.parseIdent("model name"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
// options
|
||||
if p.peek() != WITH {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "WITH")
|
||||
}
|
||||
stmt.With, _, _ = p.scan()
|
||||
|
||||
stmt.Options = make([]*ModelOptionDefinition, 0)
|
||||
for {
|
||||
option, err := p.parseIdent("model option")
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
expr, err := p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.Options = append(stmt.Options, &ModelOptionDefinition{
|
||||
Name: option,
|
||||
OptionExpr: expr,
|
||||
})
|
||||
if p.peek() == AS {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.peek() != AS {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "AS")
|
||||
}
|
||||
stmt.As, _, _ = p.scan()
|
||||
|
||||
if stmt.ModelQuery, err = p.parseSelectStatement(false, nil); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseIdent(desc string) (*Ident, error) {
|
||||
pos, tok, lit := p.scan()
|
||||
switch tok {
|
||||
|
|
@ -2362,87 +2152,82 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (
|
|||
// }
|
||||
//}
|
||||
|
||||
if p.peek() != SELECT {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "SELECT")
|
||||
}
|
||||
switch p.peek() {
|
||||
/*case VALUES:
|
||||
stmt.Values, _, _ = p.scan()
|
||||
|
||||
stmt.Select, _, _ = p.scan()
|
||||
|
||||
// Parse optional "DISTINCT".
|
||||
if tok := p.peek(); tok == DISTINCT {
|
||||
stmt.Distinct, _, _ = p.scan()
|
||||
}
|
||||
|
||||
if p.peek() == TOP {
|
||||
stmt.Top, _, _ = p.scan()
|
||||
if p.peek() == LP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
if stmt.TopExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
if p.peek() == RP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
}
|
||||
|
||||
if p.peek() == TOPN {
|
||||
stmt.TopN, _, _ = p.scan()
|
||||
if p.peek() == LP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
if stmt.TopExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
if p.peek() == RP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
}
|
||||
|
||||
// Parse result columns.
|
||||
for {
|
||||
col, err := p.parseResultColumn()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
var list ExprList
|
||||
if p.peek() != LP {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "left paren")
|
||||
}
|
||||
stmt.Columns = append(stmt.Columns, col)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
p.scan()
|
||||
}
|
||||
|
||||
// Parse FROM clause.
|
||||
if p.peek() == FROM {
|
||||
stmt.From, _, _ = p.scan()
|
||||
if stmt.Source, err = p.parseSource(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse WHERE clause.
|
||||
if p.peek() == WHERE {
|
||||
stmt.Where, _, _ = p.scan()
|
||||
if stmt.WhereExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GROUP BY/HAVING clause.
|
||||
if p.peek() == GROUP {
|
||||
stmt.Group, _, _ = p.scan()
|
||||
if p.peek() != BY {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "BY")
|
||||
}
|
||||
stmt.GroupBy, _, _ = p.scan()
|
||||
list.Lparen, _, _ = p.scan()
|
||||
|
||||
for {
|
||||
expr, err := p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.GroupByExprs = append(stmt.GroupByExprs, expr)
|
||||
list.Exprs = append(list.Exprs, expr)
|
||||
|
||||
if p.peek() == RP {
|
||||
break
|
||||
} else if p.peek() != COMMA {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "comma or right paren")
|
||||
}
|
||||
p.scan()
|
||||
}
|
||||
list.Rparen, _, _ = p.scan()
|
||||
stmt.ValueLists = append(stmt.ValueLists, &list)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
p.scan()
|
||||
|
||||
}*/
|
||||
|
||||
case SELECT:
|
||||
stmt.Select, _, _ = p.scan()
|
||||
|
||||
// Parse optional "DISTINCT".
|
||||
if tok := p.peek(); tok == DISTINCT {
|
||||
stmt.Distinct, _, _ = p.scan()
|
||||
}
|
||||
|
||||
if p.peek() == TOP {
|
||||
stmt.Top, _, _ = p.scan()
|
||||
if p.peek() == LP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
if stmt.TopExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
if p.peek() == RP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
}
|
||||
|
||||
if p.peek() == TOPN {
|
||||
stmt.TopN, _, _ = p.scan()
|
||||
if p.peek() == LP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
if stmt.TopExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
if p.peek() == RP {
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
}
|
||||
|
||||
// Parse result columns.
|
||||
for {
|
||||
col, err := p.parseResultColumn()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.Columns = append(stmt.Columns, col)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
|
|
@ -2450,61 +2235,101 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (
|
|||
p.scan()
|
||||
}
|
||||
|
||||
// Parse optional HAVING clause.
|
||||
if p.peek() == HAVING {
|
||||
stmt.Having, _, _ = p.scan()
|
||||
if stmt.HavingExpr, err = p.ParseExpr(); err != nil {
|
||||
// Parse FROM clause.
|
||||
if p.peek() == FROM {
|
||||
stmt.From, _, _ = p.scan()
|
||||
if stmt.Source, err = p.parseSource(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse WHERE clause.
|
||||
if p.peek() == WHERE {
|
||||
stmt.Where, _, _ = p.scan()
|
||||
if stmt.WhereExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse GROUP BY/HAVING clause.
|
||||
if p.peek() == GROUP {
|
||||
stmt.Group, _, _ = p.scan()
|
||||
if p.peek() != BY {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "BY")
|
||||
}
|
||||
stmt.GroupBy, _, _ = p.scan()
|
||||
|
||||
for {
|
||||
expr, err := p.ParseExpr()
|
||||
if err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
stmt.GroupByExprs = append(stmt.GroupByExprs, expr)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
p.scan()
|
||||
}
|
||||
|
||||
// Parse optional HAVING clause.
|
||||
if p.peek() == HAVING {
|
||||
stmt.Having, _, _ = p.scan()
|
||||
if stmt.HavingExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse WINDOW clause.
|
||||
if p.peek() == WINDOW {
|
||||
stmt.Window, _, _ = p.scan()
|
||||
|
||||
for {
|
||||
var window Window
|
||||
if window.Name, err = p.parseIdent("window name"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
if p.peek() != AS {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "AS")
|
||||
}
|
||||
window.As, _, _ = p.scan()
|
||||
|
||||
if window.Definition, err = p.parseWindowDefinition(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
stmt.Windows = append(stmt.Windows, &window)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
p.scan()
|
||||
}
|
||||
}
|
||||
default:
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "SELECT")
|
||||
}
|
||||
|
||||
// Parse WINDOW clause.
|
||||
// if p.peek() == WINDOW {
|
||||
// stmt.Window, _, _ = p.scan()
|
||||
|
||||
// for {
|
||||
// var window Window
|
||||
// if window.Name, err = p.parseIdent("window name"); err != nil {
|
||||
// return &stmt, err
|
||||
// }
|
||||
|
||||
// if p.peek() != AS {
|
||||
// return &stmt, p.errorExpected(p.pos, p.tok, "AS")
|
||||
// }
|
||||
// window.As, _, _ = p.scan()
|
||||
|
||||
// if window.Definition, err = p.parseWindowDefinition(); err != nil {
|
||||
// return &stmt, err
|
||||
// }
|
||||
|
||||
// stmt.Windows = append(stmt.Windows, &window)
|
||||
|
||||
// if p.peek() != COMMA {
|
||||
// break
|
||||
// }
|
||||
// p.scan()
|
||||
// }
|
||||
// }
|
||||
|
||||
// Optionally compound additional SELECT/VALUES.
|
||||
// switch tok := p.peek(); tok {
|
||||
// case UNION, INTERSECT, EXCEPT:
|
||||
// if tok == UNION {
|
||||
// stmt.Union, _, _ = p.scan()
|
||||
// if p.peek() == ALL {
|
||||
// stmt.UnionAll, _, _ = p.scan()
|
||||
// }
|
||||
// } else if tok == INTERSECT {
|
||||
// stmt.Intersect, _, _ = p.scan()
|
||||
// } else {
|
||||
// stmt.Except, _, _ = p.scan()
|
||||
// }
|
||||
switch tok := p.peek(); tok {
|
||||
case UNION, INTERSECT, EXCEPT:
|
||||
if tok == UNION {
|
||||
stmt.Union, _, _ = p.scan()
|
||||
if p.peek() == ALL {
|
||||
stmt.UnionAll, _, _ = p.scan()
|
||||
}
|
||||
} else if tok == INTERSECT {
|
||||
stmt.Intersect, _, _ = p.scan()
|
||||
} else {
|
||||
stmt.Except, _, _ = p.scan()
|
||||
}
|
||||
|
||||
// if stmt.Compound, err = p.parseSelectStatement(true, nil); err != nil {
|
||||
// return &stmt, err
|
||||
// }
|
||||
// }
|
||||
if stmt.Compound, err = p.parseSelectStatement(true, nil); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ORDER BY clause.
|
||||
if !compounded && p.peek() == ORDER {
|
||||
|
|
@ -2528,13 +2353,6 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (
|
|||
}
|
||||
}
|
||||
|
||||
// Parse LIMIT clause.
|
||||
if !compounded && p.peek() == LIMIT {
|
||||
stmt.Limit, _, _ = p.scan()
|
||||
if stmt.LimitExpr, err = p.ParseExpr(); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
}
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
|
|
@ -2766,85 +2584,29 @@ func (p *Parser) parseQualifiedTableName(ident *Ident) (_ *QualifiedTableName, e
|
|||
}
|
||||
}
|
||||
|
||||
// handle query option
|
||||
if p.peek() == WITH {
|
||||
tbl.With, _, _ = p.scan()
|
||||
// Parse optional "INDEXED BY index-name" or "NOT INDEXED".
|
||||
/*switch p.peek() {
|
||||
case INDEXED:
|
||||
tbl.Indexed, _, _ = p.scan()
|
||||
if p.peek() != BY {
|
||||
return &tbl, p.errorExpected(p.pos, p.tok, "BY")
|
||||
}
|
||||
tbl.IndexedBy, _, _ = p.scan()
|
||||
|
||||
tbl.QueryOptions = make([]*TableQueryOption, 0)
|
||||
if p.peek() != LP {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "left paren")
|
||||
if tbl.Index, err = p.parseIdent("index name"); err != nil {
|
||||
return &tbl, err
|
||||
}
|
||||
tbl.LParen, _, _ = p.scan()
|
||||
case NOT:
|
||||
tbl.Not, _, _ = p.scan()
|
||||
if p.peek() != INDEXED {
|
||||
return &tbl, p.errorExpected(p.pos, p.tok, "INDEXED")
|
||||
}
|
||||
tbl.NotIndexed, _, _ = p.scan()
|
||||
}*/
|
||||
|
||||
if tok := p.peek(); !isIdentToken(tok) {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "identifier")
|
||||
}
|
||||
for {
|
||||
qo, err := p.parseTableQueryOption()
|
||||
if err != nil {
|
||||
return &tbl, err
|
||||
}
|
||||
tbl.QueryOptions = append(tbl.QueryOptions, qo)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
if p.peek() != RP {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "right paren")
|
||||
}
|
||||
tbl.RParen, _, _ = p.scan()
|
||||
}
|
||||
return &tbl, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseTableQueryOption() (_ *TableQueryOption, err error) {
|
||||
var opt TableQueryOption
|
||||
opt.OptionParams = make([]*Ident, 0)
|
||||
|
||||
if tok := p.peek(); !isIdentToken(tok) {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "identifier")
|
||||
}
|
||||
|
||||
oi, err := p.parseIdent("query option")
|
||||
if err != nil {
|
||||
return &opt, err
|
||||
}
|
||||
|
||||
opt.OptionName = oi
|
||||
|
||||
if p.peek() != LP {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "left paren")
|
||||
}
|
||||
opt.LParen, _, _ = p.scan()
|
||||
|
||||
for {
|
||||
if tok := p.peek(); !isIdentToken(tok) {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "identifier")
|
||||
}
|
||||
|
||||
opi, err := p.parseIdent("query option parameter")
|
||||
if err != nil {
|
||||
return &opt, err
|
||||
}
|
||||
|
||||
opt.OptionParams = append(opt.OptionParams, opi)
|
||||
|
||||
if p.peek() != COMMA {
|
||||
break
|
||||
}
|
||||
_, _, _ = p.scan()
|
||||
}
|
||||
|
||||
if p.peek() != RP {
|
||||
return nil, p.errorExpected(p.pos, p.tok, "right paren")
|
||||
}
|
||||
opt.RParen, _, _ = p.scan()
|
||||
|
||||
return &opt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseTableValuedFunction(ident *Ident) (_ *TableValuedFunction, err error) {
|
||||
var tbl TableValuedFunction
|
||||
|
||||
|
|
@ -2942,26 +2704,6 @@ func (p *Parser) parseTableValuedFunction(ident *Ident) (_ *TableValuedFunction,
|
|||
return &cte, nil
|
||||
}*/
|
||||
|
||||
func (p *Parser) parsePredictStatement() (_ *PredictStatement, err error) {
|
||||
assert(p.peek() == PREDICT)
|
||||
|
||||
var stmt PredictStatement
|
||||
stmt.Predict, _, _ = p.scan()
|
||||
if p.peek() != USING {
|
||||
return &stmt, p.errorExpected(p.pos, p.tok, "USING")
|
||||
}
|
||||
stmt.Using, _, _ = p.scan()
|
||||
|
||||
if stmt.ModelName, err = p.parseIdent("model name"); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
|
||||
if stmt.InputQuery, err = p.parseSelectStatement(false, nil); err != nil {
|
||||
return &stmt, err
|
||||
}
|
||||
return &stmt, nil
|
||||
}
|
||||
|
||||
func (p *Parser) mustParseLiteral() Expr {
|
||||
assert(isLiteralToken(p.tok))
|
||||
pos, tok, lit := p.scan()
|
||||
|
|
@ -3000,13 +2742,9 @@ func (p *Parser) parseOperand() (expr Expr, err error) {
|
|||
case VARIABLE:
|
||||
return &Variable{Name: lit, NamePos: pos}, nil
|
||||
case MIN, MAX:
|
||||
pk := p.peek()
|
||||
if pk == LP {
|
||||
ident := &Ident{Name: lit, NamePos: pos, Quoted: false}
|
||||
return p.parseCall(ident)
|
||||
}
|
||||
return nil, p.errorExpected(p.pos, pk, "call expression")
|
||||
case STRING, BLOB:
|
||||
ident := &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT}
|
||||
return p.parseCall(ident)
|
||||
case STRING:
|
||||
return &StringLit{ValuePos: pos, Value: lit}, nil
|
||||
case FLOAT:
|
||||
return &FloatLit{ValuePos: pos, Value: lit}, nil
|
||||
|
|
@ -3891,22 +3629,6 @@ func isBulkInsertOptionStartToken(tok Token, p *Parser) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func isCopyOptionStartToken(tok Token, p *Parser) bool {
|
||||
switch tok {
|
||||
case IDENT:
|
||||
ident, err := p.parseIdent("copy option")
|
||||
defer p.unscan()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch strings.ToUpper(ident.Name) {
|
||||
case "URL", "APIKEY":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isConstraintStartToken returns true if tok is the initial token of a constraint.
|
||||
func isConstraintStartToken(tok Token, isTable bool) bool {
|
||||
switch tok {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,6 @@ func TestParser_ParseMinMaxColumnConstraints(t *testing.T) {
|
|||
t.Run("ErrNoKey", func(t *testing.T) {
|
||||
AssertParseStatementError(t, `CREATE TABLE tbl (col1 INT MIN`, `1:30: expected expression, found 'EOF'`)
|
||||
})
|
||||
t.Run("ErrNoCall", func(t *testing.T) {
|
||||
AssertParseStatementError(t, `SELECT MIN;`, `1:11: expected call expression, found ';'`)
|
||||
})
|
||||
t.Run("Simple", func(t *testing.T) {
|
||||
AssertParseStatement(t, `CREATE TABLE tbl (col1 INT MIN 0)`, &parser.CreateTableStatement{
|
||||
Create: pos(0),
|
||||
|
|
@ -472,7 +469,7 @@ func TestParser_ParseAlterStatement(t *testing.T) {
|
|||
|
||||
func TestParser_ParseFunctionStatement(t *testing.T) {
|
||||
t.Run("CreateFunction", func(t *testing.T) {
|
||||
AssertParseStatement(t, `CREATE FUNCTION IF NOT EXISTS func (@param1 int, @param2 string) returns int as begin end`, &parser.CreateFunctionStatement{
|
||||
AssertParseStatement(t, `CREATE FUNCTION IF NOT EXISTS func (@param1 int, @param2 string) returns @scalar int as begin end`, &parser.CreateFunctionStatement{
|
||||
Create: pos(0),
|
||||
Function: pos(7),
|
||||
If: pos(16),
|
||||
|
|
@ -490,12 +487,15 @@ func TestParser_ParseFunctionStatement(t *testing.T) {
|
|||
Type: &parser.Type{Name: &parser.Ident{NamePos: pos(57), Name: "string"}},
|
||||
},
|
||||
},
|
||||
Rparen: pos(63),
|
||||
Returns: pos(65),
|
||||
ReturnType: &parser.Type{Name: &parser.Ident{NamePos: pos(73), Name: "int"}},
|
||||
As: pos(77),
|
||||
Begin: pos(80),
|
||||
End: pos(86),
|
||||
Rparen: pos(63),
|
||||
Returns: pos(65),
|
||||
ReturnDef: &parser.ParameterDefinition{
|
||||
Name: &parser.Variable{Name: "@scalar", NamePos: pos(73)},
|
||||
Type: &parser.Type{Name: &parser.Ident{NamePos: pos(81), Name: "int"}},
|
||||
},
|
||||
As: pos(85),
|
||||
Begin: pos(88),
|
||||
End: pos(94),
|
||||
})
|
||||
// AssertParseStatement(t, `CREATE TRIGGER IF NOT EXISTS trig BEFORE INSERT ON tbl BEGIN DELETE FROM new; END`, &parser.CreateFunctionStatement{
|
||||
// Create: pos(0),
|
||||
|
|
@ -658,18 +658,8 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
Show: pos(0),
|
||||
Tables: pos(5),
|
||||
})
|
||||
AssertParseStatement(t, `SHOW TABLES WITH SYSTEM`, &parser.ShowTablesStatement{
|
||||
Show: pos(0),
|
||||
Tables: pos(5),
|
||||
With: pos(12),
|
||||
System: &parser.Ident{
|
||||
Name: "SYSTEM",
|
||||
NamePos: pos(17),
|
||||
},
|
||||
})
|
||||
AssertParseStatementError(t, `SHOW`, `1:4: expected DATABASES, TABLES, COLUMNS or CREATE, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SHOW BLAH`, `1:6: expected DATABASES, TABLES, COLUMNS or CREATE, found BLAH`)
|
||||
AssertParseStatementError(t, `SHOW TABLES WITH`, `1:16: expected show tables option, found 'EOF'`)
|
||||
})
|
||||
|
||||
t.Run("ShowColumns", func(t *testing.T) {
|
||||
|
|
@ -962,7 +952,7 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
AssertParseStatementError(t, `CREATE`, `1:1: expected DATABASE, TABLE, VIEW, FUNCTION or MODEL`)
|
||||
AssertParseStatementError(t, `CREATE`, `1:1: expected DATABASE, TABLE, VIEW or FUNCTION`)
|
||||
AssertParseStatementError(t, `CREATE DATABASE`, `1:15: expected database name, found 'EOF'`)
|
||||
AssertParseStatementError(t, `CREATE DATABASE IF`, `1:18: expected NOT, found 'EOF'`)
|
||||
AssertParseStatementError(t, `CREATE DATABASE IF NOT`, `1:22: expected EXISTS, found 'EOF'`)
|
||||
|
|
@ -2222,29 +2212,29 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
Having: pos(22),
|
||||
HavingExpr: &parser.BoolLit{ValuePos: pos(29), Value: true},
|
||||
})
|
||||
// AssertParseStatement(t, `SELECT * WINDOW win1 AS (), win2 AS ()`, &parser.SelectStatement{
|
||||
// Select: pos(0),
|
||||
// Columns: []*parser.ResultColumn{{Star: pos(7)}},
|
||||
// Window: pos(9),
|
||||
// Windows: []*parser.Window{
|
||||
// {
|
||||
// Name: &parser.Ident{NamePos: pos(16), Name: "win1"},
|
||||
// As: pos(21),
|
||||
// Definition: &parser.WindowDefinition{
|
||||
// Lparen: pos(24),
|
||||
// Rparen: pos(25),
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// Name: &parser.Ident{NamePos: pos(28), Name: "win2"},
|
||||
// As: pos(33),
|
||||
// Definition: &parser.WindowDefinition{
|
||||
// Lparen: pos(36),
|
||||
// Rparen: pos(37),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
AssertParseStatement(t, `SELECT * WINDOW win1 AS (), win2 AS ()`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{{Star: pos(7)}},
|
||||
Window: pos(9),
|
||||
Windows: []*parser.Window{
|
||||
{
|
||||
Name: &parser.Ident{NamePos: pos(16), Name: "win1"},
|
||||
As: pos(21),
|
||||
Definition: &parser.WindowDefinition{
|
||||
Lparen: pos(24),
|
||||
Rparen: pos(25),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: &parser.Ident{NamePos: pos(28), Name: "win2"},
|
||||
As: pos(33),
|
||||
Definition: &parser.WindowDefinition{
|
||||
Lparen: pos(36),
|
||||
Rparen: pos(37),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
AssertParseStatement(t, `SELECT * ORDER BY foo ASC, bar DESC`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
|
|
@ -2259,64 +2249,64 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
// AssertParseStatement(t, `SELECT * UNION SELECT * ORDER BY foo`, &parser.SelectStatement{
|
||||
// Select: pos(0),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(7)},
|
||||
// },
|
||||
// Union: pos(9),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Select: pos(15),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(22)},
|
||||
// },
|
||||
// },
|
||||
// Order: pos(24),
|
||||
// OrderBy: pos(30),
|
||||
// OrderingTerms: []*parser.OrderingTerm{
|
||||
// {X: &parser.Ident{NamePos: pos(33), Name: "foo"}},
|
||||
// },
|
||||
// })
|
||||
// AssertParseStatement(t, `SELECT * UNION ALL SELECT *`, &parser.SelectStatement{
|
||||
// Select: pos(0),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(7)},
|
||||
// },
|
||||
// Union: pos(9),
|
||||
// UnionAll: pos(15),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Select: pos(19),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(26)},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// AssertParseStatement(t, `SELECT * INTERSECT SELECT *`, &parser.SelectStatement{
|
||||
// Select: pos(0),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(7)},
|
||||
// },
|
||||
// Intersect: pos(9),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Select: pos(19),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(26)},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// AssertParseStatement(t, `SELECT * EXCEPT SELECT *`, &parser.SelectStatement{
|
||||
// Select: pos(0),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(7)},
|
||||
// },
|
||||
// Except: pos(9),
|
||||
// Compound: &parser.SelectStatement{
|
||||
// Select: pos(16),
|
||||
// Columns: []*parser.ResultColumn{
|
||||
// {Star: pos(23)},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
AssertParseStatement(t, `SELECT * UNION SELECT * ORDER BY foo`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(7)},
|
||||
},
|
||||
Union: pos(9),
|
||||
Compound: &parser.SelectStatement{
|
||||
Select: pos(15),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(22)},
|
||||
},
|
||||
},
|
||||
Order: pos(24),
|
||||
OrderBy: pos(30),
|
||||
OrderingTerms: []*parser.OrderingTerm{
|
||||
{X: &parser.Ident{NamePos: pos(33), Name: "foo"}},
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT * UNION ALL SELECT *`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(7)},
|
||||
},
|
||||
Union: pos(9),
|
||||
UnionAll: pos(15),
|
||||
Compound: &parser.SelectStatement{
|
||||
Select: pos(19),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(26)},
|
||||
},
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT * INTERSECT SELECT *`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(7)},
|
||||
},
|
||||
Intersect: pos(9),
|
||||
Compound: &parser.SelectStatement{
|
||||
Select: pos(19),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(26)},
|
||||
},
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT * EXCEPT SELECT *`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(7)},
|
||||
},
|
||||
Except: pos(9),
|
||||
Compound: &parser.SelectStatement{
|
||||
Select: pos(16),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Star: pos(23)},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/*AssertParseStatement(t, `VALUES (1, 2), (3, 4)`, &parser.SelectStatement{
|
||||
Values: pos(0),
|
||||
|
|
@ -3357,106 +3347,16 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
},
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT fld1, fld2, COUNT(*) FROM tbl where fld1 = 1 group by fld1, fld2 limit 1`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{NamePos: pos(7), Name: "fld1"}},
|
||||
{Expr: &parser.Ident{NamePos: pos(13), Name: "fld2"}},
|
||||
{
|
||||
Expr: &parser.Call{
|
||||
Name: &parser.Ident{NamePos: pos(19), Name: "COUNT"},
|
||||
Lparen: pos(24),
|
||||
Star: pos(25),
|
||||
Rparen: pos(26),
|
||||
},
|
||||
},
|
||||
},
|
||||
From: pos(28),
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{NamePos: pos(33), Name: "tbl"}},
|
||||
Where: pos(37),
|
||||
WhereExpr: &parser.BinaryExpr{
|
||||
X: &parser.Ident{NamePos: pos(43), Name: "fld1"},
|
||||
OpPos: pos(48),
|
||||
Op: parser.EQ,
|
||||
Y: &parser.IntegerLit{
|
||||
ValuePos: pos(50),
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
Group: pos(52),
|
||||
GroupBy: pos(58),
|
||||
GroupByExprs: []parser.Expr{
|
||||
&parser.Ident{NamePos: pos(61), Name: "fld1"},
|
||||
&parser.Ident{NamePos: pos(67), Name: "fld2"},
|
||||
},
|
||||
Limit: pos(72),
|
||||
LimitExpr: &parser.IntegerLit{
|
||||
ValuePos: pos(78),
|
||||
Value: "1",
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT DISTINCT score FROM grouper order by score asc limit 5`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Distinct: pos(7),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{NamePos: pos(16), Name: "score"}},
|
||||
},
|
||||
From: pos(22),
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{NamePos: pos(27), Name: "grouper"}},
|
||||
Order: pos(35),
|
||||
OrderBy: pos(41),
|
||||
OrderingTerms: []*parser.OrderingTerm{
|
||||
{
|
||||
X: &parser.Ident{NamePos: pos(44), Name: "score"},
|
||||
Asc: pos(50),
|
||||
},
|
||||
},
|
||||
Limit: pos(54),
|
||||
LimitExpr: &parser.IntegerLit{
|
||||
ValuePos: pos(60),
|
||||
Value: "5",
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT DISTINCT score FROM grouper order by score desc limit 5`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Distinct: pos(7),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{NamePos: pos(16), Name: "score"}},
|
||||
},
|
||||
From: pos(22),
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{NamePos: pos(27), Name: "grouper"}},
|
||||
Order: pos(35),
|
||||
OrderBy: pos(41),
|
||||
OrderingTerms: []*parser.OrderingTerm{
|
||||
{
|
||||
X: &parser.Ident{NamePos: pos(44), Name: "score"},
|
||||
Desc: pos(50),
|
||||
},
|
||||
},
|
||||
Limit: pos(55),
|
||||
LimitExpr: &parser.IntegerLit{
|
||||
ValuePos: pos(61),
|
||||
Value: "5",
|
||||
},
|
||||
})
|
||||
AssertParseStatement(t, `SELECT fld FROM tbl limit 10`, &parser.SelectStatement{
|
||||
Select: pos(0),
|
||||
Columns: []*parser.ResultColumn{
|
||||
{Expr: &parser.Ident{NamePos: pos(7), Name: "fld"}},
|
||||
},
|
||||
From: pos(11),
|
||||
Source: &parser.QualifiedTableName{Name: &parser.Ident{NamePos: pos(16), Name: "tbl"}},
|
||||
Limit: pos(20),
|
||||
LimitExpr: &parser.IntegerLit{
|
||||
ValuePos: pos(26),
|
||||
Value: "10",
|
||||
},
|
||||
})
|
||||
// our previous SQL implementation supported "limit 10, 5" to mean a limit of 10 items,
|
||||
// starting from the 5th item. The new implementation does not support this feature yet.
|
||||
// AssertParseStatement(t, `SELECT fld FROM tbl limit 10, 5`, nil) // 1:27: expected semicolon or EOF, found 10
|
||||
// the previous SQL implementation allowed `where not [condition]` but we don't currently.
|
||||
// AssertParseStatement(t, `SELECT _id FROM tbl where not fld = 1 limit 10`, nil) // 1:31: expected EXISTS, found fld
|
||||
|
||||
if false {
|
||||
// not working because we don't support limit(x), we support top(x), i think?
|
||||
AssertParseStatement(t, `SELECT fld1, fld2, COUNT(*) FROM tbl where fld1 = 1 group by fld1, fld2 limit 1`, nil) // 1:73: expected semicolon or EOF, found limit
|
||||
AssertParseStatement(t, `SELECT DISTINCT score FROM grouper order by score asc limit 5`, nil) // 1:55: expected semicolon or EOF, found limit
|
||||
AssertParseStatement(t, `SELECT DISTINCT score FROM grouper order by score desc limit 5`, nil) // 1:56: expected semicolon or EOF, found limit
|
||||
AssertParseStatement(t, `SELECT fld FROM tbl limit 10`, nil) // 1:27: expected semicolon or EOF, found 10
|
||||
AssertParseStatement(t, `SELECT fld FROM tbl limit 10, 5`, nil) // 1:27: expected semicolon or EOF, found 10
|
||||
AssertParseStatement(t, `SELECT _id FROM tbl where not fld = 1 limit 10`, nil) // 1:31: expected EXISTS, found fld
|
||||
}
|
||||
/*AssertParseStatementError(t, `WITH `, `1:5: expected table name, found 'EOF'`)
|
||||
AssertParseStatementError(t, `WITH cte`, `1:8: expected AS, found 'EOF'`)
|
||||
AssertParseStatementError(t, `WITH cte (`, `1:10: expected column name, found 'EOF'`)
|
||||
|
|
@ -3499,11 +3399,11 @@ func TestParser_ParseStatement(t *testing.T) {
|
|||
AssertParseStatementError(t, `SELECT * GROUP BY`, `1:17: expected expression, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * GROUP BY foo bar`, `1:23: expected semicolon or EOF, found bar`)
|
||||
AssertParseStatementError(t, `SELECT * GROUP BY foo HAVING`, `1:28: expected expression, found 'EOF'`)
|
||||
// AssertParseStatementError(t, `SELECT * WINDOW`, `1:15: expected window name, found 'EOF'`)
|
||||
// AssertParseStatementError(t, `SELECT * WINDOW win1`, `1:20: expected AS, found 'EOF'`)
|
||||
// AssertParseStatementError(t, `SELECT * WINDOW win1 AS`, `1:23: expected left paren, found 'EOF'`)
|
||||
// AssertParseStatementError(t, `SELECT * WINDOW win1 AS (`, `1:25: expected right paren, found 'EOF'`)
|
||||
// AssertParseStatementError(t, `SELECT * WINDOW win1 AS () win2`, `1:28: expected semicolon or EOF, found win2`)
|
||||
AssertParseStatementError(t, `SELECT * WINDOW`, `1:15: expected window name, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * WINDOW win1`, `1:20: expected AS, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * WINDOW win1 AS`, `1:23: expected left paren, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * WINDOW win1 AS (`, `1:25: expected right paren, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * WINDOW win1 AS () win2`, `1:28: expected semicolon or EOF, found win2`)
|
||||
AssertParseStatementError(t, `SELECT * ORDER`, `1:14: expected BY, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * ORDER BY`, `1:17: expected expression, found 'EOF'`)
|
||||
AssertParseStatementError(t, `SELECT * ORDER BY 1,`, `1:20: expected expression, found 'EOF'`)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ const (
|
|||
ACTION
|
||||
ADD
|
||||
AFTER
|
||||
AGG_COLUMN
|
||||
AGG_FUNCTION
|
||||
ALL
|
||||
ALTER
|
||||
ANALYZE
|
||||
|
|
@ -104,9 +106,9 @@ const (
|
|||
COMMENT
|
||||
CONFLICT
|
||||
CONSTRAINT
|
||||
COPY
|
||||
CREATE
|
||||
CROSS
|
||||
CTIME_KW
|
||||
CURRENT
|
||||
CURRENT_DATE
|
||||
CURRENT_TIMESTAMP
|
||||
|
|
@ -165,13 +167,11 @@ const (
|
|||
LAST
|
||||
LEFT
|
||||
LIKE
|
||||
LIMIT
|
||||
LRU
|
||||
MAP
|
||||
MATCH
|
||||
MAX
|
||||
MIN
|
||||
MODEL
|
||||
NO
|
||||
NOT
|
||||
NOTBETWEEN
|
||||
|
|
@ -194,7 +194,6 @@ const (
|
|||
PLAN
|
||||
PRAGMA
|
||||
PRECEDING
|
||||
PREDICT
|
||||
PRIMARY
|
||||
QUERY
|
||||
RANGE
|
||||
|
|
@ -305,6 +304,8 @@ var tokens = [...]string{
|
|||
ACTION: "ACTION",
|
||||
ADD: "ADD",
|
||||
AFTER: "AFTER",
|
||||
AGG_COLUMN: "AGG_COLUMN",
|
||||
AGG_FUNCTION: "AGG_FUNCTION",
|
||||
ALL: "ALL",
|
||||
ALTER: "ALTER",
|
||||
ANALYZE: "ANALYZE",
|
||||
|
|
@ -331,9 +332,9 @@ var tokens = [...]string{
|
|||
COMMENT: "COMMENT",
|
||||
CONFLICT: "CONFLICT",
|
||||
CONSTRAINT: "CONSTRAINT",
|
||||
COPY: "COPY",
|
||||
CREATE: "CREATE",
|
||||
CROSS: "CROSS",
|
||||
CTIME_KW: "CTIME_KW",
|
||||
CURRENT: "CURRENT",
|
||||
CURRENT_DATE: "CURRENT_DATE",
|
||||
CURRENT_TIMESTAMP: "CURRENT_TIMESTAMP",
|
||||
|
|
@ -392,13 +393,11 @@ var tokens = [...]string{
|
|||
LAST: "LAST",
|
||||
LEFT: "LEFT",
|
||||
LIKE: "LIKE",
|
||||
LIMIT: "LIMIT",
|
||||
MAP: "MAP",
|
||||
LRU: "LRU",
|
||||
MATCH: "MATCH",
|
||||
MAX: "MAX",
|
||||
MIN: "MIN",
|
||||
MODEL: "MODEL",
|
||||
NO: "NO",
|
||||
NOT: "NOT",
|
||||
NOTBETWEEN: "NOTBETWEEN",
|
||||
|
|
@ -421,7 +420,6 @@ var tokens = [...]string{
|
|||
PLAN: "PLAN",
|
||||
PRAGMA: "PRAGMA",
|
||||
PRECEDING: "PRECEDING",
|
||||
PREDICT: "PREDICT",
|
||||
PRIMARY: "PRIMARY",
|
||||
QUERY: "QUERY",
|
||||
RANGE: "RANGE",
|
||||
|
|
|
|||
|
|
@ -582,15 +582,7 @@ func walk(v Visitor, node Node) (_ Node, err error) {
|
|||
if err := walkIdent(v, &n.Alias); err != nil {
|
||||
return node, err
|
||||
}
|
||||
if err := walkTableQueryOptionList(v, n.QueryOptions); err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
||||
case *TableQueryOption:
|
||||
if err := walkIdent(v, &n.OptionName); err != nil {
|
||||
return node, err
|
||||
}
|
||||
if err := walkIdentList(v, n.OptionParams); err != nil {
|
||||
if err := walkIdent(v, &n.Index); err != nil {
|
||||
return node, err
|
||||
}
|
||||
|
||||
|
|
@ -852,16 +844,3 @@ func walkColumnDefinitionList(v Visitor, a []*ColumnDefinition) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func walkTableQueryOptionList(v Visitor, a []*TableQueryOption) error {
|
||||
for i := range a {
|
||||
if def, err := walk(v, a[i]); err != nil {
|
||||
return err
|
||||
} else if def != nil {
|
||||
a[i] = def.(*TableQueryOption)
|
||||
} else {
|
||||
a[i] = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,120 +0,0 @@
|
|||
// Copyright 2022 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/dax"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// compileCopyStatement compiles a parser.CopyStatement AST into a PlanOperator
|
||||
func (p *ExecutionPlanner) compileCopyStatement(stmt *parser.CopyStatement) (types.PlanOperator, error) {
|
||||
query := NewPlanOpQuery(p, NewPlanOpNullTable(), p.sql)
|
||||
query.AddWarning("🦖 here there be dragons! COPY statement is experimental.")
|
||||
|
||||
// handle projections
|
||||
projections := make([]types.PlanExpression, 0)
|
||||
for _, c := range stmt.Source.PossibleOutputColumns() {
|
||||
expr := &parser.QualifiedRef{
|
||||
Table: &parser.Ident{Name: c.TableName},
|
||||
Column: &parser.Ident{Name: c.ColumnName},
|
||||
ColumnIndex: c.ColumnIndex,
|
||||
RefDataType: c.Datatype,
|
||||
}
|
||||
planExpr, err := p.compileExpr(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projections = append(projections, planExpr)
|
||||
}
|
||||
|
||||
// handle the where clause
|
||||
where, err := p.compileExpr(stmt.WhereExpr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// compile source
|
||||
source, err := p.compileSource(query, stmt.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if we did have a where, insert the filter op
|
||||
if where != nil {
|
||||
source = NewPlanOpFilter(p, where, source)
|
||||
}
|
||||
|
||||
var compiledOp types.PlanOperator
|
||||
url := ""
|
||||
apiKey := ""
|
||||
|
||||
if stmt.Url != nil {
|
||||
lit, ok := stmt.Url.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrStringLiteral(stmt.Url.Pos().Line, stmt.Url.Pos().Column)
|
||||
}
|
||||
url = lit.Value
|
||||
}
|
||||
|
||||
if stmt.ApiKey != nil {
|
||||
lit, ok := stmt.ApiKey.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrStringLiteral(stmt.ApiKey.Pos().Line, stmt.ApiKey.Pos().Column)
|
||||
}
|
||||
apiKey = lit.Value
|
||||
}
|
||||
|
||||
// get the source table
|
||||
tname := dax.TableName(stmt.Source.String())
|
||||
tbl, err := p.schemaAPI.TableByName(context.Background(), tname)
|
||||
if err != nil {
|
||||
if isTableNotFoundError(err) {
|
||||
return nil, sql3.NewErrTableNotFound(0, 0, stmt.Source.String())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// get the ddl of source table and subst target table name
|
||||
ddl := generateTableDDL(tbl, stmt.TargetName.Name)
|
||||
compiledOp = NewPlanOpCopy(p, stmt.TargetName.Name, url, apiKey, ddl, NewPlanOpProjection(projections, source))
|
||||
children := []types.PlanOperator{
|
||||
compiledOp,
|
||||
}
|
||||
return query.WithChildren(children...)
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) analyzeCopyStatement(ctx context.Context, stmt *parser.CopyStatement) error {
|
||||
|
||||
// analyze source
|
||||
var err error
|
||||
source, err := p.analyzeSource(ctx, stmt.Source, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt.Source = source
|
||||
|
||||
// analyze where
|
||||
expr, err := p.analyzeExpression(ctx, stmt.WhereExpr, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt.WhereExpr = expr
|
||||
|
||||
expr, err = p.analyzeExpression(ctx, stmt.Url, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt.Url = expr
|
||||
|
||||
expr, err = p.analyzeExpression(ctx, stmt.ApiKey, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt.ApiKey = expr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -17,10 +17,7 @@ func (p *ExecutionPlanner) compileCreateDatabaseStatement(stmt *parser.CreateDat
|
|||
databaseName := strings.ToLower(parser.IdentName(stmt.Name))
|
||||
failIfExists := !stmt.IfNotExists.IsValid()
|
||||
|
||||
// By default, a database needs at least 1 unit for simple querying, so by
|
||||
// setting the default to 1, users can simply type `CREATE DATABASE [name]` without
|
||||
// needing to append `WITH UNITS 1;`
|
||||
units := 1
|
||||
units := 0
|
||||
description := ""
|
||||
|
||||
// apply database options
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
// Copyright 2023 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// compileCreateFunctionStatement compiles a parser.CreateFunctionStatement AST into a PlanOperator
|
||||
func (p *ExecutionPlanner) compileCreateFunctionStatement(stmt *parser.CreateFunctionStatement) (types.PlanOperator, error) {
|
||||
functionName := parser.IdentName(stmt.Name)
|
||||
function := &functionSystemObject{
|
||||
name: functionName,
|
||||
}
|
||||
|
||||
lang := "sql"
|
||||
if len(stmt.Options) > 0 {
|
||||
for _, o := range stmt.Options {
|
||||
switch strings.ToLower(o.Name.String()) {
|
||||
case "language":
|
||||
lit, ok := o.OptionExpr.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrStringLiteral(o.OptionExpr.Pos().Line, o.OptionExpr.Pos().Column)
|
||||
}
|
||||
l := strings.ToLower(lit.Value)
|
||||
switch l {
|
||||
case "python":
|
||||
lang = l
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unsupported language '%s'", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function.language = lang
|
||||
|
||||
// TODO(pok) - hobble user defined functions for now
|
||||
|
||||
switch lang {
|
||||
case "sql":
|
||||
return nil, sql3.NewErrInternalf("unsupported language '%s'", lang)
|
||||
case "python":
|
||||
// return nil, sql3.NewErrInternalf("unsupported language '%s'", lang)
|
||||
|
||||
// function body is in the return statement
|
||||
if len(stmt.Body) != 1 {
|
||||
return nil, sql3.NewErrInternalf("unexpected body len '%d'", len(stmt.Body))
|
||||
}
|
||||
|
||||
rs, ok := stmt.Body[0].(*parser.ReturnStatement)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected statement type '%T'", stmt.Body[0])
|
||||
}
|
||||
|
||||
bexpr, ok := rs.ReturnExpr.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected expression type '%T'", rs.ReturnExpr)
|
||||
}
|
||||
|
||||
function.body = bexpr.Value
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unsupported language '%s'", lang)
|
||||
}
|
||||
|
||||
fn := NewPlanOpCreateFunction(p, stmt.IfNotExists.IsValid(), function)
|
||||
fn.AddWarning("🦖 here there be dragons! CREATE FUNCTION statement is experimental.")
|
||||
|
||||
query := NewPlanOpQuery(p, fn, p.sql)
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) analyzeCreateFunctionStatement(stmt *parser.CreateFunctionStatement) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
// Copyright 2022 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// TODO (pok) what does 'if not exists' do?
|
||||
|
||||
// compileCreateModelStatement compiles a parser.CreateModelStatement AST into a PlanOperator
|
||||
func (p *ExecutionPlanner) compileCreateModelStatement(stmt *parser.CreateModelStatement) (types.PlanOperator, error) {
|
||||
modelName := parser.IdentName(stmt.Name)
|
||||
|
||||
// does the model exist
|
||||
obj, err := p.getModelByName(modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if obj != nil {
|
||||
return nil, sql3.NewErrInternalf("model '%s' already exists", modelName)
|
||||
}
|
||||
|
||||
// if we got to here model does not exist
|
||||
model := &modelSystemObject{
|
||||
name: modelName,
|
||||
}
|
||||
|
||||
for _, o := range stmt.Options {
|
||||
optName := parser.IdentName(o.Name)
|
||||
|
||||
switch strings.ToLower(optName) {
|
||||
case "modeltype":
|
||||
lit, ok := o.OptionExpr.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", o.OptionExpr)
|
||||
}
|
||||
model.modelType = lit.Value
|
||||
|
||||
case "labels":
|
||||
lit, ok := o.OptionExpr.(*parser.SetLiteralExpr)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", o.OptionExpr)
|
||||
}
|
||||
model.labels = make([]string, len(lit.Members))
|
||||
for i, m := range lit.Members {
|
||||
mlit, ok := m.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", m)
|
||||
}
|
||||
model.labels[i] = mlit.Value
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unexpected model option '%s'", optName)
|
||||
}
|
||||
}
|
||||
|
||||
selOp, err := p.compileSelectStatement(stmt.ModelQuery, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// build a list of input columns for the model from the select query
|
||||
schema := selOp.Schema()
|
||||
model.inputColumns = make([]string, 0)
|
||||
for _, p := range schema {
|
||||
// if we have no column name, we have an error
|
||||
if len(p.ColumnName) == 0 {
|
||||
return nil, sql3.NewErrInternalf("query output columns used as inputs to models must be named")
|
||||
}
|
||||
// exclude any that are in the labels
|
||||
isLabel := false
|
||||
for _, l := range model.labels {
|
||||
if strings.EqualFold(p.ColumnName, l) {
|
||||
isLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isLabel {
|
||||
model.inputColumns = append(model.inputColumns, p.ColumnName)
|
||||
}
|
||||
}
|
||||
createModel := NewPlanOpCreateModel(p, model, selOp)
|
||||
createModel.AddWarning("🦖 here there be dragons! CREATE MODEL statement is experimental.")
|
||||
|
||||
query := NewPlanOpQuery(p, createModel, p.sql)
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) analyzeCreateModelStatement(ctx context.Context, stmt *parser.CreateModelStatement) error {
|
||||
// iterate the options
|
||||
for _, opt := range stmt.Options {
|
||||
optName := parser.IdentName(opt.Name)
|
||||
if !isValidModelOption(optName) {
|
||||
return sql3.NewErrInternalf("invalid model option '%s'", optName)
|
||||
}
|
||||
e, err := p.analyzeModelOptionExpr(ctx, optName, opt.OptionExpr, stmt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opt.OptionExpr = e
|
||||
}
|
||||
|
||||
// analyze the select
|
||||
_, err := p.analyzeSelectStatement(ctx, stmt.ModelQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidModelOption(name string) bool {
|
||||
switch strings.ToLower(name) {
|
||||
case "modeltype":
|
||||
return true
|
||||
|
||||
case "labels":
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) analyzeModelOptionExpr(ctx context.Context, optName string, expr parser.Expr, scope parser.Statement) (parser.Expr, error) {
|
||||
if expr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
e, err := p.analyzeExpression(ctx, expr, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(optName) {
|
||||
case "modeltype":
|
||||
|
||||
// model type needs to be a string literal
|
||||
if !(e.IsLiteral() && typeIsString(e.DataType())) {
|
||||
return nil, sql3.NewErrStringLiteral(e.Pos().Line, e.Pos().Column)
|
||||
}
|
||||
ty, ok := e.(*parser.StringLit)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", e)
|
||||
}
|
||||
|
||||
// these are the model types supported
|
||||
switch strings.ToLower(ty.Value) {
|
||||
case "linear_regresssion":
|
||||
break
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unexpected model tyoe '%s'", ty.Value)
|
||||
}
|
||||
return e, nil
|
||||
|
||||
case "labels":
|
||||
// labels needs to be a string array literal
|
||||
// TODO (pok) revist 'set' literals (should be array literal; type checking could be robustified etc.)
|
||||
if !e.IsLiteral() {
|
||||
return nil, sql3.NewErrInternalf("string array literal expected")
|
||||
}
|
||||
ok, baseType := typeIsSet(e.DataType())
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("array expression expected")
|
||||
}
|
||||
if !typeIsString(baseType) {
|
||||
return nil, sql3.NewErrInternalf("string array expected")
|
||||
}
|
||||
return e, nil
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unexpected option name '%s'", optName)
|
||||
}
|
||||
}
|
||||
|
|
@ -171,6 +171,15 @@ func (p *ExecutionPlanner) compileColumn(ctx context.Context, col *parser.Column
|
|||
unit := c.Expr.(*parser.StringLit)
|
||||
timeUnit = unit.Value
|
||||
|
||||
if c.EpochExpr != nil {
|
||||
epochString := c.EpochExpr.(*parser.StringLit)
|
||||
tm, err := time.ParseInLocation(time.RFC3339, epochString.Value, time.UTC)
|
||||
if err != nil {
|
||||
return nil, sql3.NewErrInvalidTimeEpoch(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Line, epochString.Value)
|
||||
}
|
||||
epoch = tm
|
||||
}
|
||||
|
||||
case *parser.TimeQuantumConstraint:
|
||||
unit := c.Expr.(*parser.StringLit)
|
||||
timeQuantum = pilosa.TimeQuantum(unit.Value)
|
||||
|
|
@ -376,6 +385,13 @@ func (p *ExecutionPlanner) analyzeColumn(typeName string, col *parser.ColumnDefi
|
|||
if !pilosa.IsValidTimeUnit(unit.Value) {
|
||||
return sql3.NewErrInvalidTimeUnit(c.Expr.Pos().Line, c.Expr.Pos().Column, unit.Value)
|
||||
}
|
||||
if c.EpochExpr != nil {
|
||||
//check the type of the expression
|
||||
_, ok := c.EpochExpr.(*parser.StringLit)
|
||||
if !ok {
|
||||
return sql3.NewErrStringLiteral(c.EpochExpr.Pos().Line, c.EpochExpr.Pos().Column)
|
||||
}
|
||||
}
|
||||
handledConstraints[parser.TIMEUNIT] = struct{}{}
|
||||
|
||||
case *parser.TimeQuantumConstraint:
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright 2023 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// compileDropModelStatement compiles a DROP MODEL statement into a PlanOperator.
|
||||
func (p *ExecutionPlanner) compileDropModelStatement(stmt *parser.DropModelStatement) (_ types.PlanOperator, err error) {
|
||||
modelName := parser.IdentName(stmt.Name)
|
||||
v, err := p.getModelByName(modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == nil && !stmt.IfExists.IsValid() {
|
||||
return nil, sql3.NewErrModelNotFound(0, 0, modelName)
|
||||
}
|
||||
|
||||
dropModel := NewPlanOpDropModel(p, stmt.IfExists.IsValid(), modelName)
|
||||
dropModel.AddWarning("🦖 here there be dragons! DROP MODEL statement is experimental.")
|
||||
|
||||
return NewPlanOpQuery(p, dropModel, p.sql), nil
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// Copyright 2022 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// compilePredictStatement compiles a parser.PredictStatement AST into a PlanOperator
|
||||
func (p *ExecutionPlanner) compilePredictStatement(ctx context.Context, stmt *parser.PredictStatement) (types.PlanOperator, error) {
|
||||
|
||||
// go get the model
|
||||
modelName := parser.IdentName(stmt.ModelName)
|
||||
|
||||
// does the model exist
|
||||
obj, err := p.getModelByName(modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if obj == nil {
|
||||
return nil, sql3.NewErrInternalf("model '%s' not found", modelName)
|
||||
}
|
||||
|
||||
selOp, err := p.compileSelectStatement(stmt.InputQuery, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
predict := NewPlanOpPredict(p, obj, selOp)
|
||||
predict.AddWarning("🦖 here there be dragons! PREDICT statement is experimental.")
|
||||
|
||||
query := NewPlanOpQuery(p, predict, p.sql)
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) analyzePredictStatement(ctx context.Context, stmt *parser.PredictStatement) error {
|
||||
|
||||
// analyze the select
|
||||
_, err := p.analyzeSelectStatement(ctx, stmt.InputQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -305,7 +305,7 @@ func (p *ExecutionPlanner) compileSelectStatement(stmt *parser.SelectStatement,
|
|||
}
|
||||
}
|
||||
|
||||
// insert the top operator if it exists, or limit - analyzer should have caught the case of both existing
|
||||
// insert the top operator if it exists
|
||||
if stmt.Top.IsValid() {
|
||||
topExpr, err := p.compileExpr(stmt.TopExpr)
|
||||
if err != nil {
|
||||
|
|
@ -313,14 +313,6 @@ func (p *ExecutionPlanner) compileSelectStatement(stmt *parser.SelectStatement,
|
|||
}
|
||||
compiledOp = NewPlanOpTop(topExpr, compiledOp)
|
||||
}
|
||||
// handle limit
|
||||
if stmt.Limit.IsValid() {
|
||||
limitExpr, err := p.compileExpr(stmt.LimitExpr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
compiledOp = NewPlanOpTop(limitExpr, compiledOp)
|
||||
}
|
||||
|
||||
// handle distinct
|
||||
if stmt.Distinct.IsValid() {
|
||||
|
|
@ -428,19 +420,6 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc
|
|||
return op, nil
|
||||
|
||||
}
|
||||
|
||||
// get any query hints
|
||||
queryHints := make([]*TableQueryHint, 0)
|
||||
for _, o := range sourceExpr.QueryOptions {
|
||||
h := &TableQueryHint{
|
||||
name: parser.IdentName(o.OptionName),
|
||||
}
|
||||
for _, op := range o.OptionParams {
|
||||
h.params = append(h.params, parser.IdentName(op))
|
||||
}
|
||||
queryHints = append(queryHints, h)
|
||||
}
|
||||
|
||||
// get all the columns for this table - we will eliminate unused ones
|
||||
// later on in the optimizer
|
||||
extractColumns := make([]string, 0)
|
||||
|
|
@ -452,9 +431,9 @@ func (p *ExecutionPlanner) compileSource(scope *PlanOpQuery, source parser.Sourc
|
|||
if sourceExpr.Alias != nil {
|
||||
aliasName := parser.IdentName(sourceExpr.Alias)
|
||||
|
||||
return NewPlanOpRelAlias(aliasName, NewPlanOpPQLTableScan(p, tableName, extractColumns, queryHints)), nil
|
||||
return NewPlanOpRelAlias(aliasName, NewPlanOpPQLTableScan(p, tableName, extractColumns)), nil
|
||||
}
|
||||
return NewPlanOpPQLTableScan(p, tableName, extractColumns, queryHints), nil
|
||||
return NewPlanOpPQLTableScan(p, tableName, extractColumns), nil
|
||||
|
||||
case *parser.TableValuedFunction:
|
||||
callExpr, err := p.compileCallExpr(sourceExpr.Call)
|
||||
|
|
@ -570,8 +549,6 @@ func (p *ExecutionPlanner) analyzeSource(ctx context.Context, source parser.Sour
|
|||
return paren, nil
|
||||
}
|
||||
|
||||
// if we got to here, not a view, so do table stuff
|
||||
|
||||
// check table exists
|
||||
tname := dax.TableName(objectName)
|
||||
tbl, err := p.schemaAPI.TableByName(ctx, tname)
|
||||
|
|
@ -593,35 +570,6 @@ func (p *ExecutionPlanner) analyzeSource(ctx context.Context, source parser.Sour
|
|||
source.OutputColumns = append(source.OutputColumns, soc)
|
||||
}
|
||||
|
||||
// check query hints
|
||||
for _, o := range source.QueryOptions {
|
||||
opt := parser.IdentName(o.OptionName)
|
||||
switch strings.ToLower(opt) {
|
||||
case "flatten":
|
||||
// should have 1 param and should be a column name
|
||||
if len(o.OptionParams) != 1 {
|
||||
// error
|
||||
return nil, sql3.NewErrInvalidQueryHintParameterCount(o.LParen.Column, o.LParen.Line, opt, "column name", 1, len(o.OptionParams))
|
||||
}
|
||||
for _, op := range o.OptionParams {
|
||||
param := parser.IdentName(op)
|
||||
found := false
|
||||
for _, oc := range source.OutputColumns {
|
||||
if strings.EqualFold(param, oc.ColumnName) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, sql3.NewErrColumnNotFound(op.NamePos.Line, op.NamePos.Column, param)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrUnknownQueryHint(o.OptionName.NamePos.Line, o.OptionName.NamePos.Column, opt)
|
||||
}
|
||||
}
|
||||
|
||||
return source, nil
|
||||
|
||||
case *parser.TableValuedFunction:
|
||||
|
|
@ -665,10 +613,6 @@ func (p *ExecutionPlanner) analyzeSelectStatement(ctx context.Context, stmt *par
|
|||
}
|
||||
}
|
||||
|
||||
if stmt.TopExpr != nil && stmt.LimitExpr != nil {
|
||||
return nil, sql3.NewErrErrTopLimitCannotCoexist(stmt.TopExpr.Pos().Line, stmt.TopExpr.Pos().Column)
|
||||
}
|
||||
|
||||
expr, err := p.analyzeExpression(ctx, stmt.TopExpr, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -680,17 +624,6 @@ func (p *ExecutionPlanner) analyzeSelectStatement(ctx context.Context, stmt *par
|
|||
stmt.TopExpr = expr
|
||||
}
|
||||
|
||||
expr, err = p.analyzeExpression(ctx, stmt.LimitExpr, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expr != nil {
|
||||
if !(expr.IsLiteral() && typeIsInteger(expr.DataType())) {
|
||||
return nil, sql3.NewErrIntegerLiteral(stmt.LimitExpr.Pos().Line, stmt.LimitExpr.Pos().Column)
|
||||
}
|
||||
stmt.LimitExpr = expr
|
||||
}
|
||||
|
||||
expr, err = p.analyzeExpression(ctx, stmt.HavingExpr, stmt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -73,19 +73,7 @@ func (p *ExecutionPlanner) compileShowDatabasesStatement(ctx context.Context, st
|
|||
return NewPlanOpQuery(p, NewPlanOpProjection(columns, NewPlanOpFeatureBaseDatabases(p, dbs)), p.sql), nil
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) compileShowTablesStatement(ctx context.Context, stmt *parser.ShowTablesStatement) (types.PlanOperator, error) {
|
||||
|
||||
showSystem := false
|
||||
if stmt.With.IsValid() {
|
||||
opt := parser.IdentName(stmt.System)
|
||||
|
||||
if !strings.EqualFold("system", opt) {
|
||||
return nil, sql3.NewErrUnknownShowOption(stmt.System.NamePos.Line, stmt.System.NamePos.Column, opt)
|
||||
}
|
||||
|
||||
showSystem = true
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) compileShowTablesStatement(ctx context.Context, stmt parser.Statement) (types.PlanOperator, error) {
|
||||
tbls, err := p.schemaAPI.Tables(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting tables")
|
||||
|
|
@ -147,7 +135,7 @@ func (p *ExecutionPlanner) compileShowTablesStatement(ctx context.Context, stmt
|
|||
dataType: parser.NewDataTypeString(),
|
||||
}}
|
||||
|
||||
return NewPlanOpQuery(p, NewPlanOpProjection(columns, NewPlanOpFeatureBaseTables(p, pilosa.TablesToIndexInfos(tbls), showSystem)), p.sql), nil
|
||||
return NewPlanOpQuery(p, NewPlanOpProjection(columns, NewPlanOpFeatureBaseTables(p, pilosa.TablesToIndexInfos(tbls))), p.sql), nil
|
||||
}
|
||||
|
||||
func (p *ExecutionPlanner) compileShowColumnsStatement(ctx context.Context, stmt *parser.ShowColumnsStatement) (_ types.PlanOperator, err error) {
|
||||
|
|
|
|||
|
|
@ -69,10 +69,6 @@ func (p *ExecutionPlanner) CompilePlan(ctx context.Context, stmt parser.Statemen
|
|||
rootOperator, err = p.compileSelectStatement(stmt, false)
|
||||
case *parser.ShowDatabasesStatement:
|
||||
rootOperator, err = p.compileShowDatabasesStatement(ctx, stmt)
|
||||
case *parser.CopyStatement:
|
||||
rootOperator, err = p.compileCopyStatement(stmt)
|
||||
case *parser.PredictStatement:
|
||||
rootOperator, err = p.compilePredictStatement(ctx, stmt)
|
||||
case *parser.ShowTablesStatement:
|
||||
rootOperator, err = p.compileShowTablesStatement(ctx, stmt)
|
||||
case *parser.ShowColumnsStatement:
|
||||
|
|
@ -97,23 +93,16 @@ func (p *ExecutionPlanner) CompilePlan(ctx context.Context, stmt parser.Statemen
|
|||
rootOperator, err = p.compileDropTableStatement(ctx, stmt)
|
||||
case *parser.DropViewStatement:
|
||||
rootOperator, err = p.compileDropViewStatement(ctx, stmt)
|
||||
case *parser.DropModelStatement:
|
||||
rootOperator, err = p.compileDropModelStatement(stmt)
|
||||
case *parser.InsertStatement:
|
||||
rootOperator, err = p.compileInsertStatement(ctx, stmt)
|
||||
case *parser.BulkInsertStatement:
|
||||
rootOperator, err = p.compileBulkInsertStatement(ctx, stmt)
|
||||
case *parser.DeleteStatement:
|
||||
rootOperator, err = p.compileDeleteStatement(stmt)
|
||||
case *parser.CreateModelStatement:
|
||||
rootOperator, err = p.compileCreateModelStatement(stmt)
|
||||
case *parser.CreateFunctionStatement:
|
||||
rootOperator, err = p.compileCreateFunctionStatement(stmt)
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("cannot plan statement: %T", stmt)
|
||||
}
|
||||
// optimize the plan
|
||||
// Optimize the plan.
|
||||
if err == nil {
|
||||
rootOperator, err = p.optimizePlan(ctx, rootOperator)
|
||||
}
|
||||
|
|
@ -141,10 +130,6 @@ func (p *ExecutionPlanner) analyzePlan(ctx context.Context, stmt parser.Statemen
|
|||
return err
|
||||
case *parser.ShowDatabasesStatement:
|
||||
return nil
|
||||
case *parser.CopyStatement:
|
||||
return p.analyzeCopyStatement(ctx, stmt)
|
||||
case *parser.PredictStatement:
|
||||
return p.analyzePredictStatement(ctx, stmt)
|
||||
case *parser.ShowTablesStatement:
|
||||
return nil
|
||||
case *parser.ShowColumnsStatement:
|
||||
|
|
@ -169,19 +154,12 @@ func (p *ExecutionPlanner) analyzePlan(ctx context.Context, stmt parser.Statemen
|
|||
return nil
|
||||
case *parser.DropViewStatement:
|
||||
return nil
|
||||
case *parser.DropModelStatement:
|
||||
return nil
|
||||
case *parser.InsertStatement:
|
||||
return p.analyzeInsertStatement(ctx, stmt)
|
||||
case *parser.BulkInsertStatement:
|
||||
return p.analyzeBulkInsertStatement(ctx, stmt)
|
||||
case *parser.DeleteStatement:
|
||||
return p.analyzeDeleteStatement(ctx, stmt)
|
||||
case *parser.CreateModelStatement:
|
||||
return p.analyzeCreateModelStatement(ctx, stmt)
|
||||
case *parser.CreateFunctionStatement:
|
||||
return p.analyzeCreateFunctionStatement(stmt)
|
||||
|
||||
default:
|
||||
return sql3.NewErrInternalf("cannot analyze statement: %T", stmt)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ package planner
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
pilosa "github.com/featurebasedb/featurebase/v3"
|
||||
|
|
@ -82,11 +81,6 @@ func (s *systemTableDefinitionsWrapper) Tables(ctx context.Context) ([]*dax.Tabl
|
|||
tbls = append(tbls, pilosa.IndexInfoToTable(ii))
|
||||
}
|
||||
|
||||
// order the result - by ID asc right now
|
||||
sort.Slice(tbls, func(i, j int) bool {
|
||||
return tbls[i].ID < tbls[j].ID
|
||||
})
|
||||
|
||||
return tbls, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1514,18 +1514,16 @@ func (n *inOpPlanExpression) WithChildren(children ...types.PlanExpression) (typ
|
|||
|
||||
// callPlanExpression is a function call
|
||||
type callPlanExpression struct {
|
||||
name string
|
||||
args []types.PlanExpression
|
||||
dataType parser.ExprDataType
|
||||
udfReference *functionSystemObject
|
||||
name string
|
||||
args []types.PlanExpression
|
||||
dataType parser.ExprDataType
|
||||
}
|
||||
|
||||
func newCallPlanExpression(name string, args []types.PlanExpression, dataType parser.ExprDataType, udfReference *functionSystemObject) *callPlanExpression {
|
||||
func newCallPlanExpression(name string, args []types.PlanExpression, dataType parser.ExprDataType) *callPlanExpression {
|
||||
return &callPlanExpression{
|
||||
name: name,
|
||||
args: args,
|
||||
dataType: dataType,
|
||||
udfReference: udfReference,
|
||||
name: name,
|
||||
args: args,
|
||||
dataType: dataType,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1593,9 +1591,6 @@ func (n *callPlanExpression) Evaluate(currentRow []interface{}) (interface{}, er
|
|||
case "DATETIMEDIFF":
|
||||
return n.EvaluateDatetimeDiff(currentRow)
|
||||
default:
|
||||
if n.udfReference != nil {
|
||||
return n.evaluateUserDefinedFunction(currentRow)
|
||||
}
|
||||
return nil, sql3.NewErrInternalf("unhandled function name '%s'", n.name)
|
||||
}
|
||||
}
|
||||
|
|
@ -1637,7 +1632,7 @@ func (n *callPlanExpression) WithChildren(children ...types.PlanExpression) (typ
|
|||
if len(children) != len(n.args) {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return newCallPlanExpression(n.name, children, n.dataType, n.udfReference), nil
|
||||
return newCallPlanExpression(n.name, children, n.dataType), nil
|
||||
}
|
||||
|
||||
// aliasPlanExpression is a alias ref
|
||||
|
|
@ -1724,11 +1719,6 @@ func (n *qualifiedRefPlanExpression) Evaluate(currentRow []interface{}) (interfa
|
|||
|
||||
switch n.dataType.(type) {
|
||||
case *parser.DataTypeIDSet, *parser.DataTypeIDSetQuantum:
|
||||
// this could be an []int64 or a []uint64 internally
|
||||
irow, ok := currentRow[n.columnIndex].([]int64)
|
||||
if ok {
|
||||
return irow, nil
|
||||
}
|
||||
row, ok := currentRow[n.columnIndex].([]uint64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type for current row '%T'", currentRow[n.columnIndex])
|
||||
|
|
@ -2059,30 +2049,30 @@ func (n *sysVariablePlanExpression) WithChildren(children ...types.PlanExpressio
|
|||
return n, nil
|
||||
}
|
||||
|
||||
// timestampLiteralPlanExpression is a date literal
|
||||
type timestampLiteralPlanExpression struct {
|
||||
// dateLiteralPlanExpression is a date literal
|
||||
type dateLiteralPlanExpression struct {
|
||||
value time.Time
|
||||
}
|
||||
|
||||
func newTimestampLiteralPlanExpression(value time.Time) *timestampLiteralPlanExpression {
|
||||
return ×tampLiteralPlanExpression{
|
||||
func newDateLiteralPlanExpression(value time.Time) *dateLiteralPlanExpression {
|
||||
return &dateLiteralPlanExpression{
|
||||
value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
|
||||
func (n *dateLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
|
||||
return n.value, nil
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) Type() parser.ExprDataType {
|
||||
func (n *dateLiteralPlanExpression) Type() parser.ExprDataType {
|
||||
return parser.NewDataTypeTimestamp()
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) String() string {
|
||||
func (n *dateLiteralPlanExpression) String() string {
|
||||
return n.value.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) Plan() map[string]interface{} {
|
||||
func (n *dateLiteralPlanExpression) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_expr"] = fmt.Sprintf("%T", n)
|
||||
result["description"] = n.String()
|
||||
|
|
@ -2091,11 +2081,11 @@ func (n *timestampLiteralPlanExpression) Plan() map[string]interface{} {
|
|||
return result
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) Children() []types.PlanExpression {
|
||||
func (n *dateLiteralPlanExpression) Children() []types.PlanExpression {
|
||||
return []types.PlanExpression{}
|
||||
}
|
||||
|
||||
func (n *timestampLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
|
||||
func (n *dateLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
|
|
@ -2670,7 +2660,7 @@ func (p *ExecutionPlanner) compileExpr(expr parser.Expr) (_ types.PlanExpression
|
|||
return newFloatLiteralPlanExpression(expr.Value), nil
|
||||
|
||||
case *parser.DateLit:
|
||||
return newTimestampLiteralPlanExpression(expr.Value), nil
|
||||
return newDateLiteralPlanExpression(expr.Value), nil
|
||||
|
||||
case *parser.SysVariable:
|
||||
return newSysVariablePlanExpression(expr.Name(), expr.Token), nil
|
||||
|
|
@ -2899,15 +2889,7 @@ func (p *ExecutionPlanner) compileCallExpr(expr *parser.Call) (_ types.PlanExpre
|
|||
return agg, nil
|
||||
|
||||
case "PERCENTILE":
|
||||
agg := newPercentilePlanExpression(expr.Name.NamePos, args[0], args[1], expr.ResultDataType)
|
||||
return agg, nil
|
||||
|
||||
case "CORR":
|
||||
agg := newCorrPlanExpression(args[0], args[1], expr.ResultDataType)
|
||||
return agg, nil
|
||||
|
||||
case "VAR":
|
||||
agg := newVarPlanExpression(args[0], expr.ResultDataType)
|
||||
agg := newPercentilePlanExpression(args[0], args[1], expr.ResultDataType)
|
||||
return agg, nil
|
||||
|
||||
case "MIN":
|
||||
|
|
@ -2919,12 +2901,7 @@ func (p *ExecutionPlanner) compileCallExpr(expr *parser.Call) (_ types.PlanExpre
|
|||
return agg, nil
|
||||
|
||||
default:
|
||||
// could be a udf - try to look it up in functions
|
||||
fn, err := p.getFunctionByName(strings.ToLower(callName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newCallPlanExpression(parser.IdentName(expr.Name), args, expr.ResultDataType, fn), nil
|
||||
return newCallPlanExpression(parser.IdentName(expr.Name), args, expr.ResultDataType), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2990,12 +2967,11 @@ func (p *ExecutionPlanner) compileOrderingTermExpr(expr parser.Expr, projections
|
|||
// used by the LIKE/NOT LIKE operator
|
||||
func wildCardToRegexp(pattern string) string {
|
||||
var result strings.Builder
|
||||
result.WriteString("(?i)")
|
||||
|
||||
result.WriteString("(?i)^")
|
||||
rpattern := strings.Replace(pattern, "%", ".*", -1)
|
||||
rpattern = strings.Replace(rpattern, "_", ".+", -1)
|
||||
result.WriteString(rpattern)
|
||||
result.WriteString("$")
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func TestExpressions(t *testing.T) {
|
|||
iop := newInOpPlanExpression(newIntLiteralPlanExpression(10), parser.IN, newIntLiteralPlanExpression(20))
|
||||
assert.Equal(t, iop.String(), "10 in (20)")
|
||||
|
||||
callop := newCallPlanExpression("foo", []types.PlanExpression{newIntLiteralPlanExpression(10)}, parser.NewDataTypeInt(), nil)
|
||||
callop := newCallPlanExpression("foo", []types.PlanExpression{newIntLiteralPlanExpression(10)}, parser.NewDataTypeInt())
|
||||
assert.Equal(t, callop.String(), "foo(10)")
|
||||
|
||||
alop := newAliasPlanExpression("frobny", newIntLiteralPlanExpression(10))
|
||||
|
|
@ -65,7 +65,7 @@ func TestExpressions(t *testing.T) {
|
|||
assert.Equal(t, blop.String(), "false")
|
||||
|
||||
tm, _ := time.ParseInLocation(time.RFC3339, "2012-11-01T22:08:41+00:00", time.UTC)
|
||||
dlop := newTimestampLiteralPlanExpression(tm)
|
||||
dlop := newDateLiteralPlanExpression(tm)
|
||||
assert.Equal(t, dlop.String(), "2012-11-01T22:08:41Z")
|
||||
|
||||
slop := newStringLiteralPlanExpression("foo")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package planner
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/pql"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
|
|
@ -881,7 +880,6 @@ func (n *maxPlanExpression) WithChildren(children ...types.PlanExpression) (type
|
|||
|
||||
// percentilePlanExpression handles PERCENTILE()
|
||||
type percentilePlanExpression struct {
|
||||
pos parser.Pos
|
||||
arg types.PlanExpression
|
||||
nthArg types.PlanExpression
|
||||
returnDataType parser.ExprDataType
|
||||
|
|
@ -889,9 +887,8 @@ type percentilePlanExpression struct {
|
|||
|
||||
var _ types.Aggregable = (*percentilePlanExpression)(nil)
|
||||
|
||||
func newPercentilePlanExpression(pos parser.Pos, arg types.PlanExpression, nthArg types.PlanExpression, returnDataType parser.ExprDataType) *percentilePlanExpression {
|
||||
func newPercentilePlanExpression(arg types.PlanExpression, nthArg types.PlanExpression, returnDataType parser.ExprDataType) *percentilePlanExpression {
|
||||
return &percentilePlanExpression{
|
||||
pos: pos,
|
||||
arg: arg,
|
||||
nthArg: nthArg,
|
||||
returnDataType: returnDataType,
|
||||
|
|
@ -907,7 +904,7 @@ func (n *percentilePlanExpression) Evaluate(currentRow []interface{}) (interface
|
|||
}
|
||||
|
||||
func (n *percentilePlanExpression) NewBuffer() (types.AggregationBuffer, error) {
|
||||
return nil, sql3.NewErrUnsupported(n.pos.Line, n.pos.Column, true, "Percentile call that can't be pushed down to PQL")
|
||||
return NewAggCountBuffer(n), nil
|
||||
}
|
||||
|
||||
func (n *percentilePlanExpression) FirstChildExpr() types.PlanExpression {
|
||||
|
|
@ -943,312 +940,7 @@ func (n *percentilePlanExpression) WithChildren(children ...types.PlanExpression
|
|||
if len(children) != 2 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return newPercentilePlanExpression(n.pos, children[0], children[1], n.returnDataType), nil
|
||||
}
|
||||
|
||||
// aggregator for CORR()
|
||||
type aggregateCorr struct {
|
||||
expr *corrPlanExpression
|
||||
|
||||
n int64
|
||||
sum_X float64
|
||||
sum_Y float64
|
||||
sum_XY float64
|
||||
squareSum_X float64
|
||||
squareSum_Y float64
|
||||
}
|
||||
|
||||
func NewAggCorrBuffer(child *corrPlanExpression) *aggregateCorr {
|
||||
return &aggregateCorr{
|
||||
expr: child,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *aggregateCorr) Update(ctx context.Context, row types.Row) error {
|
||||
v1, err := m.expr.arg1.Evaluate(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v2, err := m.expr.arg2.Evaluate(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip if nil
|
||||
if v1 == nil || v2 == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var xVal float64
|
||||
var yVal float64
|
||||
|
||||
switch dataType := m.expr.arg1.Type().(type) {
|
||||
case *parser.DataTypeDecimal:
|
||||
thisVal, ok := v1.(pql.Decimal)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v1)
|
||||
}
|
||||
|
||||
xVal = thisVal.Float64()
|
||||
|
||||
case *parser.DataTypeInt:
|
||||
thisVal, ok := v1.(int64)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v1)
|
||||
}
|
||||
|
||||
xVal = float64(thisVal)
|
||||
|
||||
default:
|
||||
return sql3.NewErrInternalf("unhandled aggregate expression datatype '%T'", dataType)
|
||||
}
|
||||
|
||||
switch dataType := m.expr.arg2.Type().(type) {
|
||||
case *parser.DataTypeDecimal:
|
||||
thisVal, ok := v2.(pql.Decimal)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v2)
|
||||
}
|
||||
|
||||
yVal = thisVal.Float64()
|
||||
|
||||
case *parser.DataTypeInt:
|
||||
thisVal, ok := v2.(int64)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v2)
|
||||
}
|
||||
yVal = float64(thisVal)
|
||||
|
||||
default:
|
||||
return sql3.NewErrInternalf("unhandled aggregate expression datatype '%T'", dataType)
|
||||
}
|
||||
|
||||
m.sum_X = m.sum_X + xVal
|
||||
m.sum_Y = m.sum_Y + yVal
|
||||
|
||||
m.sum_XY = m.sum_XY + xVal*yVal
|
||||
|
||||
m.squareSum_X = m.squareSum_X + xVal*xVal
|
||||
m.squareSum_Y = m.squareSum_Y + yVal*yVal
|
||||
m.n += 1
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aggregateCorr) Eval(ctx context.Context) (interface{}, error) {
|
||||
corr := float64((float64(m.n)*m.sum_XY - m.sum_X*m.sum_Y)) / (math.Sqrt(float64((float64(m.n)*m.squareSum_X - m.sum_X*m.sum_X) * (float64(m.n)*m.squareSum_Y - m.sum_Y*m.sum_Y))))
|
||||
|
||||
d, err := pql.FromFloat64WithScale(corr, 6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// corrPlanExpression handles CORR() - implement correlation coefficient
|
||||
type corrPlanExpression struct {
|
||||
arg1 types.PlanExpression
|
||||
arg2 types.PlanExpression
|
||||
returnDataType parser.ExprDataType
|
||||
}
|
||||
|
||||
var _ types.Aggregable = (*corrPlanExpression)(nil)
|
||||
|
||||
func newCorrPlanExpression(arg1 types.PlanExpression, arg2 types.PlanExpression, returnDataType parser.ExprDataType) *corrPlanExpression {
|
||||
return &corrPlanExpression{
|
||||
arg1: arg1,
|
||||
arg2: arg2,
|
||||
returnDataType: returnDataType,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
|
||||
return nil, sql3.NewErrInternalf("this should never be called")
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) NewBuffer() (types.AggregationBuffer, error) {
|
||||
return NewAggCorrBuffer(n), nil
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) FirstChildExpr() types.PlanExpression {
|
||||
return n.arg1
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) Type() parser.ExprDataType {
|
||||
return n.returnDataType
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) String() string {
|
||||
return fmt.Sprintf("corr(%s, %s)", n.arg1.String(), n.arg2.String())
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_expr"] = fmt.Sprintf("%T", n)
|
||||
result["description"] = n.String()
|
||||
result["dataType"] = n.Type().TypeDescription()
|
||||
result["arg1"] = n.arg1.Plan()
|
||||
result["arg2"] = n.arg2.Plan()
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) Children() []types.PlanExpression {
|
||||
return []types.PlanExpression{
|
||||
n.arg1,
|
||||
n.arg2,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *corrPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
|
||||
if len(children) != 2 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return newCorrPlanExpression(children[0], children[1], n.returnDataType), nil
|
||||
}
|
||||
|
||||
// aggregator for VAR()
|
||||
type aggregateVar struct {
|
||||
expr *varPlanExpression
|
||||
|
||||
// to calculate mean
|
||||
n int64
|
||||
sum float64
|
||||
|
||||
// we need to hang on to the values
|
||||
// TODO(pok) - will need to spill these to disk for big result sets
|
||||
values []float64
|
||||
}
|
||||
|
||||
func NewAggVarBuffer(child *varPlanExpression) *aggregateVar {
|
||||
return &aggregateVar{
|
||||
expr: child,
|
||||
values: make([]float64, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *aggregateVar) Update(ctx context.Context, row types.Row) error {
|
||||
v, err := m.expr.arg.Evaluate(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// skip if nil
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var val float64
|
||||
|
||||
switch dataType := m.expr.arg.Type().(type) {
|
||||
case *parser.DataTypeDecimal:
|
||||
thisVal, ok := v.(pql.Decimal)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v)
|
||||
}
|
||||
|
||||
val = thisVal.Float64()
|
||||
|
||||
case *parser.DataTypeID:
|
||||
thisVal, ok := v.(int64)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v)
|
||||
}
|
||||
|
||||
val = float64(thisVal)
|
||||
|
||||
case *parser.DataTypeInt:
|
||||
thisVal, ok := v.(int64)
|
||||
if !ok {
|
||||
return sql3.NewErrInternalf("unexpected type conversion '%T'", v)
|
||||
}
|
||||
|
||||
val = float64(thisVal)
|
||||
|
||||
default:
|
||||
return sql3.NewErrInternalf("unhandled aggregate expression datatype '%T'", dataType)
|
||||
}
|
||||
|
||||
m.sum += val
|
||||
m.n += 1
|
||||
m.values = append(m.values, val)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aggregateVar) Eval(ctx context.Context) (interface{}, error) {
|
||||
|
||||
mean := m.sum / float64(m.n)
|
||||
|
||||
var variance float64
|
||||
for _, v := range m.values {
|
||||
variance += (v - mean) * (v - mean)
|
||||
}
|
||||
|
||||
variance = variance / float64(m.n)
|
||||
|
||||
d, err := pql.FromFloat64WithScale(variance, 6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// varPlanExpression handles VAR() - variance
|
||||
type varPlanExpression struct {
|
||||
arg types.PlanExpression
|
||||
returnDataType parser.ExprDataType
|
||||
}
|
||||
|
||||
var _ types.Aggregable = (*varPlanExpression)(nil)
|
||||
|
||||
func newVarPlanExpression(arg types.PlanExpression, returnDataType parser.ExprDataType) *varPlanExpression {
|
||||
return &varPlanExpression{
|
||||
arg: arg,
|
||||
returnDataType: returnDataType,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
|
||||
return nil, sql3.NewErrInternalf("this should never be called")
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) NewBuffer() (types.AggregationBuffer, error) {
|
||||
return NewAggVarBuffer(n), nil
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) FirstChildExpr() types.PlanExpression {
|
||||
return n.arg
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) Type() parser.ExprDataType {
|
||||
return n.returnDataType
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) String() string {
|
||||
return fmt.Sprintf("var(%s)", n.arg.String())
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_expr"] = fmt.Sprintf("%T", n)
|
||||
result["description"] = n.String()
|
||||
result["dataType"] = n.Type().TypeDescription()
|
||||
result["arg"] = n.arg.Plan()
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) Children() []types.PlanExpression {
|
||||
return []types.PlanExpression{
|
||||
n.arg,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *varPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
|
||||
if len(children) != 1 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return newVarPlanExpression(children[0], n.returnDataType), nil
|
||||
return newPercentilePlanExpression(children[0], children[1], n.returnDataType), nil
|
||||
}
|
||||
|
||||
// aggregator for LAST()
|
||||
|
|
|
|||
|
|
@ -632,7 +632,7 @@ func (p *ExecutionPlanner) analyzeBinaryExpression(ctx context.Context, expr *pa
|
|||
if ok {
|
||||
//we have a select in the expression list so make sure it is the only thing in the expression list
|
||||
if len(lst.Exprs) > 1 {
|
||||
return nil, sql3.NewErrInternalf("expression list should only contain one select statement")
|
||||
return nil, sql3.NewErrInternalf("expresion list should only contain one select statement")
|
||||
}
|
||||
//make sure select only returns one column
|
||||
if len(sel.Columns) > 1 {
|
||||
|
|
|
|||
|
|
@ -124,71 +124,6 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars
|
|||
//return the data type of the referenced column
|
||||
call.ResultDataType = ref.DataType()
|
||||
|
||||
case "CORR":
|
||||
// can't do this on a *
|
||||
if call.Star.IsValid() && len(call.Args) == 0 {
|
||||
return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column)
|
||||
}
|
||||
|
||||
if len(call.Args) != 2 {
|
||||
return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 2, len(call.Args))
|
||||
}
|
||||
|
||||
// if it is a ref, we shouldn't do a corr on the _id
|
||||
arg1 := call.Args[0]
|
||||
|
||||
ref, ok := arg1.(*parser.QualifiedRef)
|
||||
if ok && strings.EqualFold(ref.Column.Name, string(dax.PrimaryKeyFieldName)) {
|
||||
return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name)
|
||||
}
|
||||
|
||||
// make sure the ref is the right type
|
||||
if !(typeIsInteger(arg1.DataType()) || typeIsDecimal(arg1.DataType()) || typeIsTimestamp(arg1.DataType())) {
|
||||
return nil, sql3.NewErrIntOrDecimalOrTimestampExpressionExpected(arg1.Pos().Line, arg1.Pos().Column)
|
||||
}
|
||||
|
||||
// if it is a ref, we shouldn't do a corr on the _id
|
||||
arg2 := call.Args[1]
|
||||
|
||||
ref, ok = arg2.(*parser.QualifiedRef)
|
||||
if ok && strings.EqualFold(ref.Column.Name, string(dax.PrimaryKeyFieldName)) {
|
||||
return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[1].Pos().Line, call.Args[1].Pos().Column, call.Name.Name)
|
||||
}
|
||||
|
||||
// make sure the ref is the right type
|
||||
if !(typeIsInteger(arg2.DataType()) || typeIsDecimal(arg2.DataType()) || typeIsTimestamp(arg2.DataType())) {
|
||||
return nil, sql3.NewErrIntOrDecimalOrTimestampExpressionExpected(arg2.Pos().Line, arg2.Pos().Column)
|
||||
}
|
||||
|
||||
// return the data type of the referenced column
|
||||
call.ResultDataType = parser.NewDataTypeDecimal(6)
|
||||
|
||||
case "VAR":
|
||||
// can't do this on a *
|
||||
if call.Star.IsValid() && len(call.Args) == 0 {
|
||||
return nil, sql3.NewErrExpectedColumnReference(call.Star.Line, call.Star.Column)
|
||||
}
|
||||
|
||||
if len(call.Args) != 1 {
|
||||
return nil, sql3.NewErrCallParameterCountMismatch(call.Rparen.Line, call.Rparen.Column, call.Name.Name, 1, len(call.Args))
|
||||
}
|
||||
|
||||
// first arg should be a qualified ref
|
||||
arg1 := call.Args[0]
|
||||
|
||||
ref, ok := arg1.(*parser.QualifiedRef)
|
||||
if ok && strings.EqualFold(ref.Column.Name, string(dax.PrimaryKeyFieldName)) {
|
||||
return nil, sql3.NewErrIdColumnNotValidForAggregateFunction(call.Args[0].Pos().Line, call.Args[0].Pos().Column, call.Name.Name)
|
||||
}
|
||||
|
||||
// make sure the ref is the right type
|
||||
if !(typeIsInteger(arg1.DataType()) || typeIsDecimal(arg1.DataType()) || typeIsTimestamp(arg1.DataType())) {
|
||||
return nil, sql3.NewErrIntOrDecimalOrTimestampExpressionExpected(arg1.Pos().Line, arg1.Pos().Column)
|
||||
}
|
||||
|
||||
// return the data type of the referenced column
|
||||
call.ResultDataType = parser.NewDataTypeDecimal(6)
|
||||
|
||||
case "MIN", "MAX":
|
||||
// can't do an min/max on a *
|
||||
if call.Star.IsValid() && len(call.Args) == 0 {
|
||||
|
|
@ -335,15 +270,6 @@ func (p *ExecutionPlanner) analyzeCallExpression(ctx context.Context, call *pars
|
|||
case "DATETIMEDIFF":
|
||||
return p.analyzeFunctionDateTimeDiff(call, scope)
|
||||
default:
|
||||
// could be a udf - try to look it up in functions
|
||||
fn, err := p.getFunctionByName(call.Name.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fn != nil {
|
||||
return p.analyzeUserDefinedFunction(call, scope, fn)
|
||||
}
|
||||
|
||||
return nil, sql3.NewErrCallUnknownFunction(call.Name.NamePos.Line, call.Name.NamePos.Column, call.Name.Name)
|
||||
}
|
||||
return call, nil
|
||||
|
|
|
|||
|
|
@ -499,19 +499,18 @@ func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, ex
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cond := &pql.Condition{Op: pqlOp}
|
||||
switch val := pqlValue.(type) {
|
||||
case float64:
|
||||
cond.Value = pql.FromFloat64(val)
|
||||
case int64:
|
||||
cond.Value = pql.FromInt64(val, 0)
|
||||
default:
|
||||
val, ok := pqlValue.(float64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T", pqlValue)
|
||||
}
|
||||
d := pql.FromFloat64(val)
|
||||
return &pql.Call{
|
||||
Name: "Row",
|
||||
Args: map[string]interface{}{
|
||||
lhs.columnName: cond,
|
||||
lhs.columnName: &pql.Condition{
|
||||
Op: pqlOp,
|
||||
Value: d,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
|
|
@ -542,7 +541,7 @@ func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, ex
|
|||
pqlOp = pql.NEQ
|
||||
}
|
||||
switch typ := expr.lhs.Type().(type) {
|
||||
case *parser.DataTypeID, *parser.DataTypeString, *parser.DataTypeIDSet, *parser.DataTypeStringSet, *parser.DataTypeBool:
|
||||
case *parser.DataTypeID, *parser.DataTypeString, *parser.DataTypeIDSet, *parser.DataTypeStringSet:
|
||||
if strings.EqualFold(lhs.columnName, string(dax.PrimaryKeyFieldName)) {
|
||||
return nil, sql3.NewErrInvalidColumnInFilterExpression(0, 0, string(dax.PrimaryKeyFieldName), "is/is not null")
|
||||
}
|
||||
|
|
@ -605,7 +604,7 @@ func planExprToValue(expr types.PlanExpression) (interface{}, error) {
|
|||
return expr.value, nil
|
||||
case *stringLiteralPlanExpression:
|
||||
return expr.value, nil
|
||||
case *timestampLiteralPlanExpression:
|
||||
case *dateLiteralPlanExpression:
|
||||
return expr.value, nil
|
||||
case *boolLiteralPlanExpression:
|
||||
return expr.value, nil
|
||||
|
|
|
|||
|
|
@ -94,11 +94,11 @@ func (i *alterTableRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
fos := i.columnDef.fos
|
||||
|
||||
fld, err := pilosa.FieldFromFieldOptions(fname, fos...)
|
||||
// all newly created fields unconditionally have TrackExistence turned on.
|
||||
fld.Options.TrackExistence = true
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// all newly created fields unconditionally have TrackExistence turned on.
|
||||
fld.Options.TrackExistence = true
|
||||
|
||||
if err := i.planner.schemaAPI.CreateField(ctx, tname, fld); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -867,7 +867,7 @@ func processColumnValue(rawValue interface{}, targetType parser.ExprDataType) (t
|
|||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unable to convert '%s", rawValue)
|
||||
}
|
||||
return newTimestampLiteralPlanExpression(tval), nil
|
||||
return newDateLiteralPlanExpression(tval), nil
|
||||
|
||||
case *parser.DataTypeString:
|
||||
sval, ok := rawValue.(string)
|
||||
|
|
|
|||
|
|
@ -1,515 +0,0 @@
|
|||
// Copyright 2022 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
pilosa "github.com/featurebasedb/featurebase/v3"
|
||||
"github.com/featurebasedb/featurebase/v3/pql"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// PlanOpCopy is a copy operator
|
||||
type PlanOpCopy struct {
|
||||
planner *ExecutionPlanner
|
||||
targetTable string
|
||||
url string
|
||||
apiKey string
|
||||
ddl string
|
||||
ChildOp types.PlanOperator
|
||||
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpCopy(planner *ExecutionPlanner, targetName string, url string, apiKey string, ddl string, child types.PlanOperator) *PlanOpCopy {
|
||||
return &PlanOpCopy{
|
||||
planner: planner,
|
||||
targetTable: targetName,
|
||||
url: url,
|
||||
apiKey: apiKey,
|
||||
ddl: ddl,
|
||||
ChildOp: child,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Schema() types.Schema {
|
||||
return types.Schema{}
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
|
||||
child, err := p.ChildOp.Iterator(ctx, row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.url != "" {
|
||||
return newRemoteCopyIterator(p.planner, p.targetTable, p.url, p.apiKey, p.ddl, p.ChildOp.Schema(), child), nil
|
||||
}
|
||||
return newCopyIterator(p.planner, p.targetTable, p.ddl, p.ChildOp.Schema(), child), nil
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
|
||||
if len(children) != 1 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return NewPlanOpCopy(p.planner, p.targetTable, p.url, p.apiKey, p.ddl, children[0]), nil
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Children() []types.PlanOperator {
|
||||
return []types.PlanOperator{
|
||||
p.ChildOp,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_op"] = fmt.Sprintf("%T", p)
|
||||
result["_schema"] = p.Schema().Plan()
|
||||
result["child"] = p.ChildOp.Plan()
|
||||
result["child"] = p.ChildOp.Plan()
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) AddWarning(warning string) {
|
||||
p.warnings = append(p.warnings, warning)
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Warnings() []string {
|
||||
return p.warnings
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) Expressions() []types.PlanExpression {
|
||||
return []types.PlanExpression{}
|
||||
}
|
||||
|
||||
func (p *PlanOpCopy) WithUpdatedExpressions(exprs ...types.PlanExpression) (types.PlanOperator, error) {
|
||||
if len(exprs) > 0 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of exprs '%d'", len(exprs))
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
type copyIterator struct {
|
||||
planner *ExecutionPlanner
|
||||
targetTableName string
|
||||
copySchema types.Schema
|
||||
ddl string
|
||||
child types.RowIterator
|
||||
hasStarted *struct{}
|
||||
}
|
||||
|
||||
func newCopyIterator(planner *ExecutionPlanner, targetTableName string, ddl string, copySchema types.Schema, childIter types.RowIterator) *copyIterator {
|
||||
return ©Iterator{
|
||||
planner: planner,
|
||||
targetTableName: targetTableName,
|
||||
ddl: ddl,
|
||||
copySchema: copySchema,
|
||||
child: childIter,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *copyIterator) Next(ctx context.Context) (types.Row, error) {
|
||||
if i.hasStarted == nil {
|
||||
// parse and execute the ddl to create the table
|
||||
ast, err := parser.NewParser(strings.NewReader(i.ddl)).ParseStatement()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct, ok := ast.(*parser.CreateTableStatement)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected ast type")
|
||||
}
|
||||
// analyze
|
||||
err = i.planner.analyzeCreateTableStatement(ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctOp, err := i.planner.compileCreateTableStatement(ctx, ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctIter, err := ctOp.Iterator(context.Background(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = ctIter.Next(ctx)
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetColumns := make([]*qualifiedRefPlanExpression, 0)
|
||||
|
||||
for _, s := range i.copySchema {
|
||||
targetColumns = append(targetColumns, newQualifiedRefPlanExpression(i.targetTableName, s.ColumnName, 0, s.Type))
|
||||
}
|
||||
|
||||
// build an insert iterator for the target table
|
||||
insertIter := &insertRowIter{
|
||||
planner: i.planner,
|
||||
tableName: i.targetTableName,
|
||||
targetColumns: targetColumns,
|
||||
}
|
||||
|
||||
batchCount := 0
|
||||
insertBatch := make([][]types.PlanExpression, 0)
|
||||
|
||||
for {
|
||||
// get a source row
|
||||
row, err := i.child.Next(ctx)
|
||||
if err != nil {
|
||||
if err == types.ErrNoMoreRows {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add it to target batch
|
||||
|
||||
irow := make([]types.PlanExpression, len(row))
|
||||
for i, s := range i.copySchema {
|
||||
switch ty := s.Type.(type) {
|
||||
case *parser.DataTypeID, *parser.DataTypeInt:
|
||||
val, ok := row[i].(int64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
irow[i] = newIntLiteralPlanExpression(val)
|
||||
|
||||
case *parser.DataTypeDecimal:
|
||||
val, ok := row[i].(pql.Decimal)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
irow[i] = newFloatLiteralPlanExpression(val.String())
|
||||
|
||||
case *parser.DataTypeString:
|
||||
val, ok := row[i].(string)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
irow[i] = newStringLiteralPlanExpression(val)
|
||||
|
||||
case *parser.DataTypeBool:
|
||||
val, ok := row[i].(bool)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
irow[i] = newBoolLiteralPlanExpression(val)
|
||||
|
||||
case *parser.DataTypeTimestamp:
|
||||
val, ok := row[i].(time.Time)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
irow[i] = newTimestampLiteralPlanExpression(val)
|
||||
|
||||
case *parser.DataTypeStringSet:
|
||||
val, ok := row[i].([]string)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
|
||||
members := make([]types.PlanExpression, 0)
|
||||
for _, m := range val {
|
||||
members = append(members, newStringLiteralPlanExpression(m))
|
||||
}
|
||||
irow[i] = newExprSetLiteralPlanExpression(members, parser.NewDataTypeStringSet())
|
||||
|
||||
case *parser.DataTypeIDSet:
|
||||
val, ok := row[i].([]int64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
|
||||
members := make([]types.PlanExpression, 0)
|
||||
for _, m := range val {
|
||||
members = append(members, newIntLiteralPlanExpression(m))
|
||||
}
|
||||
irow[i] = newExprSetLiteralPlanExpression(members, parser.NewDataTypeIDSet())
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unhandled type '%T'", ty)
|
||||
}
|
||||
}
|
||||
insertBatch = append(insertBatch, irow)
|
||||
|
||||
// inc batch count
|
||||
batchCount += 1
|
||||
if batchCount > 1000 {
|
||||
// do the insert
|
||||
insertIter.insertValues = insertBatch
|
||||
_, err = insertIter.Next(context.Background())
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
// reset
|
||||
batchCount = 0
|
||||
insertBatch = make([][]types.PlanExpression, 0)
|
||||
}
|
||||
}
|
||||
if len(insertBatch) > 0 {
|
||||
// do the insert
|
||||
insertIter.insertValues = insertBatch
|
||||
_, err = insertIter.Next(context.Background())
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i.hasStarted = &struct{}{}
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
||||
type remoteCopyIterator struct {
|
||||
planner *ExecutionPlanner
|
||||
targetTableName string
|
||||
copySchema types.Schema
|
||||
url string
|
||||
apiKey string
|
||||
ddl string
|
||||
child types.RowIterator
|
||||
hasStarted *struct{}
|
||||
}
|
||||
|
||||
func newRemoteCopyIterator(planner *ExecutionPlanner, targetTableName string, url string, apiKey string, ddl string, copySchema types.Schema, childIter types.RowIterator) *remoteCopyIterator {
|
||||
return &remoteCopyIterator{
|
||||
planner: planner,
|
||||
targetTableName: targetTableName,
|
||||
url: url,
|
||||
apiKey: apiKey,
|
||||
ddl: ddl,
|
||||
copySchema: copySchema,
|
||||
child: childIter,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *remoteCopyIterator) remoteExec(ctx context.Context, sql string) (*pilosa.WireQueryResponse, error) {
|
||||
// Create HTTP request.
|
||||
req, err := http.NewRequest("POST", i.url, strings.NewReader(sql))
|
||||
if err != nil {
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Length", strconv.Itoa(len(sql)))
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "pilosa/"+i.planner.systemAPI.Version())
|
||||
if len(i.apiKey) > 0 {
|
||||
req.Header.Set("X-API-Key", i.apiKey)
|
||||
}
|
||||
|
||||
// Execute request against the host.
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
if resp.StatusCode == 401 {
|
||||
return nil, sql3.NewErrRemoteUnauthorized(0, 0, i.url)
|
||||
}
|
||||
// we have an error
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %d, %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
sqlResponse := &pilosa.WireQueryResponse{}
|
||||
err = sqlResponse.UnmarshalJSONTyped([]byte(body), true)
|
||||
if err != nil {
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %s", err.Error())
|
||||
}
|
||||
|
||||
if len(sqlResponse.Error) > 0 {
|
||||
return nil, sql3.NewErrInternalf("error executing remotely: %s", sqlResponse.Error)
|
||||
}
|
||||
|
||||
return sqlResponse, nil
|
||||
}
|
||||
|
||||
func (i *remoteCopyIterator) Next(ctx context.Context) (types.Row, error) {
|
||||
if i.hasStarted == nil {
|
||||
// execute the ddl to create the table
|
||||
_, err := i.remoteExec(ctx, i.ddl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// build bulk insert statement
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("bulk insert into ")
|
||||
fmt.Fprintf(&buf, "%s", i.targetTableName)
|
||||
buf.WriteString(" (")
|
||||
|
||||
for i, s := range i.copySchema {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%s", s.ColumnName)
|
||||
}
|
||||
buf.WriteString(") map (")
|
||||
for i, s := range i.copySchema {
|
||||
if i > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&buf, "'$._%d' %s", i, s.Type.TypeDescription())
|
||||
}
|
||||
buf.WriteString(") from x'")
|
||||
header := buf.String()
|
||||
|
||||
batchCount := 0
|
||||
var batchBuf bytes.Buffer
|
||||
|
||||
for {
|
||||
// get a source row
|
||||
row, err := i.child.Next(ctx)
|
||||
if err != nil {
|
||||
if err == types.ErrNoMoreRows {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add it to target batch
|
||||
var rowBuf bytes.Buffer
|
||||
rowBuf.WriteString("{")
|
||||
for i, s := range i.copySchema {
|
||||
if i > 0 {
|
||||
rowBuf.WriteString(",")
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, `"_%d":`, i)
|
||||
|
||||
if row[i] == nil {
|
||||
rowBuf.WriteString("null")
|
||||
continue
|
||||
}
|
||||
|
||||
switch ty := s.Type.(type) {
|
||||
case *parser.DataTypeID, *parser.DataTypeInt:
|
||||
val, ok := row[i].(int64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, "%d", val)
|
||||
|
||||
case *parser.DataTypeString:
|
||||
val, ok := row[i].(string)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
// escape single quotes
|
||||
val = strings.ReplaceAll(val, `'`, `''`)
|
||||
// and double quotes
|
||||
val = strings.ReplaceAll(val, `"`, `\"`)
|
||||
// and line feeds
|
||||
if strings.Contains(val, "\n") {
|
||||
val = strings.ReplaceAll(val, "\n", "\\n")
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, `"%s"`, val)
|
||||
|
||||
case *parser.DataTypeBool:
|
||||
val, ok := row[i].(bool)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, "%v", val)
|
||||
|
||||
case *parser.DataTypeTimestamp:
|
||||
val, ok := row[i].(time.Time)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, `"%s"`, val.Format(time.RFC3339Nano))
|
||||
|
||||
case *parser.DataTypeStringSet:
|
||||
val, ok := row[i].([]string)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
rowBuf.WriteString("[")
|
||||
for j, s := range val {
|
||||
if j > 0 {
|
||||
rowBuf.WriteString(",")
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, `"%s"`, s)
|
||||
}
|
||||
rowBuf.WriteString("]")
|
||||
|
||||
case *parser.DataTypeIDSet:
|
||||
val, ok := row[i].([]int64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type '%T'", row[i])
|
||||
}
|
||||
rowBuf.WriteString("[")
|
||||
for j, s := range val {
|
||||
if j > 0 {
|
||||
rowBuf.WriteString(",")
|
||||
}
|
||||
fmt.Fprintf(&rowBuf, `%d`, s)
|
||||
}
|
||||
rowBuf.WriteString("]")
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unhandled type '%T'", ty)
|
||||
}
|
||||
}
|
||||
rowBuf.WriteString("}\n")
|
||||
batchBuf.Write(rowBuf.Bytes())
|
||||
|
||||
// inc batch count
|
||||
batchCount += 1
|
||||
if batchCount > 10000 {
|
||||
// do the insert
|
||||
|
||||
var reqBuf bytes.Buffer
|
||||
reqBuf.WriteString(header)
|
||||
reqBuf.Write(batchBuf.Bytes())
|
||||
reqBuf.WriteString("' with batchsize 10000 input 'STREAM' format 'NDJSON'")
|
||||
|
||||
_, err := i.remoteExec(ctx, reqBuf.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// reset
|
||||
batchCount = 0
|
||||
batchBuf.Reset()
|
||||
}
|
||||
}
|
||||
if batchCount > 0 {
|
||||
// do the insert
|
||||
|
||||
var reqBuf bytes.Buffer
|
||||
reqBuf.WriteString(header)
|
||||
reqBuf.Write(batchBuf.Bytes())
|
||||
reqBuf.WriteString("' with batchsize 10000 input 'STREAM' format 'NDJSON'")
|
||||
|
||||
_, err := i.remoteExec(ctx, reqBuf.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
i.hasStarted = &struct{}{}
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
// Copyright 2023 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// PlanOpCreateFunction implements the CREATE FUNCTION operator
|
||||
type PlanOpCreateFunction struct {
|
||||
planner *ExecutionPlanner
|
||||
function *functionSystemObject
|
||||
ifNotExists bool
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpCreateFunction(planner *ExecutionPlanner, ifNotExists bool, function *functionSystemObject) *PlanOpCreateFunction {
|
||||
return &PlanOpCreateFunction{
|
||||
planner: planner,
|
||||
function: function,
|
||||
ifNotExists: ifNotExists,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) Schema() types.Schema {
|
||||
return types.Schema{}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
|
||||
return newCreateFunctionIter(p.planner, p.ifNotExists, p.function), nil
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) Children() []types.PlanOperator {
|
||||
return []types.PlanOperator{}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
|
||||
if len(children) != 0 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return NewPlanOpCreateFunction(p.planner, p.ifNotExists, p.function), nil
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_op"] = fmt.Sprintf("%T", p)
|
||||
result["_schema"] = p.Schema().Plan()
|
||||
result["model"] = p.function.name
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) AddWarning(warning string) {
|
||||
p.warnings = append(p.warnings, warning)
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateFunction) Warnings() []string {
|
||||
var w []string
|
||||
w = append(w, p.warnings...)
|
||||
return w
|
||||
}
|
||||
|
||||
type createFunctionIter struct {
|
||||
planner *ExecutionPlanner
|
||||
function *functionSystemObject
|
||||
ifNotExists bool
|
||||
}
|
||||
|
||||
func newCreateFunctionIter(planner *ExecutionPlanner, ifNotExists bool, function *functionSystemObject) *createFunctionIter {
|
||||
return &createFunctionIter{
|
||||
planner: planner,
|
||||
function: function,
|
||||
ifNotExists: ifNotExists,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *createFunctionIter) Next(ctx context.Context) (types.Row, error) {
|
||||
// now check in the functions table to see if it is exists
|
||||
v, err := i.planner.getFunctionByName(i.function.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v != nil {
|
||||
if i.ifNotExists {
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
return nil, sql3.NewErrViewExists(0, 0, i.function.name)
|
||||
}
|
||||
|
||||
// now store the view into fb_functions
|
||||
err = i.planner.insertFunction(i.function)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
// Copyright 2022 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/pql"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/parser"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// PlanOpCreateModel implements the CREATE MODEL operator
|
||||
type PlanOpCreateModel struct {
|
||||
ChildOp types.PlanOperator
|
||||
planner *ExecutionPlanner
|
||||
model *modelSystemObject
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpCreateModel(planner *ExecutionPlanner, model *modelSystemObject, child types.PlanOperator) *PlanOpCreateModel {
|
||||
return &PlanOpCreateModel{
|
||||
ChildOp: child,
|
||||
planner: planner,
|
||||
model: model,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) Schema() types.Schema {
|
||||
return types.Schema{}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
|
||||
// get the query iterator
|
||||
iter, err := p.ChildOp.Iterator(ctx, row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(p.model.modelType) {
|
||||
case "linear_regresssion":
|
||||
return newCreateModelIter(p.planner, p.model, newLinearRegressionModelIter(p.planner, p.model, p.ChildOp.Schema(), iter)), nil
|
||||
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unexpected model tyoe '%s'", p.model.modelType)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) Children() []types.PlanOperator {
|
||||
return []types.PlanOperator{
|
||||
p.ChildOp,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
|
||||
if len(children) != 1 {
|
||||
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
|
||||
}
|
||||
return NewPlanOpCreateModel(p.planner, p.model, children[0]), nil
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_op"] = fmt.Sprintf("%T", p)
|
||||
sc := make([]string, 0)
|
||||
for _, e := range p.Schema() {
|
||||
sc = append(sc, fmt.Sprintf("'%s', '%s', '%s'", e.ColumnName, e.RelationName, e.Type.TypeDescription()))
|
||||
}
|
||||
result["_schema"] = sc
|
||||
result["model"] = p.model.name // TODO(pok) - add a Plan() method here (or some such)
|
||||
result["child"] = p.ChildOp.Plan()
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) AddWarning(warning string) {
|
||||
p.warnings = append(p.warnings, warning)
|
||||
}
|
||||
|
||||
func (p *PlanOpCreateModel) Warnings() []string {
|
||||
var w []string
|
||||
w = append(w, p.warnings...)
|
||||
w = append(w, p.ChildOp.Warnings()...)
|
||||
return w
|
||||
}
|
||||
|
||||
type createModelIter struct {
|
||||
child types.RowIterator
|
||||
planner *ExecutionPlanner
|
||||
model *modelSystemObject
|
||||
hasStarted *struct{}
|
||||
}
|
||||
|
||||
func newCreateModelIter(planner *ExecutionPlanner, model *modelSystemObject, child types.RowIterator) *createModelIter {
|
||||
return &createModelIter{
|
||||
planner: planner,
|
||||
model: model,
|
||||
child: child,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *createModelIter) Next(ctx context.Context) (types.Row, error) {
|
||||
if i.hasStarted == nil {
|
||||
// store the model into fb_models and set the model status to 'training'
|
||||
i.model.status = "TRAINING"
|
||||
err := i.planner.insertModel(i.model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// do the actual training
|
||||
_, err = i.child.Next(ctx)
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// update the model to ready
|
||||
i.model.status = "READY"
|
||||
err = i.planner.updateModel(i.model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.hasStarted = &struct{}{}
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
||||
type linearRegressionModelIter struct {
|
||||
child types.RowIterator
|
||||
planner *ExecutionPlanner
|
||||
model *modelSystemObject
|
||||
childSchema types.Schema
|
||||
hasStarted *struct{}
|
||||
}
|
||||
|
||||
func newLinearRegressionModelIter(planner *ExecutionPlanner, model *modelSystemObject, childSchema types.Schema, child types.RowIterator) *linearRegressionModelIter {
|
||||
return &linearRegressionModelIter{
|
||||
planner: planner,
|
||||
model: model,
|
||||
childSchema: childSchema,
|
||||
child: child,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *linearRegressionModelIter) Next(ctx context.Context) (types.Row, error) {
|
||||
if i.hasStarted == nil {
|
||||
// this is linear regression now, so we actually 'train' when we predict (later)
|
||||
// for now we just store the values from the query in fb_model_data
|
||||
|
||||
// delete anything from fb_model_data for this model
|
||||
err := i.planner.ensureModelDataSystemTableExists()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
diter := &filteredDeleteRowIter{
|
||||
planner: i.planner,
|
||||
tableName: "fb_model_data",
|
||||
filter: newBinOpPlanExpression(
|
||||
newQualifiedRefPlanExpression("fb_model_data", "model_id", 0, parser.NewDataTypeString()),
|
||||
parser.EQ,
|
||||
newStringLiteralPlanExpression(i.model.name),
|
||||
parser.NewDataTypeBool(),
|
||||
),
|
||||
}
|
||||
_, err = diter.Next(context.Background())
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iter := &insertRowIter{
|
||||
planner: i.planner,
|
||||
tableName: "fb_model_data",
|
||||
targetColumns: []*qualifiedRefPlanExpression{
|
||||
newQualifiedRefPlanExpression("fb_model_data", "_id", 0, parser.NewDataTypeString()),
|
||||
newQualifiedRefPlanExpression("fb_model_data", "model_id", 0, parser.NewDataTypeString()),
|
||||
newQualifiedRefPlanExpression("fb_model_data", "data", 0, parser.NewDataTypeString()),
|
||||
},
|
||||
insertValues: [][]types.PlanExpression{},
|
||||
}
|
||||
|
||||
trainingRefs := make([]*qualifiedRefPlanExpression, 0)
|
||||
|
||||
// make sure label column exists and is type compatible with float
|
||||
labelColumn := i.model.labels[0]
|
||||
found := false
|
||||
for i, s := range i.childSchema {
|
||||
if strings.EqualFold(labelColumn, s.ColumnName) {
|
||||
if !typesAreAssignmentCompatible(parser.NewDataTypeDecimal(4), s.Type) {
|
||||
return nil, sql3.NewErrInternalf("types not assignment compatible")
|
||||
}
|
||||
trainingRefs = append(trainingRefs, newQualifiedRefPlanExpression("", labelColumn, i, s.Type))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, sql3.NewErrInternalf("label column found found")
|
||||
}
|
||||
|
||||
// make sure input columns exists and are type compatible with float
|
||||
for _, ic := range i.model.inputColumns {
|
||||
found := false
|
||||
for i, s := range i.childSchema {
|
||||
if strings.EqualFold(ic, s.ColumnName) {
|
||||
if !typesAreAssignmentCompatible(parser.NewDataTypeDecimal(4), s.Type) {
|
||||
return nil, sql3.NewErrInternalf("types not assignment compatible")
|
||||
}
|
||||
trainingRefs = append(trainingRefs, newQualifiedRefPlanExpression("", ic, i, s.Type))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, sql3.NewErrInternalf("input column found found")
|
||||
}
|
||||
}
|
||||
|
||||
// go run the query and iterate
|
||||
for {
|
||||
row, err := i.child.Next(ctx)
|
||||
if err != nil {
|
||||
if err == types.ErrNoMoreRows {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fdata := make([]float64, 0)
|
||||
|
||||
for _, ref := range trainingRefs {
|
||||
val, err := ref.Evaluate(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cval, err := coerceValue(ref.dataType, parser.NewDataTypeDecimal(4), val, parser.Pos{Line: 0, Column: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dval := cval.(pql.Decimal)
|
||||
fdata = append(fdata, dval.Float64())
|
||||
}
|
||||
|
||||
data, err := json.Marshal(fdata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rowID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tuple := []types.PlanExpression{
|
||||
newStringLiteralPlanExpression(rowID.String()),
|
||||
newStringLiteralPlanExpression(i.model.name),
|
||||
newStringLiteralPlanExpression(string(data)),
|
||||
}
|
||||
iter.insertValues = append(iter.insertValues, tuple)
|
||||
|
||||
fmt.Printf("%v", row)
|
||||
|
||||
}
|
||||
_, err = iter.Next(context.Background())
|
||||
if err != nil && err != types.ErrNoMoreRows {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i.hasStarted = &struct{}{}
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
|
@ -112,11 +112,11 @@ func (i *createTableRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
|
||||
for _, f := range i.columns {
|
||||
fld, err := pilosa.FieldFromFieldOptions(dax.FieldName(f.name), f.fos...)
|
||||
// We unconditionally turn on TrackExistence for all newly-created fields.
|
||||
fld.Options.TrackExistence = true
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "creating field from field options: %s", f.name)
|
||||
}
|
||||
// We unconditionally turn on TrackExistence for all newly-created fields.
|
||||
fld.Options.TrackExistence = true
|
||||
fields = append(fields, fld)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
// Copyright 2023 Molecula Corp. All rights reserved.
|
||||
|
||||
package planner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/featurebasedb/featurebase/v3/sql3"
|
||||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
// PlanOpDropModel plan operator to drop a view.
|
||||
type PlanOpDropModel struct {
|
||||
planner *ExecutionPlanner
|
||||
modelName string
|
||||
ifExists bool
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpDropModel(p *ExecutionPlanner, ifExists bool, modelName string) *PlanOpDropModel {
|
||||
return &PlanOpDropModel{
|
||||
planner: p,
|
||||
modelName: modelName,
|
||||
ifExists: ifExists,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) Plan() map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
result["_op"] = fmt.Sprintf("%T", p)
|
||||
result["modelName"] = p.modelName
|
||||
result["isExists"] = p.ifExists
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) AddWarning(warning string) {
|
||||
p.warnings = append(p.warnings, warning)
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) Warnings() []string {
|
||||
return p.warnings
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) Schema() types.Schema {
|
||||
return types.Schema{}
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) Children() []types.PlanOperator {
|
||||
return []types.PlanOperator{}
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
|
||||
return &dropModelRowIter{
|
||||
planner: p.planner,
|
||||
ifExists: p.ifExists,
|
||||
modelName: p.modelName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PlanOpDropModel) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type dropModelRowIter struct {
|
||||
planner *ExecutionPlanner
|
||||
ifExists bool
|
||||
modelName string
|
||||
}
|
||||
|
||||
var _ types.RowIterator = (*dropModelRowIter)(nil)
|
||||
|
||||
func (i *dropModelRowIter) Next(ctx context.Context) (types.Row, error) {
|
||||
err := i.planner.checkAccess(ctx, i.modelName, accessTypeDropObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check in the models table to see if it exists
|
||||
v, err := i.planner.getModelByName(i.modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == nil {
|
||||
if i.ifExists {
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
return nil, sql3.NewErrModelNotFound(0, 0, i.modelName)
|
||||
}
|
||||
|
||||
err = i.planner.deleteModel(i.modelName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, types.ErrNoMoreRows
|
||||
}
|
||||
|
|
@ -17,18 +17,16 @@ import (
|
|||
// PlanOpFeatureBaseTables wraps a []*IndexInfo that is returned from
|
||||
// schemaAPI.Schema().
|
||||
type PlanOpFeatureBaseTables struct {
|
||||
planner *ExecutionPlanner
|
||||
indexInfo []*pilosa.IndexInfo
|
||||
withSystem bool
|
||||
warnings []string
|
||||
planner *ExecutionPlanner
|
||||
indexInfo []*pilosa.IndexInfo
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpFeatureBaseTables(planner *ExecutionPlanner, indexInfo []*pilosa.IndexInfo, withSystem bool) *PlanOpFeatureBaseTables {
|
||||
func NewPlanOpFeatureBaseTables(planner *ExecutionPlanner, indexInfo []*pilosa.IndexInfo) *PlanOpFeatureBaseTables {
|
||||
return &PlanOpFeatureBaseTables{
|
||||
planner: planner,
|
||||
indexInfo: indexInfo,
|
||||
withSystem: withSystem,
|
||||
warnings: make([]string, 0),
|
||||
planner: planner,
|
||||
indexInfo: indexInfo,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,85 +105,65 @@ func (p *PlanOpFeatureBaseTables) Children() []types.PlanOperator {
|
|||
|
||||
func (p *PlanOpFeatureBaseTables) Iterator(ctx context.Context, row types.Row) (types.RowIterator, error) {
|
||||
return &showTablesRowIter{
|
||||
planner: p.planner,
|
||||
indexInfo: p.indexInfo,
|
||||
withSystem: p.withSystem,
|
||||
planner: p.planner,
|
||||
indexInfo: p.indexInfo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PlanOpFeatureBaseTables) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
|
||||
return NewPlanOpFeatureBaseTables(p.planner, p.indexInfo, p.withSystem), nil
|
||||
return NewPlanOpFeatureBaseTables(p.planner, p.indexInfo), nil
|
||||
}
|
||||
|
||||
type showTablesRowIter struct {
|
||||
planner *ExecutionPlanner
|
||||
indexInfo []*pilosa.IndexInfo
|
||||
withSystem bool
|
||||
|
||||
result types.Rows
|
||||
planner *ExecutionPlanner
|
||||
indexInfo []*pilosa.IndexInfo
|
||||
rowIndex int
|
||||
}
|
||||
|
||||
var _ types.RowIterator = (*showTablesRowIter)(nil)
|
||||
|
||||
func (i *showTablesRowIter) Next(ctx context.Context) (types.Row, error) {
|
||||
if i.result == nil {
|
||||
i.result = make(types.Rows, 0)
|
||||
if i.rowIndex < len(i.indexInfo) {
|
||||
|
||||
for _, idx := range i.indexInfo {
|
||||
indexName := i.indexInfo[i.rowIndex].Name
|
||||
|
||||
indexName := idx.Name
|
||||
|
||||
// if we don't want system tables filter them out (currently by name prefix)
|
||||
// TODO(pok) - we need an is_system attribute so we can filter on that instead
|
||||
if !i.withSystem && strings.HasPrefix(indexName, "fb_") {
|
||||
continue
|
||||
var err error
|
||||
var spaceUsed pilosa.DiskUsage
|
||||
switch strings.ToLower(indexName) {
|
||||
case fbDatabaseInfo, fbDatabaseNodes, fbPerformanceCounters, fbExecRequests, fbTableDDL:
|
||||
spaceUsed = pilosa.DiskUsage{
|
||||
Usage: 0,
|
||||
}
|
||||
default:
|
||||
u := i.planner.systemAPI.DataDir()
|
||||
|
||||
var err error
|
||||
var spaceUsed pilosa.DiskUsage
|
||||
switch strings.ToLower(indexName) {
|
||||
case fbDatabaseInfo, fbDatabaseNodes, fbPerformanceCounters, fbExecRequests, fbTableDDL:
|
||||
spaceUsed = pilosa.DiskUsage{
|
||||
Usage: 0,
|
||||
}
|
||||
default:
|
||||
u := i.planner.systemAPI.DataDir()
|
||||
// TODO(tlt): GetDiskUsage needs to be behind an interface because
|
||||
// this doesn't work in serverless. For now I'm just going to skip
|
||||
// it based on the emtpy DataDir, but let's do this the right way.
|
||||
if u != "" {
|
||||
u = fmt.Sprintf("%s/indexes/%s", u, indexName)
|
||||
|
||||
// TODO(tlt): GetDiskUsage needs to be behind an interface because
|
||||
// this doesn't work in serverless. For now I'm just going to skip
|
||||
// it based on the emtpy DataDir, but let's do this the right way.
|
||||
if u != "" {
|
||||
u = fmt.Sprintf("%s/indexes/%s", u, indexName)
|
||||
|
||||
spaceUsed, err = pilosa.GetDiskUsage(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spaceUsed, err = pilosa.GetDiskUsage(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
createdAt := time.Unix(0, idx.CreatedAt)
|
||||
updatedAt := time.Unix(0, idx.UpdatedAt)
|
||||
row := []interface{}{
|
||||
indexName,
|
||||
indexName,
|
||||
idx.Owner,
|
||||
idx.LastUpdateUser,
|
||||
createdAt.Format(time.RFC3339),
|
||||
updatedAt.Format(time.RFC3339),
|
||||
idx.Options.Keys,
|
||||
spaceUsed.Usage,
|
||||
idx.Options.Description,
|
||||
}
|
||||
i.result = append(i.result, row)
|
||||
}
|
||||
}
|
||||
|
||||
if len(i.result) > 0 {
|
||||
row := i.result[0]
|
||||
|
||||
// Move to next result element.
|
||||
i.result = i.result[1:]
|
||||
createdAt := time.Unix(0, i.indexInfo[i.rowIndex].CreatedAt)
|
||||
updatedAt := time.Unix(0, i.indexInfo[i.rowIndex].UpdatedAt)
|
||||
row := []interface{}{
|
||||
indexName,
|
||||
indexName,
|
||||
i.indexInfo[i.rowIndex].Owner,
|
||||
i.indexInfo[i.rowIndex].LastUpdateUser,
|
||||
createdAt.Format(time.RFC3339),
|
||||
updatedAt.Format(time.RFC3339),
|
||||
i.indexInfo[i.rowIndex].Options.Keys,
|
||||
spaceUsed.Usage,
|
||||
i.indexInfo[i.rowIndex].Options.Description,
|
||||
}
|
||||
i.rowIndex += 1
|
||||
return row, nil
|
||||
}
|
||||
return nil, types.ErrNoMoreRows
|
||||
|
|
|
|||
|
|
@ -230,15 +230,17 @@ func (i *pqlAggregateRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
return nil, sql3.NewErrInternalf("unexpected aggregate nth arg type '%T'", coercedNthValue)
|
||||
}
|
||||
|
||||
if cond == nil {
|
||||
cond = &pql.Call{Name: "All"}
|
||||
}
|
||||
|
||||
call = &pql.Call{
|
||||
Name: "Percentile",
|
||||
Args: map[string]interface{}{
|
||||
"field": expr.columnName,
|
||||
"nth": nth,
|
||||
},
|
||||
}
|
||||
if cond != nil {
|
||||
call.Args["filter"] = cond
|
||||
Children: []*pql.Call{cond},
|
||||
}
|
||||
|
||||
default:
|
||||
|
|
@ -294,10 +296,6 @@ func (i *pqlAggregateRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
default:
|
||||
return nil, sql3.NewErrInternalf("unhandled return type '%T'", i.aggregate.Type())
|
||||
}
|
||||
case nil:
|
||||
// it's valid for an aggregate to yield a NULL in some cases, such as
|
||||
// when it's called on what turns out to be an empty set.
|
||||
i.resultValue = nil
|
||||
default:
|
||||
return nil, sql3.NewErrInternalf("unexpected result type '%T'", queryResponse.Results[0])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,18 +265,26 @@ func (i *distinctScanRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
row[0] = pql.NewDecimal(val, t.Scale)
|
||||
|
||||
case *parser.DataTypeIDSet:
|
||||
val, ok := result.(int64)
|
||||
val, ok := result.([]uint64)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type for column value '%T'", result)
|
||||
}
|
||||
row[0] = []uint64{uint64(val)}
|
||||
if val == nil {
|
||||
row[0] = nil
|
||||
} else {
|
||||
row[0] = val
|
||||
}
|
||||
|
||||
case *parser.DataTypeStringSet:
|
||||
val, ok := result.(string)
|
||||
val, ok := result.([]string)
|
||||
if !ok {
|
||||
return nil, sql3.NewErrInternalf("unexpected type for column value '%T'", result)
|
||||
}
|
||||
row[0] = []string{val}
|
||||
if val == nil {
|
||||
row[0] = nil
|
||||
} else {
|
||||
row[0] = val
|
||||
}
|
||||
|
||||
default:
|
||||
row[0] = result
|
||||
|
|
|
|||
|
|
@ -232,12 +232,7 @@ func (i *pqlGroupByRowIter) Next(ctx context.Context) (types.Row, error) {
|
|||
if g.Value != nil {
|
||||
row[idx] = *g.Value
|
||||
} else if g.RowKey != "" {
|
||||
switch c.Type().(type) {
|
||||
case *parser.DataTypeStringSet:
|
||||
row[idx] = []string{g.RowKey}
|
||||
default:
|
||||
row[idx] = g.RowKey
|
||||
}
|
||||
row[idx] = g.RowKey
|
||||
} else {
|
||||
switch c.Type().(type) {
|
||||
case *parser.DataTypeIDSet:
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ import (
|
|||
"github.com/featurebasedb/featurebase/v3/sql3/planner/types"
|
||||
)
|
||||
|
||||
type TableQueryHint struct {
|
||||
name string
|
||||
params []string
|
||||
}
|
||||
|
||||
// PlanOpPQLTableScan plan operator handles a PQL table scan
|
||||
type PlanOpPQLTableScan struct {
|
||||
planner *ExecutionPlanner
|
||||
|
|
@ -28,17 +23,15 @@ type PlanOpPQLTableScan struct {
|
|||
filter types.PlanExpression
|
||||
timeQuantumFilters []types.PlanExpression
|
||||
topExpr types.PlanExpression
|
||||
hints []*TableQueryHint
|
||||
warnings []string
|
||||
}
|
||||
|
||||
func NewPlanOpPQLTableScan(p *ExecutionPlanner, tableName string, columns []string, hints []*TableQueryHint) *PlanOpPQLTableScan {
|
||||
func NewPlanOpPQLTableScan(p *ExecutionPlanner, tableName string, columns []string) *PlanOpPQLTableScan {
|
||||
return &PlanOpPQLTableScan{
|
||||
planner: p,
|
||||
tableName: tableName,
|
||||
columns: columns,
|
||||
timeQuantumFilters: make([]types.PlanExpression, 0),
|
||||
hints: hints,
|
||||
warnings: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue