Compare commits

...

23 commits

Author SHA1 Message Date
LJ Sinclair
6222e9eb58
Update README.md
changed links to the community help repo
2024-02-22 10:20:41 +11:00
Коrd Campbell
c31eb2b64e
Update README.md with community 2023-05-30 10:26:47 -05:00
Коrd Campbell
c59a714d37
Create OPENSOURCE.md 2023-05-30 10:24:36 -05:00
Seebs
6383a96ac5 treat Percentile as an error if we can't use PQL Percentile
If we can't successfully generate a PQL Percentile call, error
out rather than implementing an actual Percentile function in SQL.
This can be revisited if anyone needs it.
2023-04-07 15:52:26 -05:00
Seebs
c658e771b0 make percentile work on Decimals, also make Percentile slightly better
So there's a lot going on here.

Percentile just did not work, even a little, with decimals.

In theory we try to make the int val part of ValCount work, in
ValCountize, but you can't actually use that for everything because
it unconditionally adds bsig.Base even when it shouldn't. But it
doesn't matter that we were returning those values from, say,
(Field).MinForShard, because ValCount.Smaller was not preserving them
when identifying the smaller of two Decimal ValCounts anyway.
And even if it did, the logic in Percentile wouldn't have worked
with passing the raw unscaled integer in as a value to compare
against.

But that's fine because the logic was also more generally wrong.
According to the existing logic, a value is the median value if
exactly as many values are less than it as are greater than it.

This is... not actually very accurate to what we usually mean by
"median". Because some values are *equal* to a given value. So
for instance, say you have the values {1, 1, 1, [a million 2s], 3}.
Our logic would regard 2 as being too high to be the median, because
3 times as many values are lower as are higher.

New interpretation: Imagine a sorted list of all your values, with
N entries. You want the Nth percentile, which is to say, you want N%
of values to be less than the vale you pick, and (100-N)% to be greater.
You can round both of these down. So for instance, if you have 6 values,
and want the median, you want 3 values greater, and 3 values less. To
be picky, we could demand the average of those middle two values, but
we're not in a good position to do that in this implementation.

If the number of desired things less than, or greater than, a target
is 0, we can short-circuit to the minimum or maximum value. This can
happen when nth is close to an end and the number of things is small,
not just at nth=0/nth=100.

So we rework this, and we rework the tests for this behavior to reflect
that logic.

We change executePercentile to be able to return a nil rather than
a weird ValCount in cases where there's no result, such as when
there's no values to compute a percentile of.

We also change the SQL tests to match the new behavior, since some
of them were expecting everything done on a decimal field with values
10-13 to come back as 10.00 as a decimal because that is what the
code returned.

We also propagate these changes to DAX, and along the way, fix up a
TODO item in the DAX copy, and stop skipping the test that was
failing because of that TODO item.
2023-04-07 15:52:26 -05:00
Seebs
7cf2c5b07e handle integers as comparisons for decimal fields
It's reasonable to allow "where x > 13" on decimal
fields. Handle at least int64 and float64.

Once this is up, we find that aggregates can return
non-values, such as nil, in some cases; for instance,
`percentile(x) where x > 13` can yield a nil if x is
never greater than 13, rather than making up a value
from zero data points. So we accept nil as a valid
result type in PQL aggregates.

As a result of this, change two tests which were
unintentionally testing for an arcane edge case bug
in which (1) we can't render a condition to PQL,
such as because you specified an integer for a decimal
field, and (2) the filter is using an aliased name,
in which we would end up failing to generate a PQL
filter, but *also* losing the SQL-layer filter, and
produce wrong results as though there were no filter.

We also alter the tests to use `o.price > 9`, because
this lets us generate three user names, but only two
distinct user names, so the test using DISTINCT returns
a different value than the test not using DISTINCT,
which helps us verify that it's actually working and
not just lucky.

As part of fixing that, there was an intermediate
state where we rejected as an error any case where
generating the PQL filter failed. This broke 21 more
test cases, but in all of those cases, the SQL filter
was actually working.

