feat: add initial support for DDL and data modification operator (#128)#236
feat: add initial support for DDL and data modification operator (#128)#236curino wants to merge 17 commits intosubstrait-io:mainfrom
Conversation
|
For this I had to move |
|
You can just use |
Good point. |
Generally looks good. I wouldn't include pure DDL operations in the WriteRel, they feel different (create/drop view, create table, drop table, alter table). Basically, there seem to be two main types of writes: writes that take a set of records as input (e.g. CTAS) and writes that don't (CREATE TABLE). Keeping the second separate feels better than combining here. In general, I have a little question of whether we need to even support DDL at the Substrait level. It feels very database specific (and not requiring any of the other parts of Substrait). I also don't really understand what the output Rel is. I'd expect something more like: That's just a rough sketch. |
|
@jacques-n, I am ok to move the DDL into a separate type of operator ``DDLOp` or whatnot. The only issue is splitting CTAS and CREATE TABLE, that feels a bit odd. I think Substrait providing a minimum coverage of DDL is essential for backends to be able to depend solely on Substrait as their interface. Talking with folks at SIGMOD the pitch that was resonating very well was "You build the backend, be compatible with Substrait and don't worry about a thing"... if they don't get DDL coverage there the dream is half fullfilled. Similar needs arise for tools in governance/provenance like the Atlas integration that Ashvin is working on, or for other tools that need visibility of the whole workload for example for view/index selection tools (I know other folks would like to use Substrait for that). Bottomline, a minimum support for DDL allows to consider Substrait a complete replacement for the entire parsing/interpretation stack in the system. A partial coverage makes the value prop much less appealing (as folks would need to integrate with other existing parser, and then Substrait is just another dependency to add). As for the output, what you have is fine (a bit low-level), but if you look at the |
I think I agree with @jacques-n here but maybe I'm coming at things from a mechanical / implementation perspective. Coming from the analytics world, the table metadata is often stored completely separate from the table data. For example, hive, or iceberg. The DDL work is then carried out by a completely different system.
On the read side of things we don't have any API right now for reading table metadata so it seems a bit asymmetric. Perhaps there could be layers to the API? Arrow/Acero, for instance, isn't going to have support for any DDL writing or reading, so having it in a separate layer would make it easier to simply not implement. |
proto/substrait/algebra.proto
Outdated
| } | ||
|
|
||
| // The operator that modifies the schema/content of a database | ||
| message WriteRel { |
There was a problem hiding this comment.
This seems to be the analogue / opposite of a "named table read". In Arrow we also have a write rel but it is the analogue / opposite of a "local files read". We take in options like "destination directory", "file format", "partitioning", etc.
It would be nice to structure things in a way that the ReadRel mirrors the WriteRel which, in this case, I think would mean introducing a WriteType and having this content (table, base_schema, defaults) be inside a NamedTableWrite message or something similar.
There was a problem hiding this comment.
Regarding WriteType I like, let me and @jcamachor try.
As for DDL, I am not worried about the "read" path, as this is handled by most (all?) RDBMS by simple using SELECT on a special DB that is about metadata of the system (usually called information_schema), so things would look like:
SELECT table_name, table_schema, table_type
FROM information_schema.tables
(And there are other tables for views, indexes, constraints, columns, etc..)
There was a problem hiding this comment.
Ok, we tried to implement both the split suggest by @jacques-n and the NamedTableWrite suggested by @westonpace.
I am still very convinced that DDL would be very very nice to have (and seems to add minimal complexity).
There was a problem hiding this comment.
If you like this version, I can propagate to website etc. We will also likely do in a separate patch the isthmus changes needed to test all this out.
Once again, we have been co-developing this with @jcamachor.
There was a problem hiding this comment.
usually called
information_schema
If one of the goals of Substrait is to try to remedy the current situation where each system has its own flavor of SQL, rather than just being binary SQL, I don't think that should be the way to go. It also doesn't extend to metadata queries for file data sources.
I'm trying not to get too involved in these additions because with my background I feel like an outsider looking in, but as that outsider, generally I feel like lots of things are currently being specified based on how SQL has always pragmatically done things, rather than based on what makes sense. For example, why should something like a drop table or insert into return a table? Virtually every other language has a statement-vs-expression-like concept to allow for things that don't return anything. Elsewhere I joked about SELECT * FROM (DROP TABLE *) to masquerade a malicious plan as something immutable; I don't know if SQL allows this but if these DDL statements are modeled as regular 1-input 1-output relations I see no reason why Substrait wouldn't. I personally liked the original proposal that extended the PlanRel oneof a lot more than using WriteRel for this reason. Just my two cents I guess.
There was a problem hiding this comment.
I wonder whether we could "extend to metadata queries for file data sources" by introducing an operator that extract metadata from a file as a relation, and then allow the standard SELECT * FROM information_schema.tables type tricks, which I find very pleasant in RDBMSs as you get to use all the tools and language expressivity you have for data also for metadata.
drop table (and CREATE/ALTER for table and VIEWS) do not return a rel (in the latest patch), but INSERT/DELETE/UPDATE do. Why does SQL support that I am not sure, but poking around I have seen folks in blogs/discussions do interesting things to compactly execute a statement and verify properties about what happened in the DBMS, avoiding odd workaround like selecting into temporary tables, executing and then dropping temp tables, etc. Intuitively it helps keep the language as "close" as possible, which I think is very convenient in many ways (I think @jacques-n hints at that below).
|
Couple of comments here: The reason I think CREATE, ALTER, DROP should be distinct from DELETE, UPDATE, INSERT: one takes an input tree and one does not. In some ways, this is akin to the Arrow Flight distinction between "actions" and get/put. I'm fine with DDL but I agree @jvanstraten that it should be less SQL specific. I also suggest we start with the big ones: DROP a table, change a table schema and create a table. For the schema change, we should think about the operation int the context of Iceberg and how we would express it here. WRT to the top-level concept some more thoughts... I'm really mixed on introducing this at that level. Composability of relations is a very powerful thing, even if those relations are writes. I've definitely built multiple systems modeled this way and when you think about complex write paths like Apache Iceberg, treating parquet write as a transformer (transforming many records into pointers to parquet files) that then hands those off to another writer (transforms parquet pointers into manifest pointers) handing off to another writer (transforms manifest pointers into manifest list pointers), this makes things much cleaner from an implementation & scaling perspective. I also agree that there are times where that composability is unnecessary. Introducing these concepts as two discrete things feels excessive and thus I was inclined to only keeping as a rel (rel's feel "cheaper" in the spec than introducing top concepts with special semantics at other layers). I think the |
|
I love the discussion going on here! I can go either way on the top-level vs not argument. @jacques-n proposal (everything is a As for CREATE/ALTER/DROP I am fine to keep it separate as it allows us to at least remove the The way I was thinking about the
One more thought around the "closeness" of |
|
I posted #239 and got pointed here. I could probably provide some feedback. |
| } | ||
|
|
||
| // The operator that modifies the schema/content of a database | ||
| message WriteRel { |
There was a problem hiding this comment.
#239 has the goal of multiple outputs from the Substrait plan, and in particular it distinguishes between the table it writes out to the table it passes through. Could this goal be met here? What table does WriteRel here pass to a relation that has it in its Rel input field?
There was a problem hiding this comment.
That would be the Rel output, however that is typically used to report on which tuples have been modified (often simply counting them and returning that as an aggregate, or reporting before/after images for an update, etc.. lots of semantics exist, hence allowing for an arbitrary query on it).
I think it "might" work, but I would suggest you take a look at it once we merge this, and evolve it as needed. I would probably think of a multi-output as a spool followed by simple writers, but this is just me.
| // Definition of which type of write operation is to be performed | ||
| oneof write_type { | ||
| NamedTableWrite named_table = 1; | ||
| ReadRel.LocalFiles local_files = 2; |
There was a problem hiding this comment.
I don't think ReadRel.LocalFiles works here. For one thing, the start and length field are meaningless for writing, and likely so are future specific read options (e.g., for ParquetReadOptions. If the desire is to reuse fields, common fields (for reading and writing) should be refactored into a common place, such as CommonRel.LocalFiles.
There was a problem hiding this comment.
I was going for this based on some previous comment to avoid breaking changes where possible. Moving LocalFiles outside of ReadRel will be a breaking change.
| oneof write_type { | ||
| NamedTableWrite named_table = 1; | ||
| ReadRel.LocalFiles local_files = 2; | ||
| ReadRel.ExtensionTable extension_table = 3; |
There was a problem hiding this comment.
This should be refactored to a common place (for reading and writing) too.
There was a problem hiding this comment.
I was going for this based on some previous comment to avoid breaking changes where possible. Moving LocalFiles outside of ReadRel will be a breaking change.
| // affected tuples (per common behavior of most RDBMS). Defaults to no output. | ||
| Rel output = 6; | ||
|
|
||
| enum WriteOp { |
There was a problem hiding this comment.
Is there a need for merge operations, which define the written result as a function of both the query result's row and the one already stored? For example, such a merge operation could append to a string column, or increment a numeric column, or even used for distributed statistical aggregation.
There was a problem hiding this comment.
Yes, that does exist and I think eventually we should cover it, but looking at the potential richness of semantics (e.g., see example for SQLServer) it does look a bit complex, so I was leaving this for a later patch (as we are already taking on a few things here).
| // A base table for writing. The list of string is used to represent namespacing (e.g., mydb.mytable). | ||
| // This assumes shared catalog between systems exchanging a message. | ||
| // it also includes a base schema, and default types | ||
| message NamedTableWrite { |
There was a problem hiding this comment.
Could you explain the rationale for the fields in this message? I thought the DdlRel.query field already defines the schema except for the names; this schema-and-names pattern is also seen in RelRoot adding names to its Rel input's schema. I'm also not sure why the default values are not implicit in the query.
There was a problem hiding this comment.
I should note that my DB background is mostly as a user, so I'm not familiar with some of the terminology.
There was a problem hiding this comment.
Two reasons:
- for a
CREATE TABLEwe would not have ainputand only specify the schema (which must include defaults, which are missing inRel) - I missed to reuse this field in
WriteRel, where we could supportINSERTthat specify only a subset of the fields.
The alternative could be expecting input to include constants for all defaults in the "right places" (which I think is what you suggest). That could work, but feels less natural as most systems assume they can specify only a subset of fields in the INSERT, and if I am doing a CREATE TABLE feels more natural to feel-out the defaults list than to create a query of constants.
…ions (substrait-io#196) * feat: introduce compound (parameterizable) extension types and variations
BREAKING CHANGE: The signature of divide functions for multiple types now specify an enumeration prior to specifying operands.
|
I created a new PR (#252) to fix the Lint commits issues (tried the other way, but always something going wrong, so cut a new branch and moved the patch). |
|
This is superseded by #252 |
|
Folks, can you please comment (or approve ;-)) #252 ? |
|
Superseded by #252 |
No description provided.