... But in two of them, we SHOULD have been able to
generate PQL, because they were testing bools for
null, which works fine. We just had a list of
field types we allowed null tests against and
omitted bool because I forgot that bool isn't always
just treated as a kind of mutex.
2023-04-07 15:52:26 -05:00
Seebs
0412a505c9 Pass filters down to Percentile correctly
When pushing an expression down to PQL Percentile, if we have
a filter, it has to be passed as the argument "filter", not as
an additional child argument. We don't need to pass in `All()`
as a filter if there's no filter, Percentile works fine with
no filter provided.
2023-04-07 15:52:26 -05:00
Pat Okeeffe
2bdc30c4f0
fix regex generation (#2377) 2023-04-07 14:10:16 -05:00
David Kagan
24a45bc30d
Cloud 1475 (#2371)
* working on incorporating regex logic

* Implemented a validation check for database name with given rules in doc within controller

* fixing tests to pass

* reflecting changes to match docs

* fixed tests further, hopefully

* for sure fixed integration tests, and moved validation check

* integration tests passed, go test now will pass

* fixed name size to 230 due to previous commit acknowledgement

* fixed field test negative validations

* added missing comma
2023-04-07 15:08:07 -04:00
Pat Okeeffe
7f75193cf2
tidy up show tables behavior (#2374)
* tidy up show tables behavior

* made cli integration test whole again

* Update fbsql \d meta-command to show system tables (#2376)

---------

Co-authored-by: Travis Turner <travis@molecula.com>
2023-04-07 12:58:09 -05:00
Adrian Walker
3b142af2c7
CLOUD-1456: SERVERLESS - CREATE DATABASE (#2375)
statement without a units qualifier causes it to be set to 0

Co-authored-by: Adrian Walker <adrian.walker@molecula.comm>
2023-04-07 12:44:45 -05:00
Pat Okeeffe
c619b7d94e
implemented query hints (flatten) (fb-2124) (#2373)
* implemented query hints (flatten)

* improved testing
2023-04-06 17:28:24 -05:00
Seebs
c66d392c87 uncomment old LIMIT tests, make them pass
We forward-ported a handful of tests from the previous parser
which relied on LIMIT clauses, but then we didn't support that.
Now that we do, we uncomment most of these tests, and actually
give them the correct data structures to compare with.

We leave two tests commented out. One was using `limit 10, 5` to
express a limit plus offset, and the other is using `not fld = 1`
as a WHERE clause, but we don't support unary-not to negate
other expressions.

In the process, we discover that converting a SELECT with a
LIMIT clause back to a string has a missing space, and fix that.
2023-04-06 11:49:11 -05:00
Lory Cloutier
875999e30d
Add test coverage to expressionanalyzer.go (#2370)
analyzeExpression - tupleLiteralExpression was covered by something
else between the ticket getting filed and me starting on it.
(*ExecutionPlanner).analyzeBinaryExpression now has increased
coverage for IN / NOT IN. Several bugs got revealed by adding tests;
those tests are commented out but can be re-enabled by whoever ends
up working on the bugs. Tickets are filed.
2023-04-06 09:55:09 -05:00
Travis Turner
ea72396b4d
Remove Node from data model; standardize on Worker (#2366)
* Remove Node from data model; standardize on Worker

This commit does a lot of things, but in general it attempts to simplify
the data model by getting rid of the Node and NodeRole models. Instead,
these will use the Worker model, which itself has individual boolean
fields for role types.

Get rid of roleType in some FreeWorker methods

rename NodeService to WorkerRegistry

simplify the freeworker interface

fix the tests

* Remove DeleteWorker method from workerJobService
2023-04-04 20:20:53 -05:00
Pat Okeeffe
284f62dcb9
create model, create function... all the goodies (#2264)
* create function, create/drop model; re-introduced limit; added COPY; var(); corr()

* review feedback
2023-04-04 17:44:29 -05:00
Seebs
c8c88ab0ee don't panic on failed table creation
The attempt to set the TrackExistence option for fields
happened before checking whether the field was created
successfully or not. Credit to Rachith for spotting this.
Bug was introduced with the TrackExistence stuff, but
we apparently never had a test case for invalid min/max
values.
2023-04-03 16:29:21 -05:00
Seebs
2af417d5c2 don't panic on a MIN that isn't a call
parseOperand was assuming that any reference to MIN in a place
where an operand was expected was a call, which it should be,
but it might not be. parseCallExpression panics if it doesn't
find a parenthesis, because it's never supposed to be called
when we don't know we have one.

The test for this is in with MinMaxColumnConstraints, even though it's
actually a test of MinMaxFunctionCalls, because that's where the other
tests involving the special MIN/MAX tokens live.

We also stop checking whether MIN or MAX might actually be QIDENT.
If you use a quoted identifier, we're over in the QIDENT case,
not the MIN/MAX case. If the token was MIN or MAX, it's always
unquoted.
2023-04-03 16:29:21 -05:00
Lory Cloutier
7031f7b968
Fb 2048 (#2363)
* Add test coverage for executionplanner.go
*ExecutionPlanner.mapper does not get tested in the case where its
context gets cancelled. In order to make testing this possible,
I've added a context argument to sql_test.MustQueryRow. If it's
nil, MustQueryRow creates a context for itself just like it always
has, but if a context is provided, it uses that.

* Adds test coverage for ExecutionPlanner.mapper in executionplanner.go
The case where the context gets cancelled mid-query is now covered.
The test is timing-dependent - the cancel call has to happen after
the query has been started but before it finishes, and in just the
right part of MustRunQuery, in order to actually produce a context
cancelled error, and not, say, a query cancelled error. May have to
adjust timing if the current delays don't work in CI testing.

* Addressed review notes
-reordered arguments for MustRunQuery
-moved MustRunQuery out of a goroutine, put the cancel in one
2023-04-03 12:04:41 -05:00
David Kagan
9e67f1dddd
Cluster nodes for serverless (#2336)
* slowly making a serverless systemAPI for ClusterNodes()

* implemented some methods for fb_database_info

* fixed linting

* fixed comments
2023-04-03 11:13:08 -04:00
Vengata Krishnan
b5dfb07118
Improve test coverage for ast components in ast.go (#2355)
*Tests are added to extend coverage for statement, expression and source types and many of the ast helper functions
*For those SQL language elements where ast exists but parsing is not implemented, test coverage is added to test only the ast correctness
*Also, removed timestamp EPOCH related compiler code as they become unreachable after their ast equivalent were removed in a previous PR.
2023-03-31 14:27:18 -04:00
HHans09
52f9703585
fb-2030 - added test cases for Joins in sql3 (#2359)
* fb-2030 - added test cases for Joins in sql3

* test cases for joins

* Revert "test cases for joins"

This reverts commit 1501f7b202.
2023-03-31 11:52:19 -04:00
Travis Turner
8fca15e936
RetryWithTx (#2348)
* First pass at RetryWithTx

* Refactor RetryWithTx to take a writable bool (instead of reads, writes)

* Implment DirectiveMethodDiff

This commit adds support for a Directive to contain only the diffs (as
opposed to the full Directive).

* Update controller tests to allow for DirectiveMethodDiff (over Full)

* Update RetryWithTx to retry on duplicate key constraint.

If two concurrent processes call IngestShard() for the same shard, both
were trying to insert the same job into the jobs table. That resulted in
a duplicate key error from the database. We want to include that error
in the list of errors for which RetryWithTx should retry.

* Remove unused method: Directive.TranslatePartitions()

* Replace query in a loop with a single query

We had a query which was looking to see if a job already existed. That
query was inside a loop, and could potentially generate 256 queries (for
example). This commit replaces that logic so that we use a single query
wiht an `IN ()` clause.

* Convert to directive version-by-address

This commit uses a separate directive version per address. It moves the
version get/increment back inside the buildDirective method so that if
two concurrent processes are building a directive for the same address,
one of them will get rolled back trying to commit the version update.

* Migration for directive version by address

* Add a comment about DirectiveVersion lock/unlock logic

* Remove AddLastWins

* fix linter

* handle error in walkdir

* fix test failures from removing AddLastWins
2023-03-30 20:54:37 -05:00
118 changed files with 8920 additions and 2535 deletions

45
OPENSOURCE.md Normal file
View file

@ -0,0 +1,45 @@
## 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.

View file

@ -1,4 +1,10 @@
# FeatureBase
# FeatureBase Community
FeatureBase Community is now archived and no longer maintained.
* [FeatureBase Community Help](https://github.com/FeatureBaseDB/FB-community-help)
## Pilosa is now FeatureBase
@ -10,6 +16,8 @@ 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.
@ -19,42 +27,20 @@ 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].
### More Information
* [Learn about Data Modeling](https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/concepts/concepts-home.md)
[Installation][Install]
[Configuration][Config]
### 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)
## Community
You can email us at community@featurebase.com or learn more about contributing at [https://www.featurebase.com/community][Community].
You can email us at community@featurebase.com and [learn more about contributing](https://github.com/FeatureBaseDB/featurebase/blob/master/OPENSOURCE.md).
Chat with us: [https://discord.gg/FBn2vEp7Na][Discord]
@ -73,13 +59,14 @@ 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]: 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
[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
[Discord]: https://discord.gg/FBn2vEp7Na
[HomePage]: http://featurebase.com?utm_campaign=Open%20Source&utm_source=GitHub
[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
[Ingest]: https://github.com/FeatureBaseDB/FB-community-help/blob/main/docs/community/com-ingest/com-ingest-manage.md
[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

View file

@ -35,6 +35,22 @@ 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:

View file

@ -30,7 +30,7 @@ func TestAPI_Directive(t *testing.T) {
// Empty directive (and empty holder).
{
d := &dax.Directive{
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
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.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
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.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
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.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl2,
tbl3,

View file

@ -364,7 +364,7 @@ Input/Output
\warn [-n] [STRING] write string to standard error (-n for no newline)
Informational
\d list tables
\d list tables, including system tables
\d NAME describe table
\dt list tables
\dv list views
@ -504,8 +504,17 @@ 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.
return newMetaListTables().execute(cmd)
// 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
case 1:
// Describe with a single arg will assume the arg is a table name, so it

40
cli/testdata/table vendored
View file

@ -1,5 +1,5 @@
// Show tables for database using SHOW TABLES.
SEND:SHOW TABLES;
// 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:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
@ -11,8 +11,8 @@ EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ |
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
EXPECT:
// Show tables for database using \dt.
SEND:\dt
// Show tables for database using \d.
SEND:\d
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
EXPECT:| _id | name | owner | updated_by | created_at | updated_at | keys | space_used | description |
EXPECT:+-------------------------+-------------------------+-------+------------+----------------------+----------------------+-------+------------+-------------+
@ -24,6 +24,23 @@ EXPECTCOMP:WithFormat:| fb_____________________ | fb_____________________ |
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:
// 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:
// Create a table. That can be used for general testing.
SEND:CREATE TABLE users (_id id, name string, age int);
EXPECT:
@ -33,16 +50,11 @@ 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 | |
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:+-------+-------+-------+------------+----------------------+----------------------+-------+------------+-------------+
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:
// We don't select from users until AFTER we check SHOW TABLES above because

View file

@ -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)
// FreeWorkers dissociates the given workers from a database.
FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error
// ReleaseWorkers dissociates the given workers from a database.
ReleaseWorkers(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) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
func (b *NopBalancer) ReleaseWorkers(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) {

View file

@ -28,7 +28,7 @@ type Balancer struct {
// current represents the current state of worker/job assigments.
current WorkerJobService
nodeService controller.NodeService
workerRegistry controller.WorkerRegistry
// 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,47 +44,37 @@ type Balancer struct {
}
// New returns a new instance of Balancer.
func New(ns controller.NodeService, fjs FreeJobService, wjs WorkerJobService, fws FreeWorkerService, schemar schemar.Schemar, logger logger.Logger) *Balancer {
func New(wr controller.WorkerRegistry, fjs FreeJobService, wjs WorkerJobService, fws FreeWorkerService, schemar schemar.Schemar, logger logger.Logger) *Balancer {
return &Balancer{
current: wjs,
nodeService: ns,
freeJobs: fjs,
freeWorkers: fws,
schemar: schemar,
logger: logger,
current: wjs,
workerRegistry: wr,
freeJobs: fjs,
freeWorkers: fws,
schemar: schemar,
logger: logger,
}
}
// 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.).
// 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.
func (b *Balancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.WorkerDiff, error) {
addr := node.Address
b.logger.Debugf("AddWorker(%s)", addr)
b.logger.Debugf("AddWorker(%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)
if err := b.workerRegistry.AddWorker(tx, node); err != nil {
return nil, errors.Wrapf(err, "creating node on node service: %s", node.Address)
}
diffs := NewInternalDiffs()
// 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.
// Process the newly added workers.
// TODO(tlt): this is a little heavy-handed. I'm sure we'll need to be more
// intentional about knowing which databases needs workers, as opposed to
// intentional about knowing which databases need 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", addr)
return nil, errors.Wrapf(err, "balancing new worker: %s", node.Address)
} else {
diffs.Merge(diff)
}
@ -92,23 +82,9 @@ func (b *Balancer) AddWorker(tx dax.Transaction, node *dax.Node) ([]dax.WorkerDi
return diffs.Output(), nil
}
// 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
}
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)
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 {
@ -122,10 +98,12 @@ func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType) (
return InternalDiffs{}, nil
}
// Get all database and their minWorkerCount (Database.Options.WorkersMin).
qdbs, err := b.schemar.Databases(tx, "")
// 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)
if err != nil {
return nil, errors.Wrap(err, "getting all database")
return nil, errors.Wrap(err, "getting database")
}
// Create a map[database]int where int is the number of workers required to
@ -169,7 +147,7 @@ func (b *Balancer) assignMinWorkers(tx dax.Transaction, roleType dax.RoleType) (
diffs := NewInternalDiffs()
// Create an ordered slice of map keys so that tests are predicatable.
// Create an ordered slice of map keys so that tests are predictable.
qdbids := make([]dax.QualifiedDatabaseID, 0, len(m))
for qdbid := range m {
qdbids = append(qdbids, qdbid)
@ -244,13 +222,12 @@ 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()
////// The rest is database specific. ////////////
// See if the worker is assigned to a database. If it's not, return early.
// See if the worker is assigned to a database. If it is, disassociate the
// worker from all of its jobs for the database.
dbkey := b.current.DatabaseForWorker(tx, addr)
if dbkey != "" {
qdbid := dbkey.QualifiedDatabaseID()
for _, rt := range []dax.RoleType{dax.RoleTypeCompute, dax.RoleTypeTranslate} {
for _, rt := range dax.AllRoleTypes {
if diff, err := b.removeDatabaseWorker(tx, rt, qdbid, addr); err != nil {
return nil, errors.Wrapf(err, "removing worker: (%s) %s", rt, addr)
} else {
@ -259,15 +236,8 @@ func (b *Balancer) RemoveWorker(tx dax.Transaction, addr dax.Address) ([]dax.Wor
}
}
// 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 {
// Remove the worker from the worker registry.
if err := b.workerRegistry.RemoveWorker(tx, addr); err != nil {
return nil, errors.Wrapf(err, "deleting node from node service: %s", addr)
}
@ -284,6 +254,8 @@ 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 {
@ -291,13 +263,8 @@ func (b *Balancer) removeDatabaseWorker(tx dax.Transaction, roleType dax.RoleTyp
}
// Before removing the worker, mark its 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")
if err := b.freeJobs.MarkJobsAsFree(tx, roleType, qdbid, jobs); err != nil {
return nil, errors.Wrap(err, "marking jobs as free")
}
// Even though this may not be useful to the caller (for example, in the
@ -311,8 +278,8 @@ func (b *Balancer) removeDatabaseWorker(tx dax.Transaction, roleType dax.RoleTyp
return diff, nil
}
func (b *Balancer) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
return errors.Wrap(b.current.FreeWorkers(tx, addrs...), "freeing workers")
func (b *Balancer) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error {
return errors.Wrap(b.current.ReleaseWorkers(tx, addrs...), "freeing workers")
}
func (b *Balancer) AddJobs(tx dax.Transaction, roleType dax.RoleType, qtid dax.QualifiedTableID, jobs ...dax.Job) ([]dax.WorkerDiff, error) {
@ -366,7 +333,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, "assigning min workers: (%s)", roleType)
return nil, errors.Wrapf(err, "balancing database for role: (%s)", roleType)
} else {
diffs.Merge(diff)
}
@ -396,6 +363,7 @@ 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...))
@ -437,12 +405,12 @@ func (b *Balancer) addDatabaseJobs(tx dax.Transaction, roleType dax.RoleType, qd
jobCounts[lowWorker]++
}
for worker, jobs := range jobsToCreate {
if err := b.current.CreateJobs(tx, roleType, qdbid, worker, jobs...); err != nil {
for addr, jobs := range jobsToCreate {
if err := b.current.AssignWorkerToJobs(tx, roleType, qdbid, addr, jobs...); err != nil {
return nil, errors.Wrap(err, "creating job")
}
for _, job := range jobs {
diffs.Added(worker, job)
diffs.Added(addr, job)
}
}
@ -527,7 +495,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.RoleType{dax.RoleTypeCompute, dax.RoleTypeTranslate} {
for _, role := range dax.AllRoleTypes {
diff, err := b.balanceDatabaseForRole(tx, role, qdbid)
if err != nil {
return nil, errors.Wrapf(err, "getting worker count: (%s) %s", role, qdbid)
@ -544,8 +512,7 @@ func (b *Balancer) balanceDatabaseForRole(tx dax.Transaction, roleType dax.RoleT
// Before balancing, make sure the database has its minimum number of
// workers satisfied.
// TODO(tlt): make assignMinWorkers database specific.
if diff, err := b.assignMinWorkers(tx, roleType); err != nil {
if diff, err := b.assignMinWorkers(tx, roleType, qdbid); err != nil {
return nil, errors.Wrapf(err, "assigning min workers: (%s) %s", roleType, qdbid)
} else {
diffs.Merge(diff)
@ -809,11 +776,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.nodeService.ReadNode(tx, addr)
return b.workerRegistry.Worker(tx, addr)
}
func (b *Balancer) Nodes(tx dax.Transaction) ([]*dax.Node, error) {
return b.nodeService.Nodes(tx)
return b.workerRegistry.Workers(tx)
}
type WorkerJobService interface {
@ -823,10 +790,9 @@ 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
DeleteWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error
FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error
ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error
CreateJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job ...dax.Job) error
AssignWorkerToJobs(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)
@ -840,12 +806,10 @@ 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)
MergeJobs(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, jobs dax.Jobs) error
MarkJobsAsFree(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)
}

View file

@ -39,6 +39,11 @@ 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)
@ -49,22 +54,22 @@ func TestFreeJobService(t *testing.T) {
require.NoError(t, err)
require.ElementsMatch(t, dax.Jobs{job1, job3}, jobs)
fwSvc := sqldb.NewFreeWorkerService(nil)
err = fwSvc.AddWorkers(tx, role, nodeAddr)
workerReg := sqldb.NewWorkerRegistry(nil)
err = workerReg.AddWorker(tx, node)
require.NoError(t, err)
wjSvc := sqldb.NewWorkerJobService(nil)
err = wjSvc.CreateWorker(tx, role, qdbid, nodeAddr)
require.NoError(t, err)
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job1)
err = wjSvc.AssignWorkerToJobs(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.MergeJobs(tx, role, qdbid, dax.Jobs{job1})
err = fjSvc.MarkJobsAsFree(tx, role, qdbid, dax.Jobs{job1})
require.NoError(t, err)
jobs, err = fjSvc.ListJobs(tx, role, qdbid)

View file

@ -20,12 +20,25 @@ func TestFreeWorkerService(t *testing.T) {
}
}()
fwSvc := sqldb.NewFreeWorkerService(nil)
err = fwSvc.AddWorkers(tx, role, nodeAddr, nodeAddr2, nodeAddr3, nodeAddr4, nodeAddr5)
require.NoError(t, err)
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}
err = fwSvc.RemoveWorker(tx, role, nodeAddr2)
require.NoError(t, err)
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)
addrs, err := fwSvc.ListWorkers(tx, role)
require.NoError(t, err)

View file

@ -18,7 +18,7 @@ const (
nodeAddr5 = "myaddress5"
)
func TestNodeService(t *testing.T) {
func TestWorkerRegistry(t *testing.T) {
tx, err := SQLTransactor.BeginTx(context.Background(), true)
require.NoError(t, err, "getting transaction")
@ -29,34 +29,34 @@ func TestNodeService(t *testing.T) {
}
}()
nodeSvc := sqldb.NewNodeService(nil)
workerReg := sqldb.NewWorkerRegistry(nil)
err = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr, RoleTypes: []dax.RoleType{"compute"}})
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr, RoleTypes: []dax.RoleType{dax.RoleTypeCompute}})
require.NoError(t, err)
node, err := nodeSvc.ReadNode(tx, nodeAddr)
node, err := workerReg.Worker(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 = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr2, RoleTypes: []dax.RoleType{"translate", "compute"}})
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr2, RoleTypes: []dax.RoleType{dax.RoleTypeTranslate, dax.RoleTypeCompute}})
require.NoError(t, err, "create node 2")
err = nodeSvc.CreateNode(tx, dax.Address(""), &dax.Node{Address: nodeAddr3, RoleTypes: []dax.RoleType{"compute"}})
err = workerReg.AddWorker(tx, &dax.Node{Address: nodeAddr3, RoleTypes: []dax.RoleType{dax.RoleTypeCompute}})
require.NoError(t, err, "create node 3")
nodes, err := nodeSvc.Nodes(tx)
nodes, err := workerReg.Workers(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 = nodeSvc.DeleteNode(tx, nodeAddr2)
err = workerReg.RemoveWorker(tx, nodeAddr2)
require.NoError(t, err, "deleting node")
nodes, err = nodeSvc.Nodes(tx)
nodes, err = workerReg.Workers(tx)
require.NoError(t, err)
require.EqualValues(t, 2, len(nodes))
for _, node := range nodes {

View file

@ -40,9 +40,14 @@ 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
fwSvc := sqldb.NewFreeWorkerService(nil)
err = fwSvc.AddWorkers(tx, role, nodeAddr)
workerReg := sqldb.NewWorkerRegistry(nil)
err = workerReg.AddWorker(tx, node)
require.NoError(t, err)
err = wjSvc.CreateWorker(tx, role, qdbid, nodeAddr)
@ -65,7 +70,7 @@ func TestWorkerJobService(t *testing.T) {
err = fjSvc.CreateJobs(tx, role, qdbid, job1, job2, job3)
require.NoError(t, err)
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job1, job2)
err = wjSvc.AssignWorkerToJobs(tx, role, qdbid, nodeAddr, job1, job2)
require.NoError(t, err)
jobs, err := wjSvc.ListJobs(tx, role, qdbid, nodeAddr)
@ -85,7 +90,7 @@ func TestWorkerJobService(t *testing.T) {
require.NoError(t, err)
require.ElementsMatch(t, dax.Addresses{nodeAddr}, addrs)
err = wjSvc.CreateJobs(tx, role, qdbid, nodeAddr, job3)
err = wjSvc.AssignWorkerToJobs(tx, role, qdbid, nodeAddr, job3)
require.NoError(t, err)
jcs, err := wjSvc.JobCounts(tx, role, qdbid, nodeAddr)
@ -110,7 +115,7 @@ func TestWorkerJobService(t *testing.T) {
dk := wjSvc.DatabaseForWorker(tx, nodeAddr)
require.EqualValues(t, "db__orgid__blah", dk)
err = wjSvc.DeleteWorker(tx, role, qdbid, nodeAddr)
err = wjSvc.ReleaseWorkers(tx, nodeAddr)
require.NoError(t, err)
addrs, err = wjSvc.ListWorkers(tx, role, qdbid)

File diff suppressed because it is too large Load diff

View file

@ -159,14 +159,16 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(0),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 2,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 2,
},
}
got = director.flush()
@ -194,7 +196,7 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{},
ComputeRoles: []dax.ComputeRole{},
TranslateRoles: []dax.TranslateRole{},
Version: 3,
Version: 1,
},
}
got = director.flush()
@ -216,7 +218,7 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{},
ComputeRoles: []dax.ComputeRole{},
TranslateRoles: []dax.TranslateRole{},
Version: 4,
Version: 1,
},
}
got = director.flush()
@ -224,7 +226,71 @@ func TestController(t *testing.T) {
assert.Equal(t, exp, got)
// Add more shards.
addShards(t, ctx, con, tbl0.QualifiedID(), dax.NewShardNums(1, 2, 3, 5, 8)...)
// 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)...)
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(3),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(1),
},
},
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 2,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(2),
},
},
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{
{
@ -233,14 +299,16 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(1),
Shards: dax.NewShardNums(5),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 5,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
{
Address: node2.Address,
@ -248,59 +316,16 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(2),
Shards: dax.NewShardNums(8),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 6,
},
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(0, 3),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 7,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(1, 5),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 8,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbl0,
},
ComputeRoles: []dax.ComputeRole{
{
TableKey: tbl0.Key(),
Shards: dax.NewShardNums(2, 8),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 9,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 3,
},
}
got = director.flush()
@ -311,11 +336,14 @@ 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, 13)...)
addShards(t, ctx, con, tbl1.QualifiedID(), dax.NewShardNums(3, 5, 8)...)
exp = []*dax.Directive{
{
@ -323,80 +351,76 @@ func TestController(t *testing.T) {
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(3),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(0, 3),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 10,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(5),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(1, 5),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 11,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(8),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(2, 8),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 12,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 4,
},
}
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],
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(3, 13),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(0, 3),
Shards: dax.NewShardNums(13),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 13,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 5,
},
}
got = director.flush()
@ -411,21 +435,18 @@ func TestController(t *testing.T) {
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
},
ComputeRoles: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(3, 13),
},
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(0, 1, 3),
Shards: dax.NewShardNums(1),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 14,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 6,
},
{
Address: node2.Address,
@ -434,18 +455,20 @@ func TestController(t *testing.T) {
tbls[0],
tbls[1],
},
ComputeRoles: []dax.ComputeRole{
ComputeRolesAdded: []dax.ComputeRole{
{
TableKey: tbls[0].Key(),
Shards: dax.NewShardNums(5, 8),
Shards: dax.NewShardNums(5),
},
{
TableKey: tbls[1].Key(),
Shards: dax.NewShardNums(2, 5, 8),
Shards: dax.NewShardNums(5),
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 15,
ComputeRolesRemoved: []dax.ComputeRole{},
TranslateRolesAdded: []dax.TranslateRole{},
TranslateRolesRemoved: []dax.TranslateRole{},
Version: 5,
},
}
got = director.flush()
@ -458,7 +481,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -474,7 +497,7 @@ func TestController(t *testing.T) {
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 16,
Version: 6,
},
}
got = director.flush()
@ -521,7 +544,7 @@ func TestController(t *testing.T) {
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 17,
Version: 1,
},
}
got = director.flush()
@ -534,7 +557,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node3.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -550,7 +573,7 @@ func TestController(t *testing.T) {
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 18,
Version: 2,
},
}
got = director.flush()
@ -565,7 +588,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node3.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -581,7 +604,7 @@ func TestController(t *testing.T) {
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 19,
Version: 3,
},
}
got = director.flush()
@ -594,7 +617,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node3.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
},
@ -605,7 +628,7 @@ func TestController(t *testing.T) {
},
},
TranslateRoles: []dax.TranslateRole{},
Version: 20,
Version: 4,
},
}
got = director.flush()
@ -690,7 +713,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl0,
},
@ -727,7 +750,7 @@ func TestController(t *testing.T) {
Tables: []*dax.QualifiedTable{},
ComputeRoles: []dax.ComputeRole{},
TranslateRoles: []dax.TranslateRole{},
Version: 3,
Version: 1,
},
}
assert.Equal(t, exp, director.flush())
@ -743,7 +766,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl0,
},
@ -754,11 +777,11 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(0, 1, 2),
},
},
Version: 4,
Version: 3,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbl0,
},
@ -769,7 +792,7 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(3, 5, 7),
},
},
Version: 5,
Version: 2,
},
{
Address: node2.Address,
@ -784,7 +807,7 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(4, 6),
},
},
Version: 6,
Version: 1,
},
}
assert.Equal(t, exp, director.flush())
@ -803,7 +826,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -819,11 +842,11 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(0, 1, 2),
},
},
Version: 7,
Version: 4,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -839,11 +862,11 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(3, 5, 7),
},
},
Version: 8,
Version: 3,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
tbls[1],
@ -859,7 +882,7 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(4, 6),
},
},
Version: 9,
Version: 2,
},
}
assert.Equal(t, exp, director.flush())
@ -871,7 +894,7 @@ func TestController(t *testing.T) {
exp = []*dax.Directive{
{
Address: node0.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
},
@ -882,11 +905,11 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(1, 4, 7, 10, 13, 16, 19, 22),
},
},
Version: 10,
Version: 5,
},
{
Address: node1.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
},
@ -897,11 +920,11 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(2, 5, 8, 11, 14, 17, 20, 23),
},
},
Version: 11,
Version: 4,
},
{
Address: node2.Address,
Method: dax.DirectiveMethodDiff,
Method: dax.DirectiveMethodFull,
Tables: []*dax.QualifiedTable{
tbls[0],
},
@ -912,7 +935,7 @@ func TestController(t *testing.T) {
Partitions: dax.NewPartitionNums(0, 3, 6, 9, 12, 15, 18, 21),
},
},
Version: 12,
Version: 3,
},
}
assert.Equal(t, exp, director.flush())

View file

@ -1,36 +0,0 @@
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
}

View file

@ -9,7 +9,7 @@ import (
type Config struct {
AddressManager dax.AddressManager
NodeService dax.NodeService
WorkerRegistry dax.WorkerRegistry
NodePoller NodePoller
PollInterval time.Duration
Logger logger.Logger

View file

@ -16,7 +16,7 @@ type Poller struct {
addressManager dax.AddressManager
nodeService dax.NodeService
workerRegistry dax.WorkerRegistry
nodePoller NodePoller
pollInterval time.Duration
@ -30,7 +30,7 @@ type Poller struct {
func New(cfg Config) *Poller {
p := &Poller{
addressManager: dax.NewNopAddressManager(),
nodeService: dax.NewNopNodeService(),
workerRegistry: dax.NewNopWorkerRegistry(),
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.NodeService != nil {
p.nodeService = cfg.NodeService
if cfg.WorkerRegistry != nil {
p.workerRegistry = cfg.WorkerRegistry
}
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.nodeService.Nodes(context.Background())
nodes, err := p.workerRegistry.Workers(context.Background())
if err != nil {
p.logger.Errorf("POLLER: unable to get nodes from node service: %v", err)
}

View file

@ -25,7 +25,7 @@ import (
func TestPoller(t *testing.T) {
ctx := context.Background()
nodeService := newMemNodeService()
workerRegistry := newMemWorkerRegistry()
// node 1
node1 := newMockNode(t, "health", 0)
@ -44,7 +44,7 @@ func TestPoller(t *testing.T) {
}
// manager
manager := newMockManager(t, ctx, "deregister-nodes", nodeService)
manager := newMockManager(t, ctx, "deregister-nodes", workerRegistry)
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),
NodeService: nodeService,
WorkerRegistry: workerRegistry,
}
p := poller.New(cfg)
@ -62,9 +62,9 @@ func TestPoller(t *testing.T) {
close(done)
}()
// Add nodes to nodeService so they are available to the poller.
nodeService.CreateNode(ctx, addr1, daxNode1)
nodeService.CreateNode(ctx, addr2, daxNode2)
// Add workers to workerRegistry so they are available to the poller.
workerRegistry.AddWorker(ctx, addr1, daxNode1)
workerRegistry.AddWorker(ctx, addr2, daxNode2)
go p.Run()
defer p.Stop()
@ -83,13 +83,13 @@ type mockManager struct {
t *testing.T
server *httptest.Server
nodeService dax.NodeService
workerRegistry dax.WorkerRegistry
}
func newMockManager(t *testing.T, ctx context.Context, deregisterPath string, nodeService dax.NodeService) *mockManager {
func newMockManager(t *testing.T, ctx context.Context, deregisterPath string, wr dax.WorkerRegistry) *mockManager {
mm := &mockManager{
t: t,
nodeService: nodeService,
t: t,
workerRegistry: wr,
}
// 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, no
// the Poller.
deregister := func(addrs ...dax.Address) {
for _, addr := range addrs {
mm.nodeService.DeleteNode(context.Background(), addr)
mm.workerRegistry.RemoveWorker(context.Background(), addr)
}
}
@ -176,25 +176,25 @@ func (m *mockNode) Close() {
}
}
type memNodeService struct {
type memWorkerRegistry struct {
mu sync.RWMutex
addresses map[dax.Address]*dax.Node
}
func newMemNodeService() *memNodeService {
return &memNodeService{
func newMemWorkerRegistry() *memWorkerRegistry {
return &memWorkerRegistry{
addresses: make(map[dax.Address]*dax.Node),
}
}
func (m *memNodeService) CreateNode(ctx context.Context, addr dax.Address, node *dax.Node) error {
func (m *memWorkerRegistry) AddWorker(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 *memNodeService) ReadNode(ctx context.Context, addr dax.Address) (*dax.Node, error) {
func (m *memWorkerRegistry) Worker(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 *memNodeService) ReadNode(ctx context.Context, addr dax.Address) (*dax.N
return node, nil
}
func (m *memNodeService) DeleteNode(ctx context.Context, addr dax.Address) error {
func (m *memWorkerRegistry) RemoveWorker(ctx context.Context, addr dax.Address) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.addresses, addr)
return nil
}
func (m *memNodeService) Nodes(ctx context.Context) ([]*dax.Node, error) {
func (m *memWorkerRegistry) Workers(ctx context.Context) ([]*dax.Node, error) {
m.mu.RLock()
defer m.mu.RUnlock()

View file

@ -28,7 +28,7 @@ func NewErrDatabaseIDInvalid(databaseID dax.DatabaseID) error {
func NewErrDatabaseNameInvalid(databaseName dax.DatabaseName) error {
return errors.New(
ErrCodeDatabaseNameInvalid,
fmt.Sprintf("database name '%s' is invalid", databaseName),
fmt.Sprintf("invalid database name %s", databaseName),
)
}

View file

@ -11,7 +11,7 @@ func NewBalancer(log logger.Logger) *balancer.Balancer {
fjs := NewFreeJobService(log)
wjs := NewWorkerJobService(log)
fws := NewFreeWorkerService(log)
ns := NewNodeService(log)
ns := NewWorkerRegistry(log)
return balancer.New(ns, fjs, wjs, fws, schemar, log)
}

View file

@ -20,16 +20,51 @@ type directiveVersion struct {
log logger.Logger
}
func (d *directiveVersion) Increment(tx dax.Transaction, delta uint64) (uint64, error) {
func (d *directiveVersion) GetCurrent(tx dax.Transaction, addr dax.Address) (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.RawQuery("UPDATE directive_versions SET version = version + ? WHERE id = ? RETURNING id, version", delta, 1).First(dv)
if err != nil {
return 0, errors.Wrap(err, "updating directive_version")
err := dt.C.Find(dv, addr)
if err == nil {
return uint64(dv.Version), 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)
if err != nil {
return errors.Wrapf(err, "updating directive_version for address: %s", addr)
}
return nil
}

View file

@ -0,0 +1,51 @@
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))
})
}

View file

@ -28,16 +28,38 @@ func (fj *freeJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType,
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
jobs := make(models.Jobs, len(job))
for i, j := range job {
jobs[i] = models.Job{
// 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{
Name: j,
Role: roleType,
DatabaseID: qdbid.DatabaseID,
}
})
}
if len(jobs) == 0 {
return nil
}
err := dt.C.Create(jobs)
return errors.Wrap(err, "creating jobs")
return errors.Wrap(err, "creating free jobs")
}
func (fj *freeJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, job dax.Job) error {
@ -80,8 +102,9 @@ func (fj *freeJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, qd
return djs, nil
}
// 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 {
// 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 {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")

View file

@ -1,6 +1,8 @@
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"
@ -21,34 +23,6 @@ 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 {
@ -58,7 +32,8 @@ func (fw *freeWorkerService) PopWorkers(tx dax.Transaction, roleType dax.RoleTyp
results := make([]struct {
Address dax.Address `db:"address"`
}, 0, num)
err := dt.C.RawQuery("select address from workers where role = ? and database_id is NULL limit ?", roleType, num).All(&results)
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)
if err != nil {
return nil, errors.Wrap(err, "querying")
}
@ -81,9 +56,10 @@ func (fw *freeWorkerService) ListWorkers(tx dax.Transaction, roleType dax.RoleTy
}
workers := make(models.Workers, 0)
err := dt.C.Select("address").Where("role = ? and database_id is NULL", roleType).Order("address asc").All(&workers)
where := fmt.Sprintf("role_%s = true and database_id is NULL", roleType)
err := dt.C.Select("address").Where(where).Order("address asc").All(&workers)
if err != nil {
return nil, errors.Wrap(err, "querying for workers")
return nil, errors.Wrap(err, "querying for free workers")
}
ret := make(dax.Addresses, len(workers))

View file

@ -5,6 +5,7 @@ import (
"io/fs"
"strings"
"github.com/featurebasedb/featurebase/v3/errors"
"github.com/featurebasedb/featurebase/v3/logger"
"github.com/gobuffalo/pop/v6"
)
@ -58,39 +59,45 @@ 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 {
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())
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())
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 err
}
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
})
}

View file

@ -1,116 +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.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
}

View file

@ -7,6 +7,7 @@ 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"
@ -34,8 +35,8 @@ func (s *Schemar) CreateDatabase(tx dax.Transaction, qdb *dax.QualifiedDatabase)
return schemar.NewErrDatabaseIDInvalid(qdb.ID)
}
// Ensure the database name is not blank.
if qdb.Name == "" {
// Sanitizing database name
if err := featurebase.ValidateName(string(qdb.Name)); err != nil {
return schemar.NewErrDatabaseNameInvalid(qdb.Name)
}

View file

@ -2,7 +2,6 @@ package sqldb
import (
"context"
"strings"
"database/sql"
@ -67,16 +66,14 @@ 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{ReadOnly: !writable})
cn, err := t.NewTransactionContextOptions(ctx, &sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
ReadOnly: !writable,
})
if err != nil {
return nil, errors.Wrap(err, "getting SQL transaction")
}

View file

@ -0,0 +1,139 @@
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
}

View file

@ -24,37 +24,59 @@ 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{}
err := dt.C.Eager().Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Order("address asc").All(&workers)
sql := fmt.Sprintf("role_%s = true and database_id = ?", roleType)
err := dt.C.Where(sql, 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
ret[i].Jobs = make([]dax.Job, len(worker.Jobs))
for j, job := range worker.Jobs {
ret[i].Jobs[j] = job.Name
jobs, err := jobsForWorker(dt, &worker, roleType)
if err != nil {
return nil, errors.Wrap(err, "getting jobs for worker")
}
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{}
cnt, err := dt.C.Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Count(worker)
sql := fmt.Sprintf("role_%s = true and database_id = ?", roleType)
cnt, err := dt.C.Where(sql, qdbid.DatabaseID).Count(worker)
return cnt, errors.Wrap(err, "getting count")
}
@ -65,7 +87,8 @@ func (w *workerJobService) ListWorkers(tx dax.Transaction, roleType dax.RoleType
}
workers := models.Workers{}
err := dt.C.Select("address").Where("role = ? and database_id = ?", roleType, qdbid.DatabaseID).Order("address asc").All(&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)
if err != nil {
return nil, errors.Wrap(err, "getting workers")
}
@ -85,12 +108,13 @@ func (w *workerJobService) CreateWorker(tx dax.Transaction, roleType dax.RoleTyp
}
worker := &models.Worker{}
err := dt.C.RawQuery("UPDATE workers SET database_id = ? WHERE role = ? and address = ? RETURNING workers.ID", qdbid.DatabaseID, roleType, addr).First(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)
return errors.Wrap(err, "associating worker to database")
}
func (w *workerJobService) FreeWorkers(tx dax.Transaction, addrs ...dax.Address) error {
func (w *workerJobService) ReleaseWorkers(tx dax.Transaction, addrs ...dax.Address) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
@ -104,34 +128,17 @@ func (w *workerJobService) FreeWorkers(tx dax.Transaction, addrs ...dax.Address)
return errors.Wrap(err, "updating workers")
}
func (w *workerJobService) DeleteWorker(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address) error {
func (w *workerJobService) AssignWorkerToJobs(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 = ? 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)
sql := fmt.Sprintf("address = ? and role_%s = true", roleType)
err := dt.C.Where(sql, addr).First(worker)
if err != nil {
return errors.Wrap(err, "getting worker")
return errors.Wrapf(err, "getting worker: (%s) %s", roleType, addr)
}
jobs := models.Jobs{}
@ -140,35 +147,35 @@ func (w *workerJobService) CreateJobs(tx dax.Transaction, roleType dax.RoleType,
return errors.Wrap(err, "updating jobs")
}
// create jobs not in "jobs"
toCreate := jobsNotUpdated(job, jobs, worker)
// Assign jobs not in "jobs", and therefore didn't get updated by the
// previous sql statement.
toBeAssigned := jobsNotAssigned(job, jobs, roleType, worker)
err = dt.C.Create(toCreate)
if err != nil {
if err := dt.C.Create(toBeAssigned); err != nil {
return errors.Wrap(err, "creating jobs")
}
return errors.Wrap(err, "creating jobs")
return nil
}
func jobsNotUpdated(incomingJobs []dax.Job, created models.Jobs, worker *models.Worker) (toCreate models.Jobs) {
func jobsNotAssigned(incomingJobs []dax.Job, assigned models.Jobs, roleType dax.RoleType, worker *models.Worker) (toBeAssigned models.Jobs) {
outer:
for _, incJob := range incomingJobs {
for _, createdJob := range created {
if createdJob.Name == incJob {
for _, assignedJob := range assigned {
if assignedJob.Name == incJob {
continue outer
}
}
toCreate = append(toCreate,
toBeAssigned = append(toBeAssigned,
models.Job{
Name: incJob,
Role: worker.Role,
Role: roleType,
DatabaseID: dax.DatabaseID(worker.DatabaseID.String),
Worker: worker,
},
)
}
return toCreate
return toBeAssigned
}
func (w *workerJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addr dax.Address, job dax.Job) error {
@ -178,7 +185,8 @@ func (w *workerJobService) DeleteJob(tx dax.Transaction, roleType dax.RoleType,
}
worker := &models.Worker{}
err := dt.C.Select("id").Where("role = ? and database_id = ? and address = ?", roleType, qdbid.DatabaseID, addr).First(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)
if err != nil {
return errors.Wrap(err, "getting worker")
}
@ -227,7 +235,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, addr ...dax.Address) (map[dax.Address]int, error) {
func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType, qdbid dax.QualifiedDatabaseID, addrs ...dax.Address) (map[dax.Address]int, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
@ -238,18 +246,23 @@ func (w *workerJobService) JobCounts(tx dax.Transaction, roleType dax.RoleType,
Count int `db:"count"`
}{}
var err error
if len(addr) == 0 {
if len(addrs) == 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 = ?
where w.database_id = ? and w.role_%s = true
and j.role = ?
group by w.address`
err = dt.C.RawQuery(qstring, qdbid.DatabaseID, roleType).All(&results)
sql := fmt.Sprintf(qstring, roleType)
err = dt.C.RawQuery(sql, 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.address in (?) and w.database_id = ? and w.role = ?
where w.database_id = ? and w.role_%s = true
and j.role = ?
and w.address in (?)
group by w.address`
err = dt.C.RawQuery(qstring, addr, qdbid.DatabaseID, roleType).All(&results)
sql := fmt.Sprintf(qstring, roleType)
err = dt.C.RawQuery(sql, qdbid.DatabaseID, roleType, addrs).All(&results)
}
if err != nil {
return nil, errors.Wrap(err, "querying for jobs")
@ -269,20 +282,15 @@ func (w *workerJobService) ListJobs(tx dax.Transaction, roleType dax.RoleType, q
}
worker := &models.Worker{}
err := dt.C.Eager().Where("role = ? and database_id = ? and address = ?", roleType, qdbid.DatabaseID, addr).First(worker)
sql := fmt.Sprintf("role_%s = true and database_id = ? and address = ?", roleType)
err := dt.C.Where(sql, qdbid.DatabaseID, addr).First(worker)
if isNoRowsError(err) {
return nil, nil
} else if err != nil {
return nil, errors.Wrap(err, "getting worker")
}
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
return jobsForWorker(dt, worker, roleType)
}
func (w *workerJobService) DatabaseForWorker(tx dax.Transaction, addr dax.Address) dax.DatabaseKey {

View file

@ -21,19 +21,19 @@ func TestJobsNotUpdated(t *testing.T) {
},
}
toCreate := jobsNotUpdated(incJobs, created, &models.Worker{
ID: u2,
Role: "compute",
DatabaseID: nulls.NewString("dbid"),
toCreate := jobsNotAssigned(incJobs, created, dax.RoleTypeCompute, &models.Worker{
ID: u2,
RoleCompute: true,
DatabaseID: nulls.NewString("dbid"),
})
require.Equal(t, 3, len(toCreate))
// Test when 0 jobs are updated
toCreate = jobsNotUpdated(incJobs, models.Jobs{}, &models.Worker{
ID: u2,
Role: "compute",
DatabaseID: nulls.NewString("dbid"),
toCreate = jobsNotAssigned(incJobs, models.Jobs{}, dax.RoleTypeCompute, &models.Worker{
ID: u2,
RoleCompute: true,
DatabaseID: nulls.NewString("dbid"),
})
require.Equal(t, 4, len(toCreate))

View file

@ -1,17 +0,0 @@
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
}

40
dax/controller/worker.go Normal file
View file

@ -0,0 +1,40 @@
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
}

View file

@ -1,5 +1,7 @@
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.
@ -16,11 +18,30 @@ 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 {
Increment(tx Transaction, delta uint64) (uint64, error)
GetCurrent(tx Transaction, addr Address) (uint64, error)
SetNext(tx Transaction, addr Address, current, next uint64) error
}
// DirectiveMethod is used to tell the compute node how it should handle the
@ -28,8 +49,15 @@ type DirectiveVersion interface {
type DirectiveMethod string
const (
// DirectiveMethodDiff tells the compute node to diff the Directive with its
// local, cached Directive and only apply the differences.
// 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 DirectiveMethod = "diff"
// DirectiveMethodReset tells the compute node to delete all of its existing
@ -91,21 +119,23 @@ func (d *Directive) ComputeShardsMap() map[TableKey]ShardNums {
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{}
// 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
}
for _, tr := range d.TranslateRoles {
if tr.TableKey == tbl {
return tr.Partitions
for _, cr := range d.ComputeRoles {
m[cr.TableKey] = make(map[ShardNum]struct{})
for _, shardNum := range cr.Shards {
m[cr.TableKey][shardNum] = struct{}{}
}
}
return PartitionNums{}
return m
}
// TranslatePartitionsMap returns a map of table to partitions. It assumes that
@ -130,6 +160,53 @@ 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.
@ -171,9 +248,167 @@ 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].Version < d[j].Version }
func (d Directives) Less(i, j int) bool { return d[i].Address < d[j].Address }
func (d Directives) Swap(i, j int) { d[i], d[j] = d[j], d[i] }

View file

@ -1,32 +0,0 @@
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)
}

View file

@ -1,4 +1,9 @@
drop_table("columns")
drop_table("tables")
drop_table("organizations")
drop_table("databases")
drop_table("organizations")
drop_table("tables")
drop_table("columns")
drop_table("nodes")
drop_table("node_roles")
drop_table("workers")
drop_table("jobs")
drop_table("directive_versions")

View file

@ -43,40 +43,42 @@ 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()
}

View file

@ -0,0 +1,13 @@
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")

View file

@ -0,0 +1,16 @@
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")

View file

@ -7,7 +7,7 @@ import (
// DirectiveVersion holds what version the current directive is
type DirectiveVersion struct {
ID int `json:"id" db:"id"`
ID string `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"`

View file

@ -39,6 +39,16 @@ 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) {

View file

@ -1,56 +0,0 @@
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
}

View file

@ -1,31 +0,0 @@
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)
}

View file

@ -5,6 +5,7 @@ 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"
@ -15,13 +16,15 @@ 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"`
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"`
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"`
}
// String is not required by pop and may be deleted
@ -30,6 +33,23 @@ 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

View file

@ -894,145 +894,297 @@ func (o *orchestrator) executeMax(ctx context.Context, tableKeyer dax.TableKeyer
return other, nil
}
// 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) {
// 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) {
span, ctx := tracing.StartSpanFromContext(ctx, "Executor.executePercentile")
defer span.Finish()
// get nth
var nthFloat float64
nthArg, ok := c.Args["nth"]
if !ok {
return featurebase.ValCount{}, errors.New(errors.ErrUncoded, "Percentile(): nth required")
}
nthArg := c.Args["nth"]
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 featurebase.ValCount{}, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
return nil, 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 featurebase.ValCount{}, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
return nil, 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 featurebase.ValCount{}, errors.New(errors.ErrUncoded, "Percentile(): field required")
return nil, errors.New(errors.ErrUncoded, "Percentile(): field required")
}
field, err := o.schemaFieldInfo(ctx, tableKeyer, fieldName)
if err != nil {
return featurebase.ValCount{}, ErrFieldNotFound
return nil, 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
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
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
}
}
// get max
q, _ = pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
q, err := pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
if err != nil {
return nil, errors.Wrap(err, "parsing max call for Percentile")
}
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 featurebase.ValCount{}, errors.Wrap(err, "executing Max call for Percentile")
return nil, errors.Wrap(err, "executing Max call for Percentile")
}
// set up reusables
var countCall, rangeCall *pql.Call
if filterCall == nil {
countQuery, _ := pql.ParseString(fmt.Sprintf("Count(Row(%s < 0))", fieldName))
countCall = countQuery.Calls[0]
rangeCall = countCall.Children[0]
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 {
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]
// 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
}
}
k := (100 - nthFloat) / nthFloat
// set up reusable pql.Call objects representing a count (or intersectioncount,
// if we have a filter) with a condition we can alter.
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},
}
} else {
countCall = &pql.Call{
Name: "Count",
Children: []*pql.Call{
{
Name: "Intersect",
Children: []*pql.Call{rangeCall, filterCall},
},
},
}
}
min, max := minVal.Val, maxVal.Val
// estimate nth val, eg median when nth=0.5
for min < max {
// 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() {
// compute average without integer overflow, then correct for division of
// odd numbers by 2
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)
possibleNthVal = averageMinMax()
rangeCondition.Value = possibleNthVal
rangeCondition.Op = pql.LT
leftCount, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
if err != nil {
return featurebase.ValCount{}, errors.Wrap(err, "executing Count call L for Percentile")
return nil, errors.Wrap(err, "executing Count call L for Percentile")
}
leftCount := int64(leftCountUint64)
// get right count
rangeCall.Args[fieldName] = &pql.Condition{
Op: pql.Token(pql.GT),
Value: possibleNthVal,
// 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
}
rightCountUint64, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
rangeCondition.Op = pql.GT
rightCount, err := o.executeCount(ctx, tableKeyer, countCall, shards, opt)
if err != nil {
return featurebase.ValCount{}, errors.Wrap(err, "executing Count call R for Percentile")
return nil, errors.Wrap(err, "executing Count call R for Percentile")
}
rightCount := int64(rightCountUint64)
// '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
// If there's more things greater than the desired number, we need to look to the right.
if rightCount > desiredGreater {
minValueOver(possibleNthVal)
continue
}
}
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()
// 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 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)
}
valCount.Val = val + base
return valCount
}
// executeMinRow executes a MinRow() call.

View file

@ -39,6 +39,8 @@ type Queryer struct {
controller dax.Controller
systemLayer *systemlayer.SystemLayer
logger logger.Logger
}
@ -47,6 +49,7 @@ func New(cfg Config) *Queryer {
q := &Queryer{
controller: dax.NewNopController(),
orchestrators: make(map[dax.QualifiedDatabaseID]*qualifiedOrchestrator),
systemLayer: systemlayer.NewSystemLayer(),
logger: logger.NopLogger,
}
@ -198,17 +201,14 @@ 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 := &featurebase.NopSystemAPI{}
systemLayer := systemlayer.NewSystemLayer()
sysapi := newSystemAPI(q.controller, qdbid)
// 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, systemLayer, imp, q.logger, "")
pl := planner.NewExecutionPlanner(q.Orchestrator(qdbid), sapi, sysapi, q.systemLayer, imp, q.logger, "")
planOp, err := pl.CompilePlan(ctx, st)
if err != nil {

58
dax/queryer/system_api.go Normal file
View file

@ -0,0 +1,58 @@
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"
}

View file

@ -6,6 +6,11 @@ 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

View file

@ -256,19 +256,11 @@ 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.
err := mc.trans.TruncateAll()
if err != nil {
if err := mc.trans.TruncateAll(); err != nil {
tb.Fatalf("truncating DB: %v", err)
}
// 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 {
if err := mc.trans.Close(); err != nil {
tb.Fatalf("Closing conn after truncating all tables: %v", err)
}

View file

@ -142,10 +142,11 @@ 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-tests/test-1", // don't know why this is failing at all
"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
"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",
@ -274,7 +275,7 @@ func TestDAXIntegration(t *testing.T) {
// TODO: implement this without a sleep.
time.Sleep(5 * time.Second)
// ensure paritions are still covered
// ensure partitions are still covered
nodes, err = controllerClient.TranslateNodes(context.Background(), qtid, append(partitions0, partitions1...)...)
assert.NoError(t, err)
if assert.Len(t, nodes, 1) {
@ -371,7 +372,7 @@ func TestDAXIntegration(t *testing.T) {
qtid, err := controllerClient.TableID(ctx, qdbid, dax.TableName(defs.Keyed.Name(0)))
assert.NoError(t, err)
controllerClient.SnapshotTable(ctx, qtid)
assert.NoError(t, controllerClient.SnapshotTable(ctx, qtid))
// Ingest more data.
t.Run("ingest and query more data", func(t *testing.T) {
@ -464,7 +465,7 @@ func TestDAXIntegration(t *testing.T) {
assert.NoError(t, svcmgr.ControllerStart())
assert.True(t, mc.Healthy(controllerKey))
// ensure paritions are still covered
// ensure partitions are still covered
nodes, err = controllerClient.TranslateNodes(context.Background(), qtid, partitions...)
assert.NoError(t, err)
if assert.Len(t, nodes, 1) {
@ -692,7 +693,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.

View file

@ -1,9 +1,103 @@
package dax
import "context"
import (
"context"
"strings"
"github.com/featurebasedb/featurebase/v3/errors"
)
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
}

175
dax/transaction_test.go Normal file
View file

@ -0,0 +1,175 @@
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))
})
}

View file

@ -53,34 +53,34 @@ type AssignedNode struct {
Role Role `json:"role"`
}
// 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)
// 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)
}
// Ensure type implements interface.
var _ NodeService = &nopNodeService{}
var _ WorkerRegistry = &nopWorkerRegistry{}
// nopNoder is a no-op implementation of the Noder interface.
type nopNodeService struct{}
// nopWorkerRegistry is a no-op implementation of the WorkerRegistry interface.
type nopWorkerRegistry struct{}
func NewNopNodeService() *nopNodeService {
return &nopNodeService{}
func NewNopWorkerRegistry() *nopWorkerRegistry {
return &nopWorkerRegistry{}
}
func (n *nopNodeService) CreateNode(context.Context, Address, *Node) error {
func (n *nopWorkerRegistry) AddWorker(context.Context, Address, *Node) error {
return nil
}
func (n *nopNodeService) ReadNode(context.Context, Address) (*Node, error) {
func (n *nopWorkerRegistry) Worker(context.Context, Address) (*Node, error) {
return nil, nil
}
func (n *nopNodeService) DeleteNode(context.Context, Address) error {
func (n *nopWorkerRegistry) RemoveWorker(context.Context, Address) error {
return nil
}
func (n *nopNodeService) Nodes(context.Context) ([]*Node, error) {
func (n *nopWorkerRegistry) Workers(context.Context) ([]*Node, error) {
return []*Node{}, nil
}

View file

@ -42,7 +42,7 @@ type WorkerDiff struct {
RemovedJobs []Job
}
// Add adds w2 to w. It panics of w and w2 don't have teh same worker
// Add adds w2 to w. It panics if w and w2 don't have the 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,14 +55,15 @@ 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.Slice()
w.RemovedJobs = removed.Slice()
w.AddedJobs = added.Sorted()
w.RemovedJobs = removed.Sorted()
}
// WorkerDiffs is a sortable slice of WorkerDiff.
@ -72,6 +73,34 @@ 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{}

View file

@ -79,3 +79,54 @@ 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)
}

View file

@ -1294,129 +1294,310 @@ func (e *executor) executeMax(ctx context.Context, qcx *Qcx, index string, c *pq
}
// executePercentile executes a Percentile() call.
func (e *executor) executePercentile(ctx context.Context, qcx *Qcx, index string, c *pql.Call, shards []uint64, opt *ExecOptions) (_ ValCount, err error) {
//
// 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)
// }()
span, ctx := tracing.StartSpanFromContext(ctx, "executor.executePercentile")
defer span.Finish()
// get nth
var nthFloat float64
nthArg, ok := c.Args["nth"]
if !ok {
return ValCount{}, errors.New("Percentile(): nth required")
}
nthArg := c.Args["nth"]
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 ValCount{}, errors.Errorf("Percentile(): invalid nth='%v' of type (%[1]T), should be a number between 0 and 100 inclusive", c.Args["nth"])
return nil, 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 ValCount{}, errors.Errorf("Percentile(): invalid nth value (%f), should be a number between 0 and 100 inclusive", nthFloat)
return nil, 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 ValCount{}, errors.New("Percentile(): field required")
return nil, errors.New("Percentile(): field required")
}
field := e.Holder.Field(index, fieldName)
if field == nil {
return ValCount{}, ErrFieldNotFound
return nil, 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
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
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
}
}
// get max
q, _ = pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
q, err := pql.ParseString(fmt.Sprintf(`Max(field="%s")`, fieldName))
if err != nil {
return nil, errors.Wrap(err, "parsing max call for Percentile")
}
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 ValCount{}, errors.Wrap(err, "executing Max call for Percentile")
return nil, errors.Wrap(err, "executing Max call for Percentile")
}
// set up reusables
var countCall, rangeCall *pql.Call
if filterCall == nil {
countQuery, _ := pql.ParseString(fmt.Sprintf("Count(Row(%s < 0))", fieldName))
countCall = countQuery.Calls[0]
rangeCall = countCall.Children[0]
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 {
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]
// 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
}
}
k := (100 - nthFloat) / nthFloat
// set up reusable pql.Call objects representing a count (or intersectioncount,
// if we have a filter) with a condition we can alter.
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},
}
} else {
countCall = &pql.Call{
Name: "Count",
Children: []*pql.Call{
{
Name: "Intersect",
Children: []*pql.Call{rangeCall, filterCall},
},
},
}
}
min, max := minVal.Val, maxVal.Val
// estimate nth val, eg median when nth=0.5
for min < max {
// 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() {
// compute average without integer overflow, then correct for division of
// odd numbers by 2
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)
possibleNthVal = averageMinMax()
rangeCondition.Value = possibleNthVal
rangeCondition.Op = pql.LT
leftCount, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
if err != nil {
return ValCount{}, errors.Wrap(err, "executing Count call L for Percentile")
return nil, errors.Wrap(err, "executing Count call L for Percentile")
}
leftCount := int64(leftCountUint64)
// get right count
rangeCall.Args[fieldName] = &pql.Condition{
Op: pql.Token(pql.GT),
Value: possibleNthVal,
// 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
}
rightCountUint64, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
rangeCondition.Op = pql.GT
rightCount, err := e.executeCount(ctx, qcx, index, countCall, shards, opt)
if err != nil {
return ValCount{}, errors.Wrap(err, "executing Count call R for Percentile")
return nil, errors.Wrap(err, "executing Count call R for Percentile")
}
rightCount := int64(rightCountUint64)
// '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)
// If there's more things greater than the desired number, we need to look to the right.
if rightCount > desiredGreater {
minValueOver(possibleNthVal)
continue
}
// 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)
}
return field.valCountize(min, 1, nil)
}
// executeMinRow executes a MinRow() call.

View file

@ -7641,13 +7641,22 @@ func variousQueriesOnPercentiles(t *testing.T, c *test.Cluster) {
if nth == 0.0 {
return min
}
k := (100 - nth) / nth
if nth == 100.0 {
return max
}
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 := int64(0), int64(0)
leftCount, rightCount := 0, 0
for _, num := range nums {
if num < possibleNthVal {
leftCount++
@ -7656,11 +7665,9 @@ func variousQueriesOnPercentiles(t *testing.T, c *test.Cluster) {
}
}
leftCountWeighted := int64(math.Round(k * float64(leftCount)))
if leftCountWeighted > rightCount {
if leftCount > desiredLess {
max = possibleNthVal - 1
} else if leftCountWeighted < rightCount {
} else if rightCount > desiredGreater {
min = possibleNthVal + 1
} else { // perfectly balanced, as all things should be
return possibleNthVal

View file

@ -1627,6 +1627,11 @@ 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)
@ -1637,10 +1642,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
View file

@ -137,6 +137,7 @@ 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 (
@ -197,6 +198,7 @@ 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
View file

@ -1056,6 +1056,8 @@ 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=
@ -1684,6 +1686,7 @@ 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=

View file

@ -191,13 +191,11 @@ func (m *importer) EncodeImportValues(ctx context.Context, tid dax.TableID, fld
return "", nil, errors.Wrapf(err, "getting qtbl")
}
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)
// 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("")
if err != nil {
return "", nil, errors.Wrap(err, "getting featurebase client")
}
@ -216,13 +214,11 @@ func (m *importer) EncodeImport(ctx context.Context, tid dax.TableID, fld *dax.F
return "", nil, errors.Wrapf(err, "getting qtbl")
}
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)
// 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("")
if err != nil {
return "", nil, errors.Wrap(err, "getting featurebase client")
}

View file

@ -135,6 +135,10 @@ 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.

View file

@ -13,10 +13,13 @@ const (
ErrUnsupported errors.Code = "ErrUnsupported"
ErrCacheKeyNotFound errors.Code = "ErrCacheKeyNotFound"
ErrDuplicateColumn errors.Code = "ErrDuplicateColumn"
ErrUnknownType errors.Code = "ErrUnknownType"
ErrUnknownIdentifier errors.Code = "ErrUnknownIdentifier"
// syntax/semantic errors
ErrDuplicateColumn errors.Code = "ErrDuplicateColumn"
ErrUnknownType errors.Code = "ErrUnknownType"
ErrUnknownIdentifier errors.Code = "ErrUnknownIdentifier"
ErrTopLimitCannotCoexist errors.Code = "ErrTopLimitCannotCoexist"
// type related errors
ErrTypeIncompatibleWithBitwiseOperator errors.Code = "ErrTypeIncompatibleWithBitwiseOperator"
ErrTypeIncompatibleWithLogicalOperator errors.Code = "ErrTypeIncompatibleWithLogicalOperator"
ErrTypeIncompatibleWithEqualityOperator errors.Code = "ErrTypeIncompatibleWithEqualityOperator"
@ -40,8 +43,6 @@ const (
ErrTimeQuantumExpressionExpected errors.Code = "ErrTimeQuantumExpressionExpected"
ErrSingleRowExpected errors.Code = "ErrSingleRowExpected"
// type related errors
// decimal
ErrDecimalScaleExpected errors.Code = "ErrDecimalScaleExpected"
@ -94,6 +95,9 @@ 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"
@ -141,6 +145,16 @@ 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 {
@ -164,6 +178,13 @@ 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)
@ -647,6 +668,20 @@ 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,
@ -876,3 +911,35 @@ 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),
)
}

View file

@ -34,11 +34,13 @@ 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() {}
@ -48,6 +50,7 @@ func (*DropIndexStatement) node() {}
func (*DropTableStatement) node() {}
func (*DropFunctionStatement) node() {}
func (*DropViewStatement) node() {}
func (*DropModelStatement) node() {}
func (*Exists) node() {}
func (*ExplainStatement) node() {}
func (*ExprList) node() {}
@ -73,18 +76,21 @@ 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() {}
@ -111,6 +117,7 @@ func (*AlterTableStatement) stmt() {}
func (*AlterViewStatement) stmt() {}
func (*AnalyzeStatement) stmt() {}
func (*BeginStatement) stmt() {}
func (*CopyStatement) stmt() {}
func (*BulkInsertStatement) stmt() {}
func (*ShowDatabasesStatement) stmt() {}
func (*ShowTablesStatement) stmt() {}
@ -121,6 +128,7 @@ func (*CreateDatabaseStatement) stmt() {}
func (*CreateIndexStatement) stmt() {}
func (*CreateTableStatement) stmt() {}
func (*CreateFunctionStatement) stmt() {}
func (*CreateModelStatement) stmt() {}
func (*CreateViewStatement) stmt() {}
func (*DeleteStatement) stmt() {}
func (*DropDatabaseStatement) stmt() {}
@ -128,9 +136,12 @@ 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() {}
@ -177,10 +188,14 @@ 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:
@ -305,6 +320,12 @@ 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))
}
@ -519,11 +540,19 @@ 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 {
return "SHOW TABLES"
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()
}
func (s *ShowTablesStatement) Clone() *ShowTablesStatement {
@ -1237,10 +1266,8 @@ func (c *CacheTypeConstraint) String() string {
}
type TimeUnitConstraint struct {
TimeUnit Pos // position of TIMEUNIT keyword
Expr Expr // expression
Epoch Pos // position of TIMEUNIT keyword
EpochExpr Expr // expression
TimeUnit Pos // position of TIMEUNIT keyword
Expr Expr // expression
}
// Clone returns a deep copy of c.
@ -1258,10 +1285,6 @@ 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()
}
@ -1690,8 +1713,10 @@ 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 pre-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 system 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
@ -1711,11 +1736,11 @@ func (svar *SysVariable) Clone() *SysVariable {
return &other
}
func (svar *SysVariable) Name() string {
return tokens[svar.Token]
return svar.String()
}
func (svar *SysVariable) String() string {
return svar.Name()
return svar.Token.String()
}
func (svar *SysVariable) DataType() ExprDataType {
@ -2906,6 +2931,35 @@ 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
@ -2994,6 +3048,11 @@ 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
@ -3011,8 +3070,11 @@ type CreateFunctionStatement struct {
Parameters []*ParameterDefinition // parameters
Rparen Pos // position of parameter RParen
Returns Pos // position of RETURNS keyword
ReturnDef *ParameterDefinition // return def
Returns Pos // position of RETURNS keyword
ReturnType *Type // return def
With Pos // position of WITH keyword
Options []*FunctionOptionDefinition // options
As Pos // position of AS keyword
@ -3053,7 +3115,21 @@ func (s *CreateFunctionStatement) String() string {
}
buf.WriteString(" RETURNS ")
fmt.Fprintf(&buf, "%s %s", s.ReturnDef.Name, s.ReturnDef.Type.Name)
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(")")
}
}
buf.WriteString(" AS BEGIN")
for i := range s.Body {
@ -3092,6 +3168,86 @@ 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
@ -3115,8 +3271,6 @@ 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()
}
@ -3202,35 +3356,65 @@ 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
@ -3613,6 +3797,77 @@ 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
@ -3653,6 +3908,8 @@ 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.
@ -3662,7 +3919,6 @@ 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)
@ -3672,6 +3928,7 @@ 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
}
@ -3710,29 +3967,10 @@ 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())
}
@ -3778,7 +4016,6 @@ func (s *SelectStatement) String() string {
buf.WriteString(window.String())
}
}
// }
// Write compound operator.
if s.Compound != nil {
@ -3808,6 +4045,10 @@ func (s *SelectStatement) String() string {
}
}
if s.Limit.IsValid() {
fmt.Fprintf(&buf, " LIMIT %s", s.LimitExpr.String())
}
return buf.String()
}
@ -3907,15 +4148,45 @@ 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
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
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
OutputColumns []*SourceOutputColumn // output columns - populated during analysis
}
@ -3932,6 +4203,17 @@ 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 {
@ -3940,7 +4222,7 @@ func (n *QualifiedTableName) Clone() *QualifiedTableName {
other := *n
other.Name = n.Name.Clone()
other.Alias = n.Alias.Clone()
other.Index = n.Index.Clone()
other.QueryOptions = cloneQueryOptions(n.QueryOptions)
return &other
}
@ -3955,10 +4237,15 @@ func (n *QualifiedTableName) String() string {
fmt.Fprintf(&buf, " %s", n.Alias.String())
}
if n.Index != nil {
fmt.Fprintf(&buf, " INDEXED BY %s", n.Index.String())
} else if n.NotIndexed.IsValid() {
buf.WriteString(" NOT INDEXED")
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(")")
}
return buf.String()
}
@ -4139,6 +4426,7 @@ 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
}

View file

@ -5,6 +5,7 @@ import (
"reflect"
"strings"
"testing"
"time"
"github.com/featurebasedb/featurebase/v3/sql3/parser"
"github.com/go-test/deep"
@ -403,11 +404,8 @@ func TestCreateFunctionStatement_String(t *testing.T) {
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
},
},
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`)
ReturnType: &parser.Type{Name: &parser.Ident{Name: "int"}},
}, `CREATE FUNCTION func (@param1 int) RETURNS int AS BEGIN END`)
AssertStatementStringer(t, &parser.CreateFunctionStatement{
IfNotExists: pos(0),
@ -418,11 +416,8 @@ func TestCreateFunctionStatement_String(t *testing.T) {
Type: &parser.Type{Name: &parser.Ident{Name: "int"}},
},
},
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`)
ReturnType: &parser.Type{Name: &parser.Ident{Name: "int"}},
}, `CREATE FUNCTION IF NOT EXISTS func (@param1 int) RETURNS int AS BEGIN END`)
}
func TestCreateViewStatement_String(t *testing.T) {
@ -701,6 +696,88 @@ 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) {
@ -737,6 +814,22 @@ 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{
@ -767,34 +860,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{
@ -815,38 +908,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)}},
@ -901,6 +994,228 @@ 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) {
@ -953,20 +1268,58 @@ func TestUpdateStatement_String(t *testing.T) {
},
}, `UPDATE OR IGNORE tbl SET x = 100`)
// AssertStatementStringer(t, &sql.UpdateStatement{
// WithClause: &sql.WithClause{
// CTEs: []*sql.CTE{{
// TableName: &sql.Ident{Name: "cte"},
// Select: &sql.SelectStatement{
// Columns: []*sql.ResultColumn{{Star: pos(0)}},
// AssertStatementStringer(t, &parser.UpdateStatement{
// WithClause: &parser.WithClause{
// CTEs: []*parser.CTE{{
// TableName: &parser.Ident{Name: "cte"},
// Select: &parser.SelectStatement{
// Columns: []*parser.ResultColumn{{Star: pos(0)}},
// },
// As: parser.Pos{Column: 1},
// }},
// },
// Table: &sql.QualifiedTableName{Name: &sql.Ident{Name: "tbl"}},
// Assignments: []*sql.Assignment{
// {Columns: []*sql.Ident{{Name: "x"}}, Expr: &sql.NumberLit{Value: "100"}},
// Table: &parser.QualifiedTableName{Name: &parser.Ident{Name: "tbl"}},
// Assignments: []*parser.Assignment{
// {Columns: []*parser.Ident{{Name: "x"}}, Expr: &parser.IntegerLit{Value: "100"}},
// },
// }, `WITH "cte" AS (SELECT *) UPDATE "tbl" SET "x" = 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)
}
}
}
func TestIdent_String(t *testing.T) {
@ -992,6 +1345,80 @@ 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)`)
}
@ -999,6 +1426,7 @@ 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`)
}

View file

@ -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,7 +64,6 @@ 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
@ -89,7 +88,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) {
@ -102,10 +101,14 @@ 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:
@ -173,12 +176,18 @@ func (p *Parser) parseShowDatabasesStatement(showPos Pos) (*ShowDatabasesStateme
}
}
func (p *Parser) parseShowTablesStatement(showPos Pos) (*ShowTablesStatement, error) {
func (p *Parser) parseShowTablesStatement(showPos Pos) (_ *ShowTablesStatement, err 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")
@ -337,8 +346,10 @@ 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 or FUNCTION")
return nil, p.errorExpected(pos, tok, "DATABASE, TABLE, VIEW, FUNCTION or MODEL")
}
}
@ -373,6 +384,8 @@ 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")
}
@ -1158,6 +1171,72 @@ 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)
@ -1286,6 +1365,29 @@ 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)
@ -1457,11 +1559,35 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
return &stmt, p.errorExpected(p.pos, p.tok, "RETURNS")
}
stmt.Returns, _, _ = p.scan()
stmt.ReturnDef, err = p.parseParameterDefinition()
stmt.ReturnType, err = p.parseType()
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")
}
@ -1473,7 +1599,7 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
stmt.Begin, _, _ = p.scan()
for {
s, err := p.parseFunctionBodyStatement()
s, err := p.parseFunctionBodyStatement(&stmt)
if err != nil {
return &stmt, err
}
@ -1494,8 +1620,15 @@ func (p *Parser) parseCreateFunctionStatement(createPos Pos) (_ *CreateFunctionS
return &stmt, nil
}
func (p *Parser) parseFunctionBodyStatement() (stmt Statement, err error) {
func (p *Parser) parseFunctionBodyStatement(cf *CreateFunctionStatement) (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:
@ -1507,6 +1640,20 @@ func (p *Parser) parseFunctionBodyStatement() (stmt Statement, err error) {
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)
@ -1530,6 +1677,69 @@ 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 {
@ -2152,82 +2362,87 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (
// }
//}
switch p.peek() {
/*case VALUES:
stmt.Values, _, _ = p.scan()
if p.peek() != SELECT {
return &stmt, p.errorExpected(p.pos, p.tok, "SELECT")
}
for {
var list ExprList
if p.peek() != LP {
return &stmt, p.errorExpected(p.pos, p.tok, "left paren")
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()
}
list.Lparen, _, _ = 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
}
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()
for {
expr, err := p.ParseExpr()
if err != nil {
return &stmt, err
}
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)
stmt.GroupByExprs = append(stmt.GroupByExprs, expr)
if p.peek() != COMMA {
break
@ -2235,101 +2450,61 @@ func (p *Parser) parseSelectStatement(compounded bool, withClause *WithClause) (
p.scan()
}
// Parse FROM clause.
if p.peek() == FROM {
stmt.From, _, _ = p.scan()
if stmt.Source, err = p.parseSource(); err != nil {
// Parse optional HAVING clause.
if p.peek() == HAVING {
stmt.Having, _, _ = p.scan()
if stmt.HavingExpr, err = p.ParseExpr(); 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 {
@ -2353,6 +2528,13 @@ 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
}
@ -2584,29 +2766,85 @@ func (p *Parser) parseQualifiedTableName(ident *Ident) (_ *QualifiedTableName, e
}
}
// 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()
// handle query option
if p.peek() == WITH {
tbl.With, _, _ = p.scan()
if tbl.Index, err = p.parseIdent("index name"); err != nil {
return &tbl, err
tbl.QueryOptions = make([]*TableQueryOption, 0)
if p.peek() != LP {
return nil, p.errorExpected(p.pos, p.tok, "left paren")
}
case NOT:
tbl.Not, _, _ = p.scan()
if p.peek() != INDEXED {
return &tbl, p.errorExpected(p.pos, p.tok, "INDEXED")
}
tbl.NotIndexed, _, _ = p.scan()
}*/
tbl.LParen, _, _ = 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
@ -2704,6 +2942,26 @@ 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()
@ -2742,9 +3000,13 @@ func (p *Parser) parseOperand() (expr Expr, err error) {
case VARIABLE:
return &Variable{Name: lit, NamePos: pos}, nil
case MIN, MAX:
ident := &Ident{Name: lit, NamePos: pos, Quoted: tok == QIDENT}
return p.parseCall(ident)
case STRING:
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:
return &StringLit{ValuePos: pos, Value: lit}, nil
case FLOAT:
return &FloatLit{ValuePos: pos, Value: lit}, nil
@ -3629,6 +3891,22 @@ 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 {

View file

@ -40,6 +40,9 @@ 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),
@ -469,7 +472,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 @scalar int as begin end`, &parser.CreateFunctionStatement{
AssertParseStatement(t, `CREATE FUNCTION IF NOT EXISTS func (@param1 int, @param2 string) returns int as begin end`, &parser.CreateFunctionStatement{
Create: pos(0),
Function: pos(7),
If: pos(16),
@ -487,15 +490,12 @@ func TestParser_ParseFunctionStatement(t *testing.T) {
Type: &parser.Type{Name: &parser.Ident{NamePos: pos(57), Name: "string"}},
},
},
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),
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),
})
// AssertParseStatement(t, `CREATE TRIGGER IF NOT EXISTS trig BEFORE INSERT ON tbl BEGIN DELETE FROM new; END`, &parser.CreateFunctionStatement{
// Create: pos(0),
@ -658,8 +658,18 @@ 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) {
@ -952,7 +962,7 @@ func TestParser_ParseStatement(t *testing.T) {
},
})
AssertParseStatementError(t, `CREATE`, `1:1: expected DATABASE, TABLE, VIEW or FUNCTION`)
AssertParseStatementError(t, `CREATE`, `1:1: expected DATABASE, TABLE, VIEW, FUNCTION or MODEL`)
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'`)
@ -2212,29 +2222,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),
@ -2249,64 +2259,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),
@ -3347,16 +3357,106 @@ func TestParser_ParseStatement(t *testing.T) {
},
},
})
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
}
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
/*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'`)
@ -3399,11 +3499,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'`)

View file

@ -78,8 +78,6 @@ const (
ACTION
ADD
AFTER
AGG_COLUMN
AGG_FUNCTION
ALL
ALTER
ANALYZE
@ -106,9 +104,9 @@ const (
COMMENT
CONFLICT
CONSTRAINT
COPY
CREATE
CROSS
CTIME_KW
CURRENT
CURRENT_DATE
CURRENT_TIMESTAMP
@ -167,11 +165,13 @@ const (
LAST
LEFT
LIKE
LIMIT
LRU
MAP
MATCH
MAX
MIN
MODEL
NO
NOT
NOTBETWEEN
@ -194,6 +194,7 @@ const (
PLAN
PRAGMA
PRECEDING
PREDICT
PRIMARY
QUERY
RANGE
@ -304,8 +305,6 @@ var tokens = [...]string{
ACTION: "ACTION",
ADD: "ADD",
AFTER: "AFTER",
AGG_COLUMN: "AGG_COLUMN",
AGG_FUNCTION: "AGG_FUNCTION",
ALL: "ALL",
ALTER: "ALTER",
ANALYZE: "ANALYZE",
@ -332,9 +331,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",
@ -393,11 +392,13 @@ 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",
@ -420,6 +421,7 @@ var tokens = [...]string{
PLAN: "PLAN",
PRAGMA: "PRAGMA",
PRECEDING: "PRECEDING",
PREDICT: "PREDICT",
PRIMARY: "PRIMARY",
QUERY: "QUERY",
RANGE: "RANGE",

View file

@ -582,7 +582,15 @@ func walk(v Visitor, node Node) (_ Node, err error) {
if err := walkIdent(v, &n.Alias); err != nil {
return node, err
}
if err := walkIdent(v, &n.Index); err != nil {
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 {
return node, err
}
@ -844,3 +852,16 @@ 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
}

120
sql3/planner/compilecopy.go Normal file
View file

@ -0,0 +1,120 @@
// 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
}

View file

@ -17,7 +17,10 @@ func (p *ExecutionPlanner) compileCreateDatabaseStatement(stmt *parser.CreateDat
databaseName := strings.ToLower(parser.IdentName(stmt.Name))
failIfExists := !stmt.IfNotExists.IsValid()
units := 0
// 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
description := ""
// apply database options

View file

@ -0,0 +1,79 @@
// 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
}

View file

@ -0,0 +1,180 @@
// 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)
}
}

View file

@ -171,15 +171,6 @@ 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)
@ -385,13 +376,6 @@ 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:

View file

@ -0,0 +1,26 @@
// 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
}

View file

@ -0,0 +1,50 @@
// 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
}

View file

@ -305,7 +305,7 @@ func (p *ExecutionPlanner) compileSelectStatement(stmt *parser.SelectStatement,
}
}
// insert the top operator if it exists
// insert the top operator if it exists, or limit - analyzer should have caught the case of both existing
if stmt.Top.IsValid() {
topExpr, err := p.compileExpr(stmt.TopExpr)
if err != nil {
@ -313,6 +313,14 @@ 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() {
@ -420,6 +428,19 @@ 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)
@ -431,9 +452,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)), nil
return NewPlanOpRelAlias(aliasName, NewPlanOpPQLTableScan(p, tableName, extractColumns, queryHints)), nil
}
return NewPlanOpPQLTableScan(p, tableName, extractColumns), nil
return NewPlanOpPQLTableScan(p, tableName, extractColumns, queryHints), nil
case *parser.TableValuedFunction:
callExpr, err := p.compileCallExpr(sourceExpr.Call)
@ -549,6 +570,8 @@ 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)
@ -570,6 +593,35 @@ 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:
@ -613,6 +665,10 @@ 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
@ -624,6 +680,17 @@ 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

View file

@ -73,7 +73,19 @@ 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.Statement) (types.PlanOperator, error) {
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
}
tbls, err := p.schemaAPI.Tables(ctx)
if err != nil {
return nil, errors.Wrap(err, "getting tables")
@ -135,7 +147,7 @@ func (p *ExecutionPlanner) compileShowTablesStatement(ctx context.Context, stmt
dataType: parser.NewDataTypeString(),
}}
return NewPlanOpQuery(p, NewPlanOpProjection(columns, NewPlanOpFeatureBaseTables(p, pilosa.TablesToIndexInfos(tbls))), p.sql), nil
return NewPlanOpQuery(p, NewPlanOpProjection(columns, NewPlanOpFeatureBaseTables(p, pilosa.TablesToIndexInfos(tbls), showSystem)), p.sql), nil
}
func (p *ExecutionPlanner) compileShowColumnsStatement(ctx context.Context, stmt *parser.ShowColumnsStatement) (_ types.PlanOperator, err error) {

View file

@ -69,6 +69,10 @@ 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:
@ -93,16 +97,23 @@ 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)
}
@ -130,6 +141,10 @@ 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:
@ -154,12 +169,19 @@ 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)
}

View file

@ -4,6 +4,7 @@ package planner
import (
"context"
"sort"
"time"
pilosa "github.com/featurebasedb/featurebase/v3"
@ -81,6 +82,11 @@ 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
}

View file

@ -1514,16 +1514,18 @@ func (n *inOpPlanExpression) WithChildren(children ...types.PlanExpression) (typ
// callPlanExpression is a function call
type callPlanExpression struct {
name string
args []types.PlanExpression
dataType parser.ExprDataType
name string
args []types.PlanExpression
dataType parser.ExprDataType
udfReference *functionSystemObject
}
func newCallPlanExpression(name string, args []types.PlanExpression, dataType parser.ExprDataType) *callPlanExpression {
func newCallPlanExpression(name string, args []types.PlanExpression, dataType parser.ExprDataType, udfReference *functionSystemObject) *callPlanExpression {
return &callPlanExpression{
name: name,
args: args,
dataType: dataType,
name: name,
args: args,
dataType: dataType,
udfReference: udfReference,
}
}
@ -1591,6 +1593,9 @@ 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)
}
}
@ -1632,7 +1637,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), nil
return newCallPlanExpression(n.name, children, n.dataType, n.udfReference), nil
}
// aliasPlanExpression is a alias ref
@ -1719,6 +1724,11 @@ 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])
@ -2049,30 +2059,30 @@ func (n *sysVariablePlanExpression) WithChildren(children ...types.PlanExpressio
return n, nil
}
// dateLiteralPlanExpression is a date literal
type dateLiteralPlanExpression struct {
// timestampLiteralPlanExpression is a date literal
type timestampLiteralPlanExpression struct {
value time.Time
}
func newDateLiteralPlanExpression(value time.Time) *dateLiteralPlanExpression {
return &dateLiteralPlanExpression{
func newTimestampLiteralPlanExpression(value time.Time) *timestampLiteralPlanExpression {
return &timestampLiteralPlanExpression{
value: value,
}
}
func (n *dateLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
func (n *timestampLiteralPlanExpression) Evaluate(currentRow []interface{}) (interface{}, error) {
return n.value, nil
}
func (n *dateLiteralPlanExpression) Type() parser.ExprDataType {
func (n *timestampLiteralPlanExpression) Type() parser.ExprDataType {
return parser.NewDataTypeTimestamp()
}
func (n *dateLiteralPlanExpression) String() string {
func (n *timestampLiteralPlanExpression) String() string {
return n.value.Format(time.RFC3339Nano)
}
func (n *dateLiteralPlanExpression) Plan() map[string]interface{} {
func (n *timestampLiteralPlanExpression) Plan() map[string]interface{} {
result := make(map[string]interface{})
result["_expr"] = fmt.Sprintf("%T", n)
result["description"] = n.String()
@ -2081,11 +2091,11 @@ func (n *dateLiteralPlanExpression) Plan() map[string]interface{} {
return result
}
func (n *dateLiteralPlanExpression) Children() []types.PlanExpression {
func (n *timestampLiteralPlanExpression) Children() []types.PlanExpression {
return []types.PlanExpression{}
}
func (n *dateLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
func (n *timestampLiteralPlanExpression) WithChildren(children ...types.PlanExpression) (types.PlanExpression, error) {
return n, nil
}
@ -2660,7 +2670,7 @@ func (p *ExecutionPlanner) compileExpr(expr parser.Expr) (_ types.PlanExpression
return newFloatLiteralPlanExpression(expr.Value), nil
case *parser.DateLit:
return newDateLiteralPlanExpression(expr.Value), nil
return newTimestampLiteralPlanExpression(expr.Value), nil
case *parser.SysVariable:
return newSysVariablePlanExpression(expr.Name(), expr.Token), nil
@ -2889,7 +2899,15 @@ func (p *ExecutionPlanner) compileCallExpr(expr *parser.Call) (_ types.PlanExpre
return agg, nil
case "PERCENTILE":
agg := newPercentilePlanExpression(args[0], args[1], expr.ResultDataType)
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)
return agg, nil
case "MIN":
@ -2901,7 +2919,12 @@ func (p *ExecutionPlanner) compileCallExpr(expr *parser.Call) (_ types.PlanExpre
return agg, nil
default:
return newCallPlanExpression(parser.IdentName(expr.Name), args, expr.ResultDataType), nil
// 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
}
}
@ -2967,11 +2990,12 @@ 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()
}

View file

@ -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())
callop := newCallPlanExpression("foo", []types.PlanExpression{newIntLiteralPlanExpression(10)}, parser.NewDataTypeInt(), nil)
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 := newDateLiteralPlanExpression(tm)
dlop := newTimestampLiteralPlanExpression(tm)
assert.Equal(t, dlop.String(), "2012-11-01T22:08:41Z")
slop := newStringLiteralPlanExpression("foo")

View file

@ -5,6 +5,7 @@ package planner
import (
"context"
"fmt"
"math"
"github.com/featurebasedb/featurebase/v3/pql"
"github.com/featurebasedb/featurebase/v3/sql3"
@ -880,6 +881,7 @@ 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
@ -887,8 +889,9 @@ type percentilePlanExpression struct {
var _ types.Aggregable = (*percentilePlanExpression)(nil)
func newPercentilePlanExpression(arg types.PlanExpression, nthArg types.PlanExpression, returnDataType parser.ExprDataType) *percentilePlanExpression {
func newPercentilePlanExpression(pos parser.Pos, arg types.PlanExpression, nthArg types.PlanExpression, returnDataType parser.ExprDataType) *percentilePlanExpression {
return &percentilePlanExpression{
pos: pos,
arg: arg,
nthArg: nthArg,
returnDataType: returnDataType,
@ -904,7 +907,7 @@ func (n *percentilePlanExpression) Evaluate(currentRow []interface{}) (interface
}
func (n *percentilePlanExpression) NewBuffer() (types.AggregationBuffer, error) {
return NewAggCountBuffer(n), nil
return nil, sql3.NewErrUnsupported(n.pos.Line, n.pos.Column, true, "Percentile call that can't be pushed down to PQL")
}
func (n *percentilePlanExpression) FirstChildExpr() types.PlanExpression {
@ -940,7 +943,312 @@ func (n *percentilePlanExpression) WithChildren(children ...types.PlanExpression
if len(children) != 2 {
return nil, sql3.NewErrInternalf("unexpected number of children '%d'", len(children))
}
return newPercentilePlanExpression(children[0], children[1], n.returnDataType), nil
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
}
// aggregator for LAST()

View file

@ -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("expresion list should only contain one select statement")
return nil, sql3.NewErrInternalf("expression list should only contain one select statement")
}
//make sure select only returns one column
if len(sel.Columns) > 1 {

View file

@ -124,6 +124,71 @@ 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 {
@ -270,6 +335,15 @@ 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

View file

@ -499,18 +499,19 @@ func (p *ExecutionPlanner) generatePQLCallFromBinaryExpr(ctx context.Context, ex
if err != nil {
return nil, err
}
val, ok := pqlValue.(float64)
if !ok {
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:
return nil, sql3.NewErrInternalf("unexpected type '%T", pqlValue)
}
d := pql.FromFloat64(val)
return &pql.Call{
Name: "Row",
Args: map[string]interface{}{
lhs.columnName: &pql.Condition{
Op: pqlOp,
Value: d,
},
lhs.columnName: cond,
},
}, nil
@ -541,7 +542,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:
case *parser.DataTypeID, *parser.DataTypeString, *parser.DataTypeIDSet, *parser.DataTypeStringSet, *parser.DataTypeBool:
if strings.EqualFold(lhs.columnName, string(dax.PrimaryKeyFieldName)) {
return nil, sql3.NewErrInvalidColumnInFilterExpression(0, 0, string(dax.PrimaryKeyFieldName), "is/is not null")
}
@ -604,7 +605,7 @@ func planExprToValue(expr types.PlanExpression) (interface{}, error) {
return expr.value, nil
case *stringLiteralPlanExpression:
return expr.value, nil
case *dateLiteralPlanExpression:
case *timestampLiteralPlanExpression:
return expr.value, nil
case *boolLiteralPlanExpression:
return expr.value, nil

View file

@ -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

View file

@ -867,7 +867,7 @@ func processColumnValue(rawValue interface{}, targetType parser.ExprDataType) (t
if !ok {
return nil, sql3.NewErrInternalf("unable to convert '%s", rawValue)
}
return newDateLiteralPlanExpression(tval), nil
return newTimestampLiteralPlanExpression(tval), nil
case *parser.DataTypeString:
sval, ok := rawValue.(string)

515
sql3/planner/opcopy.go Normal file
View file

@ -0,0 +1,515 @@
// 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 &copyIterator{
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
}

View file

@ -0,0 +1,104 @@
// 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
}

View file

@ -0,0 +1,279 @@
// 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
}

View file

@ -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)
}

102
sql3/planner/opdropmodel.go Normal file
View file

@ -0,0 +1,102 @@
// 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
}

View file

@ -17,16 +17,18 @@ import (
// PlanOpFeatureBaseTables wraps a []*IndexInfo that is returned from
// schemaAPI.Schema().
type PlanOpFeatureBaseTables struct {
planner *ExecutionPlanner
indexInfo []*pilosa.IndexInfo
warnings []string
planner *ExecutionPlanner
indexInfo []*pilosa.IndexInfo
withSystem bool
warnings []string
}
func NewPlanOpFeatureBaseTables(planner *ExecutionPlanner, indexInfo []*pilosa.IndexInfo) *PlanOpFeatureBaseTables {
func NewPlanOpFeatureBaseTables(planner *ExecutionPlanner, indexInfo []*pilosa.IndexInfo, withSystem bool) *PlanOpFeatureBaseTables {
return &PlanOpFeatureBaseTables{
planner: planner,
indexInfo: indexInfo,
warnings: make([]string, 0),
planner: planner,
indexInfo: indexInfo,
withSystem: withSystem,
warnings: make([]string, 0),
}
}
@ -105,65 +107,85 @@ 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,
planner: p.planner,
indexInfo: p.indexInfo,
withSystem: p.withSystem,
}, nil
}
func (p *PlanOpFeatureBaseTables) WithChildren(children ...types.PlanOperator) (types.PlanOperator, error) {
return NewPlanOpFeatureBaseTables(p.planner, p.indexInfo), nil
return NewPlanOpFeatureBaseTables(p.planner, p.indexInfo, p.withSystem), nil
}
type showTablesRowIter struct {
planner *ExecutionPlanner
indexInfo []*pilosa.IndexInfo
rowIndex int
planner *ExecutionPlanner
indexInfo []*pilosa.IndexInfo
withSystem bool
result types.Rows
}
var _ types.RowIterator = (*showTablesRowIter)(nil)
func (i *showTablesRowIter) Next(ctx context.Context) (types.Row, error) {
if i.rowIndex < len(i.indexInfo) {
if i.result == nil {
i.result = make(types.Rows, 0)
indexName := i.indexInfo[i.rowIndex].Name
for _, idx := range i.indexInfo {
var err error
var spaceUsed pilosa.DiskUsage
switch strings.ToLower(indexName) {
case fbDatabaseInfo, fbDatabaseNodes, fbPerformanceCounters, fbExecRequests, fbTableDDL:
spaceUsed = pilosa.DiskUsage{
Usage: 0,
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
}
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)
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()
spaceUsed, err = pilosa.GetDiskUsage(u)
if err != nil {
return nil, err
// 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
}
}
}
}
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,
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)
}
i.rowIndex += 1
}
if len(i.result) > 0 {
row := i.result[0]
// Move to next result element.
i.result = i.result[1:]
return row, nil
}
return nil, types.ErrNoMoreRows

View file

@ -230,17 +230,15 @@ 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,
},
Children: []*pql.Call{cond},
}
if cond != nil {
call.Args["filter"] = cond
}
default:
@ -296,6 +294,10 @@ 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])
}

View file

@ -265,26 +265,18 @@ func (i *distinctScanRowIter) Next(ctx context.Context) (types.Row, error) {
row[0] = pql.NewDecimal(val, t.Scale)
case *parser.DataTypeIDSet:
val, ok := result.([]uint64)
val, ok := result.(int64)
if !ok {
return nil, sql3.NewErrInternalf("unexpected type for column value '%T'", result)
}
if val == nil {
row[0] = nil
} else {
row[0] = val
}
row[0] = []uint64{uint64(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)
}
if val == nil {
row[0] = nil
} else {
row[0] = val
}
row[0] = []string{val}
default:
row[0] = result

View file

@ -232,7 +232,12 @@ func (i *pqlGroupByRowIter) Next(ctx context.Context) (types.Row, error) {
if g.Value != nil {
row[idx] = *g.Value
} else if g.RowKey != "" {
row[idx] = g.RowKey
switch c.Type().(type) {
case *parser.DataTypeStringSet:
row[idx] = []string{g.RowKey}
default:
row[idx] = g.RowKey
}
} else {
switch c.Type().(type) {
case *parser.DataTypeIDSet:

View file

@ -15,6 +15,11 @@ 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
@ -23,15 +28,17 @@ type PlanOpPQLTableScan struct {
filter types.PlanExpression
timeQuantumFilters []types.PlanExpression
topExpr types.PlanExpression
hints []*TableQueryHint
warnings []string
}
func NewPlanOpPQLTableScan(p *ExecutionPlanner, tableName string, columns []string) *PlanOpPQLTableScan {
func NewPlanOpPQLTableScan(p *ExecutionPlanner, tableName string, columns []string, hints []*TableQueryHint) *